Terraform Zero to One

November 20, 2024

If you have heard of "Terraform" or "Infrastructure as Code" and were curious to try your hand at deploying some simple infrastructure with AWS, this guide is a great starting point.

This guide was originally written for my O'Reilly course "Learn Terraform in 4 Hours".

The code for this project can be found here along with a bunch of other examples.

What is infrastructure as code

Rather than managing infrastructure in a GUI like the AWS Console or Google Cloud console, with infrastructure-as-code tools such as Terraform, you can manage infrastructure in configuration files.

Infrastructure as Code has several benefits over manually managing infrastructure.

  • Create, deploy, and manage infrastructure in a consitent way
  • You can commit to source control and collaborate on infrastructure
  • You can reuse and share configurations

Examples of infrastructure as code tools:

  • Terraform
  • Serverless Framework
  • Cloud Formation

What is Terraform

Terraform is HashiCorp's infrastructure as code tool. It uses the HashiCorp configuration language to allow devops engineers to write readable, declarative, reusable configurations for infrastructure. It also provids a command-line tool that provides a straight-forward workflow for updates.

Terraform has some advantages over other IaC options:

  • You can use it to deploy to multiple cloud platforms and services.
  • It uses HashiCorp Configuration Language (HCL) which is easy to read, understand, and write.
  • It has a built-in workflow.
  • Provider specific definitions

Terraform Use Cases

Terraform use cases are as varied as all devops projects.

  • Create cloud networking environment
  • Deploy web applications
  • Declare data-pipelines
  • Manage Security around infrastructure
  • And many more...

Example Code

resource "aws_instance" "test_server" {
  ami = "ami-0f19d220602031aed"
  instance_type = "t2.nano"

  key_name = "sshkeyname"

  tags = {
    name: "test server"
  }
}

How it works

Either Terraform, or third-parties, write "providers" which map command-line tools from service providers to resources that can be used in Terraform configurations. Providers exist for AWS, Google Cloud, Azure, Heroku, Kubernetes, Digital Ocean, Terraform Cloud, and many more.

First the practitioner writes Terraform code. Then they plan and apply their changes. This uses the provider definitions to deploy the defined resources to the one or many clouds.

Terraform Configuration Grammar

HashiCorp Configuration Language

The Terraform configuration language is a superset of the HashiCorp Configuration language or HCL. HCL is used for other tools created by HashiCorp as well.

Declarative

One key aspect of Terraform's language is that it is declarative. The code will be interpeted the same way every time we run it. There is no runtime logic, code forking, or loops.

Blocks, Arguments, and Identifiers

Blocks

Blocks are the chuncks of code that do things in Terraform. For example, the following block has the resource block type.

resource "aws_instance" "test_instance" {}

Each block type defines how many labels are expected after the type. In this case, resource expects 2 labels: The resource type and the resource name.

Arguments

Inside the {} we can include arguments as seen below.

resource "aws_instance" "test_instance" {
  ami = "ami-0f19d220602031aed"
  instance_type = "t2.nano"

  tags = {
    name: "test server"
  }
}

The arguments depend on the particular block type. Variable blocks allow type, default, sensitive and description arguments. Resource arguments are determined by their definition from the provider. For example, see the aws_instance resource.

Identifiers

Argument names, block type names, and labels are all know as identifiers. Pretty much its a name for any given item in Terraform.

Comments

There are 3 comment types. The first two are single line, the last is for multi-line comments.

# comment
// comment
/* comment */

Workflow

Next we'll learn about the Terraform workflow by creating and deploying an EC2 instance.

Configuration

First lets create a configuration. Here we are using 3 different block types: terraform, provider, and resource.

terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~>3.27"
    }
  }

  required_version = ">= 0.14.9"
}

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

resource "aws_instance" "test_server" {
  ami = "ami-0f19d220602031aed"
  instance_type = "t2.nano"

  key_name = "terraformclass"

  tags = {
    name: "Test server"
  }
}

Terraform Block

The opening terraform {} block is a sort of requirements declaration for the project. This is where we can tell Terraform which providers, and which versions of providers are required in the project. (more on providers below) The required_providers block lets us pass in providers with version constraints.

Providers include a library of resources available. There are different versions of each provider. Restricting the version is important for maintaining consistent configuration across machines. If you don't include a provider version, Terraform will try to pick the latest. This can cause mis-matches across machines. If the project uses more than one cloud provider, you can include the definition for each here.

Provider Blocks

The provider {} block defines a specific provider. If we have multiple providers such as AWS and Google Cloud, then we will have a separate block for each. Also, if we are working with different regions in AWS, we will need a separate provider block for each. In our case, we pass a profile argument to tell our provider which AWS profile to use, and a region to tell the provider where we want this to be deployed to.

Resources

The resource block is used to define resources in our infrastructure. In this example we are creating an aws_instance and calling it test_server. The name test_server needs to be unique within our configs. Elsewhere in the configs we can use this name as a reference: aws_instance.test_server. aws_instance has many outputs that we can reference as well (more on that later).

The definition of aws_instance is given by the AWS provider. Terraform provides a reference document for all supported resources.

Set up

Before we get into the workflow, we have to have install and configure aws-cli. You will need an aws profile with access to create resources.

CLI Workflow

The workflow in Terraform is simple and powerful. The commandline tool is focused on the basics while the configurations focus on the specifics.

Initialize

terraform init

Once your configuration is ready, run terraform init. This creates a .terraform directory and installs the required provider. You will have run this again when importing modules or other providers. More on that later.

Plan

terraform plan

Another optional step is to run terraform plan this will display the execution plan. It compares your configuration with what exists in the cloud without risking a deploy. You can use this to review your changes.

Apply

terraform apply

Deploying our infrastructure is as simple as running terraform apply, this will check formatting, validate the configs, and show the execution plan. You will be prompted to review the execution plan and type "yes" if you are ready to create the resources.

If everything works you'll see a message like Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

When we make a change to the configuration, we can deploy the changes with terraform apply. Let's change the instance type.

--  ami = "ami-0f19d220602031aed"
++  ami = "ami-0b614a5d911900a9b"

Then run terraform apply. You can see this change will require the instance be torn down and recreated. Other changes can happen without tearing down the resource. Pay close attention when deploying production code to avoid taking down your applications. Or better yet, set up your application to allow replacement of resources without going down.

Destroy

terraform apply -destroy

When we no longer need infrastructure, we can destroy it permanently using terraform apply -destroy or terraform destroy. There is no turning back once this runs, though, we can always use our configuration to spin up our project again.

Format and Validate

terraform fmt
terraform validate

An optional step you can take is to run terraform fmt to check for any malformated files. terraform validate will determine if your configuration is valid. This checks for things like syntax errors or missing references, resources, and modules.

Conclusion

There you have it! If you followed along you have deployed your first infrastructure with Terraform.

Did you find this helpful? Want me to write more? Drop me a line and let me know!