Well, it took a little longer than expected to get enough free time, but I managed to rebuild this site using CloudFormation. As a prerequisite, I rebuilt everything in my account (mostly just DNS stuff) using CloudFormation first, and put it all in a CodeCommit repository. The DNS zone for cloudformation.ninja is built using that new CloudFormation stack, and it exports the zone ID. When you look at the CloudFormation template below, be aware that that’s what the ZoneIdParameterName parameter is for - it’s the name of the parameter exported by the other CloudFormation stack, which will in turn contain the ID of the DNS zone we add our record to.

I made a few changes while writing my CloudFormation templates:

  • I changed from having separate IAM policies for the CodeBuild and CodePipeline roles, to just using inline policies. It’s a little simpler that way, and it’s unlikely that those policies would be reused for anything anyway.
  • I changed my S3 bucket name to <domain-name>-<account-number>[-replicated], to ensure no conflicts if it was created as e.g. example.org. I also created a bucket specifically for codepipeline artifacts, which the AWS did for us automatically before.
  • I actually ended up with two templates, because I wanted to enable multi-region replication of my S3 bucket. Unfortunately this ended up being useless; more on that later. CloudFormation can only create resources in the region in which it’s running, so if you want to create resources in a different region, you have to create a stack in that region using a separate template.

The templates:

So, let’s start with the first template, which just creates an S3 bucket and policy.

This template needs to exist first, as it’s referenced in the other template. It’s very simple, and doesn’t provide any output variables. All it does is create the S3 bucket we’ll replicate to, enable versioning and lifecycle policy (delete noncurrent versions after 2 days), and make it publicly readable. You’ll want to create this stack in a region other than the one where you create the second stack. In my case, I chose us-west-2 (Oregon).

The second template (static content template) does all of the heavy lifting. You will need to provide the following input parameters:

  • The hostname you’re going to end up using for the website. E.g. cloudformation.ninja
  • The name of the parameter from which to read the ID of the DNS zone to update with the DNS record for the previous item. If you’re not set up this way, it’s trivial to change it to accept the zone ID itself. Note that this stack does not create a DNS zone; you must already have a zone to use.
  • The name of the bucket created by the other template (by default <domain-name>-<account-number>-replicated).
  • The region the bucket was created in. This is used to construct the static website URL, which is passed to CloudFront.

When the stack is created, you’ll have a CodeCommit repository called simply <domain-name>. If you commit a Jekyll website to the “publish” branch, it will automatically be deployed to the S3 bucket specified in your buildspec.yml file in that repository.

Here’s mine:

version: 0.1

phases:

  install:
    commands:
      - apt-get update -y
      - gem install jekyll 
      - gem install jekyll-feed -v "~>0.9.2"

  build:
    commands:
      - jekyll build

  post_build:
    commands:
      - aws s3 sync _site s3://cloudformation.ninja-012345678901 --delete --exclude buildspec.yml

artifacts:
  files:
    - _site/**/*

Now, a few words on S3 replicated buckets and CloudFront. The whole reason I set up bucket replication for my website was to enable failover in case there was another S3 outage, like the one a few weeks ago. Obviously this site isn’t important, but I still like to do things “right” especially when the cost difference is negligible. But it turns out, it’s not currently possible to have redundant origins with CloudFront, when the origins are S3 buckets. I found this quite hard to believe, but I can’t think of a way. When searching around, I found a number of people who thought of more or less the same things I did, but ultimately came to the same conclusion: it’s not possible. The reason for this is that CloudFront itself doesn’t have a “fail-on-fault” option for origins. Once a behavior matches an origin, that origin is used, and if it fails that’s it. We could theoretically use DNS failover, and use something like mys3backend.cloudformation.ninja as our DNS name, but if we did that, only one bucket could have that name. We can’t have to CloudFront distributions with the same CNAME, either, so we can’t set two distributions up with one origin each, and use DNS to fail between them.

There is one option that kind-of works: Have two S3 buckets: cloudformation.ninja and cloudformation.ninja-primary. Use CloudFront to front-end the cloudformation.ninja-primary bucket. Then use DNS to fail between the CloudFront distribution and the cloudformation.ninja S3 bucket. Unfortunately, this solution would not support HTTPS, because S3 static websites don’t support HTTPS. So, this is a terrible solution, and for now I’ve opted to just not be redundant. I did still set up my CloudFront distribution with both origins, but the backup bucket would receive no traffic. If there’s another extended outage, I could just make the backup my default origin, but of course it would take a while to replicate. And again, it’s not like this is a critical website.