Dynamic blocks in Terraform 0.12.x

2 minute read

Terraform


Some time ago I wrote about how to make dynamic blocks in Terraform 0.11.x, that although it solved the problem, it generated others because it wasn’t an official solution and the interpretation by Terraform was not consistent.

The workaround essentially consisted of defining the blocks dynamically using an assignment to a map list. That is, instead of doing this:


resource "aws_codebuild_project" "codebuild" {

  artifacts{
      type           = "${var.codebuild_artifacts_type}"
      location       = "${var.codebuild_artifacts_location}"
      name           = "${var.codebuild_artifacts_name}"
      namespace_type = "${var.codebuild_artifacts_namespace_type}"
      packaging      = "${var.codebuild_artifacts_packaging}"
      path           = "${var.codebuild_artifacts_path}"

  }

  ...

}

It was replaced by this other:

resource "aws_codebuild_project" "codebuild" {

  artifacts = ["${local.codebuild_artifacts}"]

  ...

}

Where local.codebuild_artifacts built the block from a map list, which depending on the type defined in the variable codebuild_artifacts_type it created a different block:


locals {
  codebuild_artifacts_def = {
    "S3" = [{
      type                = "${var.codebuild_artifacts_type}"
      location            = "${var.codebuild_artifacts_location}"
      name                = "${var.codebuild_artifacts_name}"
      namespace_type      = "${var.codebuild_artifacts_namespace_type}"
      packaging           = "${var.codebuild_artifacts_packaging}"
      path                = "${var.codebuild_artifacts_path}"
      encryption_disabled = var.codebuild_artifacts_encryption_disabled
    }]

    "CODEPIPELINE" = [{
      type        Some time ago I wrote about [how to make dynamic blocks in Terraform 0.11.x] (https://lgallardo.com/en/2018/07/30/dynamic-configuration-blocks-in-terraform/) {: target = "_blank "}, that although it solved the problem generated others because it was not an official solution and the interpretation by Terraform was not consistent.        = "${var.codebuild_artifacts_type}"
      name                = "${var.codebuild_artifacts_name}"
      encryption_disabled = var.codebuild_artifacts_encryption_disabled
    }]

    "NO_ARTIFACTS" = [{
      type = "${var.codebuild_artifacts_type}"
    }]
  }

  # Retuned map
  codebuild_artifacts = "${local.codebuild_artifacts_def[var.codebuild_artifacts_type]}"
}


Dynamic blocks in Terraform 0.12.x

Terraform 0.12.x proposes dynamic blocks to solve this and other cases related to blocks. To use dynamic blocks you have to use dynamic and for-each. For example, for the above definition:

  dynamic "artifacts" {
    for_each = [local.codebuild_artifacts]
    content {
      type                = artifacts.value.type
      encryption_disabled = lookup(artifacts.value, "encryption_disabled", null)
      location            = lookup(artifacts.value, "location", null)
      name                = lookup(artifacts.value, "name", null)
      namespace_type      = lookup(artifacts.value, "namespace_type", null)
      packaging           = lookup(artifacts.value, "packaging", null)
      path                = lookup(artifacts.value, "path", null)
    }
  }

This way the local.codebuild_artifacts definition is simplify:

locals {
  codebuild_artifacts = {
    type                = var.codebuild_artifacts_type
    location            = var.codebuild_artifacts_location
    name                = var.codebuild_artifacts_name
    namespace_type      = var.codebuild_artifacts_namespace_type
    packaging           = var.codebuild_artifacts_packaging
    path                = var.codebuild_artifacts_path
    encryption_disabled = var.codebuild_artifacts_encryption_disabled
  }
}

Where is the magic?

First, in the null data type, which tells Terraform to ignore the attribute if the lookup function doesn’t find the index on the map. Let’s take a look at the the first lines in the content block, which is what is replaced in the artifacts block in each iteration:

    content {
      type                = artifacts.value.type
      encryption_disabled = lookup(artifacts.value, "encryption_disabled", null)
      location            = lookup(artifacts.value, "location", null)
      ...

Here you can see that:

  1. The attribute type is always defined according to the value of artifacts.value.type, so it’s not taken from the map.
  2. The attribute encryption_disabled is assigned if something like artifacts.value ['encryption_disabled'] is found in the map. If the index doesn’t exist, it’s assigned to null and therefore it won’t be taken into account by Terraform.
  3. And so on for the rest of the attributes, such as location.

This allows having definitions of dynamic blocks, but you have to take into account that some values must be defined together. For example, for the S3 type you must define the following variables:

codebuild_artifacts_type
codebuild_artifacts_location
codebuild_artifacts_name
codebuild_artifacts_namespace_type
codebuild_artifacts_packaging
codebuild_artifacts_path
codebuild_artifacts_encryption_disabled

While for NO_ARTIFACTS just define this one:

codebuild_artifacts_type

On the other hand, the magic is completed by the fact that we are not using an assignment for the a artifacs block but rather the dynamic block definition. In other words, instead of this:

 artifacts = ["${local.codebuild_artifacts}"]

The dynamic block definition is used:

  dynamic "artifacts" {
    for_each = [local.codebuild_artifacts]
    content {
      ...
  }

References:

Leave a Comment