Module 6: Finops For Tokens
3. Hands-on: `bedrock-budget.rego`, added to `cost-policy/`
Description
This lesson writes cost-policy/'s second file — bedrock-budget.rego, added to the same directory where finops-and-cost-guardrails-guide already left required-cost-tags.rego. None of that gets touched: bedrock-budget.rego is a new file, with package main, a sibling of required-cost-tags.rego, and also a sibling of policy/ (Module 5's security directory). Everything you see here ran for real, against the complete Andes Cargo project's real tfplan.json — and the result, the first time it runs, is a real FAIL: bedrock.tf, as Modules 3 and 4 left it, still doesn't declare the tag this policy requires.
Connection to the module
Lesson 2 established the exact limit: cost-policy/ can only evaluate what exists in tfplan.json — declared resources, never invocations. aws_bedrock_guardrail does exist there. This lesson writes the policy that requires, on that specific resource, the Workload=GenAIExtraction tag — the cost-allocation piece that, the day Bedrock Guardrails starts billing for content filtering, lets that spend be attributed exactly to this workload, not to "unclassified AI spend." Lesson 5 closes this lesson's FAIL with the corrected HCL.
Analogy: the same scale, with one new label only this cargo needs
finops-and-cost-guardrails-guide, Module 4, already compared required-cost-tags.rego to an airline counter's baggage scale — it doesn't care whether the suitcase contains something prohibited, only whether it has the destination label correctly attached. bedrock-budget.rego is the same scale, at the same counter, but with one additional label that only applies to one specific type of cargo: think of the "fragile goods" or "refrigerated cargo" code some suitcases need on top of the standard destination label. A suitcase without that specific label isn't dangerous — the general scale (required-cost-tags.rego) would already let it through if it has Project/Environment/CostCenter/Owner — but, if it contains something that needs that extra code and doesn't have it, a specialized handling system will never know to treat it differently. Workload=GenAIExtraction is exactly that extra code: it exists only on this AI workload's specific resource, never on the rest of Andes Cargo's baggage.
Step 1 — The rule, in prose, before Rego
The condition bedrock-budget.rego has to detect, in exact words: any resource of type aws_bedrock_guardrail being created ("create" in its actions), whose Workload tag is missing, or present with a value other than "GenAIExtraction". Two conditions, not one: unlike required-cost-tags.rego (which only checks that each tag's key exists, regardless of its value), this policy also checks the value — because Workload isn't a generic tag like Project, it's a specific identifier other pieces of this ecosystem (this guide's M7, when it computes SLIs per workload) will use to filter exactly this resource, and no other.
Step 2 — cost-policy/bedrock-budget.rego, complete
# cost-policy/bedrock-budget.rego -- Module 6, lesson 3 of genai-on-aws-production-guide.
# A new file in cost-policy/ (finops-and-cost-guardrails-guide, Module 4),
# sibling of required-cost-tags.rego -- nothing reinstalled, nothing replaced.
#
# required-cost-tags.rego already requires four generic tags (Project, Environment,
# CostCenter, Owner) on every "classic" billable resource (S3, Lambda, DynamoDB).
# This policy adds a fifth dimension, specific to the AI workload: the Workload
# tag, required only on aws_bedrock_guardrail -- the only resource type this
# guide's Terraform declares that is specific to Bedrock.
# Unlike required-cost-tags.rego (which only checks that the KEY exists),
# this policy also checks the exact VALUE -- Workload has to be
# "GenAIExtraction", never just any non-empty string.
package main
ai_workload_resource_types := {"aws_bedrock_guardrail"}
required_workload_tag_value := "GenAIExtraction"
# Rule 1: the Workload tag is missing entirely.
deny contains msg if {
some rc in input.resource_changes
rc.type in ai_workload_resource_types
"create" in rc.change.actions
tags := object.get(rc.change.after, "tags", {})
not tags.Workload
msg := sprintf(
"%s: missing required cost allocation tag \"Workload\" (AI workload resource type %q) -- add Workload = %q so any future Bedrock-driven cost can be attributed to this workload",
[rc.address, rc.type, required_workload_tag_value],
)
}
# Rule 2: the Workload tag exists, but with a value other than expected.
deny contains msg if {
some rc in input.resource_changes
rc.type in ai_workload_resource_types
"create" in rc.change.actions
tags := object.get(rc.change.after, "tags", {})
tags.Workload
tags.Workload != required_workload_tag_value
msg := sprintf(
"%s: tag \"Workload\" is %q, expected %q -- this workload's cost would be misattributed in Cost Explorer",
[rc.address, tags.Workload, required_workload_tag_value],
)
}
Piece by piece, everything already recognizable from required-cost-tags.rego (finops-and-cost-guardrails-guide, Module 4, lesson 4) and from bedrock-least-privilege.rego (this guide's Module 5, lesson 3):
ai_workload_resource_types := {"aws_bedrock_guardrail"}— a deliberately small, single-element set: lesson 2 already confirmed this is the only resource type this guide's Terraform declares that's specific to Bedrock, different from the IAM resources (which don't bill) and from the three "classic" resourcesrequired-cost-tags.regoalready covers.required_workload_tag_value := "GenAIExtraction"— the exact value, declared once, reused in both rules and both messages.deny contains msg if { ... }— Rego v1, the same mandatory syntax sinceconftestv0.57.0+finops-and-cost-guardrails-guideModule 4, lesson 4 already verified with its own deliberately reproducedrego_parse_error.tags.Workload(Rule 2) — unlikenot tags[tag](which checks absence),tags.Workloadin a truthy context checks presence with a non-empty value — the exact condition that makes Rule 2 fire only when the tag exists but is wrong, never when it's absent (that case is already covered separately by Rule 1).- Two independent rules, not one compound condition — the same discipline
bedrock-least-privilege.rego(Module 5, lesson 3) already established for its own two independent vectors (bedrock:*andResource: "*"): eachFAILmessage precisely points to which of the two problems occurred, without forcing the reader to guess.
Step 3 — Confirming cost-policy/ and policy/ still share nothing
Before running the policy, confirm live the guarantee this module's lesson 1 promised — zero shared files between the two rule directories:
ls policy/ cost-policy/
comm -12 <(ls policy/ | sort) <(ls cost-policy/ | sort)
What to expect (literal — executed to write this lesson):
policy/:
bedrock-least-privilege.rego
least-privilege-iam.rego
no-destroy-shipments.rego
no-public-buckets.rego
cost-policy/:
bedrock-budget.rego
required-cost-tags.rego
comm -12 prints nothing — the empty output is the confirmation: no filename appears on both lists at once. Four security files, two cost files, eight names total, zero overlap. bedrock-budget.rego and bedrock-least-privilege.rego live in different directories even though they share the word "bedrock" in their names — a naming coincidence, never one of content or purpose.
Step 4 — FAIL, against bedrock.tf's real state
tfplan.json is the same file this guide's Module 5 already generated: terraform show -json tfplan > tfplan.json over the complete project, seventeen resources, no LocalStack, no AWS account.
conftest test tfplan.json -p cost-policy/bedrock-budget.rego
What to expect (literal — executed to write this lesson):
FAIL - tfplan.json - main - module.manifest_extractor_guardrail.aws_bedrock_guardrail.this: missing required cost allocation tag "Workload" (AI workload resource type "aws_bedrock_guardrail") -- add Workload = "GenAIExtraction" so any future Bedrock-driven cost can be attributed to this workload
2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
This FAIL is the correct, expected result, not a mistake in this lesson: bedrock.tf, as Modules 3 and 4 left it, declares tags = local.common_tags on manifest_extractor_guardrail — Project, Environment, ManagedBy — and common_tags never included Workload, because that dimension is new to this module. Rule 1 fires, precisely: it names the exact resource (module.manifest_extractor_guardrail.aws_bedrock_guardrail.this), the exact type, and the exact reason. Rule 2 — the one that checks for an incorrect value when the tag does exist — doesn't fire, for the same reason you already saw in Module 5, lesson 3, Step 3: since the tag doesn't exist at all, Rule 2's tags.Workload condition is never met, so that rule counts as passed — not because the tag is correct, but because the question that rule answers doesn't yet apply to this case.
Step 5 — Confirming the second vector separately: an incorrect value
Step 4 tested the tag's total absence — Rule 1. It's worth confirming, with its own independent FAIL, that Rule 2 also fires when the tag exists but is wrong, not only when it's missing entirely:
# bedrock.tf -- proposed change, ONLY for this test; reverted before continuing
module "manifest_extractor_guardrail" {
source = "./modules/bedrock-guardrail"
# ... (rest of arguments unchanged)
tags = merge(local.common_tags, { Workload = "wrong-value" })
}
terraform plan -input=false -no-color -out=tfplan-wrong-value
terraform show -json tfplan-wrong-value > tfplan-wrong-value.json
conftest test tfplan-wrong-value.json -p cost-policy/bedrock-budget.rego
What to expect (literal — executed to write this lesson):
FAIL - tfplan-wrong-value.json - main - module.manifest_extractor_guardrail.aws_bedrock_guardrail.this: tag "Workload" is "wrong-value", expected "GenAIExtraction" -- this workload's cost would be misattributed in Cost Explorer
2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions
This is the opposite case from Step 4: here Rule 1 passes (tags.Workload does exist, so not tags.Workload is false), and Rule 2 fires with its own, distinct, specific message. No message gets confused with the other — each one precisely points to which of this policy's two axes failed. Revert this test change before continuing:
rm tfplan-wrong-value tfplan-wrong-value.json
What this lesson deliberately leaves pending
Step 4's FAIL doesn't get fixed in this lesson — it gets fixed in lesson 5, which adds Workload = "GenAIExtraction" to the real HCL. This lesson deliberately ends with cost-policy/ showing a real, unresolved finding, exactly the same sequence finops-and-cost-guardrails-guide Module 4 already followed: that guide's lesson 4 wrote the policy, lesson 5 ran it against an untagged plan (FAIL), lesson 6 tagged the real HCL (PASS). This lesson is the equivalent of those first two; this module's lesson 5 is the equivalent of the third.
Common mistakes
Writing a single deny rule that combines absence and incorrect value, instead of two independent rules (premature simplification). What happens: someone tries to condense the logic into a single condition (not tags.Workload or tags.Workload != required_workload_tag_value). How to spot it: if your policy produces a single generic message instead of two specific messages depending on which of the two problems occurred. How to fix it: this lesson's Steps 4 and 5 demonstrate, with executed evidence, why separating them matters — the same discipline bedrock-least-privilege.rego (Module 5, lesson 3) already established for its own two independent vectors.
Expecting Step 4's FAIL to be a mistake in this lesson, and looking for what's "broken" in bedrock-budget.rego (misreading the result). What happens: someone sees FAIL and assumes the policy has a bug. How to spot it: if your reaction to Step 4's FAIL is to check the policy's syntax instead of checking the HCL. How to fix it: the FAIL is correct — bedrock.tf really doesn't have the tag yet. The policy is doing exactly its job: finding unclassified spend before it reaches production. Lesson 5 is where the HCL gets fixed, not the policy.
Forgetting to restore bedrock.tf after Step 5, and carrying the test value ("wrong-value") into the next lesson (project hygiene). What happens: someone runs Step 5's scenario, sees the expected FAIL, and moves on without reverting the test change. How to spot it: if conftest test tfplan.json -p cost-policy/bedrock-budget.rego still shows the "wrong-value" message instead of "missing" when starting lesson 5. How to fix it: bedrock.tf must be left exactly as Module 4 left it (with tags = local.common_tags, no Workload at all) before moving on — the same original FAIL from Step 4, ready for lesson 5 to actually fix it.
Exercises
Exercise 1 — Explain why ai_workload_resource_types has a single element today, and what would happen if Andes Cargo added aws_bedrock_provisioned_model_throughput in the future (Module 6, lesson 2, Exercise 3). Which exact line of bedrock-budget.rego would you change?
See solution
You'd only change the set's declaration: ai_workload_resource_types := {"aws_bedrock_guardrail", "aws_bedrock_provisioned_model_throughput"}. No other line of the policy needs to change — both deny rules already iterate over rc.type in ai_workload_resource_types, so adding a resource type to the set automatically extends protection to any resource of that new type, without rewriting the rules' logic. The same incremental-extension property this guide's Module 5, lesson 3, Exercise 1 already practiced.
Exercise 2 — Predict the exact result of conftest test tfplan.json -p cost-policy/ (the complete directory, not just bedrock-budget.rego) against the current state, before lesson 5 fixes the HCL. How many tests total, how many pass, how many fail?
See solution
required-cost-tags.rego contributes one rule (evaluated against the three classic billable resources, which already had their four complete tags before this module), and bedrock-budget.rego contributes two rules (this lesson's Rule 1 and Rule 2). Against Step 4's state — Workload completely absent —, the expected result is 3 tests, 2 passed, 0 warnings, 1 failure, 0 exceptions: required-cost-tags.rego's rule passes (the tags it does require are already complete), bedrock-budget.rego's Rule 1 fails (this lesson's FAIL), and Rule 2 passes (vacuously, because the tag doesn't even exist).
Exercise 3 — A colleague proposes writing Workload directly as part of common_tags, instead of creating a new local for lesson 5. Without looking at lesson 5 yet, explain, based on common_tags's scope as you already know it from Module 3, why that proposal would be a mistake.
See solution
common_tags (declared in locals.tf since Module 1) applies to all of Andes Cargo's resources — the S3 bucket, the DynamoDB table, the deterministic Lambda function, in addition to this guide's guardrail. Adding Workload = "GenAIExtraction" directly to common_tags would incorrectly tag every one of those "classic" resources as part of the generative AI workload, even though none of them has anything to do with Bedrock. The correct solution — which lesson 5 builds — is a new, specific local, applied only to the resources that do belong to this workload: exactly the same scoping discipline finops-and-cost-guardrails-guide Module 4, lesson 2 already established for CostCenter/Owner, reserved only for billable resources, never applied "just in case" to the whole project.
Summary and next step
This lesson wrote cost-policy/bedrock-budget.rego, a second file added to cost-policy/, with two independent deny rules that require the Workload=GenAIExtraction tag on aws_bedrock_guardrail — the only resource this guide's Terraform declares that's specific to Bedrock. You confirmed, with a real comm -12, that cost-policy/ and policy/ still share no file. You ran the policy against the real tfplan.json and got a genuine FAIL (2 tests, 1 passed, 1 failure) — the correct finding, because bedrock.tf still doesn't declare that tag. You also confirmed that the policy's second vector (an incorrect tag value) fires independently, with its own precise message.
Before moving on you should be able to: write this policy's two rules from memory; explain why this lesson's FAIL is the correct result, not a mistake; and run comm -12 to confirm the isolation between policy/ and cost-policy/ without help.
Lesson 4 steps away from Rego for a moment and extends bedrock_cost_estimate.py (Module 2) with a budget threshold — this module's second mechanism, entirely outside tfplan.json.
Resources
- Open Policy Agent — Policy Reference, Rego v1 — official reference for
contains/ifsyntax and evaluating values in a map. - Conftest — Official Documentation — reference for
conftest testagainst a single file (-p cost-policy/bedrock-budget.rego) and against a complete directory. finops-and-cost-guardrails-guide, Module 4, lessons 4 and 5 — the exact origin ofrequired-cost-tags.regoand of the "write the policy, run it against a realplan, see the honestFAIL" pattern this lesson reapplies.- This course, Module 5, lesson 3 — the direct precedent for "a new file in an inherited directory, two independent rules, real
PASS/FAIL," applied there to security.