A Complete Guide to Terraform: Automate Your Infrastructure

A Complete Guide to Terraform: Automate Your Infrastructure

Why Use Terraform for Infrastructure as Code (IaC)?

Imagine you’re a DevOps engineer and you’re assigned a simple task to create an S3 bucket in AWS. Normally, you would log in to your AWS account, search for the S3 service, and manually create the bucket by filling in the necessary details. This works fine if you're only creating one bucket.

But what if you need to create 100 or even 1,000 S3 buckets? Doing this manually would take a lot of time and effort. In situations like this, you’d want a more programmatic approach—using the AWS CLI or scripting to interact with AWS APIs. With these tools, you could create all the required buckets in seconds. However, this requires good programming knowledge, and things get complicated when you need to create multiple resources together, like a VPC, EC2 instances, and S3 buckets.

To address these challenges, cloud providers offer Infrastructure as Code (IaC) tools. These tools let you define your infrastructure in code, using formats like YAML or JSON, and automate the provisioning of resources. AWS, for example, provides CloudFormation, which allows you to define AWS resources in templates.

Here are some examples of IaC tools:

  • AWS CloudFormation (for AWS)

  • Azure Resource Manager (for Azure)

  • Heat Template (for OpenStack)

Why Terraform?

With so many IaC tools available, why use Terraform? The answer lies in its flexibility and universal approach.

Let’s say you’re working with AWS and using CloudFormation, but later you switch to an organization that uses Azure. You would then need to learn Azure’s IaC tool. While you can certainly learn these tools, it can be a complex and time-consuming process.

Terraform solves this problem by providing a cloud-agnostic IaC tool. It allows you to manage infrastructure across multiple cloud platforms using a single language—HashiCorp Configuration Language (HCL). This means you don’t need to learn different IaC tools for different clouds. Just learn Terraform, and you can work with any cloud provider.

This is why Terraform has become such a popular and essential tool for DevOps and cloud engineers.


Install Terraform & aws cli on your system using official documentation

How to Configure AWS CLI with Access Keys

  • Go to your AWS account and find the secuirity credentials option

  • Inside Security Credentials, create a new Access Key. This will provide you with an Access Key ID and a Secret Access Key

  • Open your terminal and run the following command

      aws configure
    

    You will be prompted to enter the Access Key ID and Secret Access Key. Paste the keys you just created to connect your AWS account to the CLI

  • To ensure everything is working, run

      aws s3 ls
    

    This command will list all the S3 buckets in your AWS account

Create an EC2 instance using terraform

  • Create a Terraform Configuration File (main.tf)

      provider "aws" {
          region = "us-east-1"  # Set your desired AWS region
      }
    
      resource "aws_instance" "example" {
          ami           = "ami-0c55b159cbfafe1f0"  # Specify an appropriate AMI ID
          instance_type = "t2.micro"
    

  • Run the following command to initialize your Terraform project

      terraform init
    

    This command prepares the working directory by downloading the necessary provider plugins (in this case, for AWS) and setting up the environment

  • Preview Changes Using terraform plan

      terraform plan
    

    This command will show you the "execution plan"—detailing what resources Terraform will create, modify, or destroy. It helps you confirm that everything looks correct before applying changes

  • Apply Changes Using terraform apply

      terraform apply
    

    It is used to execute the changes described in your configuration files and actually create, modify, or delete infrastructure resources. After running terraform plan to preview the changes, terraform apply makes those changes happen

    If you encounter an error like this

    This happens because the AMI ID provided may not be valid in your AWS region

  • To resolve this Error

    1. Go to your AWS account

    2. In the EC2 Dashboard, search for a valid AMI ID

    3. Copy the correct AMI ID

    4. Replace the invalid AMI ID in the main.tf file with the correct one

  • Once you have the correct AMI ID, run the terraform apply command again to create the EC2 instance

    If the command runs successfully, an EC2 instance will be created in your AWS account

  • Run terraform destroy to delete all infrastructure resources that Terraform has created and is managing. This command will remove everything that is defined in your state file, essentially tearing down your entire infrastructure

Terraform State

Terraform keeps a file called a "state file" that stores the current setup of your infrastructure. This file helps Terraform figure out what changes need to be made by comparing the setup you want with what already exists. It uses this information to apply updates correctly.


Terraform Providers

In Terraform, providers are plugins that allow Terraform to communicate with cloud platforms, services, or other APIs. Providers essentially tell Terraform which services to interact with, such as AWS, Azure, Google Cloud, etc.

When setting up infrastructure, you define providers in your configuration file using the provider block. This tells Terraform what cloud services you are going to use and sets necessary parameters such as region or authentication information.

Some examples of providers:

  • azurerm - for Azure

  • google - for Google Cloud Platform

  • kubernetes - for Kubernetes

  • openstack - for OpenStack

  • vsphere - for VMware vSphere

Single Provider Example (AWS)

If you want to use Terraform to manage infrastructure on AWS, you first define the AWS provider

provider "aws" {
  region = "us-east-1"  # Specify the AWS region
}

Here, the region us-east-1 is specified, which tells Terraform that resources should be created in the US East region.

Multiple Region Setup

Terraform supports setting up resources across multiple regions within the same cloud provider by using the alias keyword. This allows you to define multiple instances of the same provider, each targeting a different region.

provider "aws" {
  alias  = "us-east-1"  # Alias to identify this provider instance
  region = "us-east-1"
}

provider "aws" {
  alias  = "us-west-2"  # Alias for another region
  region = "us-west-2"
}

resource "aws_instance" "east_instance" {
  ami           = "ami-0123456789abcdef0"
  instance_type = "t2.micro"
  provider      = "aws.us-east-1"  # Use the east region provider
}

resource "aws_instance" "west_instance" {
  ami           = "ami-0123456789abcdef0"
  instance_type = "t2.micro"
  provider      = "aws.us-west-2"  # Use the west region provider
}

You can check the ec2 instance is created in both us-east-1 & us-west-2 region

In this example

  • Two provider blocks are created for AWS, one for the US East region and one for the US West region.

  • The alias keyword helps Terraform distinguish between different instances of the same provider.

  • Each resource (EC2 instance) is tied to a specific provider using the provider attribute.ami-0123456789abcdef0

Multi-Cloud Setup

Terraform allows you to use multiple providers in one project, enabling you to manage infrastructure across different cloud platforms simultaneously. Here's how you can set this up ?

  • Create a providers.tf File

    Begin by creating a providers.tf file in the root directory of your Terraform project. This file will define the cloud providers that you want to use.

  • Define Providers in providers.tf

    In the providers.tf file, define the providers for AWS and Azure.

      provider "aws" {
        region = "us-east-1"
      }
    
      provider "azurerm" {
        subscription_id = "your-azure-subscription-id"
        client_id       = "your-azure-client-id"
        client_secret   = "your-azure-client-secret"
        tenant_id       = "your-azure-tenant-id"
      }
    

    This configuration sets up AWS and Azure as providers in your project. Replace the placeholder values with your actual credentials.

  • Use Providers in Resource Definitions

    Once you've configured the providers, you can create resources in AWS and Azure within the same project.

      resource "aws_instance" "example" {
        ami           = "ami-0123456789abcdef0"
        instance_type = "t2.micro"
      }
    
      resource "azurerm_virtual_machine" "example" {
        name     = "example-vm"
        location = "eastus"
        size     = "Standard_A1"
      }
    

    In this example

    • An EC2 instance is provisioned in AWS using the aws_instance resource.

    • A virtual machine is provisioned in Azure using the azurerm_virtual_machine resource.

Terraform Variables

Terraform variables make your configuration more dynamic, reusable, and flexible. Instead of hardcoding values directly in your code, you define variables that can be reused across your configuration. This makes it easier to adapt the same infrastructure for different environments or teams.

Types of Variables

  1. Input Variables

    It allow you to parameterize your Terraform configurations. This means you can pass values into your modules or configurations from the outside (for example, when running terraform apply). Input variables can be defined at both the module level and the root level.

    Defining Input Variables

    You can define an input variable in Terraform like this

    variable "instance_type" {
      description = "EC2 instance type"  # A description for documentation
      type        = string               # The expected type (string, number, list, etc.)
      default     = "t2.micro"           # Default value if not provided
    }
    

    This block defines an input variable instance_type, which can be set when running the configuration, or it will use the default value of "t2.micro" if not provided

    Using Input Variables

    After defining a variable, you can reference it using the var keyword inside your configuration

    resource "aws_instance" "example_instance" {
      ami           = var.ami_id         # Referencing the variable value
      instance_type = var.instance_type  # Referencing another variable
    }
    

    When you run terraform apply, you can pass the value of the input variables through the command line or in a .tfvars file

  2. Output Variables

    Output variables are used to display values after the execution of a configuration. These are useful for retrieving key information from your infrastructure, such as the IP address of a created instance or resource IDs.

    Defining Output Variables

    You can define an output variable like this

    output "public_ip" {
      description = "Public IP address of the EC2 instance"
      value       = aws_instance.example_instance.public_ip  # The value to output
    }
    

    In this case, after Terraform finishes creating the EC2 instance, it will display the public IP address of the instance as part of the output

Organizing Terraform Files

When working on large projects, it’s important to organize your Terraform files for better readability and maintenance. A typical structure might look like this:

  • providers.tf: Contains provider configurations.

  • variables.tf: Defines all input variables.

  • outputs.tf: Defines output variables.

  • main.tf: Contains the core infrastructure resources.

For projects involving multiple environments (development, production, staging), use a terraform.tfvars file to separate the actual values of the variables

Conditional Expressions

Conditional expressions in Terraform allow you to define dynamic behavior based on conditions. They work similarly to ternary operators (? :) in programming languages. You can use them to decide whether resources should be created or how they should be configured based on variable values or other conditions.

Syntax of Conditional Expressions

The syntax of a conditional expression in Terraform is

condition ? true_value : false_value

This evaluates the condition. If it’s true, it returns the true_value, otherwise, it returns the false_value.

Conditional Resource Creation

You can conditionally create resources using the count attribute:

resource "aws_instance" "example" {
  count = var.create_instance ? 1 : 0  # Create instance if create_instance is true

  ami           = "ami-0123456789abcdef0"
  instance_type = "t2.micro"
}

In this example, the instance will only be created if the variable create_instance is set to true. If false, Terraform will create 0 instances.

Conditional Resource Configuration

You can use conditional expressions within resource configuration blocks to dynamically adjust settings:

resource "aws_security_group" "example" {
  name        = "example-sg"
  description = "Example security group"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = var.enable_ssh ? ["0.0.0.0/0"] : []  # Conditionally allow SSH
  }
}

In this example, SSH access is only allowed if the variable enable_ssh is set to true. Otherwise, the security group won’t allow any incoming SSH connections.

Terraform Built-in Functions

Terraform has a set of built-in functions that make it easier to manipulate and transform data in your configuration. These functions allow you to perform operations on lists, strings, and maps

Common Built-in Functions

  1. concat(list1, list2, ...) Combines multiple lists into a single list.

     variable "list1" {
       type    = list
       default = ["a", "b"]
     }
    
     variable "list2" {
       type    = list
       default = ["c", "d"]
     }
    
     output "combined_list" {
       value = concat(var.list1, var.list2)  # Returns ["a", "b", "c", "d"]
     }
    
  2. element(list, index) Retrieves an element from a list based on the given index.

     variable "my_list" {
       type    = list
       default = ["apple", "banana", "cherry"]
     }
    
     output "selected_element" {
       value = element(var.my_list, 1)  # Returns "banana"
     }
    
  3. length(list) Returns the number of elements in a list.

     variable "my_list" {
       type    = list
       default = ["apple", "banana", "cherry"]
     }
    
     output "list_length" {
       value = length(var.my_list)  # Returns 3
     }
    
  4. lookup(map, key) Retrieves the value from a map by the specified key.

     variable "my_map" {
       type    = map
       default = {
         name  = "Alice"
         age   = 25
       }
     }
    
     output "value" {
       value = lookup(var.my_map, "name")  # Returns "Alice"
     }
    
  5. join(separator, list) Joins the elements of a list into a single string, separated by the specified separator.

     variable "my_list" {
       type    = list
       default = ["apple", "banana", "cherry"]
     }
    
     output "joined_string" {
       value = join(", ", var.my_list)  # Returns "apple, banana, cherry"
     }
    

Module in Terraform

In Terraform, modules are like building blocks that help organize and reuse your infrastructure code. Instead of writing the same code over and over, you can group related resources into a module and use it multiple times. It make your Terraform setup easier to manage, reuse, and scale without duplicating code.


Provisioners in Terraform