Brian Westphal

How I built this site

May 10, 2022

I always wanted a blog where I could:

  1. Write with markdown
  2. Hit a button and have it deploy
  3. Maintaining full control over the backend without installing Wordpress

Here’s how I made this using Terraform, AWS, Cloudflare, and Gatsby.

1. Create vendor accounts

  • Buy a domain
  • Setup AWS account
  • Create cloudflare account

You’ll need the following API keys:

  • AWS Access Key
  • AWS Secret Key
  • Cloudflare API Key
  • Github API Key

2. Initialize Env Variables

Create a .env file. (Don’t forget to .gitignore!)

GITHUB_TOKEN = {FILL_IN_HERE}
DOMAIN = {FILL_IN_HERE}
AWS_ACCESS_KEY = {FILL_IN_HERE}
AWS_SECRET_KEY = {FILL_IN_HERE}
CLOUDFLARE_EMAIL = {FILL_IN_HERE}
CLOUDFLARE_API_KEY = {FILL_IN_HERE}
GITHUB_TOKEN = {FILL_IN_HERE}

3. Create a github repo

With a github token, terraform can spin up a repo.

provider "github" {
  token = var.GITHUB_TOKEN
}

resource "github_repository" "brianwestphal" {
  name        = "brianwestphal.com"
  description = "My personal blog"

  visibility = "private"
}

4. Ship it!

Run the following script to deploy. (This assumes you use direnv)

#!/bin/bash

terraform get
terraform init -upgrade

eval "$(direnv export bash)"

export TF_VAR_DOMAIN=$DOMAIN
export TF_VAR_AWS_ACCESS_KEY=$AWS_ACCESS_KEY
export TF_VAR_AWS_SECRET_KEY=$AWS_SECRET_KEY
export TF_VAR_CLOUDFLARE_EMAIL=$CLOUDFLARE_EMAIL
export TF_VAR_CLOUDFLARE_API_KEY=$CLOUDFLARE_API_KEY
export TF_VAR_GITHUB_TOKEN=$GITHUB_TOKEN

terraform refresh
terraform plan
terraform apply -auto-approve

5. Install Gatsby

https://www.gatsbyjs.com/docs/quick-start/

6. Create static s3 website

Write a blog post (this should be the fun part).

7. Write some Terraform

This TF code will spin up our domain, HTTPS routing as well as post the static files from Gatsby to s3 to be served at your domain.

Starting with setting up our terraform repository

locals {
  s3_bucket = var.DOMAIN
  domain    = var.DOMAIN
}

# This configures s3 as our remote backend so we can use terraform across
# any machine.
terraform {
  backend "s3" {
    bucket  = "{S3_BUCKET to store site data}"
    key     = "brianwestphal.com"
    region  = "us-east-1"
    encrypt = true
  }
}

Next we’ll generate the AWS resources.

provider "aws" {
  region     = "us-west-1"
  access_key = var.AWS_ACCESS_KEY
  secret_key = var.AWS_SECRET_KEY
}

resource "aws_s3_bucket" "static_site_bucket" {
  bucket = local.s3_bucket
  acl    = "public-read"
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::${local.domain}/*"
            ]
        }
    ]
}
EOF
  website {
    index_document = "index.html"
    error_document = "index.html"
  }
}

And push the site files to s3.

resource "null_resource" "build-frontend" {
  triggers = {
    always_run = timestamp()
  }

  provisioner "local-exec" {
    when        = create
    command     = "npm run build"
    working_dir = ".."
    interpreter = ["/bin/bash", "-c"]
  }
}

resource "null_resource" "upload_to_s3" {
  triggers = {
    always_run = timestamp()
  }

  provisioner "local-exec" {
    working_dir = ".."
    command     = "aws s3 cp --recursive public/ s3://${aws_s3_bucket.static_site_bucket.bucket} --exclude 'assets/*'"
  }
  depends_on = [null_resource.build-frontend]
}

Then create our domain with HTTPS

provider "cloudflare" {
  email   = var.CLOUDFLARE_EMAIL
  api_key = var.CLOUDFLARE_API_KEY
}

resource "cloudflare_zone" "main" {
  zone = local.domain
  lifecycle {
    prevent_destroy = true
  }
}

resource "cloudflare_record" "prod_root" {
  type    = "CNAME"
  name    = local.domain
  zone_id = cloudflare_zone.main.id
  value   = aws_s3_bucket.static_site_bucket.website_endpoint
  ttl     = 1
  proxied = true
}

resource "cloudflare_record" "www" {
  type    = "CNAME"
  name    = "www"
  zone_id = cloudflare_zone.main.id
  value   = local.domain
  ttl     = 1
  proxied = true
  lifecycle {
    prevent_destroy = true
  }
}

8. Ship it!

Run the same deploy script from above. Now your edits will be live. Happy blogging.