Use templatefile to generate configuration files from Terraform variables.
Terraform's templatefile function is commonly used for cloud-init scripts, configuration files, and other resources that expect a string rather than structured HCL. In essence, it reads a file, replaces the variables in it, and returns the rendered text.
In this guide, we'll cover variables, loops, conditionals, and the cases where jsonencode or yamlencode is a better fit. We'll also explain why templates fail when they reference files created during apply, and why secrets usually should not be passed through user data.
We'll also look at when you can avoid this templating layer altogether, for example when it exists mainly to pass database or queue configuration into an application, and where Encore fits.
An EC2 instance that starts an orders service can keep its bootstrap script in cloud-init.tftpl. This example assumes Docker is already installed in the AMI:
#!/usr/bin/env bash
set -euo pipefail
image_ref=$(printf '%s' '${image_ref_base64}' | base64 --decode)
docker run --detach \
--name '${service_name}' \
--publish '${port}:${port}' \
-- "$image_ref"
The Terraform configuration passes an object as the second argument to templatefile, and every key in that object becomes a variable inside the file:
locals {
cloud_init = templatefile("${path.module}/cloud-init.tftpl", {
service_name = "orders"
port = 8080
image_ref_base64 = base64encode("${var.container_image}:${var.container_tag}")
})
}
resource "aws_instance" "orders" {
ami = var.ami_id
instance_type = "t3.micro"
user_data = local.cloud_init
}
The image reference is base64-encoded before rendering and decoded into a quoted shell variable. Shell metacharacters in an image value are therefore treated as data, and -- ends Docker's option parsing. The service name and port in this example are fixed values; deploy only images you trust.
Using path.module makes the path relative to the module containing the expression instead of whichever directory happens to run Terraform. The template must be valid UTF-8 and must exist when the run begins; templatefile is evaluated as part of configuration processing and cannot depend on a resource creating the file later.
Although Terraform does not enforce the .tftpl suffix, using it distinguishes an input template from the rendered file and gives editors enough information to apply the right syntax highlighting.
Terraform templates support interpolation with ${...} and directives such as %{ for ... } and %{ if ... }. An Nginx upstream file can render a list of addresses and include an optional health-check endpoint:
upstream ${service_name} { %{ for address in addresses ~} server ${address}:${port}; %{ endfor ~} } %{ if health_path != null ~} location ${health_path} { proxy_pass http://${service_name}; } %{ endif ~}
locals {
nginx_config = templatefile("${path.module}/upstream.tftpl", {
service_name = "orders"
addresses = ["10.0.1.10", "10.0.2.10"]
port = 8080
health_path = "/healthz"
})
}
The ~ marker strips adjacent whitespace, including newlines, on the side of the directive where it is placed, which avoids blank lines and indentation artifacts when a loop expands. Templates remain easiest to review when HCL prepares the data and the file is responsible only for presentation. Once a template starts calculating subnet ranges, merging defaults, or choosing between several object shapes, move that logic into locals and pass the finished value in.
To keep quotes, commas, and escaped characters out of the template logic, use one interpolation and let jsonencode serialize the HCL value:
${jsonencode({ service = service_name port = port backends = [for address in addresses : { address = address port = port }] })}
For YAML, use the same pattern with yamlencode. Both functions handle quoting and escaping, and the HCL expression remains type-aware until the final string is produced. If the entire document is small, the encoding call can stay directly in the resource without a separate file:
locals {
task_config = jsonencode({
service = "orders"
port = 8080
})
}
Use file() instead when the content is literal and needs no interpolation. A provider's structured arguments are better still when they exist, since Terraform can then validate and diff individual fields rather than treating the whole document as an opaque string.
Rendered content can be stored in Terraform state or exposed through a plan, provider API, instance metadata, or logs, depending on the resource that receives it. Marking a variable as sensitive only redacts some CLI output; it does not turn cloud-init or a generated configuration file into a secret store. Workloads should normally retrieve secrets at runtime through a scoped identity rather than having Terraform bake credentials into a template.
Templates also cannot call templatefile recursively, and a missing object key fails the render rather than becoming an empty string. When the inputs are known at plan time, Terraform reports the mismatch during planning. If an input is unknown until apply, an evaluation error can surface later.
When a template becomes large enough to behave like a program, it is usually time to move the behavior into an image, a configuration-management tool, or the application itself. Terraform is then responsible for selecting the artifact and passing a small amount of configuration, which gives plans a much clearer relationship to what will actually run.
Templates remain useful for machine configuration and resources that accept text documents, but not when their only job is connecting an application to its own database, queue, bucket, or scheduled job.
Encore lets those application resources be declared alongside the code that uses them, while capacity, networking, and other environment-specific settings stay in environment configuration. The local runtime starts local versions of the declared resources, and a deployment provisions their cloud equivalents, so there is no generated application config file for Terraform to render and pass between the two systems.
Terraform can continue to manage the shared network, IAM, accounts, and any resource outside Encore's model. In practice, templatefile remains useful where a cloud resource expects rendered text, but not merely as a bridge between application code and application infrastructure.
Using Terraform string-template syntax, templatefile fills the variables supplied in its second argument into a UTF-8 file on disk and returns the rendered text.
The syntax is templatefile(path, vars), where path points to a file that already exists and vars is an object whose keys become variables inside the template.
Terraform accepts any filename, but HashiCorp recommends .tftpl because editors can recognize it as a Terraform template.
No. The template file must already exist on disk when Terraform starts the run. Keep it in source control rather than having a resource or provisioner create it during the same operation.
Usually not. Rendered values can appear in plans, state, instance user data, logs, or provider APIs. Prefer a runtime secret manager and give the workload an identity that can retrieve only the secrets it needs.
Yes. With Encore, you declare the database in application code and use its SDK to access it. Encore starts the database locally and supplies the connection configuration when deploying to your cloud account, so you do not need a template just to pass database settings into the application.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.