HCL jsonencode and templatefile the Encode Shelf for filter patterns
Encode structured values. Feed templates from maps. Stop pasting JSON as a heredoc.
<!-- hal:authoritative:yaml -->
Encode structured values. Feed templates from maps. Stop pasting JSON as a heredoc when the language already owns the shape.
§I. Frame
09-20 taught dynamic blocks and for_each on an ingress map. Leave it.
Today tf_day_dev_counter is 21. Twenty-one mod 3 is 0. HCL depth. Ops wants a CloudWatch metric filter whose pattern attribute is a string AWS understands, and eventually IAM-ish documents that must be valid JSON. The grammar question is how that string is produced.
§II. Two encoding tools
**Tool one. jsonencode.** Takes any Terraform value (object, list, string, bool, number) and returns a JSON string. Numbers stay numbers. Booleans stay booleans. Nested objects stay nested. Use it when an AWS attribute expects a JSON document and you already have a map in locals.
locals {
filter_doc = {
level = "ERROR"
app = var.app_name
}
}
# Wrong shape for CloudWatch filter grammar, right shape for many IAM/policy attrs:
# policy = jsonencode(local.policy_object)
**Tool two. templatefile.** Reads a file from disk, interpolates ${...} using a vars map, returns a string. Brikman 3e uses it for user_data. Inside a module, call templatefile("${path.module}/user-data.sh", { ... }) so the path resolves relative to the module, not the root.
resource "aws_instance" "example" {
# teaching adjacency only; Ops tonight is Logs, not EC2
user_data = templatefile("${path.module}/user-data.sh", {
app_name = var.app_name
log_group = aws_cloudwatch_log_group.app.name
})
}
§III. CloudWatch filter: string grammar, not free JSON paste
The metric filter pattern on aws_cloudwatch_log_metric_filter is CloudWatch filter syntax. A JSON-log match looks like { $.level = "ERROR" }. That is not the same as jsonencode({ level = "ERROR" }), which would emit {"level":"ERROR"} and fail as a filter pattern.
Named technique: Encode Shelf. Before you call jsonencode, name the consumer:
- JSON document consumer (IAM policy, ECS task definition fragment, Secrets Manager secret string) →
jsonencodeof a map. - Template consumer (shell user-data, Prometheus scrape snippet on disk) →
templatefilewith a vars map. - Domain grammar consumer (CloudWatch filter pattern, Metric Math expression) → a string built for that grammar; do not force
jsonencode.
locals {
# Domain grammar: keep as a deliberate string (or templatefile of a .pattern file).
error_filter = "{ $.level = \"ERROR\" && $.service = \"${var.service}\" }"
# JSON document: encode the object; never hand-type braces in a heredoc.
alarm_dims = jsonencode({
service = var.service
env = var.env
})
}
resource "aws_cloudwatch_log_metric_filter" "error_rate" {
name = "${var.service}-error-count"
log_group_name = aws_cloudwatch_log_group.app.name
pattern = local.error_filter
metric_transformation {
name = "AppErrorCount"
namespace = "Hedronite/App"
value = "1"
}
}
Rule one. jsonencode is for JSON consumers. CloudWatch filter patterns are not JSON documents.
Rule two. Prefer templatefile over a multi-line heredoc when the body lives on disk and takes parameters. Prefer a short local string when the body is one line of domain grammar.
Rule three. Lab 20 shapes external JSON with jsondecode / file. Encoding is the inverse direction: HCL value → JSON string. Keep the directions straight on the exam and in review.
§III.b. The heredoc trap
A <<EOT block feels honest because you see the braces. It is also where commas go missing and where a reviewer cannot terraform console a single expression. Prefer:
# Prefer
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["logs:CreateLogStream", "logs:PutLogEvents"]
Resource = ["${aws_cloudwatch_log_group.app.arn}:*"]
}]
})
# Avoid for JSON documents
# policy = <<EOT
# { "Version": "2012-10-17", ... }
# EOT
Heredocs still win for shell scripts that are mostly literal text with a few interpolations. That is templatefile territory once the script leaves the .tf file. Keep JSON out of heredocs when jsonencode can own the shape.
§III.c. templatefile vars are a map, not free names
The second argument to templatefile is an object/map of names available inside the file. A bare ${app_name} inside the template resolves only if you passed app_name = ... in that map. Root-module variables do not leak into the template automatically.
# user-data.sh contains: echo "shipping to ${log_group}"
user_data = templatefile("${path.module}/user-data.sh", {
log_group = aws_cloudwatch_log_group.app.name
})
Forget the map entry and Terraform errors at plan with an undefined variable inside the template. That failure is louder than a silent wrong string from a heredoc you forgot to update.
§IV. Close instruction
Take Ops' metric filter. Move the pattern into a local. Parameterize the service name. Confirm terraform console prints the filter string with quotes escaped for HCL, not as jsonencode output. Then write a tiny IAM policy object and jsonencode it for contrast. Pair: Ops owns retention + census; Cert names Associate stems for encoding and collection helpers.
Related
- Tome: Brikman 3e pp.190-192 (
templatefile,path.module) — grounded-in - Bootcamp: Lab 20 (jsondecode/file) — grounded-in; Lab 27 (merge/flatten) — referenced
- Prior HCL: dynamic/for_each 09-20 · backend partial 09-11 · replace_triggered_by 09-02