Infrastructure as code already gives us consistency and reproducibility for provisioning resources. Terraform templates extend that same principle to the configuration files those resources rely on, keeping everything, infrastructure and config, managed together in a single workflow.
In this post, we go through the definition of Terraform templates, cover the key concepts behind them, and walk through practical examples of how to use them.
What are Terraform templates?
Terraform templates are configuration files that define and describe the infrastructure resources required for a particular application or environment using a declarative configuration language called Hashicorp Configuration Language (HCL).
These templates have a “.tf” extension, and they enable users to code infrastructure, ensuring consistent and reproducible deployments.
Inside a template, users can specify different resources such as virtual machines, IAM components, storage resources, or networking components and configure their relationships and properties. When executed using the Terraform CLI, these templates are translated into a set of actions that realize the described infrastructure on the targeted provider.
If you want to go beyond the CLI and bring structure, governance, and automation to your Terraform workflows, Spacelift can help. It is an infrastructure orchestration platform with built-in support for Git workflows, policy as code, dynamic credentials, context sharing, drift detection, and programmatic configuration — so your team can move faster without sacrificing control.
Terraform templates benefits
Terraform offers a way to package a group of Terraform scripts as modules. Modules are reusable infrastructure components that can be customized using input variables to control things like scale, type, and range.
Terraform Templates extend that flexibility further by managing the configuration and data files your resources depend on. Instead of maintaining static config files outside of Terraform, or relying on a separate tool to render them after provisioning, templates let you generate those files dynamically as part of a single terraform apply.
In practice, this means:
- Reusability — one
.tftplfile can serve multiple environments (dev, staging, prod) with different variable values, eliminating duplicated config files. - Format flexibility — templates work with shell scripts, YAML, JSON, INI, and more. Combined with
jsonencode()andyamlencode(), Terraform handles valid formatting automatically. - Consistency — because templates live alongside your Terraform code in version control, every config change goes through the same Git workflow as your infrastructure.
Key concepts for Terraform templates
Terraform templates combine a few features – templatefile function, string literals, and template files themselves.
templatefile function
The templatefile() function accepts a couple of inputs:
- A template file (
*.tftpl) - Variable – which could be a list or map.
The template file contains text in UTF-8 encoded text, representing the desired format data. For example, configuration files required by applications have different formats. These files support Terraform’s string literals, which help replace the actual values supplied in variables.
For the final configuration, variables provide a way to set values in these template files dynamically. Before runtime, make sure that the template files are available on the disk.
Terraform template_file
For Terraform versions 0.12 and later, the template_file data source has been superseded by the templatefile function, that can be used directly in expressions without creating a separate data source.
File provisioners
File provisioners provide a way to copy the files from the machine where Terraform code executes to the target resource. The source attribute specifies the file’s source path on the host machine, and the destination attribute specifies the desired path where the file needs to be copied on target.
Learn more about file provisioners: Terraform Provisioners : Why You Should Avoid Them
provisioner "file" {
source = "./app.conf"
destination = "/etc/app.conf"
}jsonencode and yamlencode functions
If the string being generated from the template file is a JSON or YAML file, it could become quite tedious to format it for its validity. The chances of error are high if these files are not formatted properly.
Use jsonencode function and yamlencode function in the template file to produce an output file in respective formats easily.
When to use Terraform templates
Use templatefile() when your resources depend on configuration files that vary per deployment, files that need to be present and correctly populated when a resource first boots or initializes.
Typical scenarios include:
- Writing
user_datascripts for EC2 instances that need environment-specific values like endpoints, bucket names, or feature flags. - Generating app config files (NGINX virtual hosts, Prometheus scrape configs,
resolv.conf) whose content depends on other Terraform outputs. - Provisioning multiple similar resources that each need a slightly different version of the same config file.
If your goal is to reuse a set of infrastructure resources across projects, Terraform modules are the better fit. Templates and modules are complementary — a module can use templatefile() internally to generate the config files its resources need.
How to use Terraform templates
Let’s see some examples.
Example 1 – Create user_data with Terraform
AWS EC2 instances offer support to run shell/bash scripts when the instances are booted. These scripts perform essential tasks like updating the packages, creating environment variables, installing patches, etc. These scripts are provided for EC2 creation in the form of user_data.
Given their nature, these scripts can get very complex. However, once the script is tested it may be used multiple times with slightly different values. As an example, we may need to provide a different set of environment variables for two different sets of EC2 instances.
Using Terraform, we can create a template for this script by using string literals to provide variables’ values dynamically. In the example below, the script creates a directory, cds into that directory, and creates a file within that with some name.
#!/bin/sh
sudo mkdir ${request_id}
cd ${request_id}
sudo touch ${name}.txtLet the name of above template file be script.tftpl. String literals (${ … }) are used to represent variables. To add this script as user_data for EC2 instance, we use the templatefile() function as below.
resource "aws_instance" "demo_vm" {
ami = var.ami
instance_type = var.type
user_data = templatefile("script.tftpl", { request_id = "REQ000129834", name = "John" })
key_name = "tftemplate"
tags = {
name = "Demo VM"
type = "Templated"
}
}We have passed the template file name (script.tftpl) as first parameter, and a map object with request_id and name key-value pairs for substitution. Creating an EC2 instance with a given user_data script runs as expected when Terraform code is applied.
Please note that both template and terraform code files are located in the same directory. Further, Terraform variables are used to create larger map objects for easier management of supplied values.
Example 2 – Template file with for loop
Template file used in previous section is a bash script. Similarly, strings generated by shell or any other type of script are non-repetitive in their formats. Certain file formats are repetitive in nature, i.e., certain lines may have the same format, with different values.
As an example, DNS resolution configurations are maintained in a resolv.conf file, that lists the name servers in below format.
nameserver x.x.x.x
nameserver x.x.x.x
nameserver x.x.x.xAs we can see, the expected format of this file is repetitive. Use for loop expressions to create a template file for what appears as below.
Filename: resolv.conf.tftpl
%{ for addr in ip_addrs ~}
nameserver ${addr}
%{ endfor ~}The corresponding terraform file would have file provisioner with a list type variable as the second parameter.
provisioner "file" {
source = templatefile("resolv.conf.tftpl", {ip_addrs = ["192.168.0.100", "8.8.8.8", "8.8.4.4"]})
destination = "/etc/resolv.conf"
}Applying above Terraform config results in creating the expected resolve.conf file in the target EC2 instance as below.
nameserver 192.168.0.100
nameserver 8.8.8.8
nameserver 8.8.4.4Example 3 – Creating JSON files using templatefile()
Occasionally, when applications depend on externally provided configuration files in JSON format. Since the application logic depends on JSON format, it becomes imperative for Terraform to format the configuration file accordingly.
Creating a valid JSON object using string operations can get tricky. A slight mistake in indentation, or mistyping : or " – in template files – can cause errors. Even if we successfully create a script to create a valid JSON object, there is always a risk of unhandled escape sequences from incoming data.
Terraform provides a function to create valid JSON files from the given template, without worrying about valid formatting. Let us consider that we want to host a product based on microservice architecture. Our microservices are developed in NodeJS.
For the sake of this example, it is critical that the application nodes in our microservice architecture depend on slightly different versions of dependencies. In that case, we create a template file as below – dependencies.tftpl.
${
jsonencode("dependencies": {
"cradle": ${cradle_v},
"jade": ${jade_v},
"redis": ${redis_v},
"socket.io": ${socket_v},
"twitter-node": ${twitter_v},
"express": ${express_v} })
}The corresponding Terraform code appears below. Actual version values are passed via a variable – which would generate the desired dependencies.json file in target path.
variable dep_vers {
default = {
"cradle_v": "0.5.5",
"jade_v": "0.10.4",
"redis_v": "0.5.11",
"socket_v": "0.6.16",
"twitter_v": "0.0.2",
"express_v": "2.2.0"
}
}
provisioner "file" {
source = templatefile("dependencies.tftpl", var.dep_vers)
destination = "/desired/path/dependencies.json"
}Example 4 – Conditions in Terraform templates
Extending the above example, let us assume certain applications do not need certain dependencies to be installed. In that case, developers may choose simply not to supply the corresponding version values.
The current template file (dependencies.tftpl) throws an error in that case. If conditions provide this flexibility and improve the reusability of a given template file, make the dependencies depend on whether the corresponding information is supplied or not – using if conditions as below.
${
jsonencode("dependencies": {
%{if cradle_v != "" }
"cradle": ${cradle_v},
%{ endif }
%{if jade_v != "" }
"jade": ${jade_v},
%{ endif }
%{if redis_v != "" }
"redis": ${redis_v},
%{ endif }
%{if socket_v != "" }
"socket.io": ${socket_v},
%{ endif }
%{if twitter_v != "" }
"twitter-node": ${twitter_v},
%{ endif }
%{if express_v != "" }
"express": ${express_v}
%{ endif }
})
}The summary of the above file is that if no version information for the given dependencies is provided, the dependencies will not be included in the dependencies.conf file. This makes this template file reusable with any microservice application and can be controlled by the dep_vers variable values in Terraform code.
Key points
Terraform’s string literals are a great asset of the HCL language. Terraform offers a whole range of functions to perform string templating. The combination of string literals, templatefile() function, and file provisioner can be a huge advantage when triggering the configuration management workflows on Day 0. Terraform templates offer flexibility around the file formats – thus not limiting to specific ones. Additionally, filesystem functions are used to perform validation tasks.
If you need more help with Terraform, I encourage you to check the following blog posts: How to Automate Terraform Deployments, and 12 Terraform Best Practices.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
Orchestrate your infrastructure, not just Terraform
Spacelift is an infrastructure orchestration platform that goes beyond Terraform. It brings Git-based workflows, policy as code, dynamic credentials, drift detection, and developer self-service together, so your team ships faster with governance built in.
Frequently asked questions
What is a Terraform template?
A Terraform template is a reusable HCL configuration that declaratively defines infrastructure. Templates standardize deployments across environments using variables, modules, and resource blocks.
What is the difference between the Terraform template and the templatefile() function?
templatefile()reads a file and renders it with variables, which is useful for dynamic content inside resources. “Terraform template” often means a.tplfile in template syntax that gets rendered bytemplatefile(). The oldtemplate_filedata source is deprecated in favor oftemplatefile().What is the difference between a Terraform template and a module?
A Terraform template is a single configuration file (typically .tf) that defines resources, variables, or outputs. A module is a reusable, self-contained package of one or more templates grouped in a directory, designed to encapsulate and abstract a specific piece of infrastructure. You call a module from a root configuration using a module block, passing in variables to customize its behavior across environments.
Is template_file still supported in Terraform?
The template_file data source is deprecated in Terraform. The recommended replacement is the templatefile function, which was introduced in Terraform 0.12. It accepts a file path and a map of variables, returning the rendered string directly without requiring a separate data source block.
