Dynamic Block
Terraform has a feature known as dynamic blocks. The dynamic blocks provide a way of dynamically generating repeatable nested blocks inside resource, data, provider, and provisioner blocks. It is particularly useful for resource types that have multiple levels for nested blocks. Dynamic blocks can make it difficult to read and manage, so they should be used only where absolutely necessary.
Dynamic block Syntax
The basic syntax of a dynamic block is:
dynamic block_name {
for_each = <collection>
content {
# arguments
}
}
Let's break this syntax apart:
dynamic
: A Keyword indicating that this is the start of a dynamic block. This keyword tells Terraform to generate a block dynamically.
block_name
: Name of the block that you wish to repeat.
for_each
: A keyword that specifies the collection of values to iterate over.
<collection>
: This is the actual collection of values that you want to iterate over. This can be a variable, a resource attribute, or a hardcoded value.
content
: It contains the arguments to be repeated for each item in the collection.
arguments
: Actual arguments to be repeated for each item in the collection. Any Terraform configuration syntax may be used here, including variables, functions and resource attributes.
Let's take an example where we want to create a CodeArtifact repository named "Common-repo" which requires three upstream repositories. Initial configuration will look like this:
resource "aws_codeartifact_repository" "codeartifact_repository" {
repository = "Common-repo"
description = "This is repo contains Private packages"
domain_owner = "8363872828
domain = "PrivateRepo
upstream {
repository_name = "repo1"
}
upstream {
repository_name = "repo2"
}
upstream {
repository_name = "repo3"
}
}
Here, we have three repetitions of the upstream block. Whenever we need to add or remove any upstream repository, we must update it manually, which is very tedious and most of the time error prone. In order to avoid such repetition as shown in the previous example, it is possible to use dynamic blocks which generate for us dynamically the blocks for upstream. Here's the updated configuration:
variable upstream-repos {
default = ["repo1", "repo2", "repo3"]
}
resource "aws_codeartifact_repository" "codeartifact_repository" {
repository = "Common-repo"
description = "This is repo contains Private packages"
domain_owner = "8363872828
domain = "PrivateRepo
dynamic "upstream" {
for_each = toset(var.upstream-repos)
content {
repository_name = upstream.value
}
}
}
This dynamic block gets rid of the repetition and gives us a compact, maintainable configuration. We will have to edit only the upstream-repos variable when we add or remove upstream repositories, and the configuration automatically adjusts.
NOTE: It is not possible to generate
meta-argument
blocks, such as the Lifecycle block as well as Provisioner block, using a dynamic block since Terraform needs to process the meta-argument blocks before it can evaluate the expressions. Since the Dynamic Block depends on the evaluation of an expression, it cannot be used to dynamically create these meta-argument blocks.
Using for_each with transformations
The for_each argument, in an dynamic block, accepts any collection or structural value. The two attributes available on the for_each iterator object are:
key
: The map key or list element index of the current element. (If the for_each expression produces a set value, do not use key because its value will be identical to value.)
value
: The value of the current element.
You can do transformations, conditional logic, or other operations on the data within your dynamic block using key and value.
Multi-Level Nested Block Structures
Some resource types have multiple levels of blocks nested within the resource. For example, one parent block might contain several child blocks, and each of those child blocks might in turn contain other nested blocks. To handle this level of nested structure dynamically, Terraform supports the use of nested dynamic blocks.
Nested dynamic blocks are created by placing a dynamic block inside the content part of another dynamic block. This will enable Terraform to create as many nested blocks as needed from the provided input data.
The basic syntax for Multi-Level Nested block structure is:
dynamic "block_name" {
for_each = <collection>
content {
# arguments
dynamic "block_name" {
for_each = <collection>
content {
# arguments
}
}
}
}
Let's consider an example where we would like to create a custom IAM policy that has permissions to decrypt encrypted data using the AWS KMS, but under certain conditions. We define here the policy document using a datasource "aws_iam_policy_document" block. Here is the configuration of the policy document:
data "aws_iam_policy_document" "example" {
statement {
actions = [kms:Decrypt]
resources = ["*"]
condition {
test = "ForAnyValue:StringEquals"
variable = "kms:EncryptionContext:service"
values = ["pi"]
}
condition {
test = "ForAnyValue:StringEquals"
variable = "kms:EncryptionContext:aws:pi:service"
values = ["rds"]
}
}
}
In this setup, the condition block is duplicated. This is not good, because it results in:
Repetition
: We have to repeat the condition block more than once, making the configuration more verbose, and harder to maintain.
Error-proneness
: Configuration can be updated manually in order to add or remove conditions, a procedure which is error-prone and may introduce inconsistencies.
Lack of flexibility
: If we need to make changes in conditions, then we should update the configuration manually, which sometimes may be very long and error-prone.
To avoid the mentioned above problems, we can use nested dynamic blocks for building the policy document. Here's how you can configure that:
variable "multi-blocks" {
default = {
kms:Decrypt = {
resources = "*"
conditions = {
condition1 = {
test = "ForAnyValue:StringEquals"
variable = "kms:EncryptionContext:service"
values = ["pi"]
}
condition2 = {
test = "ForAnyValue:StringEquals"
variable = "kms:EncryptionContext:aws:pi:service"
values = ["rds"]
}
}
}
}
}
data "aws_iam_policy_document" "example" {
dynamic "statement" {
for_each = var.multi-blocks
actions = [statement.key]
resources = [statement.value.resources]
content {
dynamic "condition" {
for_each = statement.value.connections
content {
test = connection.value.test
variable = connection.value.variable
values = connection.value.values
}
}
}
}
}
Here we have a new configuration in which we declare a variable named multi-blocks that holds a map of statements, where each statement has its own set of conditions. Then we use a dynamic statement block where we iterate the multi-blocks variable through the for_each argument. Inside the statement block, we use a nested dynamic condition block to iterate through the conditions of each statement. The content block inside the conditional block states the definition of test, variable, and values attributes using the syntax conditional.value.
Best Practices for Dynamic Blocks
When using dynamic blocks, follow these guidelines:
- Use sparingly: Don't overuse dynamic blocks. Having too many of them will make your configuration harder to read and maintain. Use them sparingly where you need to hide complex details in order to create a simple, reusable module interface.
- Prefer explicit blocks: Wherever possible, write out nested blocks rather than using dynamic blocks. The code will be easier to read and follow this way.
Related Pages
Feedback
Was this page helpful?