Terraform Series - Iterating Over Blocks with Dynamic Blocks

This article was last updated on: May 17, 2026 am

Series Articles

Overview

The Terraform Series Articles introduced using the Grafana Terraform Provider with Terraform’s IaC methodology to batch-create various Grafana resources automatically, including Dashboards, Datasources, and more.

Now there’s a real-world requirement:

For access control purposes, we need to enable Folder Permissions to restrict view access to a specific set of teams for a given Folder.

How do we implement this? 🤔

Solution

Use Terraform’s for_each and dynamic blocks.

Core Concepts

Dynamic Blocks

In top-level block structures such as resource, expressions can usually only be used when assigning a value to an argument using the name = expression form. This covers many use cases, but some resource types include repeatable nested blocks in their arguments, which typically represent separate objects related to (or embedded within) the containing object:

1
2
3
4
5
6
7
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name" # can use expressions here

setting {
# but the "setting" block is always a literal block
}
}

You can dynamically construct repeatable nested blocks using the special dynamic block type, which is supported in resource, data, provider, and provisioner blocks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name"
application = "${aws_elastic_beanstalk_application.tftest.name}"
solution_stack_name = "64bit Amazon Linux 2018.03 v2.11.4 running Go 1.12.6"

dynamic "setting" {
for_each = var.settings
content {
namespace = setting.value["namespace"]
name = setting.value["name"]
value = setting.value["value"]
}
}
}

A dynamic block acts much like a for expression, but produces nested blocks instead of a complex value. It iterates over a given complex value and generates a nested block for each element.

  • The label of the dynamic block (“setting” in the example above) specifies the type of nested block to generate.
  • The for_each argument provides the complex value to iterate over.
  • The iterator argument (optional) sets the name of a temporary variable that represents the current element of the complex value. If omitted, the variable name defaults to the label of the dynamic block (“setting” in the example above).
  • The labels argument (optional) is a list of strings that specifies the block labels to use for each generated block, in order. You can use the temporary iterator variable in this value.
  • The nested content block defines the body of each generated block. You can use the temporary iterator variable within this block.

Since the for_each argument accepts any collection or structural value, you can use a for expression or splat expression to transform an existing collection.

The iterator object (setting in the example above) has two attributes:

  • key is the map key or list element index of the current element. If the for_each expression produces a set value, then key is identical to value.
  • value is the value of the current element.

A dynamic block can only generate arguments that belong to the resource type, data source, provider, or provisioner being configured. It cannot generate meta-argument blocks such as lifecycle and provisioner blocks, because Terraform must process these blocks before it can safely evaluate expressions.

The for_each value must be a collection with one element per desired nested block. If you need to declare resource instances based on a nested data structure or combinations of elements from multiple data structures, you can use Terraform expressions and functions to derive a suitable value. See the flatten and setproduct functions for some common examples of such situations.

Some provider-defined resource types include multiple levels of blocks nested within each other. You can dynamically generate these nested structures when necessary by nesting dynamic blocks within the content portion of other dynamic blocks.

For example, a module might accept a complex data structure like the following:

1
2
3
4
5
6
7
variable "load_balancer_origin_groups" {
type = map(object({
origins = set(object({
hostname = string
}))
}))
}

If you need to define a resource whose type requires a block for each origin group and then a nested block for each origin within the group, you can have Terraform dynamically generate it using nested dynamic blocks as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
dynamic "origin_group" {
for_each = var.load_balancer_origin_groups
content {
name = origin_group.key

dynamic "origin" {
for_each = origin_group.value.origins
content {
hostname = origin.value.hostname
}
}
}
}

When using nested dynamic blocks, pay special attention to the iterator symbol for each block. In the example above, origin_group.value refers to the current element of the outer block, while origin.value refers to the current element of the inner block.

If a particular resource type defines nested blocks whose type names are the same as one of the type names in their parent, you can use the iterator argument in each dynamic block to choose a different iterator symbol, making it easier to distinguish between them.

Overuse of dynamic blocks can make configuration hard to read and maintain, so we recommend using them only when you need to hide details to build a clean user interface for a reusable module. Whenever possible, write nested blocks out literally.

Hands-On Example

Requirement:

For access control purposes, we need to enable Folder Permissions to restrict view access to a specific set of teams for a given Folder.

The corresponding Terraform code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
locals {
teams = {
"dev",
"busi",
"ops",
"data",
"pm"
}
}

resource "grafana_folder_permission" "foldersPermission" {

folder_uid = "demo"

dynamic "permissions" {
for_each = local.teams
content {
team_id = grafana_team.teams[each.key].id
permission = "View"
}
}
}

Notes:

  • permissions (Block Set, Min: 1) The permission items to add/update. Items not present in the list will be removed.

Done 🎉🎉🎉

📚️ References