· Estimated reading time: 11 minutes.

In this post we’ll explore a tool called Terraform, its advantages and disadvantages; and we’ll use it to spin up a test instance on Amazon AWS.

Terraform: What is it?

Developed by Hashicorp, creators of Vagrant and Packer, Terraform is a tool that enables us to define our infrastructure as code (formally known as IaC). We can add or modify resources such as computing instances, ssh-keys, network topology or firewall rules and Terraform takes care of generating and performing the necessary operations so the infrastructure provider’s state matches the one described in the code.

Terraform supports a wide range of on-site and cloud infrastructure such as Amazon AWS, Microsoft Azure, Openstack, VMware vSphere and Digital Ocean. A full list of supported providers is located on the tool’s website.

Advantages and disadvantages of using Terraform.

Pros

  • Infrastructure as code (IaC): This enables us to treat our infrastructure as a file. In plain English this means the following: we can back it up, keep a history of our hardware and configuration (including the associated firewall rules, VLANs and security policies) and rollback changes quickly and efficiently. Our datacenter is stored on a file, thus we can re-deploy our entire infrastructure in a few keystrokes. We are now able to set-up a testing or development environment that is guaranteed to be an exact replica of the actual production environment, or quickly spin-up a second datacenter in a disaster recovery scenario.
  • Speed: Terraform is fast, very fast. If the infrastructure provider supports it, Terraform parallelizes the generation and modification of resources. The result is: it takes literally a minute to provision one instance, and it takes the exact same time to provision 20 of them.
  • Supports multiple providers: Unlike Heat or CloudFormation, Terraform supports a variety of providers and can mix and match resources from any of them simultaneously. For example: We could keep the database in our local datacenter to comply with PII regulations, while hosting the frontend of the application somewhere else.
  • Flexibility: Our infrastructure is declared in .tf files. Terraform consumes all the .tf files from the folder and processes them to create the execution plan. We can separate the resources in a logical way, and easily add/substract a file with new resources for testing purposes. Cons and caveats ——————————————–

  • Terraform stores the state of the deployed resources in a local file called terraform.state, and that is the canonical source of truth. If the file is corrupted or the state described in it no longer matches the deployed infrastructure (for example, a remote resource is modified manually), Terraform will fix the discrepancy by destroying the remote resource and recreating it according to the local definition.

  • Terraform depends entirely on the provider’s API: The tool tries to mitigate potential failures by retrying and verifying API calls. Inconsistencies on the provider side can cause a situation in which Terraform is not able to match a specific remote resource to its local definition of said resource, thus generating an execution plan that destroys the remote resource and then creates another instance according to the local .tf files.
  • The declarative sintax of the .tf files is provider-specific. That means we will need to rewrite the definition of our infrastructure if we were to switch providers.

Command overview.

There are four basic commands that you must know to work with Terraform: refresh, plan, apply and destroy. Refresh updates the terraform.state file to match the provider’s state, plan outputs the changes that would be applied and apply performs the changes. Destroy does what you would expect: destroys the remote resources.

Apply does not prompt for confirmation. Any discrepancy between the remote state and the local definition will be fixed irreversibly. It’s important to double-check the plan to make sure it matches our expectations.

Installing Terraform

Terraform is available for a variety of operating systems, though we’ll use Ubuntu throughout this post. While it could be available on the repositories of your favourite OS, there are no guarantees it will be up to date. The latest version can be downloaded from its website.

It does not need to be installed, unpacking the downloaded file is all that is required.

First steps: Creating an instance on Amazon AWS.

The objective is to deploy Ubuntu 16.04 on a t2.micro instance, with as little configuration as possible to enable SSH connections.

To enable Terraform to communicate with the remote provider, we will need API keys. In the case of Amazon AWS, they can be obtained from the console.

Managing credentials on Amazon AWS

What follows is the instance.tf file we are going to use.

# Contains the API keys for AWS access.
provider "aws" {
  access_key = "AWS_KEY"
  secret_key = "AWS_KEY"
  region     = "us-east-1"
}

# Instance details

resource "aws_instance" "example" {
  ami           = "ami-2757f631" # This AMI contains Ubuntu 16.04
  instance_type = "t2.micro"
  subnet_id     = "${aws_subnet.default.id}"
}
# SSH key we are going to use to connect to the instance.
# It goes without saying that this key was created specifically for this blog post and has already been revoked.
resource "aws_key_pair" "ssh-keys" {
  key_name   = "terraform"
  public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFOuj5NcQMwA4v5E/qcbsN0NsyR0BmdWU9VeIr9BFlE8/R4jnUMS6Sa9tf52omzJyRlr6M64GykzHmSnp8Aro7CK9dM+8PuktmJF+/EEuDsj/5ynyK+JQ9VlrbspJBXJhUs84LQK+1DngxVuyj1dxFSTvFVVafl0uuYmJOgzhS5MkFa16G/P9PiV44tEsq8wjnVbNBpKmom06f85uPaqegM45gbzGmE7PEjxkDaw/7DD3UatBYJ1oA8JaCUCfr4LishPbuT+lWEIDROnhGOhzz7GlnSgOxyhDnhteVFQzMBQLIGKzwG1uNL+1OaPJ5j4jXplShe7kmfMrmC6hOtdmp josue@pruebaTerraform"
}
# A virtual private network for the instance.
resource "aws_vpc" "default" {
  cidr_block = "10.0.0.0/16"
}

# A gateway that provides internet access.
resource "aws_internet_gateway" "default" {
  vpc_id = "${aws_vpc.default.id}"
}
# We whitelist the entire internet on this vpc. Note that this does not open any ports, it merely enables us to receive traffic from the outside.
resource "aws_route" "internet_access" {
  route_table_id         = "${aws_vpc.default.main_route_table_id}"
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = "${aws_internet_gateway.default.id}"
}

# A subnet.
resource "aws_subnet" "default" {
  vpc_id                  = "${aws_vpc.default.id}"
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true
}

# We create a security group to be able to apply port policies on this vpc.
resource "aws_security_group" "elb" {
  name        = "terraform_example_elb"
  description = "First Steps Security Group"
  vpc_id      = "${aws_vpc.default.id}"

  # We enable initiating outgoing traffic on any port.
  # “-1” means both TCP and UDP.
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  # We allow incoming SSH traffic from anywhere.
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  

Inspecting the generated plan, we can clearly see that the above operations are going to be performed.

$ terraform plan
+ aws_instance.example
    ami:                          "ami-2757f631"
    associate_public_ip_address:  "<computed>"
    availability_zone:            "<computed>"
    ebs_block_device.#:           "<computed>"
    ephemeral_block_device.#:     "<computed>"
    instance_state:               "<computed>"
    instance_type:                "t2.micro"
    ipv6_address_count:           "<computed>"
    ipv6_addresses.#:             "<computed>"
    key_name:                     "<computed>"
    network_interface.#:          "<computed>"
    network_interface_id:         "<computed>"
    placement_group:              "<computed>"
    primary_network_interface_id: "<computed>"
    private_dns:                  "<computed>"
    private_ip:                   "<computed>"
    public_dns:                   "<computed>"
    public_ip:                    "<computed>"
    root_block_device.#:          "<computed>"
    security_groups.#:            "<computed>"
    source_dest_check:            "true"
    subnet_id:                    "<computed>"
    tenancy:                      "<computed>"
    volume_tags.%:                "<computed>"
    vpc_security_group_ids.#:     "<computed>"

Plan: 1 to add, 0 to change, 0 to destroy.

Note: this plan is not saved anywhere, and only exists in memory at this time. When using apply , Terraform will generate the plan again and it might be different if circumstances have changed.

The plan can be saved to disk using the -out switch, and apply can be instructed to load the plan from a file instead of generating it at runtime.

Let’s apply the plan.

$ terraform apply
aws_instance.example: Creating...
  ami:                          "" => "ami-2757f631"
  associate_public_ip_address:  "" => "<computed>"
  availability_zone:            "" => "<computed>"
  ebs_block_device.#:           "" => "<computed>"
  ephemeral_block_device.#:     "" => "<computed>"
  instance_state:               "" => "<computed>"
  instance_type:                "" => "t2.micro"
  ipv6_address_count:           "" => "<computed>"
  ipv6_addresses.#:             "" => "<computed>"
  key_name:                     "" => "<computed>"
  network_interface.#:          "" => "<computed>"
  network_interface_id:         "" => "<computed>"
  placement_group:              "" => "<computed>"
  primary_network_interface_id: "" => "<computed>"
  private_dns:                  "" => "<computed>"
  private_ip:                   "" => "<computed>"
  public_dns:                   "" => "<computed>"
  public_ip:                    "" => "<computed>"
  root_block_device.#:          "" => "<computed>"
  security_groups.#:            "" => "<computed>"
  source_dest_check:            "" => "true"
  subnet_id:                    "" => "subnet-3fd11213"
  tenancy:                      "" => "<computed>"
  volume_tags.%:                "" => "<computed>"
  vpc_security_group_ids.#:     "" => "<computed>"
aws_instance.example: Still creating... (10s elapsed)
aws_instance.example: Still creating... (20s elapsed)
aws_instance.example: Creation complete (ID: i-02fbb95386573f5ec)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Now we can inspect the state of our infrastructure using terraform show. Among other things, it shows us the public IP of the newly created instance.

aws_instance.example:
  id = i-02fbb95386573f5ec
  ami = ami-2757f631
  associate_public_ip_address = true
  availability_zone = us-east-1a
  disable_api_termination = false
  ebs_block_device.# = 0
  ebs_optimized = false
  ephemeral_block_device.# = 0
  iam_instance_profile = 
  instance_state = running
  instance_type = t2.micro
  ipv6_address_count = 0
  ipv6_addresses.# = 0
  key_name = 
  monitoring = false
  network_interface.# = 0
  network_interface_id = eni-0aaf6ac3
  primary_network_interface_id = eni-0aaf6ac3
  private_dns = ip-10-0-0-149.ec2.internal
  private_ip = 10.0.0.149
  public_dns = ec2-107-21-158-184.compute-1.amazonaws.com
  public_ip = 107.21.158.184
  root_block_device.# = 1
  root_block_device.0.delete_on_termination = true
  root_block_device.0.iops = 100
  root_block_device.0.volume_size = 8
  root_block_device.0.volume_type = gp2
  security_groups.# = 0
  source_dest_check = true
  subnet_id = subnet-3fd11213
  tags.% = 0
  tenancy = default
  volume_tags.% = 0
  vpc_security_group_ids.# = 1
  vpc_security_group_ids.713989925 = sg-535d3d2d

Let’s delete this instance with the destroy command.

$ terraform destroy
Do you really want to destroy?
  Terraform will delete all your managed infrastructure.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_instance.example: Refreshing state... (ID: i-02fbb95386573f5ec)
aws_instance.example: Destroying... (ID: i-02fbb95386573f5ec)
aws_instance.example: Still destroying... (ID: i-02fbb95386573f5ec, 10s elapsed)
aws_instance.example: Still destroying... (ID: i-02fbb95386573f5ec, 20s elapsed)
aws_instance.example: Still destroying... (ID: i-02fbb95386573f5ec, 30s elapsed)
aws_instance.example: Still destroying... (ID: i-02fbb95386573f5ec, 41s elapsed)
aws_instance.example: Still destroying... (ID: i-02fbb95386573f5ec, 51s elapsed)
aws_instance.example: Still destroying... (ID: i-02fbb95386573f5ec, 1m1s elapsed)
aws_instance.example: Still destroying... (ID: i-02fbb95386573f5ec, 1m11s elapsed)
aws_instance.example: Destruction complete

Basic provisioning of instances.

Terraform can transfer files and folders to the newly created instance, and it provides a number of facilities to execute software remotely. Using the remote-exec directive, we are going to deploy a script that writes “Hello” to a file in the /tmp/ folder.

The script will be called bootstrap.sh, and these are its contents.

#!/bin/bash
echo 'hello’ > /tmp/output.txt

We modify the instance definition to add the provisioner directive, and the SSH it will use to remotely login to the instance.

resource "aws_instance" "instancia1" {
  ami           = "ami-b374d5a5"
  instance_type = "t2.micro"
  key_name = "terraform"

  provisioner "remote-exec" {
    scripts = [
      "bootstrap.sh"
    ]
    connection {
      type     = "ssh"
      user     = "ubuntu"
      private_key = "${file("~/.ssh/clave-terraform-blog")}"
    }  
  }
}

After applying the plan, we can see that Terraform has logged-in to the instance and executed the script.

aws_instance.instancia1 (remote-exec): Connecting to remote host via SSH...
aws_instance.instancia1 (remote-exec):   Host: 54.90.2.13
aws_instance.instancia1 (remote-exec):   User: ubuntu
aws_instance.instancia1 (remote-exec):   Password: false
aws_instance.instancia1 (remote-exec):   Private key: true
aws_instance.instancia1 (remote-exec):   SSH Agent: true
aws_instance.instancia1 (remote-exec): Connected!
aws_instance.instancia1: Creation complete (ID: i-03cbd14db6ba0a896)

And after logging in manually ourselves, we can verify that it did run the script correctly.

$ ssh -i ~/.ssh/terraform-proyecto ubuntu@54.234.144.181 "cat /tmp/output.txt’”
Hello

In a future post we will explore how to use this functionality to provision instances automatically.