July 21, 2018 · DevOps IaC Terraform

Automating my Cloud Infrastructure: Terraform

Having had a very busy few months, changing jobs and moving to a new house, I've finally found the time to write a new post! In what little free time I've managed to grab over the last few months, I've been working on completely overhauling my infrastructure.

I recently started becoming increasingly frustrated with how long I was spending on deploying and configuring the servers that I use for some of the applications I host. Whenever I start a new project that requires a server, my server deployment process usually looks something like this:

While this is a perfectly valid way of doing things, it isn't particularly efficient. I would find myself spending ages performing the same manual task over and over again. Configuration changes had to be applied one by one to my servers. Performing OS/package upgrades was a manual process that had to be run periodically, and if one of my servers was accidently deleted, or I wanted to migrate it somewhere else, I would have to spend time figuring out how to recreate it in its new home.

I therefore decided to completely overhaul my infrastructure, and my application deployment process. After starting my new job, I was introduced to the Pets vs. Cattle analogy. As it stood, my servers were pets. I took care of them, nursed them, and if they were sick I would attempt to fix and make them better. My aim in this project was to treat my servers more like cattle and make them easily replaceable and quickly reprovisionable. To do this I chose a couple of tools:

  1. Terraform: For Infrastructure Deployment
  2. Ansible: For Configuration Management
  3. Containers: For all of my Applications

In this post I am going to focus on how I used Terraform for all of my infrastructure.

Enter Terraform

The first problem on my hit list was the deployment of my infrastructure. I was fed up of going into various Control Panels, deploying servers, creating/updating DNS records, and implementing firewall rules. I therefore decided to use Terraform for all of my infrastructure deployment.

Terraform is a tool that allows infrastructure to be defined as code. Instead of deploying your infrastructure (servers, DNS records, VPC's, etc) manually, you can define them as resources in Terraform using their HCL language. Once defined, Terraform will then provision these resources with the appropriate Cloud Providers.

For example, to create a Virtual Machine in Digital Ocean using Terraform, you would define the following code:

resource "digitalocean_droplet" "benjamin-maynard-server" {
  name   = "myserver1.maynard.io"
  image  = "ubuntu-16-04-x64"
  region = "lon1"
  size   = "s-1vcpu-1gb"
  backups = "true"
  private_networking = "false"
  ipv6 = "true"
  ssh_keys = [ "4e:00:d2:3d:66:72:6e:87:bb:a7:f1:7b:74:10:a8:7a" ]
  tags = ["Terraform"]
}

With the above code defined in a .tf file, you are able to run the command terraform apply and Terraform will go away and create that Virtual Machine for you in Digital Ocean.

Terraform also has "Attributes" that are exported by the various providers. For example, for the above Droplet, you are able to access information about that resource, and use them in other Terraform resources. Let’s say that you wanted to create a DNS Record in Google Cloud DNS for the Virtual Machine that was just created with the above Terraform code, you could use the following code:

resource "google_dns_record_set" "benjamin-maynard-server-A" {
  name = "${digitalocean_droplet.benjamin-maynard-server.name}"
  
  type = "A"
  ttl  = "900"
  managed_zone = "maynard.io"
  
  rrdatas = ["${digitalocean_droplet.benjamin-maynard-server.ipv4_address}"]
}

Looking at the above code, there are two important lines:

name = "${digitalocean_droplet.benjamin-maynard-server.name}"

Here Terraform dynamically pulls the name for the DNS record from the DigitalOcean Virtual Machine that was earlier defined. It will therefore create a DNS record called myserver1.maynard.io.


  rrdatas = ["${digitalocean_droplet.benjamin-maynard-server.ipv4_address}"]

Here Terraform dynamically pulls the IPv4 address from the Digital Ocean Virtual Machine, and uses that for the A record for myserver1.maynard.io


In these two blocks of code, I have defined a Virtual Machine, and automated the creation of the DNS Record. You can look at is as a "Blueprint" for my environment.

Without going too into Terraform (see Introduction to Terraform) there are also a couple of other benefits that make using it so great:

Scalability: In the above code I defined one Virtual Machine. If I wanted to scale my environment, I could simply add a line that reads count = 100 to the Virtual Machine and Terraform would create me 100 VM's.
State Monitoring: Terraform keeps a track of the state of your environment. If I delete a resource defined in my Terraform configuration, Terraform will automatically re-create it the next time it is run. It will also handle the updating of any associated DNS Records, Load Balancer Configurations, etc.

Using Terraform for my Environment

Due to the benefits and flexibility of Terraform, I decided to use it for all of my Cloud Infrastructure. Everything I create in AWS, GCP, Azure or Digital Ocean is fully defined and managed through Terraform. The Terraform Configuration is then source controlled in GitHub so changes can easily be tracked.

Below is an example of how I used Terraform to deploy the infrastructure for this blog. This blog currently runs on a single server hosted with Digital Ocean. It is fully redeployable using Terraform, Ansible, GitHub and GCR.

resource "digitalocean_volume" "nigel-web-maynard-io" {
  region      = "lon1"
  name        = "nigel-web-maynard-io-terraform"
  size        = 5
  description = "Volume for Docker Persistent Data on nigel.web.maynard.io"
}

The first step in deploying my blog, was to define a block storage device. This device is used to store all content that is uploaded to my blog. For example, images on blog posts, and data that needs to remain if the instance itself is deleted. In this configuration I am specifying a 5GB volume in the LON1 region. This is comparable to an EBS volume.

resource "digitalocean_droplet" "nigel-web-maynard-io" {
  name   = "nigel.web.maynard.io"
  image  = "ubuntu-16-04-x64"
  region = "lon1"
  size   = "s-1vcpu-1gb"
  backups = "true"
  private_networking = "false"
  ipv6 = true
  ssh_keys = [ "4e:00:d2:3d:66:72:6e:87:bb:a7:f1:7b:74:10:a8:7a" ]
  user_data = "${file("./resources/ansible-bootstrap.sh")}"
  tags = ["Terraform"]
  volume_ids = ["${digitalocean_volume.nigel-web-maynard-io.id}"]
}

The next step in the deployment was to define the compute. In this case a Digital Ocean Droplet. In the above code alongside some of the normal configuration you would expect to see, such as the name, OS, region and size, there are some values worth noting:

user_data: Here I am specifying "user data". If you're familiar with AWS, this is exactly the same concept on Digital Ocean. This is a custom bash script that runs on the creation of the Droplet. This performs some initial configuration like installing Python and creating a user for Ansible.
volume_ids: In the previous configuration block I specified a Block Storage device. Here you will see that I reference that Block Storage device, so that it will be attached to the Droplet on creation.

resource "google_dns_record_set" "nigel-web-maynard-io-A" {
  name = "nigel.web.${google_dns_managed_zone.maynard-io.dns_name}"
  type = "A"
  ttl  = "${var.maynard_io_ttl}"

  managed_zone = "${google_dns_managed_zone.maynard-io.name}"

  rrdatas = ["${digitalocean_droplet.nigel-web-maynard-io.ipv4_address}"]
}

resource "google_dns_record_set" "benjamin-maynard-io-A" {
  name = "benjamin.${google_dns_managed_zone.maynard-io.dns_name}"
  type = "A"
  ttl  = "${var.maynard_io_ttl}"

  managed_zone = "${google_dns_managed_zone.maynard-io.name}"

  rrdatas = ["${digitalocean_droplet.nigel-web-maynard-io.ipv4_address}"]
}

Now that I had the Block Storage, and the Droplet configured, the next step was to create the DNS Records. I use Google Cloud DNS, which is part of GCP for my external DNS. In the above code you will see I create two A records. The first one is for the FQDN of the server, and the second is the web address of this blog (nigel.web.maynard.io). You will see that the "rrdatas" (which is the DNS record value, in this case an A record) is dynamically pulled from the Droplet. This means if the IP address of the Droplet changes (for example if it was destroyed and recreated), Terraform will update the DNS record without me having to rewrite any code.

resource "digitalocean_firewall" "nigel-web-maynard-io" {
  name = "nigel-web-maynard-io-fw-terraform"
  droplet_ids = ["${digitalocean_droplet.nigel-web-maynard-io.id}"]


inbound_rule = [
{
  protocol           = "tcp"
  port_range         = "80"
  source_addresses   = ["0.0.0.0/0", "::/0"]
},
{
  protocol           = "tcp"
  port_range         = "443"
  source_addresses   = ["0.0.0.0/0", "::/0"]
},
   ]
outbound_rule = [
{
  protocol                = "udp"
  port_range              = "53"
  destination_addresses   = ["0.0.0.0/0", "::/0"]
},
{
  protocol                = "tcp"
  port_range              = "22"
  destination_addresses   = ["${data.github_ip_ranges.ips.git}"]
},
  ]
}

Finally, the last step was to create a Cloud Firewall to apply to my Droplet (a security group if you're familiar with AWS). Here I allow inbound web traffic on port 80 and 443, and outbound DNS.

As part of my deployment, the server has a requirement to clone some repositories from GitHub. You'll notice above that the destination_addresses paramater in the last rule references data.github_ip_ranges.ips.git.

This is where Terraform starts to get clever. As GitHub's IP ranges can frequently change, writing a static firewall rule to allow outbound connectivity to GitHub can be dangerous, because if the IP addresses change you risk being left in a position where you're unable to clone from those repositories.

GitHub fortunately have a Terraform Data Source which pulls the list of IP's of the Git Servers from the GitHub API. By referencing this in my Terraform Digital Ocean Firewall, I am able to ensure that the firewall rule always allows outbound connectivity to the correct GitHub IP addresses.

The Result

With my Terraform code complete, I am left in a position where I am able to build out the infrastructure for this blog by just running the terraform apply command. Terraform then handles the rest for me and goes and creates my defined resources with the appropriate providers.

By using Terraform instead of managing your Cloud Infrastructure manually, there are a huge range of benefits, below are just a couple of examples:

I'm continuing to use Terraform to manage all of my Cloud Infrastructure, it is incredibly powerful and I'm looking forward to exploring more and more advance features.

  • LinkedIn
  • Tumblr
  • Reddit
  • Google+
  • Pinterest
  • Pocket