Demystifying Terraform: A Beginner’s Guide to Infrastructure as Code
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
Theterraform {}
block contains Terraform settings, including the required providers Terraform will use to provision your infrastructure.
For each provider, thesource
attribute defines an optional hostname, a namespace, and the provider type. Terraform installs providers from the Terraform Registry by default. In this example configuration, theaws
provider's source is defined ashashicorp/aws
, which is shorthand forregistry.terraform.io/hashicorp/aws
.
You can also set a version constraint for each provider defined in therequired_providers
block. Theversion
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
Theprovider
block configures the specified provider, in this caseaws
. A provider is a plugin that Terraform uses to create and manage your resources. - Resources
Useresource
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 isaws_instance
and the name isapp_server
. The prefix of the type maps to the name of the provider. In the example configuration, Terraform manages theaws_instance
resource with theaws
provider. Together, the resource type and resource name form a unique ID for the resource. For example, the ID for your EC2 instance isaws_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. Modifymain.tf
to addcloud
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
Useterrafrom login
command to login to Terrafrom Cloud account with Terraform CLI in your terminal. - Initialize Terraform
Useterrform init
command to initialize your configuration and migrate your state file to Terraform Cloud. - Set workspace variables
When you runterraform init
step, it will create workspacefirst-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 variablesAWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
.
- Apply the Configuration
Runterraform 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.