Modular Infrastructure with Terraform

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 many resources available for general Terraform usage, so I'm going to focus on my use cases: rapid deploy, modification, and redeploy of cloud infrastructure.

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 to preview your changes.
  5. Run terraform apply to apply changes.
  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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
terraform/
├── environments/
│   ├── env1/
│   │   └── main.tf
│   └── env2/
│       └── main.tf
└── modules/
    ├── azure/
    │   ├── compute/
    |   |   └── t2.micro-ubuntu-16.04/
    |   |       ├── main.tf
    |   |       ├── outputs.tf
    |   |       └── variables.tf
    |   ├── network/
    |   |   ├── inbound-80-443/
    |   |   |   ├── main.tf
    |   |   |   ├── outputs.tf
    |   |   |   └── variables.tf
    |   |   └── inbound-53/
    |   |       ├── main.tf
    |   |       ├── outputs.tf
    |   |       └── variables.tf
    |   └── ssh/
    |       ...
    └── digitalocean/
        ...

Authentication

Terraform for DevOps relies on a lot of automated provisioning, so it makes sense to have the API keys for all this infrastructure available on disk somewhere. I like to avoid having that stuff laying around, so I make use of the environment variable option if it's available:

1
2
3
4
# The leading whitespace keeps it out of .bash_history
 export AWS_ACCESS_KEY_ID="<ACCESS KEY>"
 export AWS_SECRET_ACCESS_KEY="<SECRET KEY>"
 export AWS_DEFAULT_REGION="us-west-2"

Building a Module

For this blog post we're just going to use Microsoft's Azure configuration module and make a few minor changes.

When building my own, I like to keep things pretty basic. I define configuration for compute, network, and ssh. I can then pick choose the pieces I like and change them on the fly. Updating the firewall rules just means sourcing a different module.

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:

1
2
3
variable "my_variable_name" {
  description = "This variable does something"
}

Consider the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Before
resource "azurerm_resource_group" "default" {
  name     = "pancakes"
  location = "westus"
}

# After
resource "azurerm_resource_group" "default" {
  name     = "${var.vm_name}"
  location = "${var.azure_location}"
}

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module "<MODULE NAME>" {
  source         = "../../modules/<MODULE NAME>"
  azure_location = "<REGION>"
  ssh_key        = "ssh-rsa AAAA..."
  # or
  ssh_key        = "${file("~/.ssh/id_rsa.pub")}
  admin_username = "<USERNAME>"
  management_ip  = "<IP>/32"
}

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

You're now good to roll some infrastructure! terraform init, terraform plan, terraform apply and you're one Azure resource richer.

References

<<
>>