Finn Zink

Running this site for free on AWS

AWS offers two “easy” ways to host static sites out of the box. The first is to use S3, but static sites on S3 don’t support https. The other way is to use Amplify, however Amplify creates a bunch of resources on your behalf when you deploy. So while it is “easy” to setup, it is difficult to modify (or delete) your setup as there is a lot of hidden complexity behind the scenes.

Given my preferences for simple and self managed, I used to run this site on an EC2 instance running Nginx. However ever since AWS started charging for IPv4, this was running me $10+ a month.

CloudFront has a perpetual free tier of 10 million requests per month, so I decided to use it instead. Below are the steps I went through to set it up, note these were accurate when I wrote this but may have changed since:

AWS setup

  1. I purchased the domain on Route53 and created a hosted zone with default settings for the domain.

  2. Next, I created an S3 bucket to house the website’s content. Name it whatever, keep static website hosting disabled (doesn’t support https), keep block all public access on. Put a sample index.html in there for testing.

  3. Before we set up CloudFront, we need to make an ACM certificate for https to work. Go to ACM and request a (public) certificate. I entered *.finnzink.com, added another name to the certificate, and then added finnzink.com as well. Keep DNS validation selected and click “Request”. This should automatically add a CNAME record to the relevant Route53 domain for renewal.

  4. Next, we need to make a CloudFront distribution. Create a distribution, and choose the S3 bucket just created for the Origin domain. Select “Origin access control settings”. Click “Create new OAC” next to the “Select an origin access control” dropdown. Keep all the defaults and click “Create”.

  5. Select “Do not enable security protections” under WAF, unless you want to spend an extra $8 a month minimum. I selected “Use only North America and Europe” for Price class. Now, under “Alternate domain name”, add the URL of your site. I added both www.finnzink.com and finnzink.com as well. Additionally, make sure to put index.html as the default root object. Finally, click “Create distribution”.

  6. You should see a yellow warning bar that says “The S3 bucket policy needs to be updated”. Click “Copy policy”, Click “Go to S3 bucket permissions to update policy” which brings you to the “Permissions” section of the associated S3 bucket. Scroll down to “Bucket policy”, click “Edit”, and paste in the policy copied earlier. If you lose that policy, it looks like this:

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::BUCKET-NAME/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::ACCT-NUMBER:distribution/DISTRIBUTION-ID"
                }
            }
        }
    ]
}
  1. Now we need Route53 to point to our CloudFront distribution. Go to the hosted zone for the domain, click “Create record”. Type in “www” for the subdomain part of the record name. The default “A” record is fine. Click the “Alias” switch. Under the “Choose endpoint” dropdown, select “Alias to CloudFront distribution”. Then under “Choose distribution”, select the CloudFront distribution we created earlier. Click “Add another record”, and add an identical record except with the subdomain part of the record name blank. So in my case, I have two identical records for finnzink.com and www.finnzink.com, after which you can click “Create records”.

  2. After everything has updated and deployed, you should be able to go to your-domain.com and see the sample index.html from the beginning.

Super easy deployments

As this is a personal site and doesn’t involve complex collaboration, I wanted a super simple way to edit my files, make sure it looks good, then commit, push, and deploy to AWS all in one step. Rather than using Github Actions, which is slow and requires me to give Github keys to my AWS account, I use a simple shell script, a post commit hook, and the AWS CLI.

  1. First, I have a Git repo with Github as the origin which has the following structure:
    blog
    ├── README.md
    ├── deploy.sh
    └── static
        ├── blog-details.html
        ├── index.html
        ├── styles.css
        └── etc.
    
  2. The deploy.sh script looks like this:
    #!/bin/bash
    
    S3_BUCKET_NAME="BUCKET-NAME"
    DISTRIBUTION_ID="CLOUDFRONT-DIST-ID"
    
    aws s3 sync "./static" "s3://$S3_BUCKET_NAME/" --delete
    aws cloudfront create-invalidation --distribution-id "$DISTRIBUTION_ID" --paths "/*"
    
    echo "Deployment completed successfully."
    
  3. The .git/hooks/post-commit file contains:
    #!/bin/sh
    sh "./deploy.sh"
    
    Why not just put the deploy.sh directly into the hook? Because hooks are not tracked by git, so if I want do version control on the deploy script, it needs to be broken out.
  4. Finally I have this alias in my ~/.zshrc
    alias gitrn='git add . && git commit -m "Quick commit" && git push'
    

This is just meant to make it super easy to commit / push / deploy changes, obviously not meant for anything collaborative given the commit messages are unhelpful and it always pushes to main. This means when I run gitrn, I get something like this:

finnzink blog % gitrn
upload: static/styles.css to S3-URI/styles.css
upload: static/index.html to S3-URI/index.html
upload: static/blog-details.html to S3-URI/blog-details.html
{
    "Location": "https://cloudfront.amazonaws.com/2020-05-31/distribution/.../invalidation/...",
    "Invalidation": {
        "Id": "...",
        "Status": "InProgress",
        "CreateTime": "...",
        "InvalidationBatch": {
            "Paths": {
                "Quantity": 1,
                "Items": [
                    "/*"
                ]
            },
            "CallerReference": "..."
        }
    }
}
Deployment completed successfully.
[main ...] Quick commit
    3 files changed, 170 insertions(+), 1 deletion(-)
    create mode 100644 static/blog-details.html
Enumerating objects: 10, done.
Counting objects: 100% (10/10), done.
Delta compression using up to 8 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 3.41 KiB | 3.42 MiB/s, done.
Total 6 (delta 3), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
To https://github.com/finnzink/...
    ...  main -> main
finnzink blog %

The above takes care of syncing to Github, S3, and updating Cloudfront, which I find to be super convenient.

🫡