Static Website on AWS S3 Using CFN

2019-06-02

In an earlier post, I described a DIY static site CMS builder. Hosting a small personal static website on a VPS may be overkill, and I have had difficulty finding a provider that offers a right balance between pricing and provided service. Another option is to use a CDN on a platform like Amazon Web Services (AWS) for hosting which may be cheaper and more scalable, and easier to maintain. Actually, some shared hosting providers use AWS under the hood, so some may be relying on AWS even if they are not directly using it.

AWS official documentations are great and have detailed guides for hosting a static site on their platforms using the console. It is definitely worth a read if starting out, but clicking through the console may not be the most reproducible or maintainable way of creating the resources. Thus this post streamlines the creation of the needed infrastructure on AWS with HTTPS support in a reproducible manner using only cloudformation (CFN) with AWS CLI (the only dependency), even though there are other wrappers available that are easier to use than CFN. As a side note, as reliable as any service may be, always have a backup plan in case the platform provider closes your account.

Deployment

The deployment described in this post is done in a two separate steps:

  1. Create a certificate for serving HTTPS.

    Besides the obvious reasons as to why one would want to use HTTPS, from my understanding, it has positive effect on the page ranking on certain search engines.

  2. Create the S3 Bucket (storage space) and cloudfront (CDN), and (optional) Route 53 recordsets (DNS).

This is broken up into two steps because, as of this writing, the certificate must be created in us-east-1 while the S3 bucket and cloudfront may be created in any region. Since cloudformation by itself does not seem to have a straightforward way (though there are ways) of creating resources in two different regions in the same stack, it is easier to break them up into two different stacks. These two stack templates can easily be merged into a single stack but with the caveat that all the resources would have to be created in the same AWS region.

Prerequisites

Certificate

Download my certificate manager CFN template. Navigate to the location of the template, then create the stack which will create the certificate:

# Note: region must be "us-east-1". Do not change.
aws cloudformation create-stack \
    --stack-name <cert-stack-name> \
    --region "us-east-1" \ # Do not change!
    --template-body file://./cert.yaml \
    --parameters ParameterKey=DomainName,ParameterValue="<sub.example.com>" \
    ParameterKey=VerificationDomain,ParameterValue="<example.com>" \
    ParameterKey=VerificationMethod,ParameterValue="{EMAIL,DNS}" # optional

where,

DomainName::ParameterValue
Domain name being used for hosting. E.g. example.com or sub.example.com. Do not use www. in DomainName value.
VerificationMethod::ParameterValue
optional key-value pair with valid options EMAIL(default) and DNS. If the value is set to EMAIL (default, easier, faster), then AWS will dispatch confirmation emails to
administrator@<VerificationDomain>
hostmaster@<VerificationDomain>
postmaster@<VerificationDomain>
webmaster@<VerificationDomain>
admin@<VerificationDomain>

The domain must be confirmed for the cloudformation stack creation to reach completion. Otherwise, if ignored for too long, the stack creation will timeout, fail, rollback, and delete the certificate.

Alternatively, if those email addresses are not accessible, set the ValidationMethod value to DNS and enter the required DNS records for your domain and wait for AWS to verify the entries.

VerificationDomain::ParameterValue
Domain name used for sending out emails, (if using VerificationMethod set to EMAIL). If domain name was set to a subdomain (e.g. sub.example.com), this value must be the root domain (e.g. example.com).
<cert-stack-name>
named assigned to the cloudformation stack.
--region value
value must be us-east-1 for the cloudfront certificate (as of this writing). Do not change.

Note: The AWS profile used to create the stack must have appropriate permissions for creating Cloudformation stacks, S3 buckets, Cloudfront distribution, and Certificate.

The status of the stack can be viewed using AWS console or using the aws cli:

aws --region <region-name> \
   cloudformation describe-stacks --stack-name <cert-stack-name>

In particular, what is needed for the next step is the Amazon Resource Name (ARN) of the generated certificate resource. This is outputted by the CFN template to the "Output" field. There are various ways of fetching this information:

Example of describe-stacks output:

{
  "Stacks": [
    ...
    "StackStatus": "CREATE_COMPLETE",
    "Outputs": [
      {
        "Description": "Certificate ARN",
        "OutputKey": "CertArn",
        "OutputValue": "arn:aws:acm:us-east-1:1234567890:certificate/aaaaaaaa-bbbb-cccc-dddd"
      }
    ],
  ...
 ]
}

Or perhaps hackingly grep just the ARN info from the stack info

aws --region <region-name> \
    cloudformation describe-stacks --stack-name <cert-stack-name> \
    | grep -B1 -C2 "CertArn"

Second stack: S3, Cloudfront, and Route53

Download the second template. This template will create S3 bucket(s), Cloudfront CDN, and Route 53 hosted zone and recordset (optional). Then create the stack with a profile with appropriate permissions:

aws  cloudformation create-stack \
    --stack-name <s3cf-stack-name> \
    --region "<AWS-region>" \
    --template-body file://./src/s3-cloudfront.yaml \
    --parameters ParameterKey=DomainName,ParameterValue="<sub.example.com>" \
    ParameterKey=S3BucketName,ParameterValue="<bucket-name>" \
    ParameterKey=CertificateArn,ParameterValue="<long-cert-ARN-string>" \
    ParameterKey=CreateRoute53,ParameterValue="{true,false}" \ # Optional
    ParameterKey=LogCloudfront,ParameterValue="{true,false}"   # Optional

where,

DomainName::ParameterValue
same value as the one defined in the previous step e.g. example.com excluding www.
<bucket-name>
globally unique S3 bucket name, e.g. com.example
<long-cert-ARN-string>
certificate ARN (used for cloudfront) obtained in the previous step. Use the certificate's ARN, and not the cloudfront's ARN.
<AWS-region>
AWS region where the resources are created, e.g. us-west-2
<s3cf-stack-name>
name assigned to this stack
CreateRoute53 (Optional)
(defaults to false) parameter used to specify whether a Route53 hosted zone along with A ALIAS record will be created pointing to the cloudfront URL. This will result in an automatic $0.50/month charge if left for more than 12 hours.
LogCloudfront (Optional
Can be true or false (default). If true, will create a bucket named <sbucket-name>-accesslog for cloudfront logs. Note: cloudfront console provides some logging even if this is set to false.

After running it, if there are no issues, you can go and grab a cup of coffee since creation of the resources in the second stack may take a while (e.g. 20 minutes) to complete. I have had cases where it took over 25 minutes to complete.

Finishing up

Once the last stack is created, get the cloudfront URL using either the AWS web console or using the describe-stacks --stack-name <s3cf-stack-name> approach highlighted above.

If using a subdomain, you can just add a CNAME entry to your DNS configuration list:

<subdomain> CNAME <cloudfront-url>

Alternative, if using the root domain and using Google domains, a few entries may be added to the DNS records:

Alternatively if the DNS provider does not support `ALIAS` records or their equivalent, and you are using root domain, you may have to use AWS DNS (Route53). Refer to CreateRoute53 option above.

The AWS cloudfront website URL that should be redirected to in DNS is in the output fields with key CloudFrontURL:

aws --region <region-name> \
    cloudformation describe-stacks --stack-name <s3cf-stack-name>

Associated Costs

Depending on your exact use case and traffic, total associated cost can be as low as less than $1/month:

Undoing the deployment

Undoing is straightforward. Delete both stacks:

  1. Delete the certificate stack
  2. aws --region us-east-1 \ # must have been in `us-east-1`
        delete-stack --stack-name <cert-stack-name>
    
  3. If the created S3 bucket(s) (see S3BucketName and possibly <sbucket-name>-accesslog depending on selection options above) is or are non-empty, manually empty the S3 bucket(s). Stack deleting will fail if the bucket(s) are not empty.
  4. aws --region <aws-region> \
        s3 rm s3://<S3BucketName> --recursive
    
  5. Delete the S3 + Cloudfront stack
  6. aws --region <stack-region> \
        delete-stack --stack-name <s3cf-stack-name>
    

    <cert-stack-name> and <s3cf-stack-name> are the stack names used when creating the stacks (see previous section). A list of stacks in a given region using can be fetched with

    aws cloudformation list-stacks
    

    Note: the S3 bucket(s) are configured to be delete on stack deletion, i.e. the buckets will be deleted during stack deletion. If the buckets are not empty, this step will fail. See the previous step.

Updating the Stacks

While new resources cannot be added to an existing stacks, some components of the stack a may be updated using the update-stack API.

Pushing Content to AWS

So far the focus has been on creating the initial infrastructure on AWS. The contents of the local build directory can be synced with the S3 storage bucket:

aws s3 sync /path/to/local-build-dir s3://<bucket-name> \
  --delete \
  --acl public-read

The --delete flag is useful to remove files on the remote storage that no longer exist on the local directory. Without the --acl flag, the updated files may lose their publicly readable value and may become inaccessible.

The created website, using the templates expects two key documents:

/index (no extension)
Main index page
/404 (no extension)
Default 404 error page

If you're not happy with these naming convention, it's trivial to change them in the template.

A few notes:

Tips

A few additional notes or tips to consider

Resources

Official cloudformations used for creating the templates