Matthew Ahrenstein bio photo

Matthew Ahrenstein

Security Focused SRE for an amazing company, hiker, amateur radio operator, target shooter, developer, and cryptocurrency enthusiast.

Author's Website Author's Twitter Author's LinkedIn Author's Github Author's Keybase.io Author's GPG Key

A few people have been asking how I maintain such a fast blog without running a database or dedicated web server. I use a variety of tools and services to host Route1337.com and I’ve decided that I’m going to share how I set it all up.

I’m going to assume some familiarity with the technologies below and focus on the specific setup of Route1337.com instead of a “how to” for each service.

Tools and services used:

I use the following services to host the site’s code and content:

  1. Amazon Web Services - S3
  2. Amazon Web Services - CloudFront
  3. Amazon Web Services - Route53
  4. GitHub Private Repository
  5. Jekyll
  6. GitHub Actions


Color coding used in this Article:

I’m using the following syntax highlighting in this article:

  1. Variables you need to change are in red.
  2. Field names you need to look for are in teal.
  3. Special instructions relating to variables are in orange.
  4. Variables you must copy exactly are in purple.
  5. Code blocks have a grey background and the color scheme is a language syntax instead of the above rules.

Step 1: Configuring AWS S3

S3 is wonderfully simple to configure. First we’ll start with creating the two buckets we need for our site.

  1. In your AWS account create buckets for example.com and www.example.com
  2. For the example.com bucket, turn on Static Website Hosting and Redirect all requests to another host name.
  3. Set the host name to www.example.com.
  4. For the www.example.com bucket, turn on Static Website Hosting and Enable website hosting.
  5. Set Index Document to index.html and set Error Document to 404.html
  6. Since the www.example.com bucket will contain our actual site we need to set a bucket policy on it. Set the below bucket policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Allow CloudFront to read from Bucket",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::www.example.com/*",
      "Condition": {
        "StringEqualsIgnoreCase": {
          "aws:UserAgent": "Amazon CloudFront"
        }
      }
    }
  ]
}

The above code only allows the “Amazon CloudFront” user agent to view our site. That is intentional since we want to avoid the duplicate content penalty.1


Step 2: Configuring AWS CloudFront

Despite being a CDN, CloudFront works well as a way to load balance a static website globally and is also the only way to get SSL on an S3 hosted static site.

  1. In your AWS account create a CloudFront Web distribution with the following settings:
    1. Origin Domain Name: www.example.com.s3-website-us-east-1.amazonaws.com2
    2. Origin Path: LEAVE BLANK
    3. Origin ID: You can make up a descriptive name here
    4. Origin SSL Protocol: TLSv1.2, TLSv1.1, TLSv1
    5. Origin Protocol Policy: HTTP Only
    6. Viewer Protocol Policy: Redirect HTTP to HTTPS
    7. Allowed HTTP Methods: GET, HEAD
    8. Object Caching: Use Origin Cache Headers
    9. Forward Cookies: None (Improves Caching)
    10. Forward Query Strings: No (Improves Caching)
    11. Smooth Streaming: No
    12. Restrict Viewer Access: No
    13. Compress Objects Automatically: No
    14. Price Class: This one is up to you. I use "Use Only US and Europe"
    15. AWS WAF Web ACL: Also up to you. I have it turned off
    16. Alternate Domain Names: www.example.com
    17. SSL Certificate: You can use the "Request an ACM certificate" button for this or use the awscli to upload your own
    18. Default Root Object: index.html
    19. Logging: Off
    20. Comment: Something descriptive for your site
    21. Distribution State: Enabled
  2. Now click “Create Distribution”
  3. Now perform the same steps above for the example.com bucket.


Step 3: Configuring AWS IAM permissions for use with GitHub Actions

IAM needs to be configured so GitHub can perform functions in S3 and CloudFront later. This is one of the simplest things to configure for this type of site deployment.

  1. Create an IAM user called github-actions and remember to save the IAM keys somewhere safe for later.
  2. Create an IAM policy called github-actions with the following policy text and assign it to the github-actions user:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1433953872000",
            "Effect": "Allow",
            "Action": [
                "cloudfront:CreateInvalidation",
                "cloudfront:GetInvalidation",
                "cloudfront:ListInvalidations"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject",
                "s3:GetBucketLocation",
                "s3:ListBucket"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::example.com",
                "arn:aws:s3:::www.example.com",
                "arn:aws:s3:::example.com/*",
                "arn:aws:s3:::www.example.com/*"
            ]
        }
    ]
}


Step 4: Configuring Route53

In order to serve CloudFront properly we will use Amazon’s Route53 DNS service. I’m going to assume you have Route53 configured as your DNS provider for example.com already.

  1. Create an Alias record for example.com and point it at the matching CloudFront CDN (NOT THE S3 BUCKET)
  2. Now create an Alias record for www.example.com and point it at the matching CloudFront CDN (NOT THE S3 BUCKET)


Step 5: Configuring GitHub

Here is how you can configure GitHub for automatic deployments.

  1. Move your Jekyll code into a folder called SourceCode in the root of the repo
  2. Create a folder in the repo root called BuildAdditions
  3. Create a JSON file in BuildAdditions called CloudFront-Invalidate.json with the following contents (USE YOUR ACTUAL DISTRIBUTION ID BUT LEAVE COMMIT_HASH EXACTLY AS SHOWN):
{
    "DistributionId": "YOURCLOUDFRONTID", 
    "InvalidationBatch": {
        "Paths": {
            "Quantity": 1, 
            "Items": [
                "/*"
            ]
        },
	"CallerReference": "COMMIT_HASH" 
    } 
}


Step 6: Configuring GitHub Actions

GitHub Actions is going to do the actual automated deployment once it is triggered by a merge or push to the main branch. The setup is pretty simple though.

  1. In your repository settings create the following under Secrets:
    1. Create AWS_ACCESS_KEY_ID with the AWS access key for the github-actions user.
    2. Create AWS_SECRET_ACCESS_KEY with the AWS secret key for the github-actions user.
  2. Create a folder in your repo called .github/workflows
  3. Inside the .github/workflows folder create a file called deploy.yml containing
name: 'Site Deploy'

on:
  push:
    branches:
      - main

jobs:
  deploy-site:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup Ruby
        uses: actions/setup-ruby@v1
        with:
          ruby-version: '2.4'
      - name: Install bundle command
        run: gem install bundler
      - name: Install ruby dependencies
        run: cd SourceCode && bundle install
      - name: Build the site via Jekyll
        run: cd SourceCode && bundle exec jekyll build
      - name: Deploy AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1
      - name: Sync site to S3
        run: aws s3 sync ./SourceCode/_site/ s3://www.example.com/ --delete
      - name: Set CloudFront COMMIT_HASH variable
        run: sed -i "s/COMMIT_HASH/${GITHUB_SHA}/g" ./BuildAdditions/CloudFront-Invalidate.json
      - name: Clear CloudFront Cache
        run: aws cloudfront create-invalidation --cli-input-json file://./BuildAdditions/CloudFront-Invalidate.json


The workflow behind all of this

The workflow behind what was done here is simple. You work on your Jekyll site via a development branch in GitHub. When you are ready to publish changes, merge it into the main branch. This triggers GitHub Actions to check out the repo, build with Jekyll, sync it to S3 and clear the CloudFront cache so the changes appear live. This way you only have to worry about your local text editor and GitHub.
BE WARNED: Make sure you don’t work off the main branch! Every commit or merge to the main branch will trigger a build.

  1. Thanks to Bryce Fisher-Fleig for that one 

  2. You need to use the S3 bucket’s Static Website Hosting Endpoint, not the bucket origin in the dropdown menu