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.


Brendan Thompson

Principal Cloud Engineer

Discuss on Twitter