April 26, 2020

Initial commit

Why even start a tech blog? Pretty simple.

I need an excuse to practice and ramp up my communication skills. Parallel to that I’m also trying to give more presentations. Maintaining a blog requires you to formalize and package ideas so they can be consumed by others.

It’s also very handy whey I do give presentations to have a platform I can use for sharing material.

So what is this blog about? There’s a recurring theme in things that interest me: they usually are technical subjects that have depth, complexity, and usually no clear-cut answer. Others will call them rabbit holes. I think they’re quaint and loveable time (or money) sinks, so we’ll settle on bunny holes.

Glenda, the Plan 9 bunny

Far from being a rabbit hole

So first bunny hole, how is this blog hosted?

I decided to avoid going down the Kubernetes route since it’s grossly overkill. (I keep Kubernetes for much more inappropriate uses, such as running my 3D printer.) Static content is much more economical and practical to deploy. It’s also very easily moved from one provider to another.

I went mostly for the approach this article suggests: put your stuff on S3, front that with some CDN.

The article are authored in Markdown. It’s still my language of choice for prose for how simple it is to parse, maintain, and store. The files are versioned controled in Git, as all things should, and post-processed with Hugo.

Why Hugo? No particular reason. It uses Go’s templating system which has a mixed bag of a reputation. That said, Hugo has a large user base, good support resources and I already knew it well enough. I picked it because I knew it wouldn’t get in the way, and it’s an easily replaced piece of the puzzle. There are plenty of Markdown post-processors out there.

You might notice the use of Openring on the index page. That’s included as a partial I bake in at compile-time with a script.

#!/usr/bin/env bash
set -o errexit \
    -o pipefail \
    -o nounset

read -ra feeds <<< "$(awk 'NF {print "-s "$1 }' webring.txt | tr "\n" ' ')"

openring \
  "${feeds[@]}" \
  < ci/assets/webring-template.html \
  > layouts/partials/webring.html

When code gets pushed to git.sr.ht, it triggers a build for every commit on the master branch. Those who know me will know I’m a vocal proponent of Concourse as a solidly designed build system. I’ve been tinkering with builds.sr.ht for a few months now and it gets the job done and I like the idea of supporting SourceHut… but it’s got a long way to go. I’ll properly complain about build systems in another post.

The build uses Hugo’s deploy function to push to some AWS S3 bucket, and triggers selective cache invalidation on CloudFront. CloudFlare‘s DNS points to the CloudFront distribution with a CNAME record. The CloudFront distribution has an AWS ACM SSL certificate strapped to it. Horray, you all get “secure” bits!

All of this is defined as code with Terraform.

# [...]

# This is abridged and is missing a bunch of things, but you get the picture.

# Let's declare a bucket
resource "aws_s3_bucket" "bunny_bucket" {
  bucket = "bunnyhole.org"
  acl    = "private"

  tags = {
    Name        = "bunnyhole.org"
    Environment = "production"
  }
}

# I've ellipsed the part where I declare the roles and policies to allow the 
# CloudFlare distribution to pull from a private bucket as it's very verbose.
# Check out the ITRocks guide. If I'm smart enough to figure this out, you are
# too.

# Let's get a domain validated certificate
resource "aws_acm_certificate" "bunnyhole_org" {
  provider = aws.us_east_1
  domain_name       = "bunnyhole.org"
  validation_method = "DNS"

  subject_alternative_names = [ "blog.bunnyhole.org", "cdn.bunnyhole.org" ]

  tags = {
    Name = "bunnyhole.org"
    Environment = "production"
  }
}

# Let's set our DNS record
resource "cloudflare_record" "bunnyhole_org" {
  zone_id = local.cloudflare_zone
  name    = "bunnyhole.org"
  value   = aws_cloudfront_distribution.bunnyhole_dist.domain_name
  type    = "CNAME"
  ttl     = 3600
}

# Populate our CDN distribution
resource "aws_cloudfront_distribution" "bunnyhole_dist" {
  origin {
    domain_name = aws_s3_bucket.bunny_bucket.bucket_regional_domain_name
    origin_id   = local.s3_origin_id
  }

  enabled             = true
  is_ipv6_enabled     = true
  default_root_object = "index.html"

  price_class = "PriceClass_100" # enable cheap mode

  aliases = concat(
    tolist([aws_acm_certificate.bunnyhole_org.domain_name]),
    aws_acm_certificate.bunnyhole_org.subject_alternative_names
  )

  default_cache_behavior {
    allowed_methods = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = local.s3_origin_id

    compress = true

    lambda_function_association {
      event_type = "origin-request"
      lambda_arn = data.aws_lambda_function.redirect_lambda.qualified_arn
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  viewer_certificate {
    acm_certificate_arn = aws_acm_certificate.bunnyhole_org.arn
    ssl_support_method = "sni-only"
  }
}

# ACM requires TXT records for domain validation, it's just incredibly
# convenient to have those set automatically across two providers. :)
resource "cloudflare_record" "cert_validation" {
  count = length(aws_acm_certificate.bunnyhole_org.domain_validation_options)

  zone_id = local.cloudflare_zone
  name    = aws_acm_certificate.bunnyhole_org.domain_validation_options[count.index].resource_record_name
  type    = aws_acm_certificate.bunnyhole_org.domain_validation_options[count.index].resource_record_type
  value   = aws_acm_certificate.bunnyhole_org.domain_validation_options[count.index].resource_record_value
  ttl     = 3600
}

# [...]

A few bunnyhole questions

Why not simply use Route53 to point to CloudFront?

There’s just something about putting all my eggs in the same basket that really rubs me the wrong way. I believe there’s value to be gained in making sure you understand how the basic technologies work, and maintaining the relationships between non-integrated services sure is good practice.

That said, a smarter setup would have multiple DNS and CDN providers, but that’s another bunny hole for another day.

Why go through the hassle of using Terraform?

I’ll put it out there, I don’t like Terraform very much. On the other hand, I also believe it’s the least worst tool for doing infrastructure as code out there. (I’m looking at you SparkleFormation! Even as a Ruby fanboy I can’t get to like the thing!)

The use of Terraform is really a gift to future-me. What makes a lot of sense to me today might not four months for now. My Terraform setup declares relationships between all my service providers, and makes direct use of my secret store. It allows future-me to make a small change to a part of the setup without having to re-grok the whole thing. While I do believe in good documentation, I also believe that intrinsically clear code can have great informational value. Documentation is a good bunny hole too… and it runs very, very, very deep.

© Alexis Vanier 2020