Matthew Ahrenstein bio photo

Matthew Ahrenstein

Enterprise Infrastructure Lead for an amazing company, hiker, avid shooter, and inventor.

Google+ Twitter YouTube LinkedIn Github GPG Key

After moving from Wordpress to a static site, a few of my friends asked me how I update the site without writing a lot of code and where I host it. I use a variety of tools and services to host Ahrenstein.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 Ahrenstein.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. Jenkins


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 Jenkins

IAM needs to be configured so Jenkins 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 jenkins and remember to save the IAM keys somewhere safe for later.
  2. Create an IAM policy called jenkins with the following policy text and assign it to the jenkins 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

I personally prefer GitHub, but GitLab and others work well for this. Here we will configure GitHub for automated deployments.

  1. Configure your repo’s Jenkins (GitHub plugin) webhook to point to https://jenkins.example.com/github-webhook/
  2. Move your Jekyll code into a folder called SourceCode in the root of the repo
  3. Create a folder in the repo root called BuildAdditions
  4. Create a JSON file in BuildAdditions called CloudFront-Invalidate.json with the following contents (USE YOUR ACTUAL DISTRIBUTION ID BUT LEAVE BUILD_ID EXACTLY AS SHOWN):
{
    "DistributionId": "YOURCLOUDFRONTID", 
    "InvalidationBatch": {
        "Paths": {
            "Quantity": 1, 
            "Items": [
                "/*"
            ]
        },
	"CallerReference": "BUILD_ID" 
    } 
}


Step 6: Configuring Jenkins

Jenkins is going to do the actual automated deployment once it is triggered by GitHub. The setup is pretty simple though.

  1. Make sure the correct version of Jekyll is installed on your Jenkins server with all the required plugins for your theme.
  2. Install the GitHub plugin plugin.
  3. SSH into your Jenkins server and install the awscli tools via pip.
  4. As the jenkins user
    1. Enable the CloudFront Preview in AWS CLI3
    2. Configure the AWS command line tools with the IAM keys for the jenkins IAM user from Step 3 above.
  5. Create a Jenkins job with the following settings:
    1. Project Name: Deploy Example.com
    2. Description: Deploys Example.com
    3. GitHub project: https://github.com/user/Example/
    4. Source Code Management:
      1. Git
      2. Repository URL: https://github.com/user/Example/
      3. Credentials: Give Jenkins a GitHub account and grant it access to your repo
      4. Branches to build: */master
      5. Repository browser: githubweb
      6. URL: https://github.com/user/Example/
    5. Build Triggers: Build when a change is pushed to GitHub
    6. Execute Shell:
echo "Building the Jekyll site..."
cd "$WORKSPACE/SourceCode/"; bundle exec jekyll build
echo "Syncing _site with S3..."
aws s3 sync $WORKSPACE/SourceCode/_site/ s3://www.example.com/ --delete --profile jenkins --region us-east-1
echo "Invalidating CloudFront cache..."
sed -i "s/BUILD_ID/$BUILD_ID/g" $WORKSPACE/BuildAdditions/CloudFront-Invalidate.json
aws cloudfront create-invalidation --cli-input-json file://$WORKSPACE/BuildAdditions/CloudFront-Invalidate.json --profile jenkins
echo "Cache invalidation command sent. This can take some time to complete on CloudFront's end..."


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 master branch. This triggers Jenkins 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 master branch! Every commit or merge to the master branch will trigger a Jenkins 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

  3. Enabling CloudFront in AWS cli