Block Deploys That Miss Cost Allocation Tags
Validate required cost tags on infrastructure plans and block resources that would be invisible to the billing report.
Task
Ensure every new cloud resource carries the tags that cost allocation depends on, before it is created.
When to use this
A new queue is created without the team tag. Monthly reports undercount the service, and the team is surprised by a bill it cannot attribute.
Prerequisites
- Conftest 0.49 or newer with Open Policy Agent
- Terraform plan JSON output in CI
- An agreed tag schema for owner, cost center, and environment
Steps
1. Convert the plan to JSON and evaluate the tag policy.
terraform show -json tfplan > plan.json && conftest test plan.json --policy policy/tagsExpected output: Every resource without the required tags fails with its resource address and the missing keys.
2. Write a policy that reports exactly which tags are missing.
package tags
required := {"owner", "cost-center", "environment"}
deny[msg] {
rc := input.resource_changes[_]
rc.change.actions[_] != "delete"
tags := object.get(rc.change.after, "tags", {})
is_object(tags)
missing := required - {key | tags[key]}
count(missing) > 0
msg := sprintf("%s is missing tags: %v", [rc.address, missing])
}
deny[msg] {
rc := input.resource_changes[_]
rc.change.actions[_] != "delete"
not is_object(object.get(rc.change.after, "tags", {}))
msg := sprintf("%s has no tags block", [rc.address])
}
Expected output: The failure message names the resource and the exact keys that must be added.
3. Audit existing resources that are already missing the cost tag.
aws resourcegroupstaggingapi get-resources \
--query 'ResourceTagMappingList[?length(Tags[?Key==`cost-center`])==`0`].ResourceARN' \
--output text
Expected output: A list of ARNs without the cost-center tag becomes the cleanup backlog.
4. Provide safe tag defaults in the module so new resources rarely fail.
variable "tags" {
type = map(string)
default = {
owner = "checkout"
"cost-center" = "cc-1042"
environment = "prod"
}
}
Expected output: Defaults cover new resources, so the gate only catches overrides that remove a required tag.
Troubleshooting
Problem
The policy fails on resource types that do not support tags
Solution
Exclude those types in the policy with a documented list, and revisit the list when providers add tag support.
Problem
Tags exist but billing still cannot attribute cost
Solution
Activate the tag keys in the billing console; unactivated keys do not appear in cost reports.
Limitations
- The gate covers Terraform-managed resources only; console-created resources need a separate audit.
- Tag keys can exist with wrong values, so validate allowed values as well as presence.
Untagged resources are unbillable resources
Cost allocation depends on a small set of tags being present and consistent. When a resource ships without them, the bill still arrives but cannot be split by team or service, and the error compounds quietly until someone tries to answer a budget question. Catching the omission in the infrastructure plan is far cheaper than reconciling a quarter of spend afterwards. The policy also documents the tag schema, which is more reliable than a wiki page nobody reads.
Enforce presence and shape
A tag policy should check keys and, where it matters, allowed values. Presence alone allows cost-center: unknown to spread through the estate, which is functionally the same as no tag. Use the plan JSON rather than the HCL, because the plan contains the final values after variables and defaults are resolved. Report the resource address and the missing keys in one message so the author can fix the change without opening a policy file.
deny[msg] {
rc := input.resource_changes[_]
rc.change.actions[_] != "delete"
tags := object.get(rc.change.after, "tags", {})
is_object(tags)
missing := required - {key | tags[key]}
count(missing) > 0
msg := sprintf("%s is missing tags: %v", [rc.address, missing])
}
Cover the existing estate
The gate only protects new changes. Run a periodic audit over deployed resources to find untagged ARNs, and feed them into a cleanup list ordered by cost. Tag defaults in a shared module reduce friction, but keep the gate strict, because a default that silently applies the wrong cost center is worse than a failed build. Finally, activate the tag keys in the billing console; a perfectly tagged resource contributes nothing to reports if the key is not enabled for cost allocation.