Terraform
user_data
variables
provider template
configuration management

Accessing Terraform variables within user_data provider template file

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When provisioning EC2 instances or other cloud VMs with Terraform, the user_data field runs a startup script on first boot. To make this script dynamic — passing in hostnames, database endpoints, API keys, or environment names — you inject Terraform variables into the script using template interpolation. Terraform supports two approaches: inline templatefile() function (recommended) and the legacy template_file data source. Both use ${variable} syntax inside the template, but they differ in how variables are passed and managed.

hcl
1# variables.tf
2variable "app_name" {
3  type    = string
4  default = "my-app"
5}
6
7variable "db_host" {
8  type = string
9}
10
11variable "environment" {
12  type    = string
13  default = "production"
14}
bash
1# scripts/init.sh (template file)
2#!/bin/bash
3echo "Bootstrapping ${app_name} in ${environment}"
4apt-get update -y
5apt-get install -y docker.io
6
7# Configure the application
8cat > /etc/app/config.json <<EOF
9{
10  "db_host": "${db_host}",
11  "environment": "${environment}",
12  "app_name": "${app_name}"
13}
14EOF
15
16systemctl start docker
17docker run -d -e DB_HOST=${db_host} ${app_name}:latest
hcl
1# main.tf
2resource "aws_instance" "app" {
3  ami           = "ami-0c55b159cbfafe1f0"
4  instance_type = "t3.micro"
5
6  user_data = templatefile("${path.module}/scripts/init.sh", {
7    app_name    = var.app_name
8    db_host     = var.db_host
9    environment = var.environment
10  })
11
12  tags = {
13    Name = var.app_name
14  }
15}

templatefile() is a built-in Terraform function that reads a file and substitutes ${variable} placeholders with the values passed in the second argument (a map). This is the recommended approach since Terraform 0.12+.

Using the Legacy template_file Data Source

hcl
1# Legacy approach (still works but deprecated)
2data "template_file" "init" {
3  template = file("${path.module}/scripts/init.sh")
4
5  vars = {
6    app_name    = var.app_name
7    db_host     = var.db_host
8    environment = var.environment
9  }
10}
11
12resource "aws_instance" "app" {
13  ami           = "ami-0c55b159cbfafe1f0"
14  instance_type = "t3.micro"
15  user_data     = data.template_file.init.rendered
16}

The template_file data source was the standard approach before templatefile() was added. It requires the template provider and creates an extra resource in the state. Prefer templatefile() for new code.

Passing Lists and Maps

hcl
1# variables.tf
2variable "allowed_ports" {
3  type    = list(number)
4  default = [80, 443, 8080]
5}
6
7variable "env_vars" {
8  type = map(string)
9  default = {
10    LOG_LEVEL = "info"
11    REGION    = "us-east-1"
12  }
13}
bash
1# scripts/init.sh
2#!/bin/bash
3
4# Iterate over a list using Terraform template directives
5%{ for port in allowed_ports ~}
6ufw allow ${port}/tcp
7%{ endfor ~}
8
9# Iterate over a map
10%{ for key, value in env_vars ~}
11export ${key}="${value}"
12%{ endfor ~}
hcl
1resource "aws_instance" "app" {
2  ami           = "ami-0c55b159cbfafe1f0"
3  instance_type = "t3.micro"
4
5  user_data = templatefile("${path.module}/scripts/init.sh", {
6    allowed_ports = var.allowed_ports
7    env_vars      = var.env_vars
8  })
9}

Template directives like %{ for ... } and %{ if ... } allow loops and conditionals inside the template. The ~ trims whitespace around the directive.

Conditional Logic in Templates

bash
1# scripts/init.sh
2#!/bin/bash
3
4%{ if environment == "production" ~}
5echo "Setting up production monitoring"
6apt-get install -y datadog-agent
7%{ else ~}
8echo "Development mode — skipping monitoring"
9%{ endif ~}
10
11# Install application
12docker pull ${docker_registry}/${app_name}:${app_version}
13docker run -d --name ${app_name} \
14  -e ENVIRONMENT=${environment} \
15  ${docker_registry}/${app_name}:${app_version}
hcl
1resource "aws_instance" "app" {
2  ami           = "ami-0c55b159cbfafe1f0"
3  instance_type = "t3.micro"
4
5  user_data = templatefile("${path.module}/scripts/init.sh", {
6    environment     = var.environment
7    app_name        = var.app_name
8    app_version     = var.app_version
9    docker_registry = var.docker_registry
10  })
11}

Inline user_data with Heredoc

hcl
1# For simple scripts, use inline heredoc instead of a separate file
2resource "aws_instance" "app" {
3  ami           = "ami-0c55b159cbfafe1f0"
4  instance_type = "t3.micro"
5
6  user_data = <<-EOF
7    #!/bin/bash
8    echo "Setting up ${var.app_name}"
9    apt-get update -y
10    apt-get install -y nginx
11    echo "server_name ${var.domain_name};" > /etc/nginx/conf.d/app.conf
12    systemctl start nginx
13  EOF
14}

Inline heredocs use standard Terraform variable interpolation (${var.name}) directly. This works for short scripts but becomes hard to maintain for complex bootstrapping.

Base64 Encoding for user_data

hcl
1# Some providers require base64-encoded user_data
2resource "aws_instance" "app" {
3  ami           = "ami-0c55b159cbfafe1f0"
4  instance_type = "t3.micro"
5
6  user_data_base64 = base64encode(templatefile("${path.module}/scripts/init.sh", {
7    app_name = var.app_name
8    db_host  = var.db_host
9  }))
10}

Common Pitfalls

  • Using ${} in shell scripts without escaping: Terraform interprets ${...} as variable interpolation. If your bash script uses ${BASH_VAR}, Terraform tries to substitute it and fails. Escape literal dollar signs as $${BASH_VAR} in the template so Terraform outputs ${BASH_VAR} in the rendered script.
  • Forgetting to pass variables to templatefile(): Every ${variable} used in the template must be included in the vars map. If a variable is referenced in the template but not passed, Terraform raises an error at plan time. Pass all required variables explicitly.
  • Changing user_data forces instance replacement: AWS treats user_data as a launch-time attribute. Changing it in Terraform forces destruction and recreation of the instance, not an in-place update. Use lifecycle { ignore_changes = [user_data] } if you want to prevent replacement, or accept that user_data changes require new instances.
  • Template file not found due to path issues: Use ${path.module}/scripts/init.sh to reference files relative to the Terraform module. Relative paths without path.module break when the module is called from a different directory.
  • Exceeding user_data size limits: AWS limits user_data to 16 KB. Long scripts with many variables may exceed this. Move complex bootstrapping logic to a configuration management tool (Ansible, cloud-init) and use user_data only to trigger it.

Summary

  • Use templatefile("file.sh", { var = value }) to inject Terraform variables into user_data scripts
  • Template files use ${variable} for interpolation and %{ for/if } for control flow
  • Escape shell ${} variables as $${} to prevent Terraform from interpreting them
  • Use ${path.module} for reliable file path resolution
  • Inline heredocs work for simple scripts; external template files are better for complex ones
  • Changing user_data forces instance replacement — plan accordingly

Course illustration
Course illustration

All Rights Reserved.