Terraform HCL Intro 5: Loops with Dynamic Block
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.
Resource-level | Attribute-level |
---|---|
The for_each argument must be a map or set of strings. |
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 is each . This magical object is an iterator wrapper object. To access the direct object, you use .key and .value . |
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 Set, the iterator’s .key and .value are the same. |
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. |
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, Terraform HCL Intro 6: Nested Loops.
The source code for the examples is available for BoltOps Learn subscribers: 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
Thanks for reading this far. If you found this article useful, I'd really appreciate it if you share this article so others can find it too! Thanks 😁 Also follow me on Twitter.
Got questions? Check out BoltOps.
You might also like
More tools:
-
Kubes
Kubes: Kubernetes Deployment Tool
Kubes is a Kubernetes Deployment Tool. It builds the docker image, creates the Kubernetes YAML, and runs kubectl apply. It automates the deployment process and saves you precious finger-typing energy.
-
Jets
Jets: The Ruby Serverless Framework
Ruby on Jets allows you to create and deploy serverless services with ease, and to seamlessly glue AWS services together with the most beautiful dynamic language: Ruby. It includes everything you need to build an API and deploy it to AWS Lambda. Jets leverages the power of Ruby to make serverless joyful for everyone.
-
Lono
Lono: The CloudFormation Framework
Building infrastructure-as-code is challenging. Lono makes it much easier and fun. It includes everything you need to manage and deploy infrastructure-as-code.