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?

Hi @sebastien.latre,

I’m not sure exactly what happened there, but I expect that the problem is more with the use of a jsondecode function result as a template than with the templatestring function itself. What you have written here is risky because JSON special characters in driver-pool-id would not get properly escaped and would cause the whole result to be invalid JSON. This is essentially a more dynamic version of the Generating JSON or YAML from a template problem.

It is possible in principle to adapt the alternative approach suggested in those docs to use templatestring, but it would require writing a very ugly expression with a “heredoc” template containing another template that calls the jsonencode function, so that the variables all get substituted before JSON encoding, rather than afterwards.

If you’re not worried about the problems I described above and just want to debug what’s going on with your current attempt then I would suggest inspecting the local.policy value to see exactly what you’re passing to templatestring as input. Unfortunately if you view it through terraform console then it will be rendered in Terraform’s string template syntax with escaping, so you would need to “read through” one level of escaping to understand what the templatestring function is receiving.

In your real implementation, is the policy template specified dynamically rather than hard-coded as a local value? If it were hardcoded then I think it would be more straightforward to factor out the template into a separate file and use templatefile, but I’m guessing that you wrote it this way just to simplify for discussion.

(This sort of confusion with multiple levels of template interpolation and escaping is, unfortunately, exactly what we were concerned about that caused us to hesitate to add this function. But hopefully we can figure out what happened here and maybe it will at least give us something specific to warn about in the documentation.)

A stablized version of this feature has now been merged into the main branch and so will be included in the forthcoming Terraform v1.9 release unless we see some feedback in the meantime that’s significant enough to warrant changing course.

Normally we close an experiment feedback topic once the experiment has concluded and ask for feedback to be recorded as GitHub issues in the hashicorp/terraform repository instead, so that we can prioritize and track each item separately. However, by coincidence a discussion started here just today and so I’m going to let this remain open for a little while longer in the hope that we can conclude that discussion here.

If you have any other feedback, not related to the discussion already in progress, please open a GitHub issue about it instead of posting here. Thanks!

Hello @apparentlymart, thanks for your verbose answer (as always^^).

I checked the jsonencode output and this doesn’t seem particularly weird for a templatestring input.
The jsonencode doesn’t apply any escaping for “$” character however so this shouldn’t be an issue, no?

What you call a risky operation is not in my case because I pretty know well the format of all my inputs and there are in the format of a normal litteral string for json (no quotes…)

Anyway, I think that I’m misusing the templatestring function here in my case. I wanted to have the local value to avoid having the template into a file but you convinced me to go on this solution.
So I think my use case is not relevant for this experiment and you can safely close this discussion.
Thanks for the time you took to reply :slight_smile: