Modular Infrastructure with Terraform

May 17, 2018

I hate doing repetitive, finicky tasks. No matter how careful I am, something, sooner or later, always gets missed. Ansible scratched a particular itch for me and I’ve relied on it ever since for system rebuilds. I’ve recently discovered Terraform and I think I’m in love.

As with much of my use of infrastructure as code (IAC) technology, it seems like my use case is a bit to the left of where the designers figured most people would be. After some trial and error I’ve come up with a modular system that works well enough for my purposes so I wanted to get a post put together sharing what I’ve settled on.

There are as many resources available for using Terraform as there are use cases, so I’m not going to spend much time talking about how to configure a cloud instance for a specific purpose; I’m laser focused on making it easy for you to pivot on a dime and provision/destroy/change cloud instances with as little drama as possible. I’ve included a few of the better resources I found towards the end of the post.

This is a work in progress and will almost certainly change over time.

Pre-Flight Checklist

  1. Install Terraform.
  2. Install Azure CLI.

Here Be Dragons

Terraform works better than you’d expect in many ways, but I’ve had a few surprises along the way. Here’s a short list:

High-Level Terraform Workflow

  1. Build environment-specific configuration.
  2. Select modules.
  3. Run terraform init to download the relevant providers.
  4. Run terraform plan -out=planfile to create a plan.
  5. Run terraform apply to apply it.
  6. Run terraform destroy to tear things down.

Directory Structure

I’ve tried to keep the directory structure as simple as possible. Terraform will process any files ending in .tf, so you can complicate things into many tiny files if you feel the need.

terraform/
├── terraform.tfvars
├── environments/
│   ├── env1/
│   │   ├── main.tf
│   │   └── providers.tf
│   └── env2/
│       ├── main.tf
│       └── providers.tf
└── modules/
    ├── module1/
    │   ├── main.tf
    |   ├── outputs.tf
    │   └── variables.tf
    └── module2/
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

Building a Module

Since we’re not really concerned with what the module is, we’re just going to use Microsoft’s Azure configuration and make a few minor changes.

Variables

The magic of modules is the variables you pass into them. Keep that in mind as you build your module; anything you’d like to be able to change on the fly should probably be a variable.

Any variables you define should live in variables.tf and look something like this:

variable "my_variable_name" {
  description = "This variable does something"
}

Consider the following:

variable "azure_location" {
  default = "westus"
  description = "This sets the location for the instance"
}

variable "ssh_key" {
  description = "SSH public key for remote administration"
}

variable "admin_username" {
  description = "Username for the administrator"
}

variable "management_ip" {
  description = "IP address that you'll be managing from"
}

Three of the above (ssh_key, admin_username, management_ip) relate to the administration of the system itself, so the only thing I can really change here is which Azure region I deploy my VM to. For demonstration purposes, this is sufficient.

Interpolation

Interpolation refers to how variables are inserted in a Terraform configuration.

# Before
resource "azurerm_resource_group" "rg" {
  name     = "pancakes"
  location = "westus"
}

# After
resource "azurerm_resource_group" "rg" {
  name     = "${local.module_name}"
  location = "${var.azure_location}"
}

One of those things is not like the other. What’s a local?

Locals

Locals allow you to specify a value once and reuse it multiple times throughout the configuration. This is known as a variable in most other programming languages. Don’t think about this too much.

My main use for it has been to add a prefix to most of my configuration items so I know who they belong to.

Here’s how you declare a local:

locals {
  module_name = "http-redir"
}

Building an Environment Configuration

This is the easy part. All you’re really doing here is selecting modules and passing the appropriate values to them.

Here’s an example:

module "<module name>" {
  source         = "../../modules/<module name>"
  version        = "0.1"
  azure_location = "<region>"
  ssh_key        = "ssh-rsa AAAA..."
  admin_username = "<username>"
  management_ip  = "<ip here>/32"
}

output "<resource name>" {
  value = "${module.<resource name>.ip_address}"
}

You’re now good to roll some infrastructure! Make sure you specify the root-level .tfvars file using -var-file="..\..\terraform.tfvars".

References