I recently got some time to play with Terraform, the cloud provisioning software from the same guys as Vagrant and Packer. If you’ve spent some time with Vagrant and Packer, then Terraform will be pretty simple.

In a nutshell, Terraform is a vendor-independent version of AWS Cloudformation. Following infrastructure-as-code principles, it allows you to define your infrastructure resources and their interconnections in a single source file, which Terraform then runs, creating everything you defined in the file. The basic philosophy of Terraform is the same as that for Packer; instead of writing your own scripts to essentially do the same thing on multiple cloud vendors so you can stop worrying about vendor lock-in and cloud migrations, Terraform allows you to use a vendor-independent syntax to create a Terraform file which it can then use to provision your infrastructure on a cloud provider of your choice.

If you haven’t spent time with either Ansible or Cloudformation to automate your infrastructure, then Terraform would be a good starting point. While it doesn’t, and shouldn’t, replace Ansible, which is a configuration management and provisioning tool, it can act as a stand-in for Cloudformation, which is the AWS version of infrastructure-as-code. The beauty of using a vendor-independent tool like Terraform is that you can use it to automate infrastructure provisioning across a variety of cloud providers. Once you’ve defined your infrastructure setup for AWS using Terraform, migrating to Google cloud or Digital ocean might become as simple as changing a few lines of code, instead of figuring out how to do it form the various menu options in the console.

Installing Terraform

As always, I won’t spend time here. On Mac, run this:

brew install terraform

Next, create a new folder where we’ll be doing our experimentation. Terraform requires a folder for a single project that could contain multiple Terraform definition files, so creating a new folder is a good idea.

Creating a test instance

Let’s create a test instance in AWS. Create a new file called test.tf and place the following in it:

provider "aws" {

    region = "us-east-1"
}

resource "aws_instance" "backend_instances" {
  
    count = 1

    ami           = "ami-cd0f5cb6"
    instance_type = "c4.large"
    subnet_id = "subnet-***"
    vpc_security_group_ids = ["sg-***"]
    key_name = "ayush_production_test"

    tags {
        "Name" = "backend_instance"
    }

    connection {
        type     = "ssh"
        user     = "ubuntu"
        private_key = "${file("key-file.pem")}"
    }

    provisioner "remote-exec" {
        inline = [
            "sudo apt -y update",
            "sudo apt install -y python"
        ]
    }
}

Replace the placeholders above with your actual key files, subnets, etc.

The file is pretty simple to read.

Once you have all this, just save your file.

The next steps would be to run this file using some terraform commands. Note that these commands run on all `.tf files you define in the current working directory.

Let’s start.

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.1.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 1.1"

Terraform has been successfully initialized!

Creating a load balancer

Let’s do something a bit more complicated. Add the following lines to the test.tf files we created above:

resource "aws_elb" "backend-elb" {

    name = "backend-load-balancer"

    listener {
        instance_port     = 80
        instance_protocol = "http"
        lb_port           = 80
        lb_protocol       = "http"
    }

    health_check {
        healthy_threshold   = 2
        unhealthy_threshold = 2
        timeout             = 5
        target              = "TCP:80"
        interval            = 30
    }

    cross_zone_load_balancing   = true
    idle_timeout                = 400
    connection_draining         = true
    connection_draining_timeout = 400

    subnets = ["subnet-***"]

    instances = ["${aws_instance.backend_instances.*.id}"]
}

So here, we’re creating a new AWS load balancer with the usual parameters it requires. The name, listener configurations, health check configurations, etc. The last line starting with instances… is new. This line is actually telling Terraform to take all instances we launched using the “aws_instance” resource above, and add them behind this load balancer. Note that the backend_instances used in this line is the same as the name we used for the AWS instance resource before. Terraform is smart enough to figure out that it needs to create the instances before it can attach them to the load balancer, so it will create and launch the instances first and make the load balancer second. Check your execution plan again by running terraform plan, and you should see a new load balancer being created.

This is just a basic warm-up of Terraform and should serve as a PoC. The executing plan feature is really great for reviewing the final changes to be made on the cluster, and will certainly take care of a lot of deployment headaches. By having this all in git, change control and review will be must faster. And once the infrastructure is well-defined is source code, replicating it to create staging and testing environments will be all that easier.

When you run Terraform, it will create some additional files in your local directory to keep track of what it created. It will use this on subsequent runs when you want to update or delete resources.