DEV Community

Cover image for Terraform for Loops Guide: Types, Examples and Best Practices
env0 Team
env0 Team

Posted on • Originally published at env0.com

Terraform for Loops Guide: Types, Examples and Best Practices

Terraform for loop helps you write simplified, repeatable code used to deploy resources. In this article, we’ll explore for loops using for_each and count, how and when to use them, example scenarios, best practices, and much more.

Disclaimer

All use cases of Terraform for loops discussed here work similarly in OpenTofu, the open-source Terraform alternative. However, to keep it simple and familiar for DevOps engineers, we will refer to these as “Terraform” for loops throughout this blog post.

What are a Terraform for Loops

Using loops in Terraform serves as an easy way to create multiple resources without repeating the same code, which is in line with the DRY (Don't Repeat Yourself) code principle.

Utilizing loops relies on the use of for_each and count meta-arguments. When used together with the for expression, it helps you transform values on the go. You can use conditionals in loops to include or exclude an item while creating some dynamic or complex configuration. 

Once the looping operation is performed, it returns a new list or map that you can use to configure your resources, outputs, and variables. Typically, loops are helpful in creating new data structures and filtering the existing map or list, producing reusable and less error-prone configurations.

When Should You Use Loops? 

Terraform loops make it easier to create multiple resources efficiently with reusable code that is easy to maintain.

Let’s look at some of the use cases of using Terraform for loop:

  • Reduce code repetition -  When deploying multiple resources, like IAM roles or security groups, writing each one individually can be time-consuming and error-prone. Using for loops in Terraform allows you to create all necessary resources within a single code block, ensuring consistency and making your infrastructure code more manageable.

  • Conditional resource deployments - When you want to deploy multiple resources based on conditions, let's say two S3 buckets for the ‘dev’ environment and four for ‘prod’, you can create them using the loops in combination with conditional logic, again saving time and ensuring consistency and code DRYness.

These are just two common use cases. Generally speaking, Terraform for loops is just a useful way to create an efficient and DRY code, and could be helpful in addressing various use cases and requirements, some of which we will dive into below.

For_each Loop

Using for_each loops enables you to create multiple resources or modules by iterating over a set of input values, such as a list or map. Instead of manually defining each resource, for_each generates an instance for every item, following the DRY principle.

When you use for_each in a resource block, Terraform provides each object with each.key and each.value. These allow you to apply specific configurations to each instance, ensuring that your infrastructure is consistent and efficiently managed without repetitive code.

Syntax of for_each loop

The syntax for the for_each loop in Terraform is:

for_each = collection
Enter fullscreen mode Exit fullscreen mode

Breaking this down, here’s what each argument means:

  • for_each:  The meta-argument used to iterate over each item in the collection.
  • collection: The map or set of values for which resources are created.

To show how this works,  here is a code that creates multiple EC2 instances with unique configurations:

resource "aws_instance" "env0" {
  for_each = var.instance_map
  ami = each.value.ami
  instance_type = each.value.instance_type
}
Enter fullscreen mode Exit fullscreen mode

Note how the for_each loop iterates over the instance_map and dynamically sets the ami and instance_type for each instance in the map.

For Expression

The for expression in Terraform helps you transform a value into a new list or map. Using for expression, you loop through each item in the collection one by one.

While you are going through each item, it can be transformed or modified to your desired output value set. Now, the transformed value can be used in your configuration.

You can also use the if clause with the for expression to evaluate the input value on the basis of a condition. Using the conditional clause, you can filter the items in your map or lists while you iterate through them.

Syntax of for expression

The syntax for the for expression in Terraform is:

[for item in collection : expression]
Enter fullscreen mode Exit fullscreen mode

Here is what each of these arguments means:

  • for item in collection: Specifies that Terraform should loop over each element in the collection (which can be a list or map).
  • expression: Shows how each item is transformed or utilized in the new list or map.

To show how this works, let’s look at an example where for is used to iterate over original_list that contains environment names: 

locals {
  original_list = [‘dev’, ‘staging’, ‘prod’]
  transformed_list = [for env in local.original_list : "${env}-environment"]
}
Enter fullscreen mode Exit fullscreen mode

Note how the list is transformed by appending -environment in each item, creating a new list transformed_list with the modified values.

Count Meta-argument

The count is used to create an exact number of instances of a resource with the same configuration.

Unlike for_each, which is ideal for resources with distinct configurations, count is simpler and more effective when you need to deploy multiple resources.

Syntax of count meta-argument

To understand how the count meta-argument is used, let’s take a look at its syntax:

count = number_of_instances
Enter fullscreen mode Exit fullscreen mode

Again, breaking things down:

  • count: Takes an integer that specifies how many resources will be created.
  • number_of_instances: The value passed to the count meta-argument.

To demonstrate how count works, let’s create three EC2 instances with the same configuration by setting the count value to three:

resource "aws_instance" "web_server" {
  count = 3
  ami = var.ami
  instance_type = var.instance_type
}
Enter fullscreen mode Exit fullscreen mode

This serves as an easy example of how count for loops could save time, streamlining repetitive tasks.

For Loop Example Scenarios

Now, let’s look at some examples of deploying AWS resources using different types of loops in Terraform.

Using Terraform for_each to Deploy EC2 Instances 

Let’s assume you need to deploy multiple EC2 instances with different configs using your Terraform code. You can use the for_each loop to create them. 

First, define an ‘instances’ variable in your var.tf file. This variable is a map where each key represents a unique EC2 instance configuration:

variable "instances" {
  type = map(object({
    ami         = string
    instance_type = string
  }))
  default = {
    instance1 = { ami = "ami-12345678", instance_type = "t2.micro" }
    instance2 = { ami = "ami-87654321", instance_type = "t2.small" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, use the for_each loop to iterate over instances and create two aws_instance resources. The configuration value for ami and instance_type is passed using the each object:

resource "aws_instance" "env0" {
  for_each = var.instances
  ami           = each.value.ami
  instance_type = each.value.instance_type
}
Enter fullscreen mode Exit fullscreen mode

To see that it works, run the terraform init and apply commands to deploy the two aws_instance resources.

To verify the creation of these aws_instance resources, go to AWS Management Console and navigate to the ‘EC2’ dashboard to see the newly created instances.

Using Terraform for_each Loop to Deploy S3 Buckets 

In this example, we’ll deploy 3 S3 buckets using Terraform. First, define a ‘bucket_count’ variable in the var.tf file, which specifies the exact number of S3 buckets to be created:

variable "bucket_count" {
  type  = number
  default = 3
}
Enter fullscreen mode Exit fullscreen mode

Next, use a for loop to dynamically generate a list of bucket_names, such as ‘infrasity-bucket-1’, ‘infrasity-bucket-2’, and ‘infrasity-bucket-3’ based on the value of ‘bucket_count’ in main.tf file. This list provides unique names for each S3 bucket, ensuring they are easily identifiable:

locals {
  bucket_names = [for i in range(var.bucket_count) : "infrasity-bucket-${i + 1}"]
}
Enter fullscreen mode Exit fullscreen mode

Then, create the resource block to deploy S3 buckets with unique names using for_each loop, iterating over the list of ‘bucket_names’ in main.tf file:

resource "aws_s3_bucket" "infrasity" {
  for_each = toset(local.bucket_names)
  bucket   = each.key
}
Enter fullscreen mode Exit fullscreen mode

To see that it works, apply the configuration to deploy the 3 S3 buckets.

You can then view your deployed S3 buckets by logging into the AWS Management Console and navigating to the ‘S3’ dashboard.

Deploy EC2 Instance using Terraform Count

In the above example, we learned to use for_each loop. But what about when you are required to deploy multiple resources with the same configuration, such as EC2 instances? 

In this scenario, using for_each loop only adds to the complexity of the code. Since this is a simple use case that does not require allocating different configurations for each instance, using Terraform count is ideal and efficient.

Write the resource block to deploy EC2 instances with count value set to 3, which instructs Terraform to create three EC2 instances:

resource "aws_instance" "Infrasity" {
  count         = 3
  ami           =  "ami-12345678"
  instance_type = "t2.micro"
}
Enter fullscreen mode Exit fullscreen mode

Run the terraform init command and apply to deploy three aws_instance resources.

You can verify the creation of these instances by checking the AWS Management Console.

Using a Loop to Deploy AWS VPC

You might need to deploy multiple resources, such as VPCs and subnets, for specific environments. In such a scenario, using the for_each loop is effective as it helps you save time and reduce extra lines of code while maintaining configuration DRY and consistent.

First, define the ‘vpc_names’ variable and ‘environments’ locals in the var.tf file:

variable "vpc_names" {
  type = map(string)
  default = {
    "vpc-prod"  = "Production VPC"
    "vpc-dev" = "Staging VPC"
  }
}
locals {
  environments = {
    prod = {
    vpc_cidr_block = "10.0.0.0/16"
    subnet_cidr_blocks = ["10.0.1.0/24", "10.0.2.0/24"]
    }
    dev = {
    vpc_cidr_block = "10.1.0.0/16"
    subnet_cidr_blocks = ["10.1.1.0/24", "10.1.2.0/24"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, create the aws_vpc resource using for_each by iterating over the list of vpc_names and deploying VPCs for each environment in main.tf file. 

The cidr_block is dynamically assigned based on the environment, ensuring the correct IP range is used, like so:

resource "aws_vpc" "network_vpcs" {
  for_each   = var.vpc_names
  cidr_block = local.environments[replace(each.key, "vpc-", "")].vpc_cidr_block
}
Enter fullscreen mode Exit fullscreen mode

After creating VPCs, create aws_subnet within the appropriate VPCs using for_each to iterate over subnets in the ‘subnet_map’. 

Also, the values for cidr_block and availability_zone are dynamically calculated to ensure that subnets are correctly configured for the respective environment.

locals {
  subnet_map = {
    for vpc_name, vpc_info in local.environments :
    vpc_name => {
    for i, subnet_cidr in vpc_info.subnet_cidr_blocks :
    "${vpc_name}-subnet-${i + 1}" => {
        cidr_block = subnet_cidr
        vpc_id  = aws_vpc.network_vpcs["vpc-${vpc_name}"].id
    }
    }
  }
}
data "aws_availability_zones" "available" {}
resource "aws_subnet" "network_subnets" {
  for_each = merge(local.subnet_map["prod"], local.subnet_map["dev"])
  vpc_id    = each.value.vpc_id
  cidr_block = each.value.cidr_block
  availability_zone = element(data.aws_availability_zones.available.names, index(local.environments[split("-", each.key)[0]].subnet_cidr_blocks, each.value.cidr_block))
  map_public_ip_on_launch = true
  tags = {
    Name        = each.key
    Environment = split("-", each.key)[0]
  }
}
Enter fullscreen mode Exit fullscreen mode

Run the terraform init and apply to deploy your infrastructure in AWS.

Once the deployment is complete, you can view the created VPCs and subnets in the AWS Management Console.

Best Practices for Using Terraform for Loop

In the above examples, we learned to use the Terraform for loop. Now, let’s learn some of the best practices you should follow while using the Terraform loop:

  1. Avoid nested loops: Using nested loops makes it harder to read and maintain your code. Instead, use a single loop in each resource block to simplify your code. For example, count to deploy multiple EC2 instances.
  2. Use clear naming conventions: Ensure that your keys and variable names reveal their purpose for an easier understanding of the code. For instance, use instance_types instead of a generic name like x.
  3. Use conditional logic when required: Use ‘if’ clauses with loops to control resource creation. This ensures that you do not create unnecessary resources. For example, create a list of teams that have enabled VPC resource creation using for expression.
  4. Prefer using for_each over count: If you want to deploy the same resources with different configurations, use for_each. For example, multiple EC2 instances can be deployed with different AMI and instance types using the for_each loop.

Using Terraform for_each Loop with env0

With env0, you can deploy multiple resources using centralized variable management, reducing repetitive code and saving time. 

Specifically, the platform allows you to automate the deployment process, manage environment variables, and handle deployments efficiently without the need to repeatedly write the same configurations.

In this section, we’ll walk you through setting up and using Terraform loops with env0’s variables to deploy multiple AWS S3 buckets, each customized for a specific environment. 

Step 1: Define the provider configuration

Set up your Terraform configuration by defining the AWS provider in your providers.tf file.

provider "aws" {
region = var.region
access_key = var.access_key
secret_key = var.secret_key
}
Enter fullscreen mode Exit fullscreen mode




Step 2: Define variables

Define the ‘region’, ‘access_key’, ‘secret_key’, and ‘environment’ variables in the var.tf file. Additionally, create a ‘bucket_count’ variable to specify the exact number of buckets to be created. 

These variables will help you customize and manage your infrastructure for different environments, like development or production.

variable "region" {
description = "AWS region"
type = string
}
variable "access_key" {
description = "AWS access key"
type = string
}
variable "secret_key" {
description = "AWS secret key"
type = string
}
variable "environment" {
description = "Environment type (dev or prod)"
type = string
default = "dev"
}
variable "bucket_count" {
description = "Number of AWS S3 buckets to create"
type = number
default = 2 # Default to 2 buckets for the dev environment
}
Enter fullscreen mode Exit fullscreen mode




Step 3: Define local values

Let's define bucket_names to dynamically generate unique, environment-specific names for the AWS S3 buckets based on the environment and the ‘bucket_count’.

locals {
bucket_names = [for idx in range(var.bucket_count) : "${var.environment}-infrasity-bucket-${idx + 1}"]
}
Enter fullscreen mode Exit fullscreen mode




Step 4: Create AWS S3 buckets using for_each loop

Use the for_each loop to create the aws_s3_bucket resources by iterating over the bucket_names, and creating each bucket accordingly.

resource "aws_s3_bucket" "infrasity" {
for_each = { for idx, name in local.bucket_names : idx => name }
bucket = each.value
tags = {
Name = each.value
Environment = var.environment
}
}
Enter fullscreen mode Exit fullscreen mode




Step 5: Setup env0

Now that your Terraform configuration is set up, we must integrate our repository with env0. Follow these steps:

  1. Create a new environment - To apply your IaC configurations, you must create an environment in env0 under your project.

  1. Connect your GitHub repository - Select VCS integration to create a new environment and attach your github repository link.

  1. Configure environment variables - You can create the environment variables that are accessible across the environment and required within your Terraform configuration by following these steps:
  • Navigate to your ‘environment’ in env0>‘Settings’>’Environment Variables’
  • Add the variables:some text
    • region: Set your desired AWS region (e.g., ap-south-1)
    • access_key: Stores your AWS access key
    • secret_key: Stores your AWS secret key
    • environment: Specify the environment type, either ‘dev’ or ‘prod’
    • bucket_count: Define the number of s3 buckets to create (e.g., 2 for dev, 4 for prod)

Step 6: Deploy the environment setup

Deploy your environment from the env0 dashboard. Click on run-deploy to start the deployment process or just save the changes in the environment variables. Once it begins, you must approve the creation of AWS resources in the terraform plan section. Once approved, the deployments will be done.

After the successful deployment, you can see three AWS S3 buckets were created in the dev environment.

With env0, you can manage your Terraform environments, apply changes, and monitor your infrastructure. env0’s environment-based management features work seamlessly with Terraform loops, enabling you to efficiently deploy resources and manage your infrastructure across different environments.

Conclusion

By now, you should have a clear understanding of for_each, count, and for and their use cases. We learned how to leverage loops to create multiple resources individually or in a complex structure using conditional logic, as well as best practices.

We also discussed how you can use env0’s centralized variables capability with loops to manage infrastructure effectively.

Frequently Asked Questions

Q. What is Terraform for loop?

Terraform for loop helps you create multiple resources by iterating over lists or maps. The key benefits of using loops are that they help you avoid extra lines of Terraform code and save time and effort.

Q. What is a loop with a count in Terraform?

A count loop in Terraform is a way to create multiple resources with the same configuration using a single block of code.

Q. What is the difference between count and for_each in Terraform?

A count is used to create a specific number of resources with exactly the same configuration. On the other hand, for_each is used to create resources based on items in a list or map, allowing for more customized configurations.

Q. How to iterate a list in Terraform?

You can use a for expression within a for_eachloop or when defining local variables to process each item in the list and apply it to resource creation or variable assignment.

Top comments (0)