Terraform Template File

Bits Lovers
Written by Bits Lovers on
Terraform Template File

Terraform has a handy way to render configuration files dynamically by injecting variables into templates. If you have ever needed to generate a user-data script, a config file, or a YAML manifest from within Terraform, the templatefile() function is the tool for the job.

The templatefile() function reads a template file and substitutes variables into it. I use it all over my Terraform projects, including inside Modules.

A Quick Note on the Old template_file Data Source

You might still find examples online that use data "template_file". That came from the hashicorp/template provider, which has been archived and is no longer maintained. The built-in templatefile() function replaced it starting with Terraform 0.12, and it is what you should use today. (Terraform 1.9 also added templatestring() for rendering templates from a string variable rather than a file.)

I will show both the old and new approaches in the examples below so you can migrate if you need to.

Basic Example: User Data with Variable Substitution

Creating User Data with Terraform

Let’s say you are launching an EC2 instance and you need to install Docker and Ansible on startup. You would pass a bash script through the user_data field. But you want the package versions to be configurable.

Here is the template file (userdata.tftpl):

#!/bin/bash -xe
yum update -y
yum install -y docker-${DOCKER_VERSION}
yum install -y ansible-${ANSIBLE_VERSION}
yum install -y zip unzip

Note: If you are using Amazon Linux 2023, amazon-linux-extras is no longer available. Use dnf install instead.

Using the templatefile() Function

locals {
  userdata = templatefile("${path.module}/userdata.tftpl", {
    DOCKER_VERSION  = var.DOCKER_VERSION
    ANSIBLE_VERSION = var.ANSIBLE_VERSION
  })
}

You can then reference it as local.userdata in your resource.

Old Approach (Deprecated)

For reference, here is what it looked like with the old template_file data source:

data "template_file" "userdata" {
  template = file("${path.module}/userdata.sh")
  vars = {
    DOCKER_VERSION  = var.DOCKER_VERSION
    ANSIBLE_VERSION = var.ANSIBLE_VERSION
  }
}

Passing the Rendered Template to a Launch Template

Here I am using aws_launch_template, which is the current standard. The older aws_launch_configuration resource is tied to EC2-Classic and has limitations.

resource "aws_launch_template" "bitslovers-lt" {
  name_prefix            = "${var.my_environment_name}-lt-"
  image_id               = "ami-12345"
  instance_type          = var.INSTANCE_TYPE
  vpc_security_group_ids = [aws_security_group.inst.id]
  user_data              = base64encode(local.userdata)
  key_name               = "bitslovers-keypair"
  iam_instance_profile {
    name = aws_iam_instance_profile.iam_profile.name
  }
}

Note: aws_launch_template requires user_data to be base64-encoded, so I wrapped it with base64encode().

For Loops in Templates

You can iterate over a list inside a template using the %{ for ... } directive. This is useful when you need to generate repeated blocks of configuration.

Example: DNSMasq Configuration

Suppose you need to configure DNSMasq with a list of DNS server IPs. The final config file should look like this:

#DNSMasq Server Configuration
bind-interfaces
user=dnsmasq
group=dnsmasq
pid-file=/var/run/dnsmasq.pid
cache-size=1000
max-cache-ttl=300
neg-ttl=60
strict-order
bogus-priv
no-resolv
expand-hosts
server=/www.bitslovers.com/10.180.1.2
server=/www.bitslovers.com/10.170.1.2
server=/www.bitslovers.com/10.160.1.2
server=8.8.8.8
no-dhcp-interface=

Here is how you generate that file dynamically with Terraform:

resource "local_file" "dnsmasq_conf" {
  filename       = "${path.module}/dnsmasq-dynamic.conf"
  file_permission = "0666"
  content        = <<-EOT
    bind-interfaces
    user=dnsmasq
    group=dnsmasq
    pid-file=/var/run/dnsmasq.pid
    cache-size=1000
    max-cache-ttl=300
    neg-ttl=60
    strict-order
    bogus-priv
    no-resolv
    expand-hosts
    %{ for ip in var.dns_servers }
    server=/www.bitslovers.com/${ip}
    %{ endfor }
    server=8.8.8.8
    no-dhcp-interface=
  EOT
}

Define the variable as a list of strings:

variable "dns_servers" {
  type        = list(string)
  description = "Private IP DNS"
}

Then pass in the values:

dns_servers = ["10.180.1.2", "10.170.1.2", "10.160.1.2"]

Terraform renders the template and writes the file with the correct server lines.

Generating JSON with jsonencode

Writing valid JSON by hand inside a template is error-prone. Terraform has a built-in jsonencode() function that handles all the quoting and escaping for you.

locals {
  sg_cidr_config_json = jsonencode({
    SG_CIDR = [for addr in var.ip_address : "${addr}/${var.subnet}"]
  })
}

Given ip_address = ["10.180.0.0", "10.170.0.0"] and subnet = "24", the result is:

{"SG_CIDR":["10.180.0.0/24","10.170.0.0/24"]}

No manual escaping needed.

Generating YAML with yamlencode

The same approach works for YAML. Terraform provides yamlencode():

locals {
  sg_cidr_config_yaml = yamlencode({
    SG_CIDR = [for addr in var.ip_address : "${addr}/${var.subnet}"]
  })
}

You can read more about this in the Locals guide.

Conditionals in Templates

IF Statements

Sometimes you need to include or exclude parts of a template based on a condition. Terraform templates support %{ if ... } / %{ endif } directives.

A common use case is the aws-auth ConfigMap for an EKS cluster. You might want to conditionally include extra IAM roles or users.

Here is the template file (auth-configmap.yaml.tftpl):

apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    ${indent(4, map_ec2_worker_roles_yaml)}
%{ if map_extra_iam_roles_yaml != "[]" }
    ${indent(4, map_extra_iam_roles_yaml)}
%{ endif }
%{ if map_additional_iam_users_yaml != "[]" }
  mapUsers: |
    ${indent(4, map_additional_iam_users_yaml)}
%{ endif }

The %{ if ... } blocks check whether the lists are empty. If map_extra_iam_roles_yaml is "[]", that section is skipped entirely.

Define your locals:

locals {
  auth_configmap_template        = "${path.module}/auth-configmap.yaml.tftpl"
  map_ec2_worker_roles_yaml      = trimspace(yamlencode(local.map_worker_roles))
  map_extra_iam_roles_yaml       = trimspace(yamlencode(var.map_additional_iam_roles))
  map_additional_iam_users_yaml  = trimspace(yamlencode(var.map_additional_iam_users))
}

Then render it with templatefile():

locals {
  auth_configmap = templatefile(local.auth_configmap_template, {
    map_ec2_worker_roles_yaml      = local.map_ec2_worker_roles_yaml
    map_extra_iam_roles_yaml       = local.map_extra_iam_roles_yaml
    map_additional_iam_users_yaml  = local.map_additional_iam_users_yaml
  })
}

Wrapping Up

Terraform templates are one of those features I rely on constantly. They let you keep configuration files readable while still making them dynamic. The templatefile() function handles the common case, jsonencode()/yamlencode() handle structured data, and the %{ if } / %{ for } directives give you control over what gets rendered.

Check also how to run Terraform on Gitlab CI with Plan, Apply and Destroy stages.

Boost your Terraform Skills:

How and When to use Terraform Modules?

Do you know what is Terraform Data and How to use it?

Learn How to use Output on your Terraform and share data across multiple configurations.

Create multiple copies of the same resource using Terraform Count.

Execute Terraform on Gitlab CI.

How to use the Gitlab CI Variables

Effective Cache Management with Maven Projects on Gitlab.

Pipeline to build Docker in Docker on Gitlab.

How to Autoscaling the Gitlab Runner.

How to use Gitlab CI: Deploy to elastic beanstalk

What is AWS KMS and how to use it?

Bits Lovers

Bits Lovers

Professional writer and blogger. Focus on Cloud Computing.

Comments

comments powered by Disqus