linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/lessons/tf-intermediate-05-templatefile

lesson ── terraform-intermediate ── ~14 мин ── 5 шагов

templatefile: render configs from HCL

Often you need to hand Terraform a text file with substitutions: a bash script for EC2 user_data, a JSON policy for IAM, YAML for cloud-init. Inline HCL is hard to read. Baking finished text into a .tf file does not work either, because you need substitutions.

templatefile() solves this: it reads a template file, substitutes the variables, and returns a string. In this lesson you render an IAM policy from a template.

▶ интерактивный sandbox

Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    Write the IAM policy template

    Create the file templates/bucket-rw-policy.json.tpl:

    bash
    cd /home/student/tf-template
    cat > templates/bucket-rw-policy.json.tpl <<'EOF'
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "ReadBucket",
          "Effect": "Allow",
          "Action": [
            "s3:GetObject",
            "s3:ListBucket"
          ],
          "Resource": [
            "${bucket_arn}",
            "${bucket_arn}/*"
          ]
        }%{ if write_enabled },
        {
          "Sid": "WriteBucket",
          "Effect": "Allow",
          "Action": [
            "s3:PutObject",
            "s3:DeleteObject"
          ],
          "Resource": "${bucket_arn}/*"
        }%{ endif }
      ]
    }
    EOF

    The syntax:

    • ${var}, a simple substitution.
    • %{ if cond }...%{ endif }, a conditional block. Here it adds a second Statement only when write_enabled = true.
    • %{ for x in list }...%{ endfor }, a loop (not used here, but it exists).

    The .tpl is just a convention. You can use .tftpl or no extension at all. Use the .tpl or .tftpl extension so your IDE and git tools know this is a template, not the final JSON.

    ✓ Template written. Now the render in HCL.

  2. 02

    Wire templatefile into HCL

    In /home/student/tf-template/main.tf:

    hcl
    resource "random_id" "suffix" {
      byte_length = 4
    }
    resource "aws_s3_bucket" "demo" {
      bucket = "linuxlab-template-${random_id.suffix.hex}"
    }
    locals {
      bucket_policy_json = templatefile(
        "${path.module}/templates/bucket-rw-policy.json.tpl",
        {
          bucket_arn    = aws_s3_bucket.demo.arn
          write_enabled = true
        },
      )
    }
    resource "aws_iam_policy" "bucket_rw" {
      name   = "linuxlab-template-rw-${random_id.suffix.hex}"
      policy = local.bucket_policy_json
    }
    output "policy_json" {
      value = local.bucket_policy_json
    }

    What matters here:

    • path.module, the absolute path to the directory of the current module (here, the root). Do not type an absolute path by hand, use path.module.
    • We pass bucket_arn from a resource that does not exist yet. Terraform builds the dependency automatically: the policy is created after the bucket. See tf-depends-on.
    • The result of templating is an ordinary string. We put it in aws_iam_policy.policy.

    ✓ HCL is ready. Now you can see the render through console.

  3. 03

    Check the render through console (before apply)

    You can look at the render result without running apply:

    bash
    cd /home/student/tf-template
    terraform init
    echo 'templatefile("${path.module}/templates/bucket-rw-policy.json.tpl", { bucket_arn = "arn:aws:s3:::test-bucket", write_enabled = true })' | terraform console -no-color

    Console prints the JSON with the ARN substituted in. Try it with write_enabled = false and you will see the second Statement disappear.

    This is the main way to debug templates. Do not run apply to find out whether something rendered correctly, ask console.

    See tf-console.

    ✓ The render is visible before apply. That saves hours of debugging.

  4. 04

    Apply: the policy is created with the right ARN

    bash
    terraform apply -auto-approve

    You should get: a bucket, an IAM policy, a random_id.

    Check the policy through the AWS CLI (LocalStack):

    bash
    aws --endpoint-url=http://localstack:4566 iam list-policies | jq '.Policies[] | select(.PolicyName | startswith("linuxlab-template"))'

    And through terraform output:

    bash
    terraform output -raw policy_json | jq .

    You see the final JSON with all the substitutions in place.

    ✓ IAM policy created from a template. templatefile in production.

    The same thing on OpenTofu

    OpenTofu keeps its CLI and state compatible with Terraform for the commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. But on your first switch, back up the state and do a trial run on a feature branch, since the differences cluster in the newer features (variables in the backend, state encryption, OCI registry-backed modules). See tf-opentofu-parity for the full matrix.

    • → OpenTofu parity
  5. 05

    Try the without-write variant

    Change the HCL:

    hcl
    locals {
      bucket_policy_json = templatefile(
        "${path.module}/templates/bucket-rw-policy.json.tpl",
        {
          bucket_arn    = aws_s3_bucket.demo.arn
          write_enabled = false   # ← was true
        },
      )
    }
    bash
    terraform plan

    The plan shows a diff: the second Statement section (WriteBucket) disappears from the policy JSON. Terraform sees this as a change to the policy attribute of aws_iam_policy.bucket_rw.

    bash
    terraform apply -auto-approve

    This is the main advantage of templatefile: one change in HCL → a deterministic re-render → a clear diff in the plan.

    ✓ The conditional block worked. The policy was reissued without write permissions.

    templatefile vs file vs inline

    Three ways to build a string in HCL:

    hcl
    # 1) inline in HCL, heredoc
    policy = <<-EOT
      { "Version": "2012-10-17", ... }
    EOT

    Good for small ones with no variables. Bad for large ones. The IDE cannot highlight JSON inside a heredoc.

    hcl
    # 2) file(): external file, no substitutions
    policy = file("${path.module}/policy.json")

    The IDE highlights the .json. But it is static, so if you need substitutions (a bucket ARN, an env name) it will not work.

    hcl
    # 3) templatefile(): external file with substitutions
    policy = templatefile("${path.module}/policy.json.tpl", { ... })

    The best of both worlds. The IDE highlights it (if the .tpl/.tftpl extension is registered as JSON-with-template). Substitutions work.

    The rule: templatefile for anything over 5 to 7 lines or that contains substitutions. heredoc and file for trivial cases.

    • → String functions
    • → templatefile + user_data

Что ты узнал

templatefile(path, vars) reads a file that uses ${var} and %{ for } syntax and returns a string. Handy for large text artifacts that need substitutions. Do not confuse it with the template_file data source, which has been deprecated since TF 0.12.

команды

  • terraform consolecheck the rendered template before plan
  • terraform plansee the final value in the plan (unless it is sensitive)

концепции

  • · templatefile: a function, not a data source; read during plan
  • · Inside the template there is no access to variables: only to the vars you pass
  • · For a large payload plus base64, handy: filebase64() / base64encode(templatefile(...))

← предыдущий

Troubleshooting Garden: the module went stale, the provider got upgraded

следующий →

Checkov, an HCL security scanner

Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies