Setting up my blog has always been easy. Previous to today, you just had to:

  1. Provision an Ubuntu 18 VM somewhere (I’ve been using Digital Ocean)
  2. Install Apache and remove its default website
  3. Use Hugo to generate the site’s static files and copy them to the VM host
  4. Done!

But, never being one to sit still, I’ve decided to up my game and move to automatic provisioning with Terraform and Ansible.

Why? Because it’s fucking cool, that’s why!

Here’s my Terraform provisioning script. It will spin up two hosts in Digital Ocean, apply a firewall policy, and then setup a load balancer in front of them to distribute traffic across them.

# Set the variable value in *.tfvars file
# or using -var="do_token=..." CLI option
variable "do_token" {}

# Configure the DigitalOcean Provider
provider "digitalocean" {
  token = "${var.do_token}"
}

# Create web servers
resource "digitalocean_droplet" "server1" {
  image    = "ubuntu-18-04-x64"
  name     = "blog-server1"
  tags     = ["blog"]
  region   = "sfo2"
  size     = "s-1vcpu-1gb"
  ssh_keys = ["762573"]
}

resource "digitalocean_droplet" "server2" {
  image    = "ubuntu-18-04-x64"
  name     = "blog-server2"
  tags     = ["blog"]
  region   = "sfo2"
  size     = "s-1vcpu-1gb"
  ssh_keys = ["762573"]
}

# Create firewall to control network traffic
resource "digitalocean_firewall" "fw" {
  name = "fw-all-the-things-except-some-of-them"

  droplet_ids = ["${digitalocean_droplet.server1.id}",
                 "${digitalocean_droplet.server2.id}"]

  inbound_rule {
      protocol           = "tcp"
      port_range         = "22"
      source_addresses   = ["0.0.0.0/0", "::/0"]
  }

  inbound_rule {
      protocol           = "tcp"
      port_range         = "80"
      source_addresses   = ["0.0.0.0/0", "::/0"]
  }

  inbound_rule {
      protocol           = "icmp"
      source_addresses   = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
      protocol                = "tcp"
      port_range              = "80"
      destination_addresses   = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
      protocol                = "tcp"
      port_range              = "53"
      destination_addresses   = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
      protocol                = "udp"
      port_range              = "53"
      destination_addresses   = ["0.0.0.0/0", "::/0"]
  }

  outbound_rule {
      protocol                = "icmp"
      destination_addresses   = ["0.0.0.0/0", "::/0"]
  }
}

# Create load balancer to spread traffic across web servers
resource "digitalocean_loadbalancer" "lb" {
  name = "blog-lb"
  region = "sfo2"

  forwarding_rule {
    entry_port      = 80
    entry_protocol  = "http"

    target_port     = 80
    target_protocol = "http"
  }

  healthcheck {
    port     = 80
    protocol = "http"
    path     = "/"
  }

  droplet_ids = ["${digitalocean_droplet.server1.id}",
                 "${digitalocean_droplet.server2.id}"]
}

# Attach resources to project
resource "digitalocean_project" "blog_project" {
  name        = "My Blog"
  description = "www.jamessimas.com"
  purpose     = "Website or blog"
  environment = "Production"
  resources   = ["${digitalocean_droplet.server1.urn}",
                 "${digitalocean_droplet.server2.urn}",
                 "${digitalocean_loadbalancer.lb.urn}"]
}

Prepping the hosts for Hugo can be done with the following Ansible playbook:

# This playbook configures an Ubuntu VM:
#   - Installs Python dependencies so that Ansible works
#   - Installs Apache and other dependencies

---
- hosts: apache
  remote_user: root
  gather_facts: False
  pre_tasks:
    - name: Install python2
      raw: apt-get update && apt -y install python-simplejson
  tags: python

- hosts: apache
  remote_user: root
  gather_facts: True
  tags: apache
  tasks:
    - name: Install packages via apt
      apt:
        name: "{{ packages }}"
      vars:
        packages:
          - apache2

    - name: Remove Apache default website files
      file:
        path: /var/www/html/
        state: absent

    - name: Create an empty directory
      file:
        path: /var/www/html/
        state: directory

    - name: Setup regular security updates
      copy:
        src: update-ubuntu
        dest: /etc/cron.weekly/update-ubuntu
        owner: root
        group: root
        mode: '0774'

I can now tear down and re-provision my entire blog within a few minutes using this orchestration.

If you’re curious how I did it, check out the source code here.