Home/Blog/Cloud Computing/A comprehensive Terraform tutorial for beginners
Home/Blog/Cloud Computing/A comprehensive Terraform tutorial for beginners

A comprehensive Terraform tutorial for beginners

Ehtesham Zahoor
Nov 18, 2024
10 min read

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

"All of our AWS infrastructure for iOS CI is specified in Terraform code that we check into source control. Each time we merge a pull request related to iOS CI, Terraform Enterprise will automatically apply our changes to our AWS account." —Michael Bachand, an engineer at Airbnb

In its early days, Airbnb struggled with scalability because of infrastructure inconsistencies. With environments spread across multiple cloud providers, manually managing infrastructure was becoming a bottleneck, leading to inconsistent deployments and resource inefficiencies.

To address these challenges, Airbnb transitioned to an infrastructure as code (IaC) approach, choosing Terraform to automate its infrastructure deployments. This shift enabled Airbnb to streamline infrastructure management across all environments, ensuring consistency, and enhanced operational efficiency.

Terraform is a popular infrastructure as code tool used for provisioning, updating, and versioning infrastructure. Its declarative approach, extensive provider support, and features like state management and execution plans make it a popular choice for DevOps teams and organizations looking to implement infrastructure as code practices.

What is infrastructure as code?#

Infrastructure as code (IaC) is a methodology to define and manage infrastructure using code instead of relying on manual processes. IaC offers many benefits because it allows for the same infrastructure to be consistently replicated across different environments. Furthermore, it offers better scalability and automation support. In addition, the infrastructure configuration can be version-controlled, allowing for easy collaboration and change management.

Infrastructure as code
Infrastructure as code

Terraform, developed by HashiCorp, is a popular IaC tool. However, alternatives are available, including:

  • AWS CloudFormation is Amazon Web Services’ native IaC tool. It allows users to define and provision AWS infrastructure using JSON or YAML templates. Unlike Terraform, CloudFormation is tightly integrated with AWS services and provides native support for AWS resources.

  • Azure Resource Manager (ARM) and Google Cloud Deployment Manager are Microsoft Azure’s and Google Cloud Platform’s native IaC tools, respectively.

  • Ansible is a configuration management and automation tool that can also be used for infrastructure provisioning. It uses simple YAML-based playbooks to define tasks and configurations. While Ansible offers some similarities to Terraform, its focus is more on configuration management rather than infrastructure provisioning.

  • Chef and Puppet are also commonly used configuration management tools, but they can also be used to provision infrastructure. However, compared to Terraform, they might not provide the same infrastructure provisioning capabilities.

The list above is non-exhaustive, and a wide range of IaC tools are available, such as Pulumi and OpenStack Heat. A thorough comparison of features provided by these IaC tools is beyond the scope of this blog.

How Terraform works#

Terraform uses declarative human-readable configuration files to define and provision infrastructure. It works with over 1000 infrastructure providers, including Amazon Web Services (AWS), Google Cloud Platform (GCP), Azure, Kubernetes, and others. It allows us to write custom providers as well.

Terraform uses HashiCorp Configuration Language (HCL) to define IaC. HCL uses a declarative syntax to describe the desired state of infrastructure rather than specifying the sequence of actions needed to achieve that state.

It is important to note that Terraform uses declarative language, and this makes it different from some other procedural IaC tools that require step-by-step instructions. Again, a thorough discussion of the pros and cons of this approach is beyond the scope of this blog.

The overall process involves first writing the desired infrastructure as code using HCL. Then, we initialize Terraform to install any plugins required to interact with the infrastructure. Afterward, we instruct Terraform to apply the required changes on the platform (we can optionally preview the changes being made first). The overall process is represented in the figure below:

Terraform workflow
Terraform workflow

Terraform uses APIs to interact with the infrastructure providers (AWS, Azure, local, and others). Terraform also maintains a state file to keep track of the current state of infrastructure and uses it to decide the changes to be made to reach the desired state.

We can use CLI commands to interact with Terraform. Some commonly used commands are listed below:

  • terraform init: It initializes the working directory, resolves dependencies, and installs required plugins. It also creates a lock file to manage versions.

  • terraform plan: It creates an execution plan by comparing the initial and the required infrastructure and provides insights into the changes required before making the changes. It does not make any changes to the infrastructure at this stage.

  • terraform apply: It applies the changes on the provider to match the desired state as specified in the configuration file.

  • terraform destroy: It destroys the provisioned infrastructure.

Dive deeper: If you’d like to learn Terraform in a live environment without the hassle of setting up and cleaning , consider taking the course Terraform: From Beginner to Master with Examples in AWS by Educative. Otherwise, read on.

Setting up your first project#

Let’s start with a simple example that does not use an external provider and simply creates a file locally. We need to first install Terraform and then create a file named main.tf with the following contents:

provider "null" {}
resource "null_resource" "create_txt_file" {
provisioner "local-exec" {
command = "echo 'Hello Terraform!' > output.txt"
}
}

The code above is written in HCL. Before going further, let’s discuss it briefly:

  • We first define a provider block, which generally allows Terraform to interact with infrastructure providers. Here, we’re using the null provider, which is a Terraform provider that does nothing. This is useful for running local-only resources or for testing purposes.

  • We then define a resource block which is generally used to configure a resource in the infrastructure provider. In the code above, we define a null_resource named create_txt_file. A null_resource is a resource that Terraform manages but has no corresponding infrastructure object. It’s often used for running local-only actions.

  • Inside the resource block, we have a provisioner block with the type local-exec. A provisioner block is used to specify actions to prepare resources on the infrastructure. In our case, it executes a local command when the resource is created.

We can initialize, optionally preview the changes required, and then apply the changes on the infrastructure using the commands below:

terraform init
terraform plan #optionally preview the changes required
terraform apply
Applying the changes on the infrastructure

On the successful execution of terraform apply, a new file named output.txt will be created in the directory with the specified contents.

Congratulations on managing your first project with Terraform! Before going further, let’s briefly discuss the key elements of HCL.

Basic syntax and blocks#

As we can infer from the example above, Terraform code files have the .tf extension. HCL supports both single-line (using #) and multi-line comments (using the /* opening and the */ closing). A Terraform file consists of different types of blocks containing key=value pairs. Some commonly used block types are mentioned below. We’ll have a working example of their usage in the last part of this blog.

  • The terraform block is a special block used to define the behavior of Terraform execution, such as specifying Terraform version and others.

  • A provider block is used to specify the provider to be used. A provider block can contain further specifications based on the provider.

  • Each resource block represents a resource in the provisioned infrastructure. These resource blocks can have further resource specifications.

  • A variable block is used to define variables that are used in the Terraform configuration file. An output block is used to return values after running the Terraform configuration.

Provisioning an AWS EC2 instance#

We’ve learned that Terraform works with over 1000 infrastructure providers, including AWS, GCP, Azure, Kubernetes, and others. Let’s improve our simple example to provision an Amazon Elastic Compute Cloud (EC2) instance.

To interact with AWS services, we need an authorized AWS account and credentials, typically IAM access keys, for authentication. One basic approach is to provide credentials in the provider configuration as follows:

#Just an example, do not use this!
provider "aws" {
access_key = "aws-access-key-id"
secret_key = "aws-secret-access-key"
...
}
Passing AWS credentials

This approach is, of course, highly insecure, and because Terraform files can be version-controlled, there is a strong possibility of access keys being compromised. Generally, hard-coding any credentials in source files should always be avoided. Luckily, we have better options, including dynamic provider credentials by AWS. However, since this blog is not focused on securing Terraform workflows, we’ll use the common approach of using environment variables using the commands below by appending the required credentials:

export AWS_ACCESS_KEY_ID=
export AWS_SECRET_ACCESS_KEY=

We can optionally provide the region as an environment variable, but we’ll handle this in our configuration file. The complete code required to provision an EC2 instance is as follows. Don’t worry, we’ll discuss the code afterward.

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.1.0"
}
}
required_version = ">= 1.5.0"
}
provider "aws" {
region = "us-east-1"
}
variable "instancename" {
description = "The Name tag for the instance"
type = string
default = "ezinstance"
}
resource "aws_instance" "example" {
ami = "ami-0e731c8a588258d0d"
instance_type = "t2.micro"
tags = {
Name = var.instancename
}
}
output "instance_id" {
description = "The ID of the created instance"
value = aws_instance.example.id
}

The code shown in the widget above is easier to understand, but before discussing it, let’s provision the infrastructure first. Assuming the file is placed in another folder and the required environment variables for AWS access keys are set up, the process is straightforward. We use terraform init followed by terraform apply and that’s it. Once the instance has been launched, we’ll get the instance_id in the output and we can also see the instance running using the AWS Management Console or CLI. Terraform is indeed easy to learn!

Remember to clean up the infrastructure and terminate the instance when no longer needed. We leave this as an exercise for you to figure out how to destroy the provisioned infrastructure using Terraform.

Let’s conclude this section by discussing the different configuration blocks shown above.

The terraform block #

We have learned that the terraform block is a special block used to define the behavior of Terraform executions, such as specifying the Terraform version and others. For our configuration, the terraform block is as follows:

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.1.0"
}
}
required_version = ">= 1.5.0"
}
The terraform block

The block specifies the required Terraform version and providers. The required_providers part specifies that the configuration requires the AWS provider from HashiCorp. We also specify the version for the AWS provider using the “pessimistic constraint operator ~>” to be 5.1.x. This is, of course, just an example; our configuration would also work with the previous versions. Similarly, the required_version specifies the minimum version of Terraform required to apply this configuration.

Notice that when we applied terraform init, it created a hidden file named terraform.lock.hcl to capture the specific versions of providers used in our configuration. This ensures that the same version will be used in any future build, allowing for reproducible builds.

The provider block#

A provider block is used to specify the provider to be used and allows for specifying attributes related to the provider. In our case, we are using the AWS provider, and in the provider block itself, we are specifying the AWS region, us-east-1 for provisioning an EC2 instance.

provider "aws" {
region = "us-east-1"
}
Specifying the region in provider block

Input and output variables#

We have learned that we can define the variables to be used in the Terraform configuration file using the variable blocks. In our case, we define a variable to hold the instance name used for provisioning the instance. Similarly, at the end of our configuration, we define an output block to return values after running the Terraform configuration. In our case, it is the id of the newly created instance.

variable "instancename" {
description = "The Name tag for the instance"
type = string
default = "ezinstance"
}
/* ... */
output "instance_id" {
description = "The ID of the created instance"
value = aws_instance.example.id
}
Defining input and output variables

The resource block#

The resource block represents a resource in the provisioned infrastructure and allows for specifying resource attributes. In our case, the resource block defines an AWS EC2 instance specified by the resource type as aws_instance named example. Inside the block, we specify resource-specific attributes, including the Amazon Machine Image (AMI) to use for the instance, determining the computing resources allocated to the instance. The tags block adds metadata to the EC2 instance. In this case, it adds a tag with the key Name and the value to be the value of instancename defined earlier.

resource "aws_instance" "example" {
ami = "ami-0e731c8a588258d0d"
instance_type = "t2.micro"
tags = {
Name = var.instancename
}
}
Specifying the resource-specific attributes

Conclusion and next steps#

In this tutorial, we introduced Terraform, its core concepts, and how to set up a basic AWS EC2 instance. However, we’ve just scratched the surface, and we encourage you to dive deeper into Terraform’s features, such as modules, provisioners, data sources, remote backends, and workspaces, and to integrate Terraform into CI/CD pipelines. Check out these Cloud Labs at Educative to gain practical, hands-on experience with using Terraform in conjunction with AWS.

Get Hands-On Practice Deploying AWS Resources using Terraform

Get Hands-On Practice Deploying AWS Resources using Terraform

These Cloud Labs provide a unique opportunity to work directly with cloud services using Terraform to solve practical challenges and implement real-world solutions. The first lab is designed for beginners, while the second provides a more advanced experience.

These Cloud Labs provide a unique opportunity to work directly with cloud services using Terraform to solve practical challenges and implement real-world solutions. The first lab is designed for beginners, while the second provides a more advanced experience.

Frequently Asked Questions

Is Terraform tough to learn?

Terraform is often viewed as easy to learn, especially for those with some background in cloud computing or infrastructure concepts.

Do I need to know coding for Terraform?

Is Terraform open-source?

Is Terraform free on AWS?

Which language is Terraform written in?


  

Free Resources