DEV Community

Josh Blair
Josh Blair

Posted on

Static Site Hosting on AWS — S3, CloudFront, ACM, and Route 53

Overview

This article covers deploying a static React site to AWS using S3 as the origin, CloudFront as the CDN, ACM for TLS certificates, and Route 53 for DNS. Everything is defined as CloudFormation infrastructure-as-code.

Architecture

Static hosting architecture


CloudFormation Infrastructure

The hosting infrastructure is split across three stacks deployed in sequence:

Stack File Region Purpose
bonefish-acm infra/acm/certificate.yml us-east-1 ACM TLS certificate
bonefish-website infra/stacks/website.yml us-west-2 S3 + CloudFront
bonefish-pipeline infra/stacks/pipeline.yml us-west-2 CI/CD (covered in article 4)

ACM Certificate (us-east-1)

Why us-east-1? CloudFront is a global service that only accepts ACM certificates provisioned in us-east-1. This is an AWS requirement regardless of where your other resources live.

# infra/acm/certificate.yml
Resources:
  Certificate:
    Type: AWS::CertificateManager::Certificate
    Properties:
      DomainName: bonefishsoftware.com
      SubjectAlternativeNames:
        - www.bonefishsoftware.com
      ValidationMethod: DNS
Enter fullscreen mode Exit fullscreen mode

DNS validation: ACM generates two CNAME records that must be added to your DNS zone. Because the domain is managed by Route 53, we added these via CLI:

aws route53 change-resource-record-sets \
  --hosted-zone-id $ZONE_ID \
  --change-batch '{ "Changes": [
    { "Action": "UPSERT", "ResourceRecordSet": {
      "Name": "_bf48ff...bonefishsoftware.com.",
      "Type": "CNAME", "TTL": 300,
      "ResourceRecords": [{"Value": "_ea68....acm-validations.aws."}]
    }}
  ]}'
Enter fullscreen mode Exit fullscreen mode

Once the domain's nameservers resolve correctly, ACM validates automatically (typically 2–5 minutes).


S3 Bucket

The S3 bucket is private — no public access whatsoever. CloudFront accesses it via OAC (Origin Access Control).

WebsiteBucket:
  Type: AWS::S3::Bucket
  Properties:
    BucketName: bonefishsoftware-com-website
    VersioningConfiguration:
      Status: Enabled
    PublicAccessBlockConfiguration:
      BlockPublicAcls: true
      BlockPublicPolicy: true
      IgnorePublicAcls: true
      RestrictPublicBuckets: true
Enter fullscreen mode Exit fullscreen mode

Bucket Policy — allow CloudFront OAC only

WebsiteBucketPolicy:
  Type: AWS::S3::BucketPolicy
  Properties:
    Bucket: !Ref WebsiteBucket
    PolicyDocument:
      Statement:
        - Effect: Allow
          Principal:
            Service: cloudfront.amazonaws.com
          Action: s3:GetObject
          Resource: !Sub '${WebsiteBucket.Arn}/*'
          Condition:
            StringEquals:
              AWS:SourceArn: !Sub >-
                arn:aws:cloudfront::${AWS::AccountId}:distribution/${Distribution}
Enter fullscreen mode Exit fullscreen mode

The AWS:SourceArn condition means only our specific CloudFront distribution can read from this bucket — not any other CloudFront distribution in any AWS account.


Origin Access Control (OAC)

OAC is the modern replacement for Origin Access Identity (OAI). Key advantages:

  • Supports all S3 API operations
  • Works with SSE-KMS encrypted buckets
  • Uses AWS SigV4 request signing (more secure)
OriginAccessControl:
  Type: AWS::CloudFront::OriginAccessControl
  Properties:
    OriginAccessControlConfig:
      Name: bonefishsoftware-com-oac
      OriginAccessControlOriginType: s3
      SigningBehavior: always
      SigningProtocol: sigv4
Enter fullscreen mode Exit fullscreen mode

CloudFront Distribution

Distribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      Enabled: true
      DefaultRootObject: index.html
      Aliases:
        - bonefishsoftware.com
        - www.bonefishsoftware.com
      ViewerCertificate:
        AcmCertificateArn: !Ref CertificateArn
        SslSupportMethod: sni-only
        MinimumProtocolVersion: TLSv1.2_2021
      HttpVersion: http2and3
      Origins:
        - Id: S3Origin
          DomainName: !GetAtt WebsiteBucket.RegionalDomainName
          S3OriginConfig:
            OriginAccessIdentity: ''
          OriginAccessControlId: !GetAtt OriginAccessControl.Id
      DefaultCacheBehavior:
        TargetOriginId: S3Origin
        ViewerProtocolPolicy: redirect-to-https
        CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6  # CachingOptimized
        Compress: true
      CustomErrorResponses:
        - ErrorCode: 403
          ResponseCode: 200
          ResponsePagePath: /index.html
          ErrorCachingMinTTL: 0
        - ErrorCode: 404
          ResponseCode: 200
          ResponsePagePath: /index.html
          ErrorCachingMinTTL: 0
      PriceClass: PriceClass_100
Enter fullscreen mode Exit fullscreen mode

Key configuration points

RegionalDomainName not DomainName

Always use WebsiteBucket.RegionalDomainName (e.g. bucket.s3.us-west-2.amazonaws.com) when configuring an S3 origin with OAC. Using the global DomainName can cause redirect loops.

PriceClass_100

CloudFront price classes control which edge locations serve your content. PriceClass_100 covers North America and Europe — the right choice for most US-based businesses. PriceClass_All includes Asia/Pacific/South America but costs more.

http2and3

Enables HTTP/2 and HTTP/3 (QUIC) — better performance for browsers that support it.


Route 53 DNS Setup

Since the domain is registered in Route 53, nameservers were updated via CLI:

aws route53domains update-domain-nameservers \
  --domain-name bonefishsoftware.com \
  --region us-east-1 \
  --nameservers \
    Name=ns-1325.awsdns-37.org \
    Name=ns-759.awsdns-30.net \
    Name=ns-1601.awsdns-08.co.uk \
    Name=ns-72.awsdns-09.com
Enter fullscreen mode Exit fullscreen mode

Two Alias records point the apex domain and www to CloudFront:

aws route53 change-resource-record-sets \
  --hosted-zone-id $ZONE_ID \
  --change-batch '{
    "Changes": [{
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "bonefishsoftware.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d39qxh6q0wxdkd.cloudfront.net",
          "EvaluateTargetHealth": false
        }
      }
    }]
  }'
Enter fullscreen mode Exit fullscreen mode

Z2FDTNDATAQYW2 is the fixed Hosted Zone ID for all CloudFront distributions — not specific to yours. Always use this value for CloudFront A alias records.


Deployment Order

The order matters because each stack depends on outputs from the previous:

1. Create Route 53 hosted zone
   → Get nameservers, update domain registrar

2. Deploy ACM certificate stack (us-east-1)
   → Add DNS validation CNAMEs to Route 53
   → Wait for ISSUED status (~2–5 min once DNS propagates)

3. Deploy website stack (us-west-2)
   → Pass CertificateArn as parameter
   → Outputs: BucketName, DistributionId, DistributionDomain

4. Deploy pipeline stack (us-west-2)
   → Pass BucketName + DistributionId from step 3
Enter fullscreen mode Exit fullscreen mode

Gradual Cutover Strategy

Rather than blocking on the ACM cert, the website.yml template supports deploying without a cert first:

Parameters:
  CertificateArn:
    Type: String
    Default: ''   # ← Leave blank for initial deploy

Conditions:
  HasCustomDomain: !And
    - !Not [!Equals [!Ref CertificateArn, '']]
    - !Not [!Equals [!Ref DomainName, '']]
Enter fullscreen mode Exit fullscreen mode

This lets you deploy the full CloudFront + S3 stack immediately, get the CloudFront URL (*.cloudfront.net), test it, and then update the stack with the cert ARN once it's issued — no downtime, no waiting.


SES DKIM Records

For outbound email from noreply@bonefishsoftware.com, three DKIM CNAME records were added to Route 53:

jbly6mlsiqbi6h7rnnfthdfkiaxzmvjn._domainkey.bonefishsoftware.com
  → jbly6mlsiqbi6h7rnnfthdfkiaxzmvjn.dkim.amazonses.com

xj7cx3tujwzexkmsxmfruyannxq3sevj._domainkey.bonefishsoftware.com
  → xj7cx3tujwzexkmsxmfruyannxq3sevj.dkim.amazonses.com

fyee6qf6o4fzqiolzxl2dyd4rjboqw4l._domainkey.bonefishsoftware.com
  → fyee6qf6o4fzqiolzxl2dyd4rjboqw4l.dkim.amazonses.com
Enter fullscreen mode Exit fullscreen mode

These allow SES to cryptographically sign outbound emails, which improves deliverability and prevents spoofing.

Top comments (0)