Terraform HCL Intro 5: Loops with Dynamic Block
source link: https://blog.boltops.com/2020/10/05/terraform-hcl-loops-with-dynamic-block
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.
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, though. We were only able to assign a single attribute for each resource per iteration. In this post, we’ll learn how to assign multiple attributes per iteration.
As a part of that, we’ll cover another Terraform looping construct, the dynamic nested block. The dynamic nested block provides a way to build repeated nested configuration blocks. This construct works at the attribute level. We’ll start with simple examples and work our way up to more complex examples.
Nested Configuration Block
First, let’s cover what a nested configuration block is. A security group with ingress rules provides a simple example:
resource "aws_security_group" "simple" {
name = "demo-simple"
description = "demo-simple"
ingress {
description = "description 0"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "description 1"
from_port = 81
to_port = 81
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
The code creates a security group with 2 security group rules. The ingress
attribute is repeated multiple times with different blocks of code. Some resource attributes can be assigned with this configuration block DSL syntax.
Expand to see terraform apply results.
$ terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
simple = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-0e53736807a424c35"
"description" = "demo-simple"
"egress" = []
"id" = "sg-0e53736807a424c35"
"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-simple"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"vpc_id" = "vpc-11111111"
}
Dynamic Nested Block Intro
Dynamic nested blocks can be used to assign multiple attributes. Here’s the first example re-written with a dynamic
block.
locals {
ports = [80, 81]
}
resource "aws_security_group" "dynamic" {
name = "demo-dynamic"
description = "demo-dynamic"
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"]
}
}
}
Expand to see terraform apply results.
$ terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
dynamic = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-08b33d072c13b71b9"
"description" = "demo-dynamic"
"egress" = []
"id" = "sg-08b33d072c13b71b9"
"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-dynamic"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"vpc_id" = "vpc-11111111"
}
It produces the same previous hardcoded result. First, let’s point out a few things initial things about this code:
- The dynamic argument is the original attribute we declared with a configuration block: “ingress”
- A
for_each
assignment is used. - The
content
block contains the original “ingress” block.
Now, let’s cover the more confusing portions:
- Terraform magically provides an
ingress
object. The object name matches the dynamic argument “ingress”. - The
ingress
object is a wrapper iterator object that contains info for each element that was assigned withfor_each = local.ports
.
In other words, the ingress
object will have these values for each iteration or pass of the loop:
Iteration
Values
1
ingress.key = 0
and ingress.value = 80
2
ingress.key = 1
and ingress.value = 81
We’re using a simple list(string)
structure for local.port
to help understand exactly what values Terraform magically assigns to the ingress
wrapper iterator object. It will also help us understand what is in ingress.value
when we make it more complicated and useful in the next section.
Useful Dynamic Block with Attrs List
Let’s make the dynamic nested block in the security group code more useful. Instead of hardcoding 0.0.0.0/0
for the rule, we’ll also make that dynamic.
To achieve this, the locals input data structure needs to be more complex. We’ll change it to a List of Maps. Each Map stores the ingress rule attributes.
locals {
rules = [{
description = "description 0",
port = 80,
cidr_blocks = ["0.0.0.0/0"],
},{
description = "description 1",
port = 81,
cidr_blocks = ["10.0.0.0/16"],
}]
}
resource "aws_security_group" "attrs" {
name = "demo-attrs"
description = "demo-attrs"
dynamic "ingress" {
for_each = local.rules
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
Expand to see terraform apply results.
$ terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
list = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-05c844b0799ff1f47"
"description" = "demo-list"
"egress" = []
"id" = "sg-05c844b0799ff1f47"
"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" = [
"10.0.0.0/16",
]
"description" = "description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "demo-list"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"vpc_id" = "vpc-11111111"
}
This example is much more useful. Pretty much any value assigned by the ingress block can now be configured.
Remember, the ingress
object is a wrapper object described in the previous example. The ingress.value
unravels the wrapper object and contains each element of the List, which is a Map. The ingress.key
was not used because it contains the index number and is not very useful.
Here’s a table with the iterations to help explain again:
Iteration
Values
1
ingress.key = 0
and ingress.value = {description = "description 0", port = 80, cidr_blocks = ["0.0.0.0/0"]
2
ingress.key = 1
and ingress.value = {description = "description 1", port = 81, cidr_blocks = ["10.0.0.0/16"]
Useful Dynamic Block with Attrs Map
locals {
map = {
"description 0" = {
port = 80,
cidr_blocks = ["0.0.0.0/0"],
}
"description 1" = {
port = 81,
cidr_blocks = ["10.0.0.0/16"],
}
}
}
resource "aws_security_group" "map" {
name = "demo-map"
description = "demo-map"
dynamic "ingress" {
for_each = local.map
content {
description = ingress.key # IE: "description 0"
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
output "map" {
value = aws_security_group.map
}
Expand to see terraform apply results.
$ terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
map = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-00a81d1d9ab6f08ae"
"description" = "demo-map"
"egress" = []
"id" = "sg-00a81d1d9ab6f08ae"
"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" = [
"10.0.0.0/16",
]
"description" = "description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "demo-map"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"vpc_id" = "vpc-11111111"
}
In this case, ingress.key
is used because it contains the description. Here’s a table with the iterations to help explain again:
Iteration
Values
1
ingress.key = "description 0"
and ingress.value = {port = 80, cidr_blocks = ["0.0.0.0/0"]
2
ingress.key = "description 1"
and ingress.value = {port = 81, cidr_blocks = ["10.0.0.0/16"]
For Each Difference: Resource vs Dynamic Block Level
In the previous post, we covered how to use the for_each
attribute at the resource level to perform looping. In this post, we’re covering for_each
at the dynamic nested block level. Even though they may look the same and have the same name, they are different.
With the resource-level for_each
:
- The
for_each
argument must be a map or set of strings. - The magical object is
each
. This magical object is an iterator wrapper object. To access the direct object, you use.key
and.value
. - When iterating over a Map, the iterator’s
.key
contains the Map key. - When iterating over a Set, the iterator’s
.key
and.value
are the same.
With the dynamic nested block for_each
:
- The
for_each
argument is requirements are more relaxed. It’s not strictly required to be a map or set of Strings. - The magical object name corresponds to the original configuration name, IE:
ingress
. This magical object is an iterator wrapper object. To access the direct object, you use.key
and.value
. - When iterating over a List, the iterator’s
.key
contains the index. - When iterating over a Map, the iterator’s
.key
contains the Map key.
As mentioned, when using dynamic nested block for_each
the magic variable is not named each
. We can set the wrapper iterator object name with iterator, though. This is useful when the name of the attribute is long and want to shorten it. Example:
locals {
map = {
"description 0" = {
port = 80,
cidr_blocks = ["0.0.0.0/0"],
}
"description 1" = {
port = 81,
cidr_blocks = ["10.0.0.0/16"],
}
}
}
resource "aws_security_group" "map" {
name = "demo-map"
description = "demo-map"
dynamic "ingress" {
for_each = local.map
# normally would be "ingress" here, but we're overriding the name
iterator = each
content {
# now we use each. instead of ingress.
description = each.key # IE: "description 0"
from_port = each.value.port
to_port = each.value.port
protocol = "tcp"
cidr_blocks = each.value.cidr_blocks
}
}
}
Expand to see terraform apply results.
$ terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
map = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-0d971b9430b6de78a"
"description" = "demo-map"
"egress" = []
"id" = "sg-0d971b9430b6de78a"
"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" = [
"10.0.0.0/16",
]
"description" = "description 1"
"from_port" = 81
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 81
},
]
"name" = "demo-map"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"vpc_id" = "vpc-11111111"
}
Configuration Blocks Are Syntactical Sugar
It is useful to understand that the configuration block syntax ability is syntactical sugar. 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
}]
}
Expand to see terraform apply results.
$ terraform apply
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
direct = {
"arn" = "arn:aws:ec2:us-west-2:111111111111:security-group/sg-00b95d25975196bf9"
"description" = "demo-direct"
"egress" = []
"id" = "sg-00b95d25975196bf9"
"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-direct"
"name_prefix" = ""
"owner_id" = "111111111111"
"revoke_rules_on_delete" = false
"vpc_id" = "vpc-11111111"
}
Here ingress
is directly assigned with the equal sign. With direct assignment, we are setting raw values, so we must also assign extra attributes like ipv6_cidr_blocks
and prefix_list_ids
. When using the syntactical sugar version, defaults are set for us. Understanding that nested configuration blocks can be assigned directly is key information to know for the next blog post.
Summary
In this post, we covered how to use the dynamic nested block loop construct. Terraform performs a bit of magic by making an iterator wrapper object available that matches the configuration block attribute’s original name. You can unravel this wrapper object with the .value
method. Understanding this will make it easier to use dynamic blocks with Terraform. In the next post, we’ll cover more complex loops.
The source code for the examples is available at: terraform-hcl-tutorials/5-dynamic-blocks
Want It to be Easier to Work with Terraform?
Check out Terraspace: The Terraform Framework.
The Terraform HCL Language Intro Tutorials
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK