How does Terraform's templatefile() function render a file, and what is allowed to be referenced from inside the template?
answer
- the vars map is the whole scope
- anchor the path on the module
- tilde trims the loop newline
- double the dollar for the shell
- encoders beat hand-built JSON
basics
~20 stemplatefile(path, vars) reads a file from disk at plan or apply time and renders it as a string template. Only the names in the vars map are in scope inside the template - it cannot see Terraform variables, locals or resources - and referencing a name you did not pass is an error.
solid answer
~50 s`templatefile("${path.module}/user_data.sh.tftpl", { port = var.port, hosts = var.hosts })` reads the file and renders it with the same template language as a quoted string: `${ }` for interpolation, `%{ for }` and `%{ if }` for directives, `$${` to escape. The scoping rule is the one people get wrong — the second argument is the *entire* variable scope. The template cannot reach `var.*`, a local, or a resource attribute; anything it needs must be passed explicitly, and a name it references that you did not pass is a hard error. The file must exist on disk when Terraform evaluates the call, the path is conventionally anchored with `path.module`, and a template may not call `templatefile` recursively. It replaced the old `template_file` data source from the archived `hashicorp/template` provider, which is why you should not add that provider to new code. For structured output, call `jsonencode` or `yamlencode` on a value instead of templating braces by hand.
code
hcl · 9 linesresource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
user_data = templatefile("${path.module}/user_data.sh.tftpl", {
port = var.app_port
hosts = var.backend_hosts
})
}go deeper
Know the call shape — a path plus a map of values — and that the template uses the same ${ } interpolation as a normal string. Be able to say why the map is needed at all.
Explain the scoping rule precisely: the map is the template's entire scope, so var, locals and resources are invisible inside it and a missing key is an error. Know path.module and the $${ escape.
Demonstrate production habits: encoders instead of hand-built JSON or YAML, whitespace trimming on directives, awareness that unknown inputs render the whole output unknown at plan, and why the archived template provider must not appear in new code.
Own where rendered configuration belongs at all. Long scripts embedded in user_data are undebuggable and unversioned separately from the image; deciding when templated bootstrap gives way to a pre-baked image or a config-management run is the tradeoff to argue, not the templating syntax.
## The call and the scope rule ```hcl resource "aws_instance" "web" { # ... user_data = templatefile("${path.module}/user_data.sh.tftpl", { port = var.app_port workers = var.worker_count hosts = var.backend_hosts }) } ``` The second argument is a map, and it is the **complete** variable scope of the template. Inside `user_data.sh.tftpl`, `${port}` resolves; `${var.app_port}` does not — there is no `var` object in there at all. A template that references a name absent from the map fails with "Invalid function argument ... vars map does not contain key". This is deliberate: the template's inputs are its interface, visible at the call site, which makes the file reviewable in isolation and reusable from more than one call. `path.module` is the filesystem path of the module containing the call. Anchoring on it rather than a bare relative path is what makes the module work when it is called from another directory or consumed from a registry — a bare `"user_data.sh.tftpl"` resolves relative to the process working directory, not the module. ## The template language inside the file It is the same language as a quoted string, so: ```bash #!/bin/bash APP_PORT=${port} %{ for h in hosts ~} echo "${h}" >> /etc/app/backends %{ endfor ~} %{ if workers > 1 } systemctl set-property app.service TasksMax=${workers * 4} %{ endif } echo "home is $${HOME}" ``` The `~` on a directive trims the adjacent whitespace and newline, without which every loop iteration leaves a blank line. `$${HOME}` escapes the sigil so the shell — not Terraform — expands it; forgetting that escape is the single most common bug in templated shell scripts, and it presents as a Terraform error about an unknown variable named `HOME`. ## Documented limits - **The file must exist on disk** when the function is evaluated. You cannot generate the template itself during the run and then render it in the same operation — the value would need to be known before the resource that produces it is created. - **No recursion.** A template rendered by `templatefile` may not itself call `templatefile`. - **Rendering happens during evaluation**, so the rendered string is a normal value: if any input is unknown at plan time, the whole rendered result is unknown and the plan shows `(known after apply)` for that argument. A template that splices in an id from a resource not yet created will always look like that in the plan, which is expected, not a bug. ## Do not build JSON or YAML with it The recommended approach for structured output is to build the value in Terraform and encode it: ```hcl locals { policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow", Action = ["s3:GetObject"], Resource = "${var.bucket_arn}/*" }] }) } ``` `jsonencode` and `yamlencode` guarantee syntactic validity and correct escaping; a hand-templated document breaks the first time an interpolated value contains a quote, a newline or a non-ASCII character, and it breaks at the consumer, not at plan time. When a template is unavoidable — a config file format with no encoder — you can still call `jsonencode` on individual values *inside* the template to escape them safely. ## Why the old template_file data source is gone Before `templatefile` existed, people used `data "template_file"` from the `hashicorp/template` provider. That provider is archived and no longer maintained, and it was never rebuilt for newer platforms — engineers on Apple Silicon hit an init failure because no matching provider binary exists. Adding it to new configuration buys a dependency you will have to remove. The function is a drop-in replacement and does not require a provider at all. Alongside it, `file()` simply reads a file's raw contents with no rendering, which is what you want for a static key or certificate. ## What an interviewer is checking Three things: that you know the vars map is the whole scope; that you anchor the path on `path.module`; and that you reach for an encoder rather than a template when the output is structured. The escaping and whitespace-trimming details show you have actually shipped a rendered script rather than read about one.
- A template renders fine locally but Terraform errors that there is no variable named HOME. What happened?The template contains a shell reference such as `${HOME}`, and Terraform tried to interpolate it because `${ }` is its own syntax. Escape it as `$${HOME}` so Terraform emits a literal dollar-brace and the shell expands it at runtime. The same applies to any consumer with brace-based templating of its own.
- Why anchor the template path on path.module instead of writing a relative path?A bare relative path resolves against the working directory of the Terraform process, not the module. The moment the module is called from another directory or pulled from a registry, the file is not found. `path.module` is the directory of the module containing the expression, so the reference stays correct wherever the module is used.
- The plan shows user_data as (known after apply) even though the template file is static. Why?Rendering happens during expression evaluation, so if any value passed in the vars map is unknown at plan time — an id or endpoint from a resource that does not exist yet — the entire rendered string is unknown. That is expected behaviour, not an error; the string is computed once its inputs are known during apply.
saying these in an interview costs you the question
- Thinks the template can reference var.* or a resource directly
- Uses a bare relative path that breaks when the module moves
- Builds an IAM policy by hand-templating JSON braces
- Adds the archived template provider to new configuration
- Forgets to escape shell variables and blames the template engine