![]() |
VOOZH | about |
We’re so glad you’re here. You can expect all the best TNS content to arrive Monday through Friday to keep you on top of the news and at the top of your game.
Check your inbox for a confirmation email where you can adjust your preferences and even join additional groups.
Follow TNS on your favorite social media networks.
Become a TNS follower on LinkedIn.
Check out the latest featured and trending stories while you wait for your first TNS newsletter.
Terraform is a powerful Infrastructure as Code (IaC) tool. It lets you define your cloud configuration in a succinct, reproducible way and does the heavy lifting of creating, updating, or deleting resources on your behalf. Whether provisioning a basic EC2 instance or orchestrating an entire multiregion infrastructure, Terraform lets you do it all with code — your infrastructure becomes predictable and version-controlled.
If you’ve used Terraform for a bit, you’ve probably been in the position where you needed to create more than one instance of the same type of resource — e.g., several EC2 instances or tens of the same security groups. Having to write out individual blocks for each of them soon becomes annoying and unmanageable. That’s exactly where Terraform’s for_each enters.
In this post, we’ll break down what for_each really does, how it works, and how you can make your Terraform code more beautiful, dynamic, and maintainable. Whether you’re building multiple resources in a single environment or dealing with multiple environments at once, for_each might be your new BFF.
Let’s dive in!
Terraform offers other commands you can use in your resource blocks to control the creation of resources. Those are called meta-arguments. Some of the most common are `count`, provider, depends_on, lifecycle, and of course — for_each.
All of them do some particular thing. While count enables you to create many instances of some resource through index numbers, for_each enables you to use a map or set of strings with finer control and legibility.
So what is for_each then?
It’s kind of like a loop in code. Instead of writing five individual EC2 blocks, you can write one and loop through a map or set to dynamically create them. It is super useful when each resource has a unique name or configuration.
For example, assume you need to create three EC2 instances with different instance types and tags. Instead of repeating code, you can pass a map and let for_each do its magic.
You might think — “wait, isn’t this the same as count?”
Not quite. Why not?
Prior to the bullet points, let’s quickly look at the larger picture. Count does simple index-based iteration. It’s great when all resources pretty much equal. But where you want finer-grain control, for_each lets you use meaningful keys and structured data.
Let us see how to use count for repeated content using an example below. First of all, we are declaring a list variable as follows, which we want to use it for our EC2 instances.
```
variable "app_node_names" {
type = list(string)
default = ["web-front","web-back","db"]
}
# number of ec2 instances to be provisioned
variable "app_node_count" {
type = number
description = "Number of nodes for applications"
default = 3
}
```
Now, we can use these variables in our aws_instance block to provision the required EC2 instances with Name tag automatically as follows.
```
resource "aws_instance" "app-nodes" {
ami = var.ami
.
.
count = var.app_node_count
instance_type = var.instance_type
tags = {
Name = var.app_node_names[count.index]
}
}
```
So, here’s a quick comparison between count and for_each
So if your resources differ slightly — or you want more predictable diffs — for_each is your go-to.
If you need to create lots of the same thing in Terraform, like a few EC2s, numerous security groups, or a list of IAM roles — you don’t need to painstakingly copy and paste each by hand. For_each is your solution, which gives each resource a unique key so they can be easily referenced and controlled individually. So instead of “create three resources,” you are authoring “create one resource for each item on this list or map.” That is clearer, less problematic, and readable within your code, especially in instances where working with complex or named configurations exists.
We shall explore in depth what sorts of data you’re able to play with via the use of for_each and its application within actual scenarios.
For_each supports:
Here’s a sample syntax using a map:
```
resource "aws_instance" "web" {
for_each = {
web-front = "t2.micro"
web-back = "t3.small"
web-db = "t3.medium"
}
ami = "ami—0c55b159cbfafe1f0" # Example AMI
instance_type = each.value
tags = {
Name = each.key
}
}
```
Simple, right? Each item in the map becomes its own instance.
When you use for_each, Terraform gives you two helpers:
Want to get fancier and need to handle complicated scenarios? You can make the value a map with multiple attributes:
```
variable "app_instances" {
description = "App instance details"
default = {
web-front = { instance_type = "t2.micro" }
web-back = { instance_type = "t3.small" }
web-db = { instance_type = "t3.small" }
}
}
```
In the above map, we are using different configurations for different EC2 instances for our applications such as instance type, disk volume size and the requirement of public IP address.
Now, we can use the map in EC2 instance creation time as follows.
```
resource "aws_instance" "app-nodes" {
for_each = var.app_instances
instance_type = each.value.instance_type
ami = var.ami
key_name = var.key_name
subnet_id = var.subnet_id
security_groups = var.vpc_security_group_ids
}
```
Now we’re talking!
Let’s get real — for_each can be a real lifesaver, but it also has some “oops” moments if you’re not careful. Here are some of the common pitfalls individuals run into, and how to avoid them:
Finally, all of these issues arise because Terraform is trying to do a lot of things ahead of time. So when you’re designing your for_each logic, think like Terraform: “Can I do this ahead of time?” If no, it’s time to rethink your input.
Let’s walk through a few real-world examples with AWS, so you can understand how for_each really shines in real-world usage. Whether you’re deploying multiple security groups, provisioning EC2 instances with different configurations, or applying reusable modules across environments, for_each gives you granular control and flexibility.
The goal here isn’t just to reveal syntax — but to help you understand how and why you’d be using for_each in different use cases.
Suppose you need to create multiple security groups for different services (web, db, cache). Instead of repeating yourself, here’s a neat way:
```hcl
locals {
sg_rules = {
web = 80
db = 3306
cache = 6379
}
}
resource "aws_security_group" "sg" {
for_each = local.sg_rules
name = "${each.key}-sg"
description = "SG for ${each.key}"
vpc_id = aws_vpc.main.id
ingress {
from_port = each.value
to_port = each.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
```
Simple and readable.
We already looked at this above, but it’s a great example, so let’s quickly revisit it:
```
variable "app_instances" {
description = "App instance details"
default = {
web-front = { instance_type = "t2.micro", volume_size = 40, public_ip_address = true }
web-back = { instance_type = "t3.small", volume_size = 80, public_ip_address = false }
web-db = { instance_type = "t3.small", volume_size = 200, public_ip_address = false }
}
}
```
In the above map, we are using different configurations for different EC2 instances for our applications such as instance type, disk volume size and the requirement of public IP address.
Now, we can use the map in EC2 instance creation time as follows.
```
resource "aws_instance" "app-nodes" {
for_each = var.app_instances
instance_type = each.value.instance_type
associate_public_ip_address = each.value.public_ip_address
ami = var.ami
key_name = var.key_name
subnet_id = var.subnet_id
security_groups = var.vpc_security_group_ids
root_block_device {
volume_size = each.value.volume_size # Size in GB
volume_type = "gp3" # General Purpose SSD (adjust if necessary)
}
tags = {
Name = each.key
}
}
```
Clean and dynamic.
Modules support for_each too! Let’s say you have a reusable EC2 module. You can do this:
```hcl
module "ec2" {
for_each = var.app_instances
source = "./modules/ec2"
instance_name = each.key
instance_type = each.value.instance_type
availability_zone = each.value.availability_zone
}
```
This pattern scales well and keeps your code DRY.
Then when should you actually reach for for_each in your Terraform projects? It’s not just about creating multiple copies of a resource — it’s about doing it intelligently and in a way that is still readable and maintainable. for_each shines when you are working with dynamic sets of resources, need environment-specific configuration, or want your code to stay DRY (Don’t Repeat Yourself). Rather than duplicative code blocks or dealing with convoluted logic, you can look to use for_each in order to ease life and make more scalable, simpler infrastructure. Let’s cover some real-world application examples whereby it can simplify life.
When you’re dealing with cloud infrastructure, you’ll often find yourself wanting to define several resources of the same type — like several S3 buckets for different teams, EC2 instances for different environments, or IAM roles with varying permissions. Defining each of those resources manually can quickly turn into a mess, not to mention a headache to update. That’s where for_each is a lifesaver.
With for_each iterating over a map or a set, you can dynamically create resources on the fly based on input values. You can keep, for example, a plain old map of bucket configurations and team names and let for_each provision one by one S3 buckets for each. The benefit? Smaller code to read, less copying-pasting incorrectly, and a much more manageable config to change later on. Five or fifty resources — you pick — is still within for_each’s reach.
When you’re deploying to more than one environment — e.g., dev, staging, and prod — things can begin to get a little repetitive. More often than not, the resources are the same kind (e.g., EC2 instances or security groups), but the underlying values — e.g., instance size, number of instances, tags, etc. — are all slightly different between environments.
Try to imagine now having to manage all of that with hardcoded blocks or nested conditionals. It would be a nightmare, wouldn’t it?
This is where for_each comes to the rescue. You can keep your environment-specific values in a map and then pass the same to your resource block using for_each. Terraform will then loop through each of those values and create the respective setup based on the current workspace or based on an environment variable you might want to pick.
Let us attempt an example for AWS EC2 instances.
You can create a variable like this in your variables.tf:
```
variable "environment_configs" {
type = map(any)
default = {
dev = {
instance_type = "t3.micro"
instance_count = 1
tags = {
Name = "dev-instance"
Env = "dev"
}
}
staging = {
instance_type = "t3.small"
instance_count = 2
tags = {
Name = "staging-instance"
Env = "staging"
}
}
prod = {
instance_type = "t3.medium"
instance_count = 3
tags = {
Name = "prod-instance"
Env = "prod"
}
}
}
}
```
Now, use a local value to extract the right config based on the current workspace:
```
locals {
current_env_config = var.environment_configs[terraform.workspace]
}
```
Next, use for_each to create multiple instances dynamically:
```
resource "aws_instance" "app" {
for_each = toset([for i in range(local.current_env_config.instance_count) : "${terraform.workspace}-${i}"])
ami = "ami-0c55b159cbfafe1f0" # Replace with your actual AMI ID
instance_type = local.current_env_config.instance_type
tags = merge(
local.current_env_config.tags,
{
Instance = each.key
}
)
}
```
Here’s what’s happening:
Using for_each with Terraform modules is a great way to keep your infrastructure clean, modular, and scalable. Instead of repeating the same code blocks for each environment, application, or team, you can define a reusable module once and then use it multiple times with different values through for_each. Not only does it reduce code duplication but also your configurations are much easier to work with and update in bulk.
For example, imagine you have a module to start an EC2 instance. You can use a map to create dev, staging, and prod environment configs and then use for_each to deploy the module for every environment automatically as you learned in the previous section. It helps bigger teams work together too — every project or team can have its own config, and the same module can handle it all behind the scenes. In short, this blend is ideal when your infrastructure starts to grow and you need flexibility as well as consistency.
Let’s run over some good recommendations to get most use out of for_each. Although for_each can make your Terraform code more clean and dynamic, it comes with its own flaws. It’s easy to get excited and turn your config into something unflaggingly perplexing or tough to debug in the future. That’s why it’s important to follow some best practices in using for_each — not just to avoid mistakes, but to make Terraform code that’s readable, reusable, and reliable.
In the sections below, we’ll look at how to choose between for_each and count, how to avoid common pitfalls when using lists, and how to keep your configuration neat and maintainable — especially when working in a team or managing a growing infrastructure.
Selecting between count and for_each is an eternal conundrum when writing Terraform code — and getting it right can be worth it in loads of confusion saved in the future. Use count when you’re provisioning lots of similar resources and don’t care which ones they are, just how many. Such as booting up three similar EC2 servers with no difference between them? Count =3 does it perfectly. Terraform will label them numeric indexes like aws_instance.example[0], aws_instance.example[1], etc.
But if every resource is slightly different — like EC2 instances with unique names, tags, or types — it’s more appropriate to use for_each. It enables you to declare resources in terms of a map or a list of strings, which means every one of them has an explicitly stated identity mapped to a specific key. This makes your code more declarative and less prone to errors, particularly when updating or deleting, since Terraform can match resources by their keys instead of indexes, which might change if the count does. So, a simple rule of thumb: if the resources are homogeneous and interchangeable, use count. If they’re heterogeneous and must be tracked individually, use for_each.
When using for_each with lists, remember that lists do not have unique keys. This lack of uniqueness can cause issues, especially when deleting or inserting items. Terraform relies on keys to track and manage resources, and when you’re working with a plain list, the order of items becomes important, which can lead to errors or bizarre behavior. To avoid this, you can convert the list to a map or set of unique keys. For example, you can use a for expression to create a map, binding each item to a unique key. This will render your configuration more stable and deterministic, especially when updating or modifying resources over time.
With the use of for_each, it’s easy to get lost in complex logic, especially in the case of multiple resources or configurations. For the sake of readability and not being confusing, a good practice is to divide your code into smaller and more digestible parts with locals or Terraform modules. For instance, if you have a complex set or map that you’re iterating using for_each, you can define it once using locals to declare it outside and give it an explicit name. This makes anyone reading (even you in the future) crystal clear on the structure and intent behind each segment. Lastly, always use descriptive key and value names that define the purpose of each resource or variable. This is especially important when teaming up, as good documentation and good comments will make your code so much easier to read. By providing context to the reasoning behind your for_each use, you make your Terraform code easy to maintain, scale, and adapt as your infrastructure evolves and expands.
Just like with anything in life, when applying infrastructure as code, it does not always turn out perfectly as planned, and for_each is no exception. It can be a typo, a misunderstanding of data types, or a resource dependency problem, so having a solid debugging workflow in place is important. While Terraform is great at providing feedback via error messages, sometimes the initial reason is not obvious at first glance. Here, we will look at some of the common issues with for_each and how you can best fix them. Understanding how to read error messages, review the plan output, and use Terraform’s built-in tools will make it simple to troubleshoot quickly and keep your infrastructure running efficiently. Let’s walk through some of the common issues and solutions.
Your friend is Terraform plan when applying for_each. It gives you an explicit preview of what Terraform will create, update, or delete — before you do anything at all. With for_each, especially with dynamic sets or maps, this command helps you visualize whether your data is being processed as you intend.
For example, when you’re passing a map of EC2 instance configs, terraform plan will show you every resource that will be planned, along with the extremely specific keys it’s being used with. This is ridiculously helpful in catching misconfigurations early — like non-unique keys, absent values, or when Terraform doesn’t get your input format.
In short, don’t wait until terraform apply to find out something’s wrong. Use terraform plan a lot throughout development — it’ll save you time, confusion, and maybe costly mistakes.
State files track your resources using the keys you defined. So if you change a key, Terraform will destroy and recreate that resource. Rename keys with caution.
Terraform’s for_each is game-changing when you’re dealing with infrastructure at scale. It helps you move away from boilerplate resource blocks and brings a newer, more dynamic way of handling multiple instances. Whatever it may be — EC2s, IAM roles, or S3 buckets — you can actually manage what’s being deployed and how, without having to unnecessarily bloat your code.
It is especially useful when you are dealing with maps or sets, where each element has some specific key or name. It gives you granular control over each resource, making your Terraform code functional, as well as readable and maintainable.
And the bonus? Your code turns DRY (Don’t Repeat Yourself). Instead of copy-pasting bunches of similar resources with modifications, for_each lets you define a pattern and fill it with changing data. Cleaner code, fewer bugs, more sleep.
You should consider using for_each when your infrastructure needs go beyond just spinning up identical resources. If you’re:
Then for_each is your friend. It shines in production environments where you want more predictability and control, and especially when your project grows in size and complexity. If you’re already using count and hitting limitations, then it’s probably time to refactor with for_each.
So, what’s next?
Try to take one of your existing resource blocks — maybe a security group with a few rules or a list of EC2 instances — and reimplement it using for_each. You’ll likely find it more intuitive once you get used to driving your resources from maps or sets.
Start with something simple, test regularly with terraform plan, and see your infrastructure code get more readable and more flexible. Once you’re comfortable, try combining it with modules for even more reusability.
And if you hit a hitch or something doesn’t behave the way you’d intuitively expect it to, well — that’s all part of learning. Drop a comment or give a yell. Happy Terraforming!
Note: The example Terraform code used in this article is available in the Git repo for reference.