Made Tech Blog

Automating infrastructure provisioning with Terraform and Packer

Here at Made we’re always trying out new technologies that will automate repetitive tasks we need to perform on each new project.

We’ve already discussed how we do this with our build pipeline, but until now the only manual task we’ve had to do repeatedly across all projects is provision infrastructure.

We’ve been using Chef to set up provision our infrastructure for a while now, but we have still had to log in to the AWS console and spin up the EC2 instances to run it against, alongside all the other infrastructure requirements like RDS, S3, and load balancers.

For this we can now use two Hashicorp tools – Packer and Terraform. These tools enable us to codify all of the configuration and commit it into source control. Having our infrastructure as code enables us to create identical server environments all the way up the deployment pipeline.

Getting started?

Before we get started there are a couple of prerequisites we need in place:

  • Login to AWS to create a new user in IAM for Terraform and Packer to use, be sure to download the credentials file, and store it in ~/.aws/credentials. This is the standard location for AWS, and its also the default path both Packer, and Terraform. Its what I shall be using throughout to omit that configuration. It’s worth mentioning that these credentials are what is used behind the scenes by the AWS libraries to create/destroy all the infrastructure.
  • Generate a new SSH key pair. This SSH key will be used by Terraform, and added to the new EC2 instances. It will also allow us to SSH in and configure the newly created instances with Chef. When you generate the key pair you will need to specify a few additional attributes. -f chef-provisioner to specify a custom filename, -P to create a key pair without a passphrase, and -m pem to specify PEM format. The full command being ssh-keygen -f chef-provisioner -e -m pem
  • Install Packer, and Terraform.

Packer up

Now, I’m going to go out on a limb here and assume you have a working knowledge of Chef-Solo, and how it works. If you are new to Chef, here is a video intro and the chef-solo docs, both of which are great starting points.

We now have Chef set up, and have confirmed it works. Provisioning a machine image with Packer is really simple, all we have to do is specify a provisioner of type chef-solo and configure it.

{
  “provisioners”: [{
    "type": "chef-solo",    “chef_environment”: “production”,
    “cookbook_paths”: “./cookbooks”,
    “data_bags_path”: “./data_bags”,
    “environments_path”: “./environments”,
    “roles_path”: “./roles”,
    "run_list": ["role[app]", "role[production]"],
    "staging_directory": "/home/ubuntu/cookbooks”,
    “skip_install”: false
  }]
}

Now that we have the configuration for machine image, how can we make it available for use within Terraform? Packer provides a number of builders. In our use case we want to use the amazon-eps builder. This takes an existing Amazon AMI (ubuntu 14.04) and provisions on top of it.

"builders": [{
  "type": "amazon-ebs",
  "region": “eu-west-1",
  "source_ami": "ami-f95ef58a",
  "instance_type": “t2.nano",
  "ssh_username": "ubuntu",
  “ssh_private_key_file”: “path/to/generated/private_key",
  "ami_name": “made-project-ami {{timestamp}}"
}]

With both the provisioner and builder combined into one json all that is left to do is run packer build path/to/packer.json and Packer does the rest! Once packer has finished it will spit out the generated AMI reference. Keep a note of this.

Let’s Terraform this…

provider "aws" {
  region = “eu-west-1"
}

resource "aws_key_pair” "chef" {
  key_name = “chef-provisioner"
  public_key = “your public key file"
}

You can further dry up the aws_key_pair definition using the file function which is one of the interpolation functions built into Terraform.

variable “azs" {
  default = {
      zone0 = “eu-west-1a"
      zone1 = “eu-west-1b"
  }
}

resource "aws_security_group” "app” {
  name = "app_sg"
  # here be security rules
}

resource "aws_instance” “app" {
  ami = “ami-m4d3am1” # Not a real AMI
  availability_zone = "${lookup(var.azs, concat("zone", count.index))}"
  count = 2
  instance_type = “t2.nano” # Can be any Amazon instance size
  key_name = "${aws_key_pair.chef.key_name}"
  security_groups = ["${aws_security_group.app.name}"]
  tags {
    Name = “${concat(“web_app_", count.index)}"
  }
}

resource “aws_eip" "app_ip" {
  count = 2
  instance = "${element(aws_instance.app.*.id, count.index)}"
  vpc = true
}

You’ll notice we have specified the availability zones for the instances above; this is to guarantee maximum availability of our app, by ensuring we are spinning up instances in multiple availability zones within our AWS region.

In order to open up our instances to the world, alongside the aws_instance resource declaration, there is an elastic IP being defined, and to secure the whole set up there is a security group defined.

Taking this further

A great iteration on this would be to get Packer to build an AMI of each successful deploy to production using the base box we built earlier on. To do this all we’d need to do would be to add an additional provisioner to checkout the latest stable production build and execute the necessary commands, e.g. bundle install. What benefit would this have? It would mean our application could scale up very quickly and we would always have a current production (or staging) AMI ready to go.

Another direction to take this would be add the Atlas post processor (another Hashicorp tool) to the Packer json to register for Atlas Enterprise and leverage its artefact directory to always serve the “latest” production artefact to Terraform. This would also hopefully take care of housekeeping all the older production machine images.

About the Author

Avatar for Seb Ashton

Seb Ashton

Lead Software Engineer at Made Tech