This post will show you how to get started with using Terraform, an open-source tool that lets you build your infrastructure using code.

What is Terraform, exactly?

Terraform is a tool designed to help you automate your cloud infrastructure. It allows you to create your infrastructure as code, using a high-level configuration language called HCL.

It is developed by HashiCorp, open-source, and licensed under Mozilla Public License 2.0.

Why Terraform?

When it comes to setting up your infrastructure, there are many considerations to be had. Almost everyone has their own unique background, and varying requirements for their systems. You might want to be geographically close to your users, or you might be operating on a tight budget. You might even want a hybrid cloud, combining resources from several cloud services.

At Redpill Linpro, we strive to accommodate our customers’ requests. Therefore, whatever the reasons, it is natural that we support the managed hosting of their infrastructure in the various clouds that are available today.

This presents us with the challenge of keeping track of the infrastructure. We definitely do not want to get into the mess of setting up everything through the various web interfaces, having no history of changes and no easy way to revert them.

Enter Terraform.

With Terraform, we can create infrastructures that are:

  • Reproducible
  • Scalable
  • Version controlled
  • Configurable by everyone in our team

How does it work?

Terraform relies on its so-called providers to interact with the API’s of the various IaaS/SaaS/PaaS services. (So you will have to install the CLI tools for the services that you want to use Terraform for.)

Each provider contains the many different resources that the service provides, such as networks, virtual servers, security groups, volumes, and so on.

There are providers for all the major players in today’s cloud environment, such as OpenStack, Amazon Web Services, Microsoft Azure, Google Cloud, and many others. You can find the complete list of providers in the documentation.

When you have created your infrastructure using code, Terraform will create an execution plan, which can then be executed in order to deploy your infrastructure.

My first Terraform configuration

In the following example, I will set up some services on Amazon Web Services.

Download the Terraform binary executable for your platform, and put the executable it in your $PATH.

Ensure that you have installed and configured the AWS CLI. (Installation and configuration of this is outside the scope of this post.)

Create a file called variables.tf:

variable "aws_region" {
  description = "AWS region"
  default     = "eu-west-1"
}

Create another file called main.tf, and put the following code inside.

provider "aws" {
  region = "${var.aws_region}"
}

Now run terraform init in order to initialize Terraform.

zcat ~/terraform-example > terraform init

Initializing provider plugins...

* Checking for available provider plugins on https://releases.hashicorp.com...
* Downloading plugin for provider "aws" (1.23.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.23"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

As you can see, Terraform successfully downloaded the aws provider plugin. At the time of writing, the latest version available was 1.23.0, YMMV.

Next, let’s create an instance! We will use the AWS_instance resource for this.

Add the following lines to your code:

# main.tf
resource "aws_instance" "web" {
  ami = "ami-3b261642"
  instance_type = "t1.micro"
  tags {
    Name = "Hello World!"
  }

Now we come to a crucial point of Terraform. We will generate the execution plan by running terraform plan.

$ zcat ~/terraform-example > terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

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.web
      id:                           <computed>
      ami:                          "ami-3b261642"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      get_password_data:            "false"
      instance_state:               <computed>
      instance_type:                "t1.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     <computed>
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      password_data:                <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>
      tags.%:                       "1"
      tags.Name:                    "Hello World!"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

You can consider terraform plan as a dry-run of your changes. Once you have reviewed the execution plan above, and confirmed that your changes are as expected, you can proceed to run terraform apply in order to have your changes applied.

$ zcat ~/terraform-example > terraform apply

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.web
      id:                           <computed>
      ami:                          "ami-3b261642"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      get_password_data:            "false"
      instance_state:               <computed>
      instance_type:                "t1.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     <computed>
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      password_data:                <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>
      tags.%:                       "1"
      tags.Name:                    "Hello World!"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_instance.web: Creating...
  ami:                          "" => "ami-3b261642"
  associate_public_ip_address:  "" => "<computed>"
  availability_zone:            "" => "<computed>"
  ebs_block_device.#:           "" => "<computed>"
  ephemeral_block_device.#:     "" => "<computed>"
  get_password_data:            "" => "false"
  instance_state:               "" => "<computed>"
  instance_type:                "" => "t1.micro"
  ipv6_address_count:           "" => "<computed>"
  ipv6_addresses.#:             "" => "<computed>"
  key_name:                     "" => "<computed>"
  network_interface.#:          "" => "<computed>"
  network_interface_id:         "" => "<computed>"
  password_data:                "" => "<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>"
  tags.%:                       "" => "1"
  tags.Name:                    "" => "Hello World!"
  tenancy:                      "" => "<computed>"
  volume_tags.%:                "" => "<computed>"
  vpc_security_group_ids.#:     "" => "<computed>"
aws_instance.web: Still creating... (10s elapsed)
aws_instance.web: Still creating... (20s elapsed)
aws_instance.web: Creation complete after 23s (ID: i-04d5d6e1228b2e3f9)

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

Ta-da! Your new, shiny instance has been created and is now up and running.

Removing components

Maybe you would like to get rid of that instance now? You can use the terraform destroy command for this.

zcat ~/terraform-example > terraform destroy -target=aws_instance.web
aws_instance.web: Refreshing state... (ID: i-04d5d6e1228b2e3f9)

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.web


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

Do you really want to destroy?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_instance.web: Destroying... (ID: i-04d5d6e1228b2e3f9)
aws_instance.web: Still destroying... (ID: i-04d5d6e1228b2e3f9, 10s elapsed)
aws_instance.web: Still destroying... (ID: i-04d5d6e1228b2e3f9, 20s elapsed)
aws_instance.web: Still destroying... (ID: i-04d5d6e1228b2e3f9, 30s elapsed)
aws_instance.web: Still destroying... (ID: i-04d5d6e1228b2e3f9, 40s elapsed)
aws_instance.web: Destruction complete after 41s

Destroy complete! Resources: 1 destroyed.

The observant reader will have noticed I used the command terraform destroy -target=aws_instance.web, explicitly specifying the name of the resource I want to destroy. I find this to be a good habit, as running terraform destroy without any parameters can have disastrous consequences.

If you want to create the instance again, just run terraform plan and terraform apply.

Enabling remote backends with state locking

Terraform has support for backends, which is a way of storing the current state. The default backend is local, which means the state will be saved in the Terraform working directory, in a file called terraform.tfstate.

But assuming you work in a team, having the current state available locally is no good, unless your team members are also able to access it.

Sure, you could commit the terraform.tfstate file between every state change, but that would be a hassle, and prone to errors, as people will undoubtedly forget to push/pull the latest changes.

In this example, we will be changing the backend to an S3 bucket. We will also create a DynamoDB table, in order to implement state locking and consistency checking of our Terraform state.

First, we will create the bucket (using Terraform, of course!) by adding the following code:

# main.tf
resource "aws_s3_bucket" "tfstatebucket" {
  bucket = "rl.tfstate"
  acl    = "private"

  # We want to have versioning enabled, because it allows us to keep track of
  # the Terraform state history
  versioning {
    enabled = true
  }

  # We also want to make sure our bucket enables server-side encryption
  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        sse_algorithm = "AES256"
      }
    }
  }

  tags {
    Name = "Terraform state bucket"
  }
}

resource "aws_dynamodb_table" "tfstatedynamotable" {
  name            = "rl.tfstate"
  hash_key        = "LockID"
  read_capacity   = 5
  write_capacity  = 5

  attribute {
    name = "LockID"
    type = "S"
  }

  tags {
    Name = "Terraform state consistency"
  }
}

Run terraform plan and terraform apply to create the bucket and the DynamoDB table.

Now that we have our S3 bucket and our DynamoDB table, we can change the Terraform backend by adding the following code:

# main.tf
terraform {
  backend "s3" {
    bucket          = "rl.tfstate"
    key             = "terraform.state"
    region          = "eu-west-1"
    encrypt         = true
    dynamodb_table  = "rl.tfstate"
  }
}

Okay, now all we have to do is to initialize our Terraform configuration again, this time with our new backend.

zcat ~/terraform-example > terraform init

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "s3" backend. No existing state was found in the newly
  configured "s3" backend. Do you want to copy this state to the new "s3"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value: yes

Releasing state lock. This may take a few moments...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

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.23"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

There you go! The Terraform state is now stored in the S3 bucket we created.

Create a VCS repository with the files you have created in this article, and share it with your team in order for them to get started using Terraform too. Just remember to remove the terraform.tfstate and terraform.tfstate.backup files from your working directory!

That’s all for now, folks

This was a basic introduction to Terraform, which hopefully will get you started on the right track.

The documentation will be your best friend when writing HCL. There are also many examples of Terraform code that you can find online, and use as a starting point for your own infrastructure.

In my next article, I will be introducing you to the concept of modules in Terraform, which allows us to re-use Terraform configurations and keep our code DRY.

Just-Make-toolbox

make is a utility for automating builds. You specify the source and the build file and make will determine which file(s) have to be re-built. Using this functionality in make as an all-round tool for command running as well, is considered common practice. Yes, you could write Shell scripts for this instead and they would be probably equally good. But using make has its own charm (and gets you karma points).

Even this ... [continue reading]

Containerized Development Environment

Published on February 28, 2024

Ansible-runner

Published on February 27, 2024