Experiment Feedback: The `templatestring` function

Hi everyone,

In yesterday’s Terraform CLI v1.9.0-alpha20240501 there is an experimental implementation of the long-requested feature of treating a string value fetched from somewhere other than the local filesystem as a Terraform template, and rendering it with a provided set of template variables.

For (very contrived) example:

terraform {
  # This experiment opt-in will be required as long
  # as this remains experimental. If the experiment
  # is successful then this won't be needed in the
  # final release.
  experiments = [template_string_func]
}

data "aws_s3_object" "example" {
  bucket = "example-example-example"
  key    = "example.tmpl"
}

output "result" {
  value = templatestring(data.aws_s3_object.example.body, {
    foo = "bar"
  })
}

Those who were following earlier discussion about this request may recall that we were concerned that adding this might reintroduce an earlier problem of inexperienced Terraform users finding this function and thinking that it’s the way to render templates, and having a very bad time trying to figure out how to escape a literal template to pass to the function. That usability hazard came up a surprising number of times with the earlier template_file data source offered by the (now-deprecated) hashicorp/template provider, and so we were cautious about repeating it.

This experiment therefore includes an attempted compromise: unlike most Terraform functions, this one is more “fussy” about what syntax it will accept for specifying its first argument. Specifically, it requires the first argument to always be a direct reference to some other named object in the current module. In the example above, that reference is data.aws_s3_object.example.body.

In particular this means that it’s forbidden to write a template expression directly into the first argument, which would suggest that someone is trying to use this function unnecessarily:

# The following is not allowed, as a special case
templatestring("Hello, $${name}", {
  name = "Martin"
})

This compromise is here only to help Terraform give better feedback and not to constrain what’s possible. If you really do want to construct a template string dynamically from parts for some reason, you can achieve that by factoring out the construction of the template into a separate local value, which we’re using as a heuristic for “I know what I’m doing, get out of my way”:

locals {
  greeting_template = "Hello, $${name}"
}

output "result" {
  value = templatestring(local.greeting_template, {
    foo = "bar"
  })
}

Since the most commonly-reported use-cases for this function were in fetching templates from elsewhere (e.g. data sources) and passing template strings through input variables – both of which involve references to specific single objects – we’re hoping that this compromise will give power users a useful tool while also narrowly avoiding a repeat of the earlier usability hazard.


Before we move forward with this in a stable release, we’d like to gather some examples of it being used both successfully and unsuccessfully, to hopefully give us a signal about whether this is a viable compromise.

If you are interested in using this new function, it would help if you could download the alpha release and try it with something as close as possible to the real situation where you’d use this function.

Since this is only an alpha release we do not recommend using this build in production. Instead, please limit your experiments to development environments that cannot affect any infrastructure whose downtime would be detrimental to you or your organization.

If you choose to participate and have feedback to share, it would be most helpful if you could reply to this topic and include all of the relevant parts of the configuration you tried with. In particular, we’d love to see:

  • Whatever enclosing block you’re calling templatestring from, so that we can understand the specific use-case. (Specific, real-world examples are helpful for understanding the real impact of adding this feature.)
  • The configuration of whatever object you’ve referred to in the first argument to templatestring. If it’s an input variable to a shared module, it would be helpful to see an example of a typical module block using the module.
  • If you run into problems, the full text of any error messages Terraform returned, or any surprising output Terraform returned despite not returning an error.

Thanks in advance for any testing and feedback!

I don’t have any feedback on the new experimental function, yet. However, I do think that in your bullet points you intended to refer to the templatestring function rather than templatefile. Just in case anyone is confused.

Also, thank you for implementing this (at least as an experiment for the time being)! I believe this will be useful.

Thanks @jcolson! You’re right. I guess I’ve typed templatefile far more often than I’ve typed templatestring at this point, so my fingers got carried away.

I’ve edited the original message to correct that error.

Hello,

I’m really eager to this function live in Terraform.
I have a use case that really fits for this feature.

I need to build some JSON similar configuration but for several Databricks clusters so I cannot have a local variable fully built with Terraform input variables.
I need to update the configuration based on a for_each variable

Until now I used this rather ugly (really) simplified code

locals {
  cluster_by_key = {
    key1 = {
      env_name = "foo"
      pool_driver = {
        id = "toto"
      }
    }
    key2 = {
      env_name = "bar"
      pool_driver = {
        id = "tata"
      }
    }
  }
  policy = jsonencode({
      "instance_pool_id" = {
        type   = "fixed"
        value  = "@driver-pool-id@"
        hidden = true
      },
      "spark_conf.spark.hadoop.fs.azure.account.auth.type.xxx@env@yyy.dfs.core.windows.net" = {
        type   = "fixed"
        value  = "OAuth"
        hidden = true
      }
  })
}

resource "databricks_cluster_policy" "cluster_policy" {
  for_each = local.cluster_by_key

  name = "Policy_${each.key}"
  definition = replace(replace(jsonencode(local.policy),
    "@env@", each.value.env_name),
    "@driver-pool-id@", each.value.pool_driver.id
  )
}

I tried to replace it through terraform_1.9.0-alpha20240516 with setting up the experimental toggle in the module and using the following template:

locals {
  policy = jsonencode({
      "instance_pool_id" = {
        type   = "fixed"
        value  = "$${driver-pool-id}"
        hidden = true
      },
      "spark_conf.spark.hadoop.fs.azure.account.auth.type.xxx$${env}yyy.dfs.core.windows.net" = {
        type   = "fixed"
        value  = "OAuth"
        hidden = true
      }
  })
}

resource "databricks_cluster_policy" "cluster_policy" {
  for_each = local.cluster_by_key

  name = "Policy_${each.key}"
  definition = templatestring(local.policy, {
    "env", each.value.env_name,
    "driver-pool-id", each.value.pool_driver.id
  })
}

but the replace didn’t work.
Can you tell me what I did wrong?