I have sometimes seen some people feel confused in choosing between count and for_each in Terraform. Both constructs help to dynamically create multiple resources, yet their use cases and behavior differ significantly. Let me break it down in simple terms and demonstrate their differences with an example: provisioning AWS Security Groups.
Note: Since creating Security Groups in AWS is free, I am using an example of provisioning AWS Security Groups to illustrate these constructs.
Understanding the Basics: count and for_each
Count
Count is the simpler of the two constructs. It creates resources based on a numeric value. It is like you’re asking Terraform, “Make X copies of this resource,” and that’s exactly what it does. Every resource created with count is identical, except for slight variations you introduce using count.index, which is a zero-based index.
For_Each
For_each, on the other hand, is designed for scenarios where you need to create resources with distinct configurations. It iterates over a collection (list, set, or map), and for each element in the collection, it creates a unique resource. Unlike count, the resulting resources are associated with the elements of the collection, allowing for easier referencing and modification later.
The Key Differences
Aspect | count | for_each |
Input Requirements | Works with a single numeric value. | Works with collections like lists or maps. |
Best Use Case | Best for identical resources or conditional creation of resources. | Ideal when resources require unique configurations. |
Referencing Resources | Resources are indexed using count.index. | Resources are referenced using the collection’s keys. |
Flexibility | Simpler and best suited for repetitive tasks. | Offers greater customization and handles complex resource configurations effectively. |
Security Group Provisioning with count
Let’s start with a simple example of creating Security Groups using count.
variable “number_of_sgs” {
default = 2
}
resource “aws_security_group” “web_sg” {
count = var.number_of_sgs
name = “web-sg-${count.index + 1}”
description = “Security Group for web instances ${count.index + 1}”
ingress {
from_port = 80
to_port = 80
protocol = “tcp”
cidr_blocks = [“0.0.0.0/0”]
}
egress {
from_port = 0
to_port = 0
protocol = “-1”
cidr_blocks = [“0.0.0.0/0”]
}
}
In this example, Terraform creates two Security Groups, named web-sg-1 and web-sg-2, because count equals 2. Each Security Group has the same ingress and egress rules, differentiated only by their count.index.
Note: By default count.index is a zero-based index. I have added +1 ensures that the names and descriptions of the security groups start from 1 instead of 0
Below is the execution output of above terraform code:
PS C:\Users\charanjit\Documents\terraform_iteration_meta-arguments> terraform apply –auto-approve
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:
# module.sg_count.aws_security_group.count_sg[0] will be created
+ resource “aws_security_group” “count_sg” {
+ arn = (known after apply)
+ description = “Security Group created using count index 1”
+ egress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “-1”
+ security_groups = []
+ self = false
+ to_port = 0
# (1 unchanged attribute hidden)
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 80
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “tcp”
+ security_groups = []
+ self = false
+ to_port = 80
# (1 unchanged attribute hidden)
},
]
+ name = “web-sg-1”
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags_all = (known after apply)
+ vpc_id = (known after apply)
}
# module.sg_count.aws_security_group.count_sg[1] will be created
+ resource “aws_security_group” “count_sg” {
+ arn = (known after apply)
+ description = “Security Group created using count index 2”
+ egress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “-1”
+ security_groups = []
+ self = false
+ to_port = 0
# (1 unchanged attribute hidden)
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 80
+ ipv6_cidr_blocks = []
+ to_port = 0
# (1 unchanged attribute hidden)
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 80
+ ipv6_cidr_blocks = []
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “tcp”
+ security_groups = []
+ self = false
+ to_port = 80
# (1 unchanged attribute hidden)
},
]
+ name = “web-sg-2”
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags_all = (known after apply)
+ vpc_id = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
module.sg_count.aws_security_group.count_sg[1]: Creating…
module.sg_count.aws_security_group.count_sg[0]: Creating…
module.sg_count.aws_security_group.count_sg[1]: Creation complete after 2s [id=sg-0ec95c43a91685316]
module.sg_count.aws_security_group.count_sg[0]: Creation complete after 2s [id=sg-0c53af016b3bac972]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
PS C:\Users\charanjit\Documents\terraform_iteration_meta-arguments>
Below are the screenshots of provisioned AWS Security Groups through count:
Conditional Creation with count
One of the strengths of count is its ability to conditionally create resources. For example, you might want to create a Security Group only if a certain variable is true:
variable “create_sg” {
default = true
}
resource “aws_security_group” “conditional_sg” {
count = var.create_sg ? 1 : 0
name = “conditional-sg”
description = “This SG is created only if create_sg is true”
ingress {
from_port = 80
to_port = 80
protocol = “tcp”
cidr_blocks = [“0.0.0.0/0”]
}
egress {
from_port = 0
to_port = 0
protocol = “-1”
cidr_blocks = [“0.0.0.0/0”]
}
}
If create_sg is set to false, Terraform will skip creating the Security Group entirely. This makes count an excellent choice for switching the resource creation or avoids unnecessary resource creation and helps manage costs in the Cloud.
Below is the execution output of above terraform code:
PS C:\Users\charanjit\Documents\terraform_iteration_meta-arguments> terraform apply –auto-approve
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ createTerraform will perform the following actions:
# module.sg_count_condition.aws_security_group.conditional_sg[0] will be created
+ resource “aws_security_group” “conditional_sg” {
+ arn = (known after apply)
+ description = “This Web Server conditional Security Group is created only if create_sg is true”
+ egress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “-1”
+ security_groups = []
+ self = false
+ to_port = 0
# (1 unchanged attribute hidden)
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 80
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “tcp”
+ security_groups = []
+ self = false
+ to_port = 80
# (1 unchanged attribute hidden)
},
]
+ name = “conditional_sg”
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags_all = (known after apply)
+ vpc_id = (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
module.sg_count_condition.aws_security_group.conditional_sg[0]: Creating…
module.sg_count_condition.aws_security_group.conditional_sg[0]: Creation complete after 2s [id=sg-061d8b0ec39ba607e]Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
PS C:\Users\charanjit\Documents\terraform_iteration_meta-arguments>
Below is the screenshot of provisioned security group through count condition:
Security Group Provisioning with for_each
Now let’s see how to achieve the same result using for_each. This is especially useful if each Security Group requires unique attributes.
variable “sg_configs” {
default = {
sg1 = { name = “web-sg-1”, port = 80 },
sg2 = { name = “web-sg-2”, port = 443 }
}
}
resource “aws_security_group” “web_sg” {
for_each = var.sg_configs
name = each.value.name
description = “Security Group for ${each.key}”
ingress {
from_port = each.value.port
to_port = each.value.port
protocol = “tcp”
cidr_blocks = [“0.0.0.0/0”]
}
egress {
from_port = 0
to_port = 0
protocol = “-1”
cidr_blocks = [“0.0.0.0/0”]
}
}
In this example:
- We define a sg_configs variable, a map with unique names and ports for each Security Group.
- for_each iterates over sg_configs, creating a Security Group for each key-value pair.
- The each.key and each.value expressions dynamically set the Security Group’s name and ingress port.
The result is two Security Groups:
- web-sg-1 with port 80 open.
- web-sg-2 with port 443 open.
Below is the execution output of above terraform code:
PS C:\Users\charanjit\Documents\terraform_iteration_meta-arguments> terraform apply –auto-approve
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:
# module.security_groups_for_each.aws_security_group.for_each_sg[“sg1”] will be created
+ resource “aws_security_group” “for_each_sg” {
+ arn = (known after apply)
+ description = “Security Group created for sg1”
+ egress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “-1”
+ security_groups = []
+ self = false
+ to_port = 0
# (1 unchanged attribute hidden)
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 80
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “tcp”
+ security_groups = []
+ self = false
+ to_port = 80
# (1 unchanged attribute hidden)
},
]
+ name = “web-sg-1”
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags_all = (known after apply)
+ vpc_id = (known after apply)
}
# module.security_groups_for_each.aws_security_group.for_each_sg[“sg2”] will be created
+ resource “aws_security_group” “for_each_sg” {
+ arn = (known after apply)
+ description = “Security Group created for sg2”
+ egress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 0
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “-1”
+ security_groups = []
+ self = false
+ to_port = 0
# (1 unchanged attribute hidden)
},
]
+ id = (known after apply)
+ ingress = [
+ {
+ cidr_blocks = [
+ “0.0.0.0/0”,
]
+ from_port = 443
+ ipv6_cidr_blocks = []
+ prefix_list_ids = []
+ protocol = “tcp”
+ security_groups = []
+ self = false
+ to_port = 443
# (1 unchanged attribute hidden)
},
]
+ name = “web-sg-2”
+ name_prefix = (known after apply)
+ owner_id = (known after apply)
+ revoke_rules_on_delete = false
+ tags_all = (known after apply)
+ vpc_id = (known after apply)
}
Plan: 2 to add, 0 to change, 0 to destroy.
module.security_groups_for_each.aws_security_group.for_each_sg[“sg1”]: Creating…
module.security_groups_for_each.aws_security_group.for_each_sg[“sg2”]: Creating…
module.security_groups_for_each.aws_security_group.for_each_sg[“sg2”]: Creation complete after 2s [id=sg-05190d6dade917b19]
module.security_groups_for_each.aws_security_group.for_each_sg[“sg1”]: Creation complete after 2s [id=sg-075986a50e78531de]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
PS C:\Users\charanjit\Documents\terraform_iteration_meta-arguments>
Below are the screenshots of provisioned security groups through for_each:
Making the Right Choice
So, when should you use count, and when should you reach for for_each? Here’s a simple way to decide:
- If all resources are identical or can be conditionally created based on a numeric count, use count.
- If each resource needs unique attributes or configurations, use for_each.
For example:
- Use count to create a fixed number of Security Groups with the same rules.
- Use for_each when each Security Group has different rules, ports, or other attributes.
Wrap Up !
Terraform’s count and for_each constructs are both powerful tools for managing resources, but they excel in different scenarios. Understanding the uniqueness of each can save you time and frustration as you scale your infrastructure.
With count, you get simplicity and conditional creation. With for_each, you get customization and flexibility. By understanding both, you’ll have the tools to build dynamic, efficient Terraform configurations that suit any need.
You can check my Below GitHub link in which you can see how I have written code as per real-world examples:
https://github.com/cjcheema/terraform_iteration_meta-arguments/tree/main
In GitHub my code is structured as below:
tree terraform/
.
├── main.tf
├── terraform.tfvars
├── modules/
│ ├── sg_count/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ ├── sg_count_condition/
│ │ ├── main.tf
│ │ ├── variables.tf
│ ├── sg_for_each/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
If you found this guide helpful, let me know in the comments or share it with your fellow Terraform enthusiasts!
- No More DynamoDB! Use Native S3 locking for Terraform State - February 7, 2025
- How to Bring and Manage Manually Created AWS Resources Under Terraform Management - January 31, 2025
- Iterating Cloud Resource Provisioning Using Terraform Count and For_Each Meta-Arguments - January 27, 2025