Publishing a Static Site With AWS


This static website is served from a CloudFront distribution with content stored in S3, and uses a Lambda@Edge function defined for rewriting URIs - why this is necessary and a guide on how to publish a static website using CloudFront, Lambda@Edge and S3 is outlined in this post.

Why serve content from S3 using CloudFront and Lambda@Edge vs. a S3 Website Enabled Bucket?

There are three main differences between serving content from S3 buckets with the S3 website feature enabled vs. CloudFront as discussed below.

S3 Website Enabled Bucket

Serving content from S3 buckets with the S3 website feature enabled provides common webserver functionality, such as default directory documents (i.e. automatically adding index.html as needed to directory paths ending in /) and HTTP permanent or temporary redirects (i.e. HTTP 301 and 302 response codes).
However, S3 websites do not support HTTPS and only support HTTP.

For example, an S3 Website enabled Bucket:

  • With S3 Website Index Document set to index.html

    • www.jeremyvincent.com => Returns HTTP 200 response with content from www.jeremyvincent.com/index.html
    • www.jeremyvincent.com/pages => Returns HTTP 200 response with content from www.jeremyvincent.com/pages/index.html
  • Redirects as S3 Object Metadata (See Redirect requests for an object for more information)

    • /path1.html with x-amz-website-redirect-location object metadata set to /path3.html => Returns HTTP 301 response redirecting to /path3.html
    • /path2.html with x-amz-website-redirect-location object metadata set to http://www.example.com => Returns HTTP 301 response redirecting to http://www.example.com
  • HTTP Only (No HTTPS)

CloudFront serving content from an S3 Bucket

Using CloudFront to serve content from S3 buckets that aren’t S3 website enabled doesn’t provide common webserver functionality, such as default directory documents (i.e. automatically adding index.html as needed to directory paths ending in /) nor HTTP permanent or temporary redirects (i.e. HTTP 301 and 302 response codes).
With CloudFront it is easy to both enable HTTPS support for the website and also redirect HTTP to HTTPS requests.

For example, CloudFront serving an S3 Bucket:

  • With CloudFront Default Root Object set to index.html

    • www.jeremyvincent.com => Returns HTTP 200 response with content from www.jeremyvincent.com/index.html
    • www.jeremyvincent.com/pages => Returns HTTP 404 error response for www.jeremyvincent.com/pages
  • Redirects are Ignored in S3 Object Metadata

    • /path1.html with x-amz-website-redirect-location object metadata set to /path3.html => Returns HTTP 200 response with content from /path1.html
    • /path2.html with x-amz-website-redirect-location object metadata set to http://www.example.com => Returns HTTP 200 response with content from /path2.html
  • HTTPS Supported

CloudFront and Lambda@Edge serving content from an S3 Bucket

However, using some simple Lambda@Edge functions you can support both default directory documents and HTTP temporary or permanent redirects easily.

Setup

Follow the below steps to configure Route 53, ACM, S3, CloudFront and Lambda@Edge to publish a static website:

  1. Use Route 53 for DNS
  2. Use ACM to create a Public Digital Certificate
  3. Use a S3 Bucket to host Website Content
  4. Use CloudFront to serve Website Content
  5. Use Lambda@Edge to serve Default Directory Documents

Use Route 53 for DNS

You need to have a DNS domain name such as jeremyvincent.com that will be used as the friendly name for your website.

  1. To create a public hosted zone for your DNS domain name (i.e. jeremyvincent.com) perform one of the following procedures in Route 53:

    • To register a domain name for your website (if you don’t already own a DNS domain name), follow Registering a new domain from the Route 53 documentation. This will also create a new public hosted zone in your AWS account that will be used to serve DNS information about your website.
    • Otherwise, create a new public hosted zone in your AWS account that will be used to serve DNS information about your website by following Creating a public hosted zone from the Route 53 documentation.

    I followed Configuring white-label name servers from the Route 53 documentation to create my public hosted zone with vanity/white-label name servers for the jeremyvincent.com domain by:

    1. Run aws route53 create-reusable-delegation-set --caller-reference 2020-09.05-jnv01 --region us-east-1 to create a reusable delegation set:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      
      jvincent$ aws route53 create-reusable-delegation-set --caller-reference 2020-09.05-jnv01 --region us-east-1
      {
        "Location": "https://route53.amazonaws.com/2013-04-01/delegationset/N05176881T8BF9ESBR4KL",
        "DelegationSet": {
          "Id": "/delegationset/N05176881T8BF9ESBR4KL",
          "CallerReference": "2020-09.05-jnv01",
          "NameServers": [
            "ns-1403.awsdns-47.org",
            "ns-785.awsdns-34.net",
            "ns-1721.awsdns-23.co.uk",
            "ns-88.awsdns-11.com"
          ]
        }
      }
      
      jvincent$ 

    2. Run aws route53 create-hosted-zone --name jeremyvincent.com. --caller-reference 2020-09.05-jnv01 --hosted-zone-config Comment="Public domain for jeremyvincent.com." --delegation-set-id "/delegationset/N05176881T8BF9ESBR4KL" --region us-east-1 to create the public hosted zone for the jeremyvincent.com domain.
      Use the DelegationSet Id from line 5 of the output of the aws route53 create-reusable-delegation-set command above as the value for the --delegation-set-id parameter:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      
      jvincent$ aws route53 create-hosted-zone --name jeremyvincent.com. --caller-reference 2020-09.05-jnv01 --hosted-zone-config Comment="Public domain for jeremyvincent.com." --delegation-set-id "/delegationset/N04321422UY3VNN5ORHSF" --region us-east-1
      {
        "Location": "https://route53.amazonaws.com/2013-04-01/hostedzone/Z08977741A5MAE6BBMBBV",
        "HostedZone": {
          "Id": "/hostedzone/Z08977741A5MAE6BBMBBV",
          "Name": "jeremyvincent.com.",
          "CallerReference": "2020-09.05-jnv01",
          "Config": {
            "Comment": "Public domain for jeremyvincent.com.",
            "PrivateZone": false
          },
          "ResourceRecordSetCount": 2
        },
        "ChangeInfo": {
          "Id": "/change/C097078738U5D1YWZBRN8",
          "Status": "PENDING",
          "SubmittedAt": "2020-09-05T05:47:57.268000+00:00"
        },
        "DelegationSet": {
          "Id": "/delegationset/N05176881T8BF9ESBR4KL",
          "CallerReference": "2020-09.05-jnv01",
          "NameServers": [
            "ns-1403.awsdns-47.org",
            "ns-785.awsdns-34.net",
            "ns-1721.awsdns-23.co.uk",
            "ns-88.awsdns-11.com"
          ]
        }
      }
      
      jvincent$ 
    3. Run dig A ns-1403.awsdns-47.org +short and dig AAAA ns-1403.awsdns-47.org +short for each of the four listed name servers to determine the IPv4 and IPv6 addresses that will be used to create the A and AAAA Host records for your white-label name servers:

      jvincent$ dig A ns-1403.awsdns-47.org +short
      205.251.197.123
      
      jvincent$ dig AAAA ns-1403.awsdns-47.org +short
      2600:9000:5305:7b00::1
      
      jvincent$ dig A ns-1721.awsdns-23.co.uk +short
      205.251.198.185
      
      jvincent$ dig AAAA ns-1721.awsdns-23.co.uk +short
      2600:9000:5306:b900::1
      
      jvincent$ dig A ns-785.awsdns-34.net +short
      205.251.195.17
      
      jvincent$ dig AAAA ns-785.awsdns-34.net +short
      2600:9000:5303:1100::1
      
      jvincent$ dig A ns-88.awsdns-11.com +short
      205.251.192.88
      
      jvincent$ dig AAAA ns-88.awsdns-11.com +short
      2600:9000:5300:5800::1
      
      jvincent$ 
    4. Create records for your white-label name servers by creating a Route 53 ChangeRecordSet file named new-ns-jeremyvincent.com.txt with the A and AAAA Host records of the four name servers.
      The ChangeRecordSet file for jeremyvincent.com is here.
      See How do I create a simple resource record set in Amazon Route 53 using the AWS CLI? for more information.

    5. Run aws route53 change-resource-record-sets --hosted-zone-id Z08977741A5MAE6BBMBBV --change-batch file://new-ns-jeremyvincent.com.txt to create your white-label name server records in the newly created public hosted zone.
      Use the HostedZone Id from line 5 of the output of the aws route53 create-hosted-zone command above as the value for the --hosted-zone-id parameter:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      jvincent$ aws route53 change-resource-record-sets --hosted-zone-id Z08977741A5MAE6BBMBBV --change-batch file://new-ns-jeremyvincent.com.txt
      {
        "ChangeInfo": {
          "Id": "/change/C013962521VQMHQB35WY6",
          "Status": "PENDING",
          "SubmittedAt": "2020-09-05T08:14:18.929000+00:00",
          "Comment": "Create R53 DNS records for vanity name servers for the jeremyvincent.com. domain in the jeremyvincent-prod account"
        }
      }
      
      jvincent$ 
    6. Run aws route53 get-change --id change/C013962521VQMHQB35WY6 to check the status of your name server record changes.
      Use the ChangeInfo Id from line 4 of the output of the aws route53 change-resource-record-sets command above as the value for the --id parameter:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      
      jvincent$ aws route53 get-change --id change/C013962521VQMHQB35WY6
      {
        "ChangeInfo": {
          "Id": "/change/C013962521VQMHQB35WY6",
          "Status": "INSYNC",
          "SubmittedAt": "2020-09-05T08:14:18.929000+00:00",
          "Comment": "Create R53 DNS records for vanity name servers for the jeremyvincent.com. domain in the jeremyvincent-prod account"
        }
      }
      
      jvincent$ 

      The DNS changes are propagated to all Route 53 DNS servers when the Status on line 5 from aws route53 get-change above returns INSYNC (typically this occurs within 60 seconds) and you can continue to the next step.
      If the Status shows PENDING then the changes are still propagating and you should wait before continuing.

    7. Update NS and SOA records for the jeremyvincent.com domain.

      • Update the SOA record by replacing the name of the Route 53 name server with the name of one of your white-label name servers.

        Replace ns-1403.awsdns-47.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400 with the name of one of your white-label name servers: ns1.jeremyvincent.com. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400.

      • Update the NS records by replacing the names of the current Route 53 name servers with the names of your four white-label name servers:

        ns-1403.awsdns-47.org.
        ns-1721.awsdns-23.co.uk.
        ns-785.awsdns-34.net.
        ns-88.awsdns-11.com.

        with

        ns1.jeremyvincent.com.
        ns2.jeremyvincent.com.
        ns3.jeremyvincent.com.
        ns4.jeremyvincent.com.

    8. Create glue records and change the registrar’s name servers for the jeremyvincent.com domain by following Adding or changing name servers and glue records for a domain.

    9. Validate your DNS changes by running dig SOA jeremyvincent.com. +short and dig NS jeremyvincent.com. +short to check the SOA and NS DNS records for the jeremyvincent.com domain:

      jvincent$ dig SOA jeremyvincent.com. +short
      ns1.jeremyvincent.com. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400
      
      jvincent$ dig NS jeremyvincent.com. +short
      ns1.jeremyvincent.com.
      ns2.jeremyvincent.com.
      ns3.jeremyvincent.com.
      ns4.jeremyvincent.com.
      
      jvincent$ 
  2. This section is complete once the newly created public hosted zone responds with the correct SOA and NS DNS records for the jeremyvincent.com domain.
    You will NOT be able to create your public digital certificate using AWS Certificate Manager (ACM) in the section below or configure your CloudFront Distribution in the later Use CloudFront to serve Website Content section until your public hosted zone has been successfully created.

Use ACM to create a Public Digital Certificate

To enable HTTPS for your website, you will need to have a public digital certificate issued for your domain name such as jeremyvincent.com.
AWS Certificate Manager (ACM) makes it easy to provision, manage, deploy, and renew SSL/TLS certificates on the AWS platform. See the ACM User Guide for more information.
Since ACM provides public digital certificates at no additional cost and integrates nicely with CloudFront, we will use it to provide our public digital certificate.

Follow the steps below to request a new public digital ceritificate from ACM:

  1. Run aws acm request-certificate --region us-east-1 --domain-name jeremyvincent.com --subject-alternative-names "www.jeremyvincent.com" "cdn.jeremyvincent.com" --options CertificateTransparencyLoggingPreference=ENABLED --validation-method DNS --idempotency-token 20201123jnv01 --tags Key="jv:purpose",Value="For the jeremyvincent.com CloudFront Web Distribution." Key="jv:owner",Value="jeremy.vincent@gmail.com" Key="jv:environment",Value="production" Key="Name",Value="jeremyvincent.com" to request a new public digital certificate for the jeremyvincent.com domain name, along with the www.jeremyvincent.com and cdn.jeremyvincent.com alternative domain names:

    IMPORTANT NOTE: We need to explicitly create our new public digital certificate with ACM in the us-east-1 region for it to be available for use by CloudFront; this is easily performed by including the --region us-east-1 AWS CLI option.

    1
    2
    3
    4
    5
    6
    
    jvincent$ aws acm request-certificate --region us-east-1 --domain-name jeremyvincent.com --subject-alternative-names "www.jeremyvincent.com" "cdn.jeremyvincent.com" --options CertificateTransparencyLoggingPreference=ENABLED --validation-method DNS --idempotency-token 20201123jnv01 --tags Key="jv:purpose",Value="For the jeremyvincent.com CloudFront Web Distribution." Key="jv:owner",Value="jeremy.vincent@gmail.com" Key="jv:environment",Value="production" Key="Name",Value="jeremyvincent.com"
    {
        "CertificateArn": "arn:aws:acm:us-east-1:418110697901:certificate/e09ad0d1-9890-4df2-a63d-0007c18604e0"
    }
    
    jvincent$ 
  2. Run aws acm describe-certificate --region us-east-1 --certificate-arn "arn:aws:acm:us-east-1:418110697901:certificate/e09ad0d1-9890-4df2-a63d-0007c18604e0" --no-paginate to view the details for the requested jeremyvincent.com ACM public digitial certificate.
    Use the CertificateArn from line 3 of the output of the aws acm request-certificate command above as the value for the --certificate-arn parameter:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    
    jvincent$ aws acm describe-certificate --region us-east-1 --certificate-arn arn:aws:acm:us-east-1:418110697901:certificate/e09ad0d1-9890-4df2-a63d-0007c18604e0 --no-paginate
    {
        "Certificate": {
            "CertificateArn": "arn:aws:acm:us-east-1:418110697901:certificate/e09ad0d1-9890-4df2-a63d-0007c18604e0",
            "DomainName": "jeremyvincent.com",
            "SubjectAlternativeNames": [
                "jeremyvincent.com",
                "www.jeremyvincent.com",
                "cdn.jeremyvincent.com"
            ],
            "DomainValidationOptions": [
                {
                    "DomainName": "jeremyvincent.com",
                    "ValidationDomain": "jeremyvincent.com",
                    "ValidationStatus": "PENDING_VALIDATION",
                    "ResourceRecord": {
                        "Name": "_c19dc234dd7c683e397588055c4bd9ea.jeremyvincent.com.",
                        "Type": "CNAME",
                        "Value": "_2f786c052ea0a4dede819fa161e05aa7.wggjkglgrm.acm-validations.aws."
                    },
                    "ValidationMethod": "DNS"
                },
                {
                    "DomainName": "www.jeremyvincent.com",
                    "ValidationDomain": "www.jeremyvincent.com",
                    "ValidationStatus": "PENDING_VALIDATION",
                    "ResourceRecord": {
                        "Name": "_83b2766e4e1a3d0ef0181a156dff84df.www.jeremyvincent.com.",
                        "Type": "CNAME",
                        "Value": "_319acc2836901aba6a358be7ae62011c.wggjkglgrm.acm-validations.aws."
                    },
                    "ValidationMethod": "DNS"
                },
                {
                    "DomainName": "cdn.jeremyvincent.com",
                    "ValidationDomain": "cdn.jeremyvincent.com",
                    "ValidationStatus": "PENDING_VALIDATION",
                    "ResourceRecord": {
                        "Name": "_9cf814487b9f5addf7b80063c9ca1706.cdn.jeremyvincent.com.",
                        "Type": "CNAME",
                        "Value": "_30fb7b4e608a32c699e2d32a4d0ab8f9.wggjkglgrm.acm-validations.aws."
                    },
                    "ValidationMethod": "DNS"
                }
            ],
            "Subject": "CN=jeremyvincent.com",
            "Issuer": "Amazon",
            "CreatedAt": "2020-11-23T16:57:34+13:00",
            "Status": "PENDING_VALIDATION",
            "KeyAlgorithm": "RSA-2048",
            "SignatureAlgorithm": "SHA256WITHRSA",
            "InUseBy": [],
            "Type": "AMAZON_ISSUED",
            "KeyUsages": [],
            "ExtendedKeyUsages": [],
            "RenewalEligibility": "INELIGIBLE",
            "Options": {
                "CertificateTransparencyLoggingPreference": "ENABLED"
            }
        }
    }
    
    jvincent$ 

    NOTE: The status of the new ACM certificate is listed as "Status": "PENDING_VALIDATION" on line 49 since we haven’t created the required DNS records in Route 53 for ACM to validate that we own the domain names listed in the public certificate request.
    Requests for ACM certificates time out if they are not validated within 72 hours.

  3. Create the required DNS validation records for your ACM public certificate by creating a Route 53 ChangeRecordSet file named new-acm-jeremyvincent.com.txt with CNAME records for each of the three ResourceRecord’s (see lines 16, 27 and 38 above) listed under the DomainValidationOptions section on line 11 above.
    The ChangeRecordSet file to enable DNS validation of the jeremyvincent.com ACM public certificate is here.
    See How do I create a simple resource record set in Amazon Route 53 using the AWS CLI? for more information.

  4. Run aws route53 change-resource-record-sets --hosted-zone-id Z08977741A5MAE6BBMBBV --change-batch file://new-acm-jeremyvincent.com.txt to create your DNS validation records for ACM in the public hosted zone that was newly created in the Use Route 53 for DNS section above.
    Use the HostedZone Id from line 5 of the output of the aws route53 create-hosted-zone command used earlier as the value for the --hosted-zone-id parameter:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    jvincent$ aws route53 change-resource-record-sets --hosted-zone-id Z08977741A5MAE6BBMBBV --change-batch file://new-acm-jeremyvincent.com.txt
    {
        "ChangeInfo": {
            "Id": "/change/C00843573TC2ZSVHM391J",
            "Status": "PENDING",
            "SubmittedAt": "2020-11-23T04:05:56.159000+00:00",
            "Comment": "Create R53 DNS records for DNS validation of the jeremyvincent.com ACM certificate for the jeremyvincent.com. domain in the jeremyvincent-prod account"
        }
    }
    
    jvincent$ 
  5. Run aws route53 get-change --id "/change/C00843573TC2ZSVHM391J" to view the status of our changes to the jeremyvincent.com domain.
    Use the ChangeInfo Id from line 4 of the output of the aws route53 change-resource-record-sets command above as the value for the --id parameter:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    jvincent$ aws route53 get-change --id "/change/C00843573TC2ZSVHM391J"
    {
        "ChangeInfo": {
            "Id": "/change/C00843573TC2ZSVHM391J",
            "Status": "INSYNC",
            "SubmittedAt": "2020-11-23T04:05:56.159000+00:00",
            "Comment": "Create R53 DNS records for DNS validation of the jeremyvincent.com ACM certificate for the jeremyvincent.com. domain in the jeremyvincent-prod account"
        }
    }
    
    jvincent$ 
    When aws route53 get-change returns a status of INSYNC then we know the changes have propagated to all Route 53 name servers and we can continue (typically this occurs within 60 seconds).

  6. Run aws acm describe-certificate --region us-east-1 --certificate-arn "arn:aws:acm:us-east-1:418110697901:certificate/e09ad0d1-9890-4df2-a63d-0007c18604e0" --no-paginate to view the details for the requested jeremyvincent.com ACM public digital certificate.
    Use the CertificateArn from line 3 of the output of the aws acm request-certificate command above as the value for the --certificate-arn parameter:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    
    jvincent$ aws acm describe-certificate --region us-east-1 --certificate-arn arn:aws:acm:us-east-1:418110697901:certificate/e09ad0d1-9890-4df2-a63d-0007c18604e0 --no-paginate
    {
        "Certificate": {
            "CertificateArn": "arn:aws:acm:us-east-1:418110697901:certificate/e09ad0d1-9890-4df2-a63d-0007c18604e0",
            "DomainName": "jeremyvincent.com",
            "SubjectAlternativeNames": [
                "jeremyvincent.com",
                "www.jeremyvincent.com",
                "cdn.jeremyvincent.com"
            ],
            "DomainValidationOptions": [
                {
                    "DomainName": "jeremyvincent.com",
                    "ValidationDomain": "jeremyvincent.com",
                    "ValidationStatus": "SUCCESS",
                    "ResourceRecord": {
                        "Name": "_c19dc234dd7c683e397588055c4bd9ea.jeremyvincent.com.",
                        "Type": "CNAME",
                        "Value": "_2f786c052ea0a4dede819fa161e05aa7.wggjkglgrm.acm-validations.aws."
                    },
                    "ValidationMethod": "DNS"
                },
                {
                    "DomainName": "www.jeremyvincent.com",
                    "ValidationDomain": "www.jeremyvincent.com",
                    "ValidationStatus": "SUCCESS",
                    "ResourceRecord": {
                        "Name": "_83b2766e4e1a3d0ef0181a156dff84df.www.jeremyvincent.com.",
                        "Type": "CNAME",
                        "Value": "_319acc2836901aba6a358be7ae62011c.wggjkglgrm.acm-validations.aws."
                    },
                    "ValidationMethod": "DNS"
                },
                {
                    "DomainName": "cdn.jeremyvincent.com",
                    "ValidationDomain": "cdn.jeremyvincent.com",
                    "ValidationStatus": "SUCCESS",
                    "ResourceRecord": {
                        "Name": "_9cf814487b9f5addf7b80063c9ca1706.cdn.jeremyvincent.com.",
                        "Type": "CNAME",
                        "Value": "_30fb7b4e608a32c699e2d32a4d0ab8f9.wggjkglgrm.acm-validations.aws."
                    },
                    "ValidationMethod": "DNS"
                }
            ],
            "Serial": "0b:35:1c:38:b9:0a:e7:24:21:1f:35:b7:84:7a:eb:5b",
            "Subject": "CN=jeremyvincent.com",
            "Issuer": "Amazon",
            "CreatedAt": "2020-11-23T16:57:34+13:00",
            "IssuedAt": "2020-11-23T17:06:27+13:00",
            "Status": "ISSUED",
            "NotBefore": "2020-11-23T13:00:00+13:00",
            "NotAfter": "2021-12-23T12:59:59+13:00",
            "KeyAlgorithm": "RSA-2048",
            "SignatureAlgorithm": "SHA256WITHRSA",
            "InUseBy": [],
            "Type": "AMAZON_ISSUED",
            "KeyUsages": [
                {
                    "Name": "DIGITAL_SIGNATURE"
                },
                {
                    "Name": "KEY_ENCIPHERMENT"
                }
            ],
            "ExtendedKeyUsages": [
                {
                    "Name": "TLS_WEB_SERVER_AUTHENTICATION",
                    "OID": "1.3.6.1.5.5.7.3.1"
                },
                {
                    "Name": "TLS_WEB_CLIENT_AUTHENTICATION",
                    "OID": "1.3.6.1.5.5.7.3.2"
                }
            ],
            "RenewalEligibility": "INELIGIBLE",
            "Options": {
                "CertificateTransparencyLoggingPreference": "ENABLED"
            }
        }
    }
    
    jvincent$ 
    When the Status on line 51 from aws acm describe-certificate above returns ISSUED then we know the requested ACM certificate has been succesfully validated and issued.

    NOTE: DNS validation of new ACM certificates typically occurs very quickly. However, some DNS providers can take 24–48 hours to propagate DNS records. ACM periodically checks for the required DNS validation record(s). This process can’t be manually checked.
    See Why is my AWS Certificate Manager (ACM) certificate DNS validation status still pending validation? for troubleshooting pending validation of your ACM certificate.

  7. This section is complete once the newly created ACM certificate has been succesfully issued.
    You will NOT be able to configure your CloudFront Distribution in the later Use CloudFront to serve Website Content section using this ACM certificate until it has been successfully issued.

Use a S3 Bucket to host Website Content

<To Do> Document S3 Bucket creation and permissions.

See the below links for more information:

Use CloudFront to serve Website Content

<To Do> Document CloudFront Distribution creation and permissions (including OAI).

See the below links for more information:

Use Lambda@Edge to serve Default Directory Documents

<To Do> Document Lambda@Edge function and CloudFront configuration.

See the below links for more information:

Updates

Follow the below steps to publish updates to your static website hosted on S3 and CloudFront:

  1. Generate Static Site Content using Hugo
  2. Publish to S3
  3. Update CloudFront Distribution

Generate Static Site Content using Hugo

Run the following commands from the macOS terminal to generate the updated static website content in the public/ subdirectory of the jvincent-website directory:

NOTE: Only content marked non-draft will be generated.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
jvincent$ cd jvincent-website
jvincent$ hugo   

Start building sites … 
hugo v0.93.1+extended darwin/amd64 BuildDate=unknown

                   | EN   
-------------------+------
  Pages            |  20  
  Paginator pages  |   0  
  Non-page files   |   0  
  Static files     | 192  
  Processed images |   0  
  Aliases          |   6  
  Sitemaps         |   1  
  Cleaned          |   0  

Total in 113 ms

jvincent$ 

Publish to S3

This will synchronise the contents of your local public/ directory with the main/ folder in the jeremyvincent-production-us-east-1-jvincent-website S3 bucket.

  1. Run aws s3 sync public/ s3://jeremyvincent-production-us-east-1-jvincent-website/main/ --size-only --exclude ".DS_Store" --exclude "*/.DS_Store" --delete --acl bucket-owner-full-control --dryrun to perform a “dry-run”; this verifies what changes would have been made to the S3 bucket but does NOT make any changes:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    jvincent$ aws s3 sync public/ s3://jeremyvincent-production-us-east-1-jvincent-website/main/ --size-only --exclude ".DS_Store" --exclude "*/.DS_Store" --delete --acl bucket-owner-full-control --dryrun
    
    (dryrun) upload: public/2020/04/my-first-post/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/2020/04/my-first-post/index.html
    (dryrun) upload: public/2020/09/publishing-a-static-site-with-aws/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/2020/09/publishing-a-static-site-with-aws/index.html
    (dryrun) upload: public/404.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/404.html
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/aws/index.html
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/aws/index.xml
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/aws/page/1/index.html
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/hugo/index.html
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/hugo/index.xml
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/hugo/page/1/index.html
    (dryrun) upload: public/categories/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/index.html
    (dryrun) upload: public/categories/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/index.xml
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/website/index.html
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/website/index.xml
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/website/page/1/index.html
    (dryrun) upload: public/favicon.ico to s3://jeremyvincent-production-us-east-1-jvincent-website/main/favicon.ico
    (dryrun) upload: public/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/index.html
    (dryrun) upload: public/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/index.xml
    (dryrun) upload: public/pages/about/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/pages/about/index.html
    (dryrun) upload: public/pages/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/pages/index.html
    (dryrun) upload: public/pages/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/pages/index.xml
    (dryrun) upload: public/posts/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/posts/index.html
    (dryrun) upload: public/posts/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/posts/index.xml
    (dryrun) upload: public/resources/publishing-a-static-site-with-aws/new-acm-jeremyvincent.com.txt to s3://jeremyvincent-production-us-east-1-jvincent-website/main/resources/publishing-a-static-site-with-aws/new-acm-jeremyvincent.com.txt
    (dryrun) upload: public/sitemap.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/sitemap.xml
    (dryrun) delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/syntax.css
    (dryrun) upload: public/tags/aws/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/aws/index.html
    (dryrun) upload: public/tags/aws/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/aws/index.xml
    (dryrun) upload: public/tags/aws/page/1/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/aws/page/1/index.html
    (dryrun) upload: public/tags/hugo/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/hugo/index.html
    (dryrun) upload: public/tags/hugo/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/hugo/index.xml
    (dryrun) upload: public/tags/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/index.html
    (dryrun) upload: public/tags/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/index.xml
    (dryrun) upload: public/tags/website/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/website/index.html
    (dryrun) upload: public/tags/website/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/website/index.xml
    
    jvincent$ 

    Review the changes that would have been made had the aws s3 sync command not been running in dry-run mode, to ensure they match with changes you have made to the website content.

  2. Run aws s3 sync public/ s3://jeremyvincent-production-us-east-1-jvincent-website/main/ --size-only --exclude ".DS_Store" --exclude "*/.DS_Store" --delete --acl bucket-owner-full-control to perform the LIVE update to the S3 bucket:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    jvincent$ aws s3 sync public/ s3://jeremyvincent-production-us-east-1-jvincent-website/main/ --size-only --exclude ".DS_Store" --exclude "*/.DS_Store" --delete --acl bucket-owner-full-control
    
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/hugo/index.xml
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/aws/page/1/index.html
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/aws/index.html
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/aws/index.xml
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/hugo/page/1/index.html
    upload: public/2020/04/my-first-post/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/2020/04/my-first-post/index.html
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/hugo/index.html
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/website/index.html
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/website/page/1/index.html
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/website/index.xml
    upload: public/404.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/404.html
    upload: public/categories/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/index.xml
    upload: public/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/index.xml
    upload: public/categories/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/categories/index.html
    upload: public/favicon.ico to s3://jeremyvincent-production-us-east-1-jvincent-website/main/favicon.ico
    upload: public/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/index.html
    upload: public/pages/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/pages/index.xml
    upload: public/posts/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/posts/index.xml
    upload: public/pages/about/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/pages/about/index.html
    delete: s3://jeremyvincent-production-us-east-1-jvincent-website/main/syntax.css
    upload: public/pages/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/pages/index.html
    upload: public/resources/publishing-a-static-site-with-aws/new-acm-jeremyvincent.com.txt to s3://jeremyvincent-production-us-east-1-jvincent-website/main/resources/publishing-a-static-site-with-aws/new-acm-jeremyvincent.com.txt
    upload: public/sitemap.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/sitemap.xml
    upload: public/posts/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/posts/index.html
    upload: public/tags/aws/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/aws/index.html
    upload: public/tags/aws/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/aws/index.xml
    upload: public/tags/aws/page/1/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/aws/page/1/index.html
    upload: public/tags/hugo/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/hugo/index.xml
    upload: public/tags/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/index.xml
    upload: public/tags/website/index.xml to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/website/index.xml
    upload: public/tags/hugo/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/hugo/index.html
    upload: public/tags/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/index.html
    upload: public/tags/website/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/tags/website/index.html
    upload: public/2020/09/publishing-a-static-site-with-aws/index.html to s3://jeremyvincent-production-us-east-1-jvincent-website/main/2020/09/publishing-a-static-site-with-aws/index.html
    
    jvincent$ 

Update CloudFront Distribution

NOTE: This is only required if you have configured your site content to be cached for a long period of time; either directly by setting cache-control headers on the uploaded S3 objects or via CloudFront caching behaviours.

This will invalidate the root page and various page listings on your CloudFront Distribution serving your static website. See Invalidating files for more information.

  1. Run aws cloudfront create-invalidation --distribution-id E8XLUV8WNKTOL --paths /index.html / "/pages/*" "/posts/*" "/tags/*" "/categories/*" "/2020/*" to invalidate the root page and page listings on your CloudFront Distribution:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    jvincent$ aws cloudfront create-invalidation --distribution-id E8XLUV8WNKTOL --paths /index.html / "/pages/*" "/posts/*" "/tags/*" "/categories/*" "/2020/*"
    
    {
        "Location": "https://cloudfront.amazonaws.com/2020-05-31/distribution/E8XLUV8WNKTOL/invalidation/I6MYTHAWI0YXG",
        "Invalidation": {
            "Id": "I6MYTHAWI0YXG",
            "Status": "InProgress",
            "CreateTime": "2022-03-05T04:10:53.809000+00:00",
            "InvalidationBatch": {
                "Paths": {
                    "Quantity": 6,
                    "Items": [
                        "/pages/*",
                        "/2020/*",
                        "/posts/*",
                        "/index.html",
                        "/tags/*",
                        "/"
                    ]
                },
                "CallerReference": "cli-1646453450-342376"
            }
        }
    }
    
    jvincent$ 

Once the invalidation is complete, all your updated website content will now be served via CloudFront.

NOTE: Web browsers may still show outdated site content due to Web browsers and/or corporate proxies caching your content at the time when the content was previously requested. This caching will be based on your object and/or CloudFront cache settings at the time of the request.

website  hugo  aws 

See also