Chaining and assignment within a Terraform for loop

Consider some string content. We’d like to perform a list of replace substitutions, to produce one final string result.

locals {
  input = "this is a test string"
  expected = "cis is b test string"

  replacements = {
    "/a/": "b",
    "/th/": "c",
  }
}

Wrong attempt:

locals {
  output = [
     for pattern, replacement in local.input: replace(local.output, pattern, replacement)
  ]
}

The loop in the example won’t work for a few reasons:

  • local.output cannot use its own result as part of its expression
  • it would produce a list of strings, each with one replace applied. Rather than the desired final result of chaining together all replacements

In an ordinary language, you’d be able to have an inner variable and assign the result of the replacement to it each iteration. Or be able to chain functions. Can Terraform perform the desired transformation?

In case anyone in the future runs into this, I later made a provider to do the chaining I needed:

Hi @dghubble,

Indeed, this sort of iterative algorithm isn’t really expressible directly in the Terraform language. It would be possible to do it with a hard-coded expression that applies the replacements through nested function calls, but I assume your real use-case is a dynamic set of replacements, rather than static as you showed here, and so that approach wouldn’t be appropriate.

I see that you wrote a provider for this particular purpose, which is indeed a typical way to glue imperative-style algorithms into Terraform’s declarative language. In case someone finds this in future with a slightly different use-case that the specialized provider can’t meet, I might also suggest considering my custom provider apparentlymart/javascript (Note: a personal project, not a HashiCorp project) which embeds a JavaScript engine and so provides a more general escape-hatch for imperative-style algorithms, or for functional-style algorithms that are easier to express in a general-purpose language.

terraform {
  required_providers {
    javascript = {
      source  = "apparentlymart/javascript"
      version = "0.0.1"
    }
  }
}

data "javascript" "example" {
  source = <<-EOT
    // (NOTE: I didn't actually test this; it's probably not 100% correct)
    _.reduce(
      replacements,
      (prev, rule) => prev.replaceAll(
        new RegExp(rule[0]),
        rule[1],
      ),
      input,
    )
  EOT
  vars = {
    replacements = [
      ["a", "b"],
      ["th", "c"],
    ]
    input = "this is a test string"
  }
}

locals {
  output = data.javascript.example.result
}
2 Likes

Common functional methods in other languages like map() and filter() can be accomplished with Terraform’s for ... in .. : <value> [if <condition>] syntax, but there is nothing like an inject()/reduce() function that would allow for simplified/sandboxed recursion, or actual functions/recursion.

With some sort of reduce syntax like:

reduce <accumulator>, <element> from [<initialize_accumulator>,] <collection> : <statement>

The problem mentioned above could be done with:

locals {
  input = "this is a test string"

  replacements = {
    "/a/": "b",
    "/th/": "c",
  }

  expected = reduce accum, element from local.input, local.replacements : replace(accum, element.key, element.value)
}

Lack of recursion of any form can be a frustrating limitation in Terraform that really cannot be worked around, and limits the usefulness of this otherwise fantastic language.

Rob

1 Like