Skip to content

Terraform Overview

Terraform#

Why use Terraform#

When dealing with procedural code, the state of the infrastructure is not fully captured in the code The reusability of procedural code is inherently limited because you have to manually take into account the current state of the codebase

With Terraform all you need to look at is the code: to know the current state of you infrastructure and how it was configured.

Meaning that Terraform codebases tend to stay small and easy to understand

The downside is there is no logic and control - loops and if statements. Rolling, zero-downtime deployment, are hard to express in purely declarative terms.

Stability: It’s still pre 1.0.0, so there is no guarantee of a stable or backward compatible API - bugs are relatively common (Terraforms biggest weakness)

Configuration#

The set of files used to describe infrastructure in Terraform is simply known as a Terraform configuration

It uses it’s own configuration language although it can also be json but json is recommended when configuration is generated by a machine.

The files end in *.tf, terraform will load all of them with that extension.

For example.tf:

provider "aws" {
  profile    = "default"
  region     = "us-east-1"
}

resource "aws_instance" "example" {
  ami           = "ami-2757f631"
  instance_type = "t2.micro"
}

It is HashiCorp recommended practice that credentials never be hardcoded into *.tf configuration files, the profile attribute refers to the AWS credentials file.

Tutorial#

To verify the correct provider details install the aws cli with:

pip3 install awscli

then run:

aws configure

Create or view your credentials at iam home

That will create your credentials in ~/.aws/

In example.tf:

provider "aws" {
  profile    = "default"
  region     = "eu-west-2"
}

resource "aws_instance" "example" {
  ami           = "ami-0c30afcb7ab02233d"
  instance_type = "t2.micro"
}

The provider block…configures the provider. The resource block defines a resource that exists within an infrastructure.

The resource blocks specifies the resource type: aws_instance and the resource name example

The prefix of type maps to a provider.

Within the resource block is the configuration for that resource based on the provider

The AMI is specified for ubuntu and the instance_type is t2.micro

Initialisation#

The first command to run is: terraform init

each “Provider” is its own encapsulated binary distributed separately from Terraform itself

Init will download the provider binary

The aws provider plugin is downloaded and installed in a subdirectory of the current working directory, along with various other book-keeping files. In a hidden folder

Then to view what is going to happen run

terraform plan

this will show you what the command will do:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create

Terraform will perform the following actions:

# aws_instance.example will be created
+ resource "aws_instance" "example" {
    + ami                          = "ami-2757f631"
    + arn                          = (known after apply)
    + associate_public_ip_address  = (known after apply)
    + availability_zone            = (known after apply)
    + cpu_core_count               = (known after apply)
    + cpu_threads_per_core         = (known after apply)
    + get_password_data            = false
    + host_id                      = (known after apply)
    + id                           = (known after apply)
    + instance_state               = (known after apply)
    + instance_type                = "t2.micro"
    + ipv6_address_count           = (known after apply)
    + ipv6_addresses               = (known after apply)
    + key_name                     = (known after apply)
    + network_interface_id         = (known after apply)
    + password_data                = (known after apply)
    + placement_group              = (known after apply)
    + primary_network_interface_id = (known after apply)
    + private_dns                  = (known after apply)
    + private_ip                   = (known after apply)
    + public_dns                   = (known after apply)
    + public_ip                    = (known after apply)
    + security_groups              = (known after apply)
    + source_dest_check            = true
    + subnet_id                    = (known after apply)
    + tenancy                      = (known after apply)
    + volume_tags                  = (known after apply)
    + vpc_security_group_ids       = (known after apply)

    + ebs_block_device {
        + delete_on_termination = (known after apply)
        + device_name           = (known after apply)
        + encrypted             = (known after apply)
        + iops                  = (known after apply)
        + snapshot_id           = (known after apply)
        + volume_id             = (known after apply)
        + volume_size           = (known after apply)
        + volume_type           = (known after apply)
        }

    + ephemeral_block_device {
        + device_name  = (known after apply)
        + no_device    = (known after apply)
        + virtual_name = (known after apply)
        }

    + network_interface {
        + delete_on_termination = (known after apply)
        + device_index          = (known after apply)
        + network_interface_id  = (known after apply)
        }

    + root_block_device {
        + delete_on_termination = (known after apply)
        + iops                  = (known after apply)
        + volume_id             = (known after apply)
        + volume_size           = (known after apply)
        + volume_type           = (known after apply)
        }
    }

The + means create (similar to git diff)

The (known after apply) means that the value has not been set and will be known after applying changes

terraform apply will run a plan anyway and then ask for approval - kind of like a network config commit.

Type yes to proceed

You might get an error:

Error: Error launching source instance: InvalidAMIID.NotFound: The image id '[ami-2757f631]' does not exist
        status code: 400, request id: 3ff4f7b9-22ab-4ca8-b245-1f9a97ce4547

I searched aws and changed the AMI

It will create the instance:

aws_instance.example: Creating...
aws_instance.example: Still creating... [10s elapsed]
aws_instance.example: Still creating... [20s elapsed]
aws_instance.example: Still creating... [30s elapsed]
aws_instance.example: Creation complete after 31s [id=i-0499e86428cd7ac4a]

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

You will see the EC2 instance now.

Terraform also wrote some data into terraform.tfstate, this file is very important it keeps track of ID’s of created resources so Terraform knows what it is managing.

This file must be saved and distributed to anyone who might run Terraform

By default the state is stored locally but when working in a team you should setup remote state.

State#

You can inspect the current state with

terraform show

resource "aws_instance" "example" {
    ami                          = "ami-0c30afcb7ab02233d"
    arn                          = "arn:aws:ec2:eu-west-2:632637550757:instance/i-0499e86428cd7ac4a"
    associate_public_ip_address  = true
    availability_zone            = "eu-west-2a"
    cpu_core_count               = 1
    cpu_threads_per_core         = 1
    disable_api_termination      = false
    ebs_optimized                = false
    get_password_data            = false
    id                           = "i-0499e86428cd7ac4a"
    instance_state               = "running"
    instance_type                = "t2.micro"
    ipv6_address_count           = 0
    ipv6_addresses               = []
    monitoring                   = false
    primary_network_interface_id = "eni-0cbe48527b1b1ec8d"
    private_dns                  = "ip-172-31-19-140.eu-west-2.compute.internal"
    private_ip                   = "172.31.19.140"
    public_dns                   = "ec2-3-8-150-188.eu-west-2.compute.amazonaws.com"
    public_ip                    = "3.8.150.188"
    security_groups              = [
        "default",
    ]
    source_dest_check            = true
    subnet_id                    = "subnet-c13c8ebb"
    tenancy                      = "default"
    volume_tags                  = {}
    vpc_security_group_ids       = [
        "sg-e6ae0689",
    ]

    credit_specification {
        cpu_credits = "standard"
    }

    root_block_device {
        delete_on_termination = true
        iops                  = 100
        volume_id             = "vol-03cec4b79fbfcb58b"
        volume_size           = 8
        volume_type           = "gp2"
    }
}

If no additional software is needed (an image based infrastructure) this is all you need.

Change Infrastructure#

Lets modify the resource

By using Terraform to change infrastructure, you can version control not only your configurations but also your state so you can see how the infrastructure evolved over time

We are going to change the AMI to be ubuntu 16…

Changing the example config to:

provider "aws" {
  profile    = "default"
  region     = "eu-west-2"
}

resource "aws_instance" "example" {
  ami           = "ami-0cbe2951c7cd54704"
  instance_type = "t2.micro"
}

Terraform will show you the changes:

aws_instance.example: Refreshing state… [id=i-0499e86428cd7ac4a]

An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: -/+ destroy and then create replacement

Terraform will perform the following actions:

aws_instance.example must be replaced#

-/+ resource "aws_instance" "example" {
    ~ ami                          = "ami-0c30afcb7ab02233d" -> "ami-0cbe2951c7cd54704" # forces replacement
    ~ arn                          = "arn:aws:ec2:eu-west-2:632637550757:instance/i-0499e86428cd7ac4a" -> (known after apply)
    ~ associate_public_ip_address  = true -> (known after apply)
    ~ availability_zone            = "eu-west-2a" -> (known after apply)
    ~ cpu_core_count               = 1 -> (known after apply)
    ~ cpu_threads_per_core         = 1 -> (known after apply)
    - disable_api_termination      = false -> null
    - ebs_optimized                = false -> null
        get_password_data            = false
    + host_id                      = (known after apply)
    ~ id                           = "i-0499e86428cd7ac4a" -> (known after apply)
    ~ instance_state               = "running" -> (known after apply)
        instance_type                = "t2.micro"
    ~ ipv6_address_count           = 0 -> (known after apply)
    ~ ipv6_addresses               = [] -> (known after apply)
    + key_name                     = (known after apply)
    - monitoring                   = false -> null
    + network_interface_id         = (known after apply)
    + password_data                = (known after apply)
    + placement_group              = (known after apply)
    ~ primary_network_interface_id = "eni-0cbe48527b1b1ec8d" -> (known after apply)
    ~ private_dns                  = "ip-172-31-19-140.eu-west-2.compute.internal" -> (known after apply)
    ~ private_ip                   = "172.31.19.140" -> (known after apply)
    ~ public_dns                   = "ec2-3-8-150-188.eu-west-2.compute.amazonaws.com" -> (known after apply)
    ~ public_ip                    = "3.8.150.188" -> (known after apply)
    ~ security_groups              = [
        - "default",
        ] -> (known after apply)
        source_dest_check            = true
    ~ subnet_id                    = "subnet-c13c8ebb" -> (known after apply)
    - tags                         = {} -> null
    ~ tenancy                      = "default" -> (known after apply)
    ~ volume_tags                  = {} -> (known after apply)
    ~ vpc_security_group_ids       = [
        - "sg-e6ae0689",
        ] -> (known after apply)

    - credit_specification {
        - cpu_credits = "standard" -> null
        }

    + ebs_block_device {
        + delete_on_termination = (known after apply)
        + device_name           = (known after apply)
        + encrypted             = (known after apply)
        + iops                  = (known after apply)
        + snapshot_id           = (known after apply)
        + volume_id             = (known after apply)
        + volume_size           = (known after apply)
        + volume_type           = (known after apply)
        }

    + ephemeral_block_device {
        + device_name  = (known after apply)
        + no_device    = (known after apply)
        + virtual_name = (known after apply)
        }

    + network_interface {
        + delete_on_termination = (known after apply)
        + device_index          = (known after apply)
        + network_interface_id  = (known after apply)
        }

    ~ root_block_device {
        ~ delete_on_termination = true -> (known after apply)
        ~ iops                  = 100 -> (known after apply)
        ~ volume_id             = "vol-03cec4b79fbfcb58b" -> (known after apply)
        ~ volume_size           = 8 -> (known after apply)
        ~ volume_type           = "gp2" -> (known after apply)
        }
    }

Plan: 1 to add, 0 to change, 1 to destroy.
  • -/+ means destroy and recreate
  • ~ means update in place

you can adjust your changes to possibly avoid destroy/create updates if they are not acceptable in some situations

As indicated by the plan, terraform destroyed the existing instance before creating a new one

aws_instance.example: Destroying... [id=i-0499e86428cd7ac4a]
aws_instance.example: Still destroying... [id=i-0499e86428cd7ac4a, 10s elapsed]
aws_instance.example: Still destroying... [id=i-0499e86428cd7ac4a, 20s elapsed]
aws_instance.example: Destruction complete after 23s
aws_instance.example: Creating...
aws_instance.example: Still creating... [10s elapsed]
aws_instance.example: Still creating... [20s elapsed]
aws_instance.example: Still creating... [30s elapsed]
aws_instance.example: Creation complete after 30s [id=i-017faa6d016ae580b]

You can run terraform show to view the current state again.

Destroy Infrastrcture#

Destroying your infrastructure is a rare event in production environments. But if you’re using Terraform to spin up multiple environments such as development, test, QA environments, then destroying is a useful action.

terraform destroy

Similar to terraform apply but it behaves as if all of the resources have been removed from the configuration.

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy

Terraform will perform the following actions:

# aws_instance.example will be destroyed
- resource "aws_instance" "example" {
    - ami                          = "ami-0cbe2951c7cd54704" -> null
    - arn                          = "arn:aws:ec2:eu-west-2:632637550757:instance/i-017faa6d016ae580b" -> null
    - associate_public_ip_address  = true -> null
    - availability_zone            = "eu-west-2a" -> null
    - cpu_core_count               = 1 -> null
    - cpu_threads_per_core         = 1 -> null
    - disable_api_termination      = false -> null
    - ebs_optimized                = false -> null
    - get_password_data            = false -> null
    - id                           = "i-017faa6d016ae580b" -> null
    - instance_state               = "running" -> null
    - instance_type                = "t2.micro" -> null
    - ipv6_address_count           = 0 -> null
    - ipv6_addresses               = [] -> null
    - monitoring                   = false -> null
    - primary_network_interface_id = "eni-03fd81e9df8b53966" -> null
    - private_dns                  = "ip-172-31-17-251.eu-west-2.compute.internal" -> null
    - private_ip                   = "172.31.17.251" -> null
    - public_dns                   = "ec2-35-177-103-61.eu-west-2.compute.amazonaws.com" -> null
    - public_ip                    = "35.177.103.61" -> null
    - security_groups              = [
        - "default",
        ] -> null
    - source_dest_check            = true -> null
    - subnet_id                    = "subnet-c13c8ebb" -> null
    - tags                         = {} -> null
    - tenancy                      = "default" -> null
    - volume_tags                  = {} -> null
    - vpc_security_group_ids       = [
        - "sg-e6ae0689",
        ] -> null

    - credit_specification {
        - cpu_credits = "standard" -> null
        }

    - root_block_device {
        - delete_on_termination = true -> null
        - iops                  = 100 -> null
        - volume_id             = "vol-005ebba18b00b544a" -> null
        - volume_size           = 8 -> null
        - volume_type           = "gp2" -> null
        }
    }

Just like with apply, Terraform determines the order in which things must be destroyed. In this case there was only one resource, so no ordering was necessary. In more complicated cases with multiple resources, Terraform will destroy them in a suitable order to respect dependencies, as we’ll see later in this guide.

Resource Dependencies#

Real infrastructure has a diverse set of resources and types. Terraform configs can have multiple resources and types among many providers.

Elastic IP#

We can assign an elastic ip with:

resource "aws_eip" "ip" {
  instance = aws_instance.example.id
}

It is a aws_eip resource type. The only parameter for aws_eip is instance.

Do an apply:

terraform apply

The output will be:

# aws_eip.ip will be created
+ resource "aws_eip" "ip" {
    + allocation_id     = (known after apply)
    + association_id    = (known after apply)
    + domain            = (known after apply)
    + id                = (known after apply)
    + instance          = (known after apply)
    + network_interface = (known after apply)
    + private_dns       = (known after apply)
    + private_ip        = (known after apply)
    + public_dns        = (known after apply)
    + public_ip         = (known after apply)
    + public_ipv4_pool  = (known after apply)
    + vpc               = (known after apply)
    }

# aws_instance.example will be created
+ resource "aws_instance" "example" {
    + ami                          = "ami-0cbe2951c7cd54704"
    + arn                          = (known after apply)

Terraform knows to create the EC2 instance before creating the elastic ip address - it can infer a dependency:

aws_instance.example: Creating...
aws_instance.example: Still creating... [10s elapsed]
aws_instance.example: Still creating... [20s elapsed]
aws_instance.example: Still creating... [30s elapsed]
aws_instance.example: Still creating... [40s elapsed]
aws_instance.example: Creation complete after 41s [id=i-02f0557a715578b7a]
aws_eip.ip: Creating...
aws_eip.ip: Creation complete after 3s [id=eipalloc-09890fe6be9a7ba9a]

Implicit and Explicit Dependencies#

The reference to aws_instance.example.id created an implicit dependency

Terraform then knows what the order should be

Implicit dependencies via interpolation expressions are the primary way to inform Terraform about these relationships, and should be used whenever possible.

Sometimes there are dependencies not visible to terraform, in that case you use depends_on argument accepted by any resource and this is a list of resources to create explicit dependencies for.

For example a the application on an EC2 instance expects to use a specific S3 bucket. So we use depends_on to ensure Terraform knows about it.

# New resource for the S3 bucket our application will use.
resource "aws_s3_bucket" "example" {
  # NOTE: S3 bucket names must be unique across _all_ AWS accounts, so
  # this name must be changed before applying this example to avoid naming
  # conflicts.
  bucket = "terraform-getting-started-guide"
  acl    = "private"
}

# Change the aws_instance we declared earlier to now include "depends_on"
resource "aws_instance" "example" {
  ami           = "ami-2757f631"
  instance_type = "t2.micro"

  # Tells Terraform that this EC2 instance must be created only after the
  # S3 bucket has been created.
  depends_on = [aws_s3_bucket.example]
}

Where there is no dependencies terraform will perform operations concurrently

Provision#

Using provisioners to intialise instances when they are created.

If you are using an image based infrastructure (created with packer) then what we have learnt so far is enough.

If however you need to do some initial setup on your instances, then provisioners let you upload files, run shell scripts or trigger config management tools

Defining a Provisioner#

Add a provisioner block within the resource block.

resource "aws_instance" "example" {
  ami           = "ami-b374d5a5"
  instance_type = "t2.micro"

  provisioner "local-exec" {
    command = "echo ${aws_instance.example.public_ip} > ip_address.txt"
  }
}

You can add multiple provisioner blocks within the resource block - defining multiple provisioning steps.

Terraform supports multiple provisioners

We’ll use local-exec which runs a command locally on the machine running terraform. (We’re using this provisioner as you don’t need to add connection info)

Running Provisioners#

Provisioners are only run when a resource is created

They are not a replacement for configuration management and changing the software of an already-running server, and are instead just meant as a way to bootstrap a server

Top tip: There is a vscode Terraform extension available for terraform autocomplete and syntax highlighting

This added the ip address to a file…you can view the ip address with:

$ cat ip_address.txt 35.178.204.26

Failed Provisioners and Tainted Resources#

If a resource successfully creates but fails during provisioning, Terraform will error and mark the resource as “tainted”. Meaning provisioning was successful but configuration failed.

On your next execution plan terraform will not attempt to restart provisioning on the tainted resource. Instead, Terraform will remove any tainted resources and create new resources, attempting to provision them again after creation.

No roll backs are done as that goes against the execution plan.

Destroy Provisioners#

Provisioners can also be defined that run only during a destroy operation - useful for cleanups and extracting data.

For many resources, using built-in cleanup mechanisms is recommended if possible (such as init scripts), but provisioners can be used if necessary.

Input Variables#

We’re still hard-coding access keys, AMIs, etc. To become truly shareable and version controlled, we need to parameterize the configurations.

Defining Variables#

Create a file variables.tf:

variable "region" {
  default = "eu-west-2"
}

It sets the default of the region variable, but will prompt you for the region.

Using variables in configuration#

provider "aws" {
    region = var.region
}

Assigning Variables#

Command line arguments:

terraform apply -var 'region=us-east-2'

From a file:

Create a file names terraform.tfvars:

region = "us-east-2"

Terraform will automatically load terraform.tfvars or *.auto.tfvars to populate variables

We don’t recommend saving usernames and password to version control, but you can create a local secret variables file and use -var-file to load it.

terraform apply -var-file="secret.tfvars" -var-file="production.tfvars"

From environment variables:

Terraform reads environment variables in the form of: TF_VAR_name

So a TF_VAR_region environment variable would populate region

Environment variables can only populate string-type variables. List and map type variables must be populated via one of the other mechanisms.

From the UI:

If you execute terraform apply with certain variables not specified, terraform will ask you to input their values interactively.

variables entered via the UI are not saved

From defaults:

If no variables are defined from the above methods, the defaults will be used.

Lists#

Implicitly:

variable "cidrs" { default = [] }

Explicitly:

variable "cidrs" { type = list }

To specify a list in terraform.tfvars:

cidrs = [ "10.0.0.0/16", "10.1.0.0/16" ]

Maps#

Unfortunately, AMIs are specific to the region that is in use. Maps are a way to create variables that are lookup tables.

variable "amis" {
    type = "map"
    default = {
        "us-east-1" = "ami-b374d5a5"
        "us-west-2" = "ami-4b32be2b"
    }
}

To use it, replace the AWS instance with:

resource "aws_instance" "example" {
    ami           = var.amis[var.region]
    instance_type = "t2.micro"
}

Maps can also be set from the command line with:

terraform apply -var 'amis={ us-east-1 = "foo", us-west-2 = "bar" }'

Output Variables#

When building potentially complex infrastructure, Terraform stores hundreds or thousands of attribute values for all your resources but you may only be interested in a few values of importance.

Outputs are a way to tell Terraform what data is important

This data is outputted when apply is called, and can be queried using the terraform output command.

Let’s define an output for the public IP of elastic ip:

output "ip" {
    value = aws_eip.ip.public_ip
}

After running terraform apply you will see the output:

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

Outputs:

ip = 3.9.152.24

You can also query the output after running apply with:

$ terraform output
ip = 3.9.152.24

This command is useful for scripts to extract outputs

Modules#

UP till now we’ve been configuring terraform by editing terraform configs directly. As infrastructure grows there are a few key problems:

  • lack of organisation
  • lack of reusability
  • difficulties in management for teams

Modules in Terraform are self-contained packages of Terraform configurations that are managed as a group. * create reusable components * improve organization * treat pieces of infrastructure as a black box

Using Modules#

There is a terraform registry of ready to use modules for common purposes. In this example we will use the Consul terraform module for AWS which will setup a complete consul cluster.

Ensure that you use the verified modules written by Hashicorp as the first port of call:

aws-consul-modules

View and use the module config given by the terraform registry page

terraform {
    required_version = "0.11.11"
}

provider "aws" {
    access_key = "AWS ACCESS KEY"
    secret_key = "AWS SECRET KEY"
    region     = "us-east-1"
}

module "consul" {
    source      = "hashicorp/consul/aws"
    num_servers = "3"
}

Modules can be retrieved from anywhere, in this case it is from the official terraform registry

After adding a new module to configuration, it is necessary to run (or re-run) terraform init

This was going to cost me money so I stopped it

Remote State Storage#

You have now seen how to build, change, and destroy infrastructure from a local machine. This is great for testing and development, but in production environments it is more responsible to share responsibility for infrastructure

The best way to do this is by running Terraform in a remote environment with shared access to state.

You need to setup remote backends

Hashicorp recommends using Terraform Cloud

For the tutorial we’ll use terraform cloud

Configure your backend with your own organisation and wordspace names:

terraform {
    backend "remote" {
        organization = "Cloud-Org"

        workspaces {
            name = "Dev-QA"
        }
    }
}

The backend section configures the backend to use.

Use terraform init to setup terraform

If you want to move back to local state, you can remove the backend configuration block from your configuration and run terraform init again

  • You can use it with a version controlled workflow to automatically queue plans when changes are commited to the repo
  • An API driven workflow where a CI pipeline can upload configurations directly
  1. Create a terraform cloud account
  2. Create an organisation and workspace
  3. Create an access token and add it to ~/.terraformrc

    credentials “app.terraform.io” { token = “....” }

  4. Add the backend to your config

  5. Reinitialise: terraform init

It should say:

Successfully configured the backend "remote"! Terraform will automatically
use this backend unless the backend configuration changes.
  1. Apply terraform apply

The state is now saved in terraform cloud

Using Multiple Tools Together#

Provisioning plus configuration management#

You use Terraform to deploy all the underlying infrastructure, including the network topology (i.e., VPCs, subnets, route tables), data stores (e.g., MySQL, Redis), load balancers, and servers. You then use Ansible to deploy your apps on top of those servers.

Many ways to get Ansible and Terraform to work together (e.g., Terraform adds special tags to your servers and Ansible uses those tags to find the server and configure them). Downside is maintenance with ansible is procedural.

Provisioning plus server templating#

You use Packer to package your apps as virtual machine images. You then use Terraform to deploy (a) servers with these virtual machine images and (b) the rest of your infrastructure, including the network topology (i.e., VPCs, subnets, route tables), data stores (e.g., MySQL, Redis), and load balancers. This is an immutable infrastructure approach, which will make maintenance easier. Downside is virtual machines take long to deploy and the deployment strategies available are limited.

Provisioning plus server templating plus orchestration#

Terraform, Packer, Docker, and Kubernetes. You use Packer to create a virtual machine image that has Docker and Kubernetes installed. You then use Terraform to deploy (a) a cluster of servers, each of which runs this virtual machine image and (b) the rest of your infrastructure, including the network topology (i.e., VPCs, subnets, route tables), data stores (e.g., MySQL, Redis), and load balancers. Finally, when the cluster of servers boots up, it forms a Kubernetes cluster that you use to run and manage your Dockerized applications.

Downside: Added complexity.

  • Providers - resources and infrastructure we can interact with - AWS, Azure, Docker, Github, GCP and vSphere - it uses API interactions
  • Resources - What you are creating - virtual switches, data stores, licensing etc.

Stages of terraform: 1. Write declarative code and terraform plan - shows what code is going to do 2. terraform apply - actually does change 3. terraform destroy

Download Terraform#

Download and Install Terraform

It is a single file (executable/binary)

Terraform and Junos NRE Tutorial#

Terraform enables you to easily and predictably plan, create, update and destroy infrastructure resources in a graph based and network CLI free way

It is thought of more as a cloud provisioner

Declarative versions of the resources that can be version controlled, opposed to procedural tools like ansible which do not give you an idea of state.

Terraform for traditional networking is relatively unchartered ground because we don’t think about switches, routers or firewalls as a set of immutable resources, or put in another way, things that can be created and destroyed easily

terraform init

As you would expect, this is idempotent and can be done multiple times and safely.

Terraform maintains state files for resource state. This is a source of truth that can inspected and manipulated through the Terraform application

You can view the data graphically with: terraform graph -type=plan

You can create a .png based on that output: terraform graph -type=plan | dot -Tpng > plan_graph.png

You can check the current state with cat terraform.tfstate

If you run terraform plan and current state is the same as what is configured you will have No changes. Infrastructure is up-to-date

When deleting resources it is important to keep things in folders, for example a customer has their own folder:

So when you do a terraform destroy the correct resources are removed

What if someone manually changed a config:

attrition: reduction in workforce due to resignations, retirement, sickness, or death

With terraform you are covered, even if manual entries were changed.

Using a variable of another resource:

${junos-qfx_vlan.vlan42_deep_thought.vlan_num}
Human Error#

When humans get involved in troubleshooting, we have a tendency to change a bunch of stuff without clearing up 100% afterwards. When this happens with Terraform configured resources, the Terraform state cache could get out of sync with reality and luckily there is a way to sync it back up.

Use terraform refresh

Refresh performs a read of the resources and updates the state cache

Parital Delete#

What happens if you don’t want to destroy everything and just delete a single resource:

terraform delete -h

You can use the -target to target a specific resource

Terraform in Reality#

It is not mature yet#
  • It’s major version is still 0, means it is not production ready and is under active development.
It is not cloud-agnostic#

You do not describe your infrastructure with generic code applicable on multiple providers - it has specific code per provider.

It does not hide the complexity of the underlying provider#

If you don’t know the components and how AWS works, terraform won’t make your life easier. You will have to deal with both AWS and terraform quirks.

AWS has global and regional resources:

  • EC2 is regional, an auto-scaling group in Virginia has nothing to do with one in Sao Paulo - they can have the same name (asg)
  • IAM is global - meaning a role can be used anywhere.
  • S3 is hybrid - it has regional scope but a global namespace - meaning no buckets with the same name across regions
It might not make things simpler#

Source#