lesson ── terraform-intermediate ── ~14 мин ── 5 шагов
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 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
Create the file templates/bucket-rw-policy.json.tpl:
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.
In /home/student/tf-template/main.tf:
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.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.aws_iam_policy.policy.✓ HCL is ready. Now you can see the render through console.
You can look at the render result without running apply:
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-colorConsole 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.
terraform apply -auto-approve
You should get: a bucket, an IAM policy, a random_id.
Check the policy through the AWS CLI (LocalStack):
aws --endpoint-url=http://localstack:4566 iam list-policies | jq '.Policies[] | select(.PolicyName | startswith("linuxlab-template"))'And through terraform output:
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.
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.
Change the 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
},
)
}
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.
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.
Three ways to build a string in 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.
# 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.
# 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.
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 planterraform plansee the final value in the plan (unless it is sensitive)концепции