Using Terraform for_each to Manage Multiple AWS Security Groups
Learn how to use Terraform's for_each meta-argument to dynamically create multiple AWS security groups from a single resource block, reducing duplication and improving maintainability.
16 Dec 2025, 23:04 UTC

Problem: Repetitive security‑group definitions
\nWhen you need dozens of AWS security groups that differ only by name or a few CIDR blocks, writing a separate aws_security_group block for each one creates a lot of duplicated code. Any change to the common structure (e.g., adding a new ingress rule) must be copied into every block, increasing the chance of drift and making the configuration harder to maintain.
Thesis: for_each lets you define a single resource block that scales with a map of inputs
\nTerraform’s for_each meta‑argument iterates over a map or set of strings, creating one instance of a resource for each key. By placing the variable data that distinguishes each security group (such as its name, description, and ingress rules) into a map, you can write one aws_security_group block and let Terraform generate as many groups as you need.
Worked example: Define a variable map and use for_each
\nFirst, declare a variable that holds a map of security‑group definitions. Each map entry’s key will become the unique identifier Terraform uses in state; the value contains the attributes for that group.
\nvariable \"sg_map\" {\n description = \"Map of security groups to create.\"\n type = map(object({\n description = string\n ingress = list(object({\n from_port = number\n to_port = number\n protocol = string\n cidr_blocks = list(string)\n }))\n egress = list(object({\n from_port = number\n to_port = number\n protocol = string\n cidr_blocks = list(string)\n }))\n }))\n default = {\n web = {\n description = \"Allow HTTP/HTTPS to the web tier\"\n ingress = [\n { from_port = 80, to_port = 80, protocol = \"tcp\", cidr_blocks = [\"0.0.0.0/0\"] },\n { from_port = 443, to_port = 443, protocol = \"tcp\", cidr_blocks = [\"0.0.0.0/0\"] }\n ],\n egress = [{ from_port = 0, to_port = 0, protocol = \"-1\", cidr_blocks = [\"0.0.0.0/0\"] }]\n },\n db = {\n description = \"Allow PostgreSQL from the web tier\"\n ingress = [\n { from_port = 5432, to_port = 5432, protocol = \"tcp\", cidr_blocks = [\"10.0.0.0/16\"] }\n ],\n egress = [{ from_port = 0, to_port = 0, protocol = \"-1\", cidr_blocks = [\"0.0.0.0/0\"] }]\n }\n }\n}\n\nresource \"aws_security_group\" \"example\" {\n for_each = var.sg_map\n\n name = each.key\n description = each.value.description\n\n dynamic \"ingress\" {\n for_each = each.value.ingress\n content {\n from_port = ingress.value.from_port\n to_port = ingress.value.to_port\n protocol = ingress.value.protocol\n cidr_blocks = ingress.value.cidr_blocks\n }\n }\n\n dynamic \"egress\" {\n for_each = each.value.egress\n content {\n from_port = egress.value.from_port\n to_port = egress.value.to_port\n protocol = egress.value.protocol\n cidr_blocks = egress.value.cidr_blocks\n }\n }\n}\n\nTo see what Terraform will create, run the following commands in the directory containing the above files. You need AWS credentials with permission to create security groups.
\n- \n
terraform init– downloads the AWS provider. \n terraform plan– shows twoaws_security_group.exampleresources (one forweb, one fordb) in the plan output. \n terraform apply– provisions the groups. After apply, you can verify with the AWS CLI:\n
\n Each group should have the ingress and egress rules defined in the map.aws ec2 describe-security-groups --filters Name=group-name,Values=web db\n terraform destroy– removes the groups, confirming that cleanup works as expected. \n
Trade‑off: State readability and key management
\nUsing for_each eliminates duplication, but it also changes how resources appear in the Terraform state. Each instance is addressed as aws_security_group.example[\"\"]. Large maps can make the state file harder to scan, and changing a key (e.g., renaming web to frontend) forces Terraform to destroy the old group and create a new one, which may cause a brief window where the old rules are missing. To mitigate this:
- \n
- Keep keys stable; treat them as immutable identifiers. \n
- If you anticipate many groups, split the map into separate modules or multiple
for_eachresources to keep each state slice manageable. \n - Ensure all values referenced inside the
for_eachblock are static or derived from inputs; avoid using resources that may change independently, as that can cause unexpected drift. \n
Actionable closing: Start small and iterate
\nBegin by migrating a subset of your existing security groups to the for_each pattern. Run terraform plan after each change to confirm that only the intended resources are affected. Once you’re comfortable with the workflow, gradually move the remaining groups into the same variable map. This incremental approach lets you gain the maintainability benefits of for_each while minimizing risk to production environments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.