

Terraform HCL Intro 6: Nested Loops
source link: https://blog.boltops.com/2020/10/06/terraform-hcl-nested-loops
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In this post, we’ll take on nested loops with Terraform. Terraform is declarative, so a nested loop can be tricky. This post hopes to help with that.
Previous Posts Review
We’ve covered loops fundamentals in the previous two blog posts:
We’re building on top of those learnings, so if you have not read those posts yet, it’ll be helpful to go back and understand those posts.
Resource Loop Starter Code
First, we’ll create 2 security groups with a for_each
loop at the resource-level using what we learned from: Terraform Intro 4: Loops with Count and For Each.
locals {
names = ["demo-example-1", "demo-example-2"]
}
resource "aws_security_group" "names" {
for_each = toset(local.names)
name = each.value # key and value is the same for sets
description = each.value
}
output "security_groups" {
value = aws_security_group.names
}
Expand to see terraform apply results.
$ terraform apply
Outputs:
security_groups = {
"demo-example-1" = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-05cec80cee397e0ef"
"description" = "demo-example-1"
"egress" = []
"id" = "sg-05cec80cee397e0ef"
"ingress" = []
"name" = "demo-example-1"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"tags" = {}
"vpc_id" = "vpc-11111111"
}
"demo-example-2" = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-0b6bd59983e8b3389"
"description" = "demo-example-2"
"egress" = []
"id" = "sg-0b6bd59983e8b3389"
"ingress" = []
"name" = "demo-example-2"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"tags" = {}
"vpc_id" = "vpc-11111111"
}
}
Dynamic Nested Block
Now, let’s “naively” add a dynamic nested block configuration using what we learned from: Terraform Intro 5: Loops with Dynamic Block.
locals {
names = ["demo-example-1", "demo-example-2"]
}
locals {
ports = [80, 81]
}
resource "aws_security_group" "names" {
for_each = toset(local.names)
name = each.value # key and value is the same for sets
description = each.value
dynamic "ingress" {
for_each = local.ports
content {
description = "description ${ingress.key}"
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
output "security_groups" {
value = aws_security_group.names
}
Expand to see terraform apply results.
$ terraform apply
Outputs:
security_groups = {
"demo-example-1" = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-05cec80cee397e0ef"
"description" = "demo-example-1"
"egress" = []
"id" = "sg-05cec80cee397e0ef"
"ingress" = [
{
"cidr_blocks" = [
"0.0.0.0/0",
]
"description" = "description 0"
"from_port" = 80
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 80
},
{
"cidr_blocks" = [
"0.0.0.0/0",
]
"description" = "description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "demo-example-1"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"tags" = {}
"vpc_id" = "vpc-11111111"
}
"demo-example-2" = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-0b6bd59983e8b3389"
"description" = "demo-example-2"
"egress" = []
"id" = "sg-0b6bd59983e8b3389"
"ingress" = [
{
"cidr_blocks" = [
"0.0.0.0/0",
]
"description" = "description 0"
"from_port" = 80
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 80
},
{
"cidr_blocks" = [
"0.0.0.0/0",
]
"description" = "description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "demo-example-2"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"tags" = {}
"vpc_id" = "vpc-11111111"
}
}
We did is “naive” because currently, the dynamic nested block has the same ingress security rules for every security group. IE: cidr_blocks = ["0.0.0.0/0"]
. That’s not very useful.
Combining the Nested Loops Properly
The key to a nested loop is having the proper data structure. Let’s combine and move the ingress rules into the primary data structure, the security groups themselves.
locals {
groups = {
example0 = {
description = "sg description 0"
rules = [{
description = "rule description 0",
port = 80,
cidr_blocks = ["10.0.0.0/16"],
},{
description = "rule description 1",
port = 81,
cidr_blocks = ["10.1.0.0/16"],
}]
},
example1 = {
description = "sg description 1"
rules = [{
description = "rule description 0",
port = 80,
cidr_blocks = ["10.2.0.0/16"],
},{
description = "rule description 1",
port = 81,
cidr_blocks = ["10.3.0.0/16"],
}]
}
}
}
resource "aws_security_group" "this" {
for_each = local.groups
name = each.key # top-level key is security group name
description = each.value.description
dynamic "ingress" {
for_each = each.value.rules # List of Maps with rule attributes
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
output "security_groups" {
value = aws_security_group.this
}
Expand to see terraform apply results.
$ terraform apply
Outputs:
security_groups = {
"example0" = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-05a49ba9dd159608c"
"description" = "sg description 0"
"egress" = []
"id" = "sg-05a49ba9dd159608c"
"ingress" = [
{
"cidr_blocks" = [
"10.0.0.0/16",
]
"description" = "rule description 0"
"from_port" = 80
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 80
},
{
"cidr_blocks" = [
"10.1.0.0/16",
]
"description" = "rule description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "example0"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"tags" = {}
"vpc_id" = "vpc-11111111"
}
"example1" = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-0ba2e3ff20952cc52"
"description" = "sg description 1"
"egress" = []
"id" = "sg-0ba2e3ff20952cc52"
"ingress" = [
{
"cidr_blocks" = [
"10.2.0.0/16",
]
"description" = "rule description 0"
"from_port" = 80
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 80
},
{
"cidr_blocks" = [
"10.3.0.0/16",
]
"description" = "rule description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "example1"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"tags" = {}
"vpc_id" = "vpc-11111111"
}
}
We can now have different ingress rules for each security group. The ingress rules are no longer hardcoded. This achieves the nested loop.
Combined the Nested Loops with Flatter Data Structures
While some folks like heirarchal data structures, some prefer to “flatten” the data structure into 2 different variables. Here’s an example of that:
locals {
groups = {
example0 = {
description = "sg description 0"
},
example1 = {
description = "sg description 1"
}
}
rules = {
example0 = [{
description = "rule description 0",
port = 80,
cidr_blocks = ["10.0.0.0/16"],
},{
description = "rule description 1",
port = 81,
cidr_blocks = ["10.1.0.0/16"],
}]
example1 = [{
description = "rule description 0",
port = 80,
cidr_blocks = ["10.2.0.0/16"],
},{
description = "rule description 1",
port = 81,
cidr_blocks = ["10.3.0.0/16"],
}]
}
}
resource "aws_security_group" "this" {
for_each = local.groups
name = each.key # top-level key is security group name
description = each.value.description
dynamic "ingress" {
for_each = local.rules[each.key] # List of Maps with rule attributes
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
output "security_groups" {
value = aws_security_group.this
}
Expand to see terraform apply results.
$ terraform apply
Outputs:
security_groups = {
"example0" = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-0cbf2a34134e612e9"
"description" = "sg description 0"
"egress" = []
"id" = "sg-0cbf2a34134e612e9"
"ingress" = [
{
"cidr_blocks" = [
"10.0.0.0/16",
]
"description" = "rule description 0"
"from_port" = 80
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 80
},
{
"cidr_blocks" = [
"10.1.0.0/16",
]
"description" = "rule description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "example0"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"vpc_id" = "vpc-11111111"
}
"example1" = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-092de6661edd12b9b"
"description" = "sg description 1"
"egress" = []
"id" = "sg-092de6661edd12b9b"
"ingress" = [
{
"cidr_blocks" = [
"10.2.0.0/16",
]
"description" = "rule description 0"
"from_port" = 80
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 80
},
{
"cidr_blocks" = [
"10.3.0.0/16",
]
"description" = "rule description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "example1"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"vpc_id" = "vpc-11111111"
}
}
We’ve achieved the same result: a nested loop that can create as many security groups as we want with different ingress rules for each security group. This time with two different variables and flatter data structures.
Consideration: Updates with Removal
There’s a subtle but important consideration with the current code. It happens when the code gets updated, particularly when previously added elements are removed.
For example, let’s say we first use the code above and run a terraform apply
. That creates security groups with rules. Then we delete the rules from the code. Running terraform apply
again will not remove the rules.
This is because when there’s an empty List, the for_each
loop never iterates. If you wish for the security group rules to maintain its current state set outside of Terraform, you may want this behavior. However, this is probably unexpected and undesirable behavior.
If you want to have Terraform remove all the security group rules, then ingress
needs to be assigned directly with a List. We’ll cover how to do that shortly.
Configuration Blocks Are Syntactical Sugar
First, it is useful to understand that the configuration block syntax ability is syntactical sugar. We covered this in the previous post, and it’s worth highlighting again. Essentially, we can also assign the attribute directly with a List of Maps. Example:
resource "aws_security_group" "direct" {
name = "demo-direct"
description = "demo-direct"
ingress = [{
description = "description 0"
from_port = "80"
to_port = "80"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = null
prefix_list_ids = null
security_groups = null
self = null
},{
description = "description 1"
from_port = "81"
to_port = "81"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
ipv6_cidr_blocks = null
prefix_list_ids = null
security_groups = null
self = null
}]
}
Here, ingress
is directly assigned with the equal sign. There are also extra attributes that must be explicitly be set like ipv6_cidr_blocks
and prefix_list_ids
. When using the syntactical sugar version, defaults are set for us. When assigned directly, we must set additional attributes because we’re setting the raw values.
Understanding that configuration blocks can be assigned directly will be useful for resetting and removing elements.
Direct Assignment Approach
For attributes designed for the block configuration syntax, we can also directly assign the attribute with an List of Maps instead. Here’s an example where we directly assign ingress:
locals {
groups = {
example0 = {
description = "sg description 0"
},
example1 = {
description = "sg description 1"
}
}
rules = {
example0 = [{
description = "rule description 0"
to_port = 80
from_port = 80
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
ipv6_cidr_blocks = null
prefix_list_ids = null
security_groups = null
self = null
},{
description = "rule description 1"
to_port = 80
from_port = 80
protocol = "tcp"
cidr_blocks = ["10.1.0.0/16"]
ipv6_cidr_blocks = null
prefix_list_ids = null
security_groups = null
self = null
}]
example1 = [] # empty List removes previous rules
}
}
resource "aws_security_group" "this" {
for_each = local.groups
name = each.key # top-level key is security group name
description = each.value.description
# direct assignment of List of Maps
ingress = lookup(local.rules, each.key, null) == null ? [] : local.rules[each.key]
}
Expand to see terraform plan results.
$ terraform plan
Terraform will perform the following actions:
# aws_security_group.this["example1"] will be updated in-place
~ resource "aws_security_group" "this" {
arn = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-020565b90055f8df1"
description = "sg description 1"
egress = []
id = "sg-020565b90055f8df1"
~ ingress = [
- {
- cidr_blocks = [
- "10.2.0.0/16",
]
- description = "rule description 0"
- from_port = 80
- ipv6_cidr_blocks = []
- prefix_list_ids = []
- protocol = "tcp"
- security_groups = []
- self = false
- to_port = 80
},
- {
- cidr_blocks = [
- "10.3.0.0/16",
]
- description = "rule description 1"
- from_port = 80
- ipv6_cidr_blocks = []
- prefix_list_ids = []
- protocol = "tcp"
- security_groups = []
- self = false
- to_port = 80
},
]
name = "example1"
owner_id = "111111111111"
revoke_rules_on_delete = false
tags = {}
vpc_id = "vpc-11111111"
}
Plan: 0 to add, 1 to change, 0 to destroy.
There are advantages to this approach:
- When rules are removed, Terraform will remove the rules also.
- We’ve removed the second inner loop. It’s simpler in the sense that there’s no nested loop anymore. There’s only one outer loop at the resource level.
There are also some cons, though:
- A bothersome thing about this code is we must to set default values for attributes like
ipv6_cidr_blocks
andprefix_list_ids
. It introduces duplication. - We did not have to set these extra attributes when we were using the configuration block syntax.
- In the next few posts, we’ll cover the
for .. in
loop, which can be used to remove this duplication.
Cleanup
Note, remember to clean up the resources:
terraform destroy
Summary
This post covered how to perform nested loops with an outer resource-level for_each
loop and an inner dynamic nested block loop. We showed examples of hierarchical and flat data structures. We pointed out that the for_each
technique does not remove existing elements. We then covered the direct assignment approach, which will remove existing elements. Terraform’s declarative loops can be tricky for those used to the procedural language loops, so hopefully, this post is helpful. In the next few posts, we’ll cover the for ... in
loop.
The source code for the examples is available at: terraform-hcl-tutorials/6-nested-loops
Want It to be Easier to Work with Terraform?
Check out Terraspace: The Terraform Framework.
The Terraform HCL Language Intro Tutorials
Recommend
-
10
In the last post, we provided an introduction to the for in construct. This post provides more examples, shows local assignment, and covers how to set default attribute values. Remember, for is useful for data struct...
-
10
In this post, we’ll cover the Terraform for in loop construct. Though it performs looping, its primary purpose is really for manipulating data structures. You can do a few things to data structures with it: ...
-
8
Terraform HCL Intro 5: Loops with Dynamic Block Posted by Tung Nguyen on Oct 5, 2020 In the previous post, we established loop fundamentals. The loops were pretty basic,...
-
13
Terraform HCL Intro 4: Loops with Count and For Each Posted by Tung Nguyen on Oct 4, 2020 In this post, we’ll cover Terraform looping constructs. Terraform is declarativ...
-
6
Terraform HCL Intro 3: Conditional Logic Posted by Tung Nguyen on Oct 3, 2020 In this post, we’ll cover how to perform conditional logic with Terraform. It’ll be a littl...
-
14
Terraform HCL Intro 2: Fu...
-
5
Terraform HCL Intro 1: Resources, Variables, Outputs Posted by Tung Nguyen on Oct 1, 2020 This is the start of a series of posts to help introduce and learn the Terrafor...
-
5
OpenMP Parallel nested for loops vs parallel internal for advertisements If I use nested parallel for loops like this: #pragma...
-
9
《Terraform 101 从入门到实践》 第五章 HCL语法 《Terraform 101 从入门到实践》 第五章 HCL语法 ...
-
6
David Drummer Posted on Mar 23
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK