Terraform Dynamic Blocks
Brendan Thompson • 29 November 2022 • 7 min read
In this post, we will cover the Terraform dynamic
Blocks expression, and how to use it. The
dynamic
block works in a very similar way to for_each
meta-argument, instead of producing
n
instances of a whole resource will produce n
instances of a particular block within a
resource. This becomes incredibly useful when you need to create multiple instances of a particular
sub-resource/block with a resource, an example being hard disks within a virtual machine.
Another good example is creating Network Security Groups, the rule
itself is a block and
can thus be serviced by the use of a dynamic
block. We will go through an example of how this works.
Using a dynamic
block
In this example, we are going to be creating Network Security Groups from a csv
file with a
dynamic
block. I am a big believer in using the right structure of data for the task at hand,
NSGs are a great example of where a csv
file makes sense. The data is the same field's n
number
of times in an incrementing priority
property. The first thing we will do is set up the csv
file
ready.
name,priority,direction,access,protocol,source_port_range,source_address_prefix,destination_port_range,destination_address_prefix
allow-http-any,1000,Inbound,Allow,Tcp,*,*,80,*
allow-https-any,1001,Inbound,Allow,Tcp,*,*,443,*
In the above you can see we have two rules in place, one to allow Tcp/80
and one for Tcp/443
.
Let's now go through the Terraform code we are going to use to turn this csv
into some rules!!
Firstly we need to ingest our csv
into an object that can be consumed by our code. Terraform
thankfully gives us the csvdecode
function for just this purpose, this will decode the csv
data
into an object accessible from local.rules
.
locals {
rules = csvdecode(file("./rules.csv"))
}
The next piece is setting ourselves up with a Resource Group.
resource "azurerm_resource_group" "this" {
name = "rg-blog-db"
location = "australiaeast"
}
Next, we come to the juicy part where we take that data and turn it into rules using dynamic
. The
security_rule
block is what we will be looping over to provide the rules within a single NSG.
Unlike a traditional for_each
where you would access the properties via each.value
and each.key
.
Instead, when using a dynamic
block the properties are accessed by the name of the block we are
looping on, in this instance security_rule
. The key to remember is whatever the ID next to dynamic
it will be the key you must use. There is a way to change that which we will touch on further down.
The content
block within dynamic
is where we will set the properties for the block we are looping
over.
resource "azurerm_network_security_group" "this" {
name = "nsg"
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
dynamic "security_rule" {
for_each = local.rules
content {
name = security_rule.value.name
priority = security_rule.value.priority
direction = security_rule.value.direction
access = security_rule.value.access
protocol = security_rule.value.protocol
source_port_range = security_rule.value.source_port_range
source_address_prefix = security_rule.value.source_address_prefix
destination_port_range = security_rule.value.destination_port_range
destination_address_prefix = security_rule.value.destination_address_prefix
}
}
}
Now if you want to be able to set the ID/key of the iterator property -above security_rule
- can
be altered by the use of the iterator
key. An example of this is below:
resource "azurerm_network_security_group" "this" {
name = "nsg"
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
dynamic "security_rule" {
for_each = local.rules
iterator = rule
content {
name = rule.value.name
...
}
}
}
In the example above we are now changing the iterator to be rule
which is shorter than the default
security_rule
. This change is purely cosmetic, but it can be extremely useful when the block that
we are operating on has a very long name.
Nested & Optional
Now that we have looked at our basic dynamic
block let's see how we can get a little funkier
with it. In this example, we are going to look at creating an Azure Container instance with
two containers, and one of those containers will have ports defined.
As we often begin here is our locals
block that will contain the data we will supply to our dynamic
in order to produce the ACI.
locals {
containers = {
app = {
image = "mcr.microsoft.com/azuredocs/aci-helloworld:latest"
cpu = 0.5
memory = 1.5
ports = {
https = {
port = 443
protocol = "TCP"
}
http = {
port = 80
protocol = "TCP"
}
}
}
sidecar = {
image = "mcr.microsoft.com/azuredocs/aci-tutorial-sidecar"
cpu = 0.5
memory = 1.5
}
}
}
We have two containers defined; app
and sidecar
, we could however have more containers if we
so wished, or perhaps only one! This example configuration above is pulled right from the Microsoft documentation
so it's easy for you to work with and test on your own. You will also be able to see that our app
container has some ports configured to allow for access to the app running on it.
Let's look at how we compose our code for nested blocks.
resource "azurerm_container_group" "this" {
name = "aci-blog"
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
ip_address_type = "Public"
dns_name_label = "aci-blog"
os_type = "Linux"
dynamic "container" {
for_each = local.containers
content {
name = container.key
image = container.value.image
cpu = container.value.cpu
memory = container.value.memory
dynamic "ports" {
for_each = can(container.value.ports) ? container.value.ports : {}
content {
port = ports.value.port
protocol = ports.value.protocol
}
}
}
}
}
Our first dynamic
operates on the container
property of the azurerm_container_group
resource
this will allow us to create one or more containers, the same way our other example creates one or
more security rules in an NSG. The container
property has a sub-property called ports
which allows
us to define ports that we want to expose, however not all containers may require this. The nested
dynamic
is declared in the same way as any other, and can either use properties of the original
for_each
(like we have here) or it could reference some other piece of configuration. In our
example we are using the can()
function to see if our property has a value, if it does
then we access that as the object/property we pass into the for_each
. Another option would be to
use the try()
function. Using try()
would be the better choice as it's able to
handle errors.
With the above example, it should be plain to see that you would be able to use something like a feature
flag in the for_each
for our dynamic
blocks. A scenario where this might be useful is if we
should use a managed identity on a particular resource, then we could have something like
var.use_managed_identity
.
Closing out
We have gone through the dynamic
block, using the example of a network security rule. We have also
looked at how we can change the name of the iterator that is used within the dynamic
block. It is
worth noting that blocks within resources are going out of fashion as more providers are opting for
the use of maps
and objects
. This does make me question the long-term usefulness of the dynamic
block. In saying that, using a dynamic
block for network security rules is extremely helpful.
We could even extend this to loop over multiple network security groups referencing either multiple
csv
files or filtering out rules by using a property within the csv
file.
If you'd like to see more examples of using the dynamic
block, or you've got a unique way of using
it yourself I'd love to hear from you.