Creating Nested Conditional Dynamic Terraform Blocks

While working on an assignment to update the AWS Cognito User Pool for the team that I’ve built using Terraform, I faced a problem where the software development team is working on revising custom attributes. They gave me a heads up that additional custom attributes maybe added at a later date.

When I initially created the user pool, I made the mistake of hard coding the schema with the expectation that the schema was set in stone. Instead of continuing to hard code the attributes, which would be cumbersome to maintain in the future and make my Terraform code longer than necessary, I’ve taken on the challenge to refactor the code.

My weapon of choice? Dynamic Blocks.

Dynamic Blocks are great because it keeps Terraform code DRY (Do not Repeat Yourself). You provide a list of data and dynamic blocks will generate the type of blocks you define.

Foundations of Dynamic Blocks

Dynamic blocks have 3 distinctive components:

  • The type that specifies the type of block you wish to generate dynamically
  • for_each meta-argument which allows you to reference a variable that contains a list of elements as data used for each block dynamically generated
  • Content block that specifies the contents of the dynamic block

To demonstrate the use of a dynamic block, I will use the example of defining recovery mechanisms for AWS Cognito user pool

resource "aws_cognito_user_pool" "main" {
  account_recovery_setting {
    recovery_mechanism {
      name     = "verified_email"
      priority = 1
    }
  }
}

Here’s an example of the same recovery_mechanism block written using dynamic block

resource "aws_cognito_user_pool" "main" {
//... other user pool settings omitted for brevity

  account_recovery_setting {
    dynamic "recovery_mechanism" {
      for_each = var.user_pool_account_recovery_mechanisms
      content {
        name     = recovery_mechanism.value["name"]
        priority = recovery_mechanism.value["priority"]
      }
    }
  }
}

The keyword “dynamic” indicates that the block being defined is a dynamic block. “recovery_mechanism” is the type of block that needs to be made dynamic and it is a type of block that is defined under aws_cognito_user_pool resource. The for_each meta-argument allows me to specify the variable that contains a list of recovery mechanisms:

// terraform.auto.tfvars
cognito_user_pool_account_recovery_mechanisms=[
  {
    name     = "verified_email"
    priority = 1
  }
]

The content block allows me to specify where to obtain the value for recovery mechanism’s blocks name and priority parameters. Notice in the content block, when referring to each recovery mechanism, I used “recover_mechanism.value” followed by square brackets and the name of the key as the string within to reference the value. This is how dynamic blocks refer to the item being iterated over. You must use the dynamic block type as a reference point to access those values.

Nested Dynamic Blocks

Now that we know how dynamic blocks work, how do we define nested dynamic blocks? Under what circumstance should nested dynamic blocks be used? And How can we make nested dynamic blocks conditional?

When Should Nested Dynamic Blocks Be Used?

Nested dynamic blocks should be used when the block you define that repeats contains a child block.

Going back to my original goal, I have a list of custom user pool attributes that need to be passed to aws_cognito_user_pool resource. This is a great use case for nested dynamic blocks. Each schema block defines a custom attribute and within that custom attribute, the string_attribute_constraints block may be defined.

resource aws_cognito_user_pool {
//...
    schema {
      name                = "my-custom-attribute"
      attribute_data_type = "String"
      is_required            = false
      is_mutable             = true

      string_attribute_constraints = [
        {
          min_length = 4
          max_length = 256
        }
      ]
    }
}

Defining Nested Dynamic Blocks

It comes with no surprise that nested dynamic blocks has the same core components as regular dynamic blocks so the way it works is very similar. The trick however, is figuring out how to structure the data to take advantage of nested dynamic blocks.

The solution is to use a map:

  {
    name                = "my-custom-attribute"
    attribute_data_type = "String"
    is_required            = false
    is_mutable             = true

    string_attribute_constraints = [
      {
        min_length = 4
        max_length = 256
      }
    ]
  }

In our Terraform codebase, this is what my code looks like:

resource "aws_cognito_user_pool" {
  dynamic "schema" {
    for_each = var.user_pool_custom_attributes
    content {
      name                = schema.value["name"]
      attribute_data_type = schema.value["attribute_data_type"]
      mutable             = schema.value["is_mutable"]
      required            = schema.value["is_required"]

      dynamic "string_attribute_constraints" {
        for_each = lookup(schema.value, "string_attribute_constraints", [])
        content {
          min_length = string_attribute_constraints.value["min_length"]
          max_length = string_attribute_constraints.value["max_length"]
        }
      }
    }
}

In the inner dynamic block, I’m defining “string_attribute_constraints” block as dynamic. Notice the for_each attribute utilizes the terraform function lookup. That function will check for “string_attribute_constraints” sub attribute within the map. This is how nested dynamic blocks can obtain its data.

for_each will pull in a list of maps that you can iterate over in which you can access by calling “string_attribute_constraints.value” and access the values by providing the name of the key in squared brackets. Dynamic blocks allow you reference each item it is iterating over when you use the type of the dynamic block as shown above.

Making Nested Dynamic Blocks Optional

We now have cognito user pool custom attributes generated as schema blocks with “string_attribute_constraint” blocks generated in a consistent manner; The only issue is for each custom attribute we define right now, the “string_attribute_constraint” block must also be generated. This is problematic because a custom attribute’s data type can be a string or a number.

To make string_attribute_constraint block dynamic, we lean on the fact that for_each meta-argument will instruct Terraform to iterate through a list or map. All we have to do is create a list with a single map for the nested map value. For custom attributes that do not require string_attributes_constraints block, we simply do not define this attribute since lookup function will automatically default to the value we provide, which in this case, is an empty list if the the attribute does not exist; This will stop our dynamic block from being generated.

Credit

Pfeifer studio wood nesting boxes – Jeri’s Organizing and Decluttering News Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s