Demystifying Terraform: A Beginner’s Guide to Infrastructure as Code

Rajesh Murali Nair
14 min readJun 7, 2023

--

What is Terraform?

HashiCorp Terraform is an infrastructure as code tool that lets you define both cloud and on-prem resources in human-readable configuration files that you can version, reuse, and share. You can then use a consistent workflow to provision and manage all of your infrastructure throughout its lifecycle. Terraform can manage low-level components like compute, storage, and networking resources, as well as high-level components like DNS entries and SaaS features.

How does Terraform works?

Terraform creates and manages resources on cloud platforms and other services through their application programming interfaces (APIs). Providers enable Terraform to work with virtually any platform or service with an accessible API.

HashiCorp and the Terraform community have already written thousands of providers to manage many different types of resources and services. You can find all publicly available providers on the Terraform Registry, including Amazon Web Services (AWS), Azure, Google Cloud Platform (GCP), Kubernetes, Helm, GitHub, Splunk, DataDog, and many more.

The core Terraform workflow consists of three stages:

  • Write: You define resources, which may be across multiple cloud providers and services. For example, you might create a configuration to deploy an application on virtual machines in a Virtual Private Cloud (VPC) network with security groups and a load balancer.
  • Plan: Terraform creates an execution plan describing the infrastructure it will create, update, or destroy based on the existing infrastructure and your configuration.
  • Apply: On approval, Terraform performs the proposed operations in the correct order, respecting any resource dependencies. For example, if you update the properties of a VPC and change the number of virtual machines in that VPC, Terraform will recreate the VPC before scaling the virtual machines.

What are the benefits of Terraform?

  • Track your infrastructure
  • Automate Changes
  • Standardize configurations
  • Collaborate

Install Terraform (Mac OS X)

  • First, install the HashiCorp tap, a repository of all our Homebrew packages.

$ brew tap hashicorp/tap

  • Now, install Terraform with

$ brew install hashicorp/tap/terraform

  • To upgrade to the latest version of Terraform, first update Homebrew

$ brew update

  • Then, run the upgrade command to download and use the latest Terraform version

$ brew upgrade hashicorp/tap/terraform

Enable tab completion

If you use either Bash or Zsh, you can enable tab completion for Terraform commands. To enable autocomplete, first ensure that a config file exists for your chosen shell.

For bash

$ touch ~/.bashrc

For Zsh

$ touch ~/.zshrc

Then install the autocomplete package.

$ terraform -install-autocomplete

Once the autocomplete support is installed, you will need to restart your shell.

Build Infrastructure

We will create a tutorial for building an infrastructure using terraform by provisioning an EC2 instance on Amazon Web Service (AWS).

Prerequisites

  • Terraform CLI installed
  • AWS CLI installed
  • AWS account and associated credentials that allow you to create resources.
  • To use your IAM credentials to authenticate the Terraform AWS provider, set the below environment variables

$ export AWS_ACCESS_KEY_ID=
$ export AWS_SECRET_ACCESS_KEY=

Write Configuration

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

We would need to create Terraform configuration in your own directory. Create a directory for your configuration

$ mkdir first-terraform-example

Change into the directory

$ cd first-terraform-example

Create a file to define your infrastructure

$ touch main.tf

Open main.tf in your vi editor, paste in the configuration below and save the file.

terraform {
required_provider {
aws = {
source = "hashicorp/aws"
verson = "~> 4.16"
}
}
required_version = ">= 1.2.0"
}

provider "aws" {
region = "us-west-2"
}

resource "aws_instance" "app_server" {
ami = "ami-0715c1897453cabd1"
instance_type = "t2.micro"

tags = {
Name = "First Terraform Example"
}
}

Description of each block of the above configuration

  • Terraform block
    The terraform {} block contains Terraform settings, including the required providers Terraform will use to provision your infrastructure.
    For each provider, the source attribute defines an optional hostname, a namespace, and the provider type. Terraform installs providers from the Terraform Registry by default. In this example configuration, the aws provider's source is defined as hashicorp/aws, which is shorthand for registry.terraform.io/hashicorp/aws.
    You can also set a version constraint for each provider defined in the required_providers block. The version attribute is optional, but we recommend using it to constrain the provider version so that Terraform does not install a version of the provider that does not work with your configuration. If you do not specify a provider version, Terraform will automatically download the most recent version during initialization.
  • Providers
    The provider block configures the specified provider, in this case aws. A provider is a plugin that Terraform uses to create and manage your resources.
  • Resources
    Use resource blocks to define components of your infrastructure. A resource might be a physical or virtual component such as an EC2 instance, or it can be a logical resource such as a Heroku application.
    Resource blocks have two strings before the block: the resource type and the resource name. In this example, the resource type is aws_instance and the name is app_server. The prefix of the type maps to the name of the provider. In the example configuration, Terraform manages the aws_instance resource with the aws provider. Together, the resource type and resource name form a unique ID for the resource. For example, the ID for your EC2 instance is aws_instance.app_server.

Initialize the directory

Initializing a configuration directory downloads and installs the providers defined in the configuration, in our case is the aws provider. Run the below command for initializing the directory.

$ terraform init

Output

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 4.16"...
- Installing hashicorp/aws v4.67.0...
- Installed hashicorp/aws v4.67.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

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.

Format and validate the configuration

The Terraform fmt command automatically updates configurations in the current directory for readability and consistency. It is used to rewrite Terraform configuration files to a canonical format and style. This command applies a subset of the Terraform language style conventions, along with other minor adjustments for readability.

$ terraform fmt

The terraform validate command validates the configuration files in a directory, referring only to the configuration and not accessing any remote services such as remote state, provider APIs, etc.

$ terraform validate

Create infrastructure

Apply the configuration now with the terraform apply command. Terraform will print output similar to what is shown below.

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
+ create

Terraform will perform the following actions:

# aws_instance.app_server will be created
+ resource "aws_instance" "app_server" {
+ ami = "ami-0715c1897453cabd1"
+ 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)
+ disable_api_stop = (known after apply)
+ disable_api_termination = (known after apply)
+ ebs_optimized = (known after apply)
+ get_password_data = false
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ iam_instance_profile = (known after apply)
+ id = (known after apply)
+ instance_initiated_shutdown_behavior = (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)
+ monitoring = (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
+ placement_partition_number = (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)
+ secondary_private_ips = (known after apply)
+ security_groups = (known after apply)
+ source_dest_check = true
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = "First Terraform Example"
}
+ tags_all = {
+ "Name" = "First Terraform Example"
}
+ tenancy = (known after apply)
+ user_data = (known after apply)
+ user_data_base64 = (known after apply)
+ user_data_replace_on_change = false
+ vpc_security_group_ids = (known after apply)
}

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.app_server: Creating...
aws_instance.app_server: Still creating... [10s elapsed]
aws_instance.app_server: Still creating... [20s elapsed]
aws_instance.app_server: Still creating... [30s elapsed]
aws_instance.app_server: Creation complete after 33s [id=i-09b0b79d1589b61e3]

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

Before Terraform applies any changes, it prints out the execution plan which describe the actions Terraform will take in order to change your infrastructure to match the configuration.

+ next to aws_instance.app_server means that Terraform will create this resource. Beneath that, it shows the attributes that will be set. (known after apply) means that the value will not be known until the resource is created.

Inspect State

After you have applied the configuration, Terraform writes data into a file called terraform.tfstate . Terraform stores the IDs and properties of the resources it manages in this file, so that it can update or destroy those resource in the future.

The Terraform state file is the only way Terraform can track which resources it manages, and often contains sensitive information, so you must store your state file securely and restrict access to only trusted team members who need to manage your infrastructure.

Manually Managing State

Terraform has a built-in command called terraform state for advanced state management. Use the list subcommand to list of the resources.

Change Infrastructure

Infrastructure is continuously evolving, and Terraform helps you manage that change. As you change Terraform configurations, Terraform builds an execution plan that only modifies what is necessary to reach your desired state.

When using Terraform in production, we recommend that you use a version control system to manage your configuration files, and store your state in a remote backend such as Terraform Cloud or Terraform Enterprise.

Update the ami of your instance. Change the aws_instance.app_server resource under the provider block in main.tf by replacing the current AMI ID with a new one.

I have change the ami = "ami-0715c1897453cabd1 from the above example configuration to ami = "ami-0bef6cc322bfff646" .

Output

aws_instance.app_server: Refreshing state... [id=i-0be09d8bc2a46b58b]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

# aws_instance.app_server must be replaced
-/+ resource "aws_instance" "app_server" {
~ ami = "ami-0715c1897453cabd1" -> "ami-0bef6cc322bfff646" # forces replacement
~ arn = "arn:aws:ec2:us-east-1:933480519557:instance/i-0be09d8bc2a46b58b" -> (known after apply)
~ associate_public_ip_address = true -> (known after apply)
~ availability_zone = "us-east-1b" -> (known after apply)
~ cpu_core_count = 1 -> (known after apply)
~ cpu_threads_per_core = 1 -> (known after apply)
~ disable_api_stop = false -> (known after apply)
~ disable_api_termination = false -> (known after apply)
~ ebs_optimized = false -> (known after apply)
- hibernation = false -> null
+ host_id = (known after apply)
+ host_resource_group_arn = (known after apply)
+ iam_instance_profile = (known after apply)
~ id = "i-0be09d8bc2a46b58b" -> (known after apply)
~ instance_initiated_shutdown_behavior = "stop" -> (known after apply)
~ instance_state = "running" -> (known after apply)
~ ipv6_address_count = 0 -> (known after apply)
~ ipv6_addresses = [] -> (known after apply)
+ key_name = (known after apply)
~ monitoring = false -> (known after apply)
+ outpost_arn = (known after apply)
+ password_data = (known after apply)
+ placement_group = (known after apply)
~ placement_partition_number = 0 -> (known after apply)
~ primary_network_interface_id = "eni-02c55fa31308a78ca" -> (known after apply)
~ private_dns = "ip-172-31-81-250.ec2.internal" -> (known after apply)
~ private_ip = "172.31.81.250" -> (known after apply)
~ public_dns = "ec2-44-201-216-160.compute-1.amazonaws.com" -> (known after apply)
~ public_ip = "44.201.216.160" -> (known after apply)
~ secondary_private_ips = [] -> (known after apply)
~ security_groups = [
- "default",
] -> (known after apply)
~ subnet_id = "subnet-0b9f00a28b1ce7a9a" -> (known after apply)
tags = {
"Name" = "First Terraform Example"
}
~ tenancy = "default" -> (known after apply)
+ user_data = (known after apply)
+ user_data_base64 = (known after apply)
~ vpc_security_group_ids = [
- "sg-0064035aff4269246",
] -> (known after apply)
# (5 unchanged attributes hidden)

- capacity_reservation_specification {
- capacity_reservation_preference = "open" -> null
}

- cpu_options {
- core_count = 1 -> null
- threads_per_core = 1 -> null
}

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

- enclave_options {
- enabled = false -> null
}

- maintenance_options {
- auto_recovery = "default" -> null
}

- metadata_options {
- http_endpoint = "enabled" -> null
- http_put_response_hop_limit = 2 -> null
- http_tokens = "required" -> null
- instance_metadata_tags = "disabled" -> null
}

- private_dns_name_options {
- enable_resource_name_dns_a_record = false -> null
- enable_resource_name_dns_aaaa_record = false -> null
- hostname_type = "ip-name" -> null
}

- root_block_device {
- delete_on_termination = true -> null
- device_name = "/dev/xvda" -> null
- encrypted = false -> null
- iops = 3000 -> null
- tags = {} -> null
- throughput = 125 -> null
- volume_id = "vol-0d5e0919324cf9d2a" -> null
- volume_size = 8 -> null
- volume_type = "gp3" -> null
}
}

Plan: 1 to add, 0 to change, 1 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.app_server: Destroying... [id=i-0be09d8bc2a46b58b]
aws_instance.app_server: Still destroying... [id=i-0be09d8bc2a46b58b, 10s elapsed]
aws_instance.app_server: Still destroying... [id=i-0be09d8bc2a46b58b, 20s elapsed]
aws_instance.app_server: Still destroying... [id=i-0be09d8bc2a46b58b, 30s elapsed]
aws_instance.app_server: Still destroying... [id=i-0be09d8bc2a46b58b, 40s elapsed]
aws_instance.app_server: Destruction complete after 41s
aws_instance.app_server: Creating...
aws_instance.app_server: Still creating... [10s elapsed]
aws_instance.app_server: Still creating... [20s elapsed]
aws_instance.app_server: Still creating... [30s elapsed]
aws_instance.app_server: Still creating... [40s elapsed]
aws_instance.app_server: Still creating... [50s elapsed]
aws_instance.app_server: Creation complete after 54s [id=i-0bbc67a8e48960b2e]

As you can see in the above output, the prefix -/+ indicates that Terraform will destroy and recreate the resource, rather than updating in-place. ~ prefix indicates that Terraform will update those attributes in-place.

Destroy Infrastructure

When you no longer want the infrastructure created then you can use terraform destroy command to terminate resources managed by your Terraform project.

Output

aws_instance.app_server: Refreshing state... [id=i-0bbc67a8e48960b2e]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
- destroy

Terraform will perform the following actions:

# aws_instance.app_server will be destroyed
- resource "aws_instance" "app_server" {
- ami = "ami-0bef6cc322bfff646" -> null
- arn = "arn:aws:ec2:us-east-1:933480519557:instance/i-0bbc67a8e48960b2e" -> null
- associate_public_ip_address = true -> null
- availability_zone = "us-east-1b" -> null
- cpu_core_count = 1 -> null
- cpu_threads_per_core = 1 -> null
- disable_api_stop = false -> null
- disable_api_termination = false -> null
- ebs_optimized = false -> null
- get_password_data = false -> null
- hibernation = false -> null
- id = "i-0bbc67a8e48960b2e" -> null
- instance_initiated_shutdown_behavior = "stop" -> null
- instance_state = "running" -> null
- instance_type = "t2.micro" -> null
- ipv6_address_count = 0 -> null
- ipv6_addresses = [] -> null
- monitoring = false -> null
- placement_partition_number = 0 -> null
- primary_network_interface_id = "eni-0f4ebccb3b7239b26" -> null
- private_dns = "ip-172-31-87-222.ec2.internal" -> null
- private_ip = "172.31.87.222" -> null
- public_dns = "ec2-34-239-103-143.compute-1.amazonaws.com" -> null
- public_ip = "34.239.103.143" -> null
- secondary_private_ips = [] -> null
- security_groups = [
- "default",
] -> null
- source_dest_check = true -> null
- subnet_id = "subnet-0b9f00a28b1ce7a9a" -> null
- tags = {
- "Name" = "First Terraform Example"
} -> null
- tags_all = {
- "Name" = "First Terraform Example"
} -> null
- tenancy = "default" -> null
- user_data_replace_on_change = false -> null
- vpc_security_group_ids = [
- "sg-0064035aff4269246",
] -> null

- capacity_reservation_specification {
- capacity_reservation_preference = "open" -> null
}

- cpu_options {
- core_count = 1 -> null
- threads_per_core = 1 -> null
}

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

- enclave_options {
- enabled = false -> null
}

- maintenance_options {
- auto_recovery = "default" -> null
}

- metadata_options {
- http_endpoint = "enabled" -> null
- http_put_response_hop_limit = 1 -> null
- http_tokens = "optional" -> null
- instance_metadata_tags = "disabled" -> null
}

- private_dns_name_options {
- enable_resource_name_dns_a_record = false -> null
- enable_resource_name_dns_aaaa_record = false -> null
- hostname_type = "ip-name" -> null
}

- root_block_device {
- delete_on_termination = true -> null
- device_name = "/dev/xvda" -> null
- encrypted = false -> null
- iops = 100 -> null
- tags = {} -> null
- throughput = 0 -> null
- volume_id = "vol-0a06b86a3d9b778fe" -> null
- volume_size = 8 -> null
- volume_type = "gp2" -> null
}
}

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

Do you really want to destroy all resources?
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.app_server: Destroying... [id=i-0bbc67a8e48960b2e]
aws_instance.app_server: Still destroying... [id=i-0bbc67a8e48960b2e, 10s elapsed]
aws_instance.app_server: Still destroying... [id=i-0bbc67a8e48960b2e, 20s elapsed]
aws_instance.app_server: Still destroying... [id=i-0bbc67a8e48960b2e, 30s elapsed]
aws_instance.app_server: Still destroying... [id=i-0bbc67a8e48960b2e, 40s elapsed]
aws_instance.app_server: Destruction complete after 42s

Destroy complete! Resources: 1 destroyed.

Define Input Variables

Define a variables in a new file called variables.tf with a block defining a new inst_name variable.

variable "inst_name" {
description = "Value of the Name tag for the EC2 instance"
type = string
default = "App Server Instance"
}

In main.tf , we can call the variable defined in variables.tf . Below is the changes made to the main.tf

resource "aws_instance" "app_server" {
ami = "ami-0bef6cc322bfff646"
instance_type = "t2.micro"

tags = {
Name = var.inst_name
}
}

Like the above example, you can define the multiple variables in variables.tf and call those variables in main.tf.

Query Data with Output

As we have seen in the above topic, how you use input variable to parameterize your Terraform configuration. Now we will see how to provide useful information to the Terraform user by using output values.

To define the output values, you will need to create a file called outputs.tf in the same directory where your main.tf exist. Below is the example of how you can define an output values inside outputs.tf file.

output "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.app_server.id
}

output "instance_public_ip" {
description = "Public IP of the EC2 instance"
value = aws_instance.app_server.public_ip
}

Output after running terraform plan

aws_instance.app_server: Refreshing state... [id=i-029567cb994ffb54e]

Changes to Outputs:
+ instance_id = "i-029567cb994ffb54e"
+ instance_public_ip = "54.242.9.24"

You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.

You will get the similar output if you run terraform apply .

Store Remote State

As already explained in the Inspect state section, terraform uses terraform.tfstate file to store all the infrastructure details created, modified & updated using Terraform project. When you apply a terraform configuration, terraform.tfstate file is created on the local. You can avoid terraform.tfstate file from being accessed by your teammates for security reasons. You can save this file in a remote state like Amazon S3, Terraform Cloud etc

In the below example, you can use Terraform Cloud to store your terraform.tfstate file.

  • Set up Terraform Cloud
    Sign up for Terraform Cloud and create an organization. Modify main.tf to add cloud block to your Terraform configuration.
terraform {
cloud {
organization = "organization-name"
workspaces {
name = "first-tf-cloud-aws"
}
}

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

required_version = ">= 1.2.0"
}

provider "aws" {
region = "us-west-2"
}

resource "aws_instance" "app_server" {
ami = "ami-08d70e59c07c61a3a"
instance_type = "t2.micro"
}

Modify organization-name with your organization name.

  • Login to Terraform Cloud
    Use terrafrom login command to login to Terrafrom Cloud account with Terraform CLI in your terminal.
  • Initialize Terraform
    Use terrform init command to initialize your configuration and migrate your state file to Terraform Cloud.
  • Set workspace variables
    When you run terraform init step, it will create workspace first-tf-cloud-aws in your Terraform Cloud organization. You need to configure your workspace with your AWS credentials to authenticate the AWS provider by adding Environmental variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY .
  • Apply the Configuration
    Run terraform apply to trigger a run in Terraform Cloud.

Terraform is now storing your state remotely in Terraform Cloud. Remote state storage makes collaboration easier and keeps state and secret information off your local disk. Remote state is loaded only in memory when it is used.

--

--