Module 6: Finops For Tokens

5. Cost allocation tagging for the AI workload

Description

Lesson 3 left a real, deliberate, unresolved FAIL: bedrock-budget.rego requires Workload=GenAIExtraction on aws_bedrock_guardrail, and bedrock.tf still doesn't declare it. This lesson closes that FAIL with real HCL — a new local, ai_workload_tags, applied to the two resources this guide declared for the AI workload (the guardrail and BedrockManifestExtractorRole). Along the way, running the complete -p cost-policy/ for the first time in this project exposes a second, older, real finding, which this lesson also closes: common_tags never carried CostCenter/Owner, the two tags finops-and-cost-guardrails-guide already established as part of Andes Cargo's complete taxonomy.

Connection to the module

This is the lesson where lesson 3's FAIL turns, with real evidence, into PASS. It's also the first time this module runs cost-policy/ as a complete library (both files together), not one file at a time — the same moment this guide's Module 5, lesson 3 already lived with the complete policy/.


Analogy: the flight tag, not just the owner's name

finops-and-cost-guardrails-guide already compared the four generic tags (Project, Environment, CostCenter, Owner) to a complete postal addressing system: family name, zone code, responsible contact. Workload is an even more specific piece — think of the flight tag attached to a suitcase at an airport with connections: it doesn't replace the owner's name tag (Project/Owner are still there), it adds the exact flight number that specific suitcase belongs to, so the baggage handling system knows, unambiguously, which itinerary it corresponds to when dozens of suitcases from the same owner are traveling on different flights the same day. Workload=GenAIExtraction is that flight tag — it unambiguously identifies that this specific resource belongs to this specific workload, even though the rest of the infrastructure shares the same Project.


Step 1 — ai_workload_tags, a new local, with a scoped boundary

In locals.tf, alongside common_tags and manifest_extractor_model_arn (Module 3):

locals {
  common_tags = {
    Project     = "andes-cargo"
    Environment = var.environment
    ManagedBy   = "terraform"
    # Module 6, lesson 5: CostCenter/Owner complete the four-tag taxonomy
    # finops-and-cost-guardrails-guide, Module 4, lesson 3 already declared as
    # canonical for Andes Cargo (LOGISTICS-001 / platform-team@andescargo.io) --
    # values cited verbatim from that lesson, not invented here.
    CostCenter  = "LOGISTICS-001"
    Owner       = "platform-team@andescargo.io"
  }

  manifest_extractor_model_arn = "arn:aws:bedrock:${var.aws_region}::foundation-model/amazon.nova-lite-v1:0"

  # Module 6, lesson 5: common_tags plus the one tag specific to this workload.
  # Applied only to the two resources this guide's Terraform declares for the AI
  # workload (the guardrail and the role) -- never merged into common_tags itself,
  # which every other Andes Cargo resource (the S3 bucket, the DynamoDB table, the
  # deterministic Lambda) also uses and has nothing to do with GenAI.
  ai_workload_tags = merge(local.common_tags, {
    Workload = "GenAIExtraction"
  })
}

Two changes, with two different reasons. First, CostCenter/Owner get added to common_tags — not because this module asks for it, but because they're part of the four-tag taxonomy finops-and-cost-guardrails-guide Module 4, lesson 3 already fixed as mandatory for every billable Andes Cargo resource, and this project's common_tags had never carried them since this guide's Module 1 declared it — a real finding, not an invention of this module, exposed in Step 3. Second, ai_workload_tags is a new local, not a modification to common_tags: merge(local.common_tags, { Workload = "GenAIExtraction" }) produces a map that includes all five tags — the four generic ones plus Workload — but it's only applied to resources that explicitly reference it, never to the whole project by automatic inheritance.


Step 2 — Applying ai_workload_tags, in bedrock.tf

Two lines change, both from tags = local.common_tags to tags = local.ai_workload_tags:

 module "manifest_extractor_guardrail" {
   source = "./modules/bedrock-guardrail"
   # ... (arguments unchanged)
-  tags = local.common_tags
+  tags = local.ai_workload_tags
 }
 module "bedrock_manifest_extractor_role" {
   source = "./modules/iam-role"
   role_name               = "BedrockManifestExtractorRole"
   trust_policy_json       = data.aws_iam_policy_document.bedrock_manifest_extractor_trust.json
   permissions_policy_json = data.aws_iam_policy_document.bedrock_manifest_extractor_permissions.json
-  tags                    = local.common_tags
+  tags                    = local.ai_workload_tags
 }

Notice an important scoping detail: BedrockManifestExtractorRole also gets ai_workload_tags, even though bedrock-budget.rego (lesson 3) never evaluates IAM resources — the exact same reason finops-and-cost-guardrails-guide Module 4, lesson 3 already established for CostCenter/Owner on roles: IAM doesn't bill, so no cost-allocation policy needs to check it. Tagging the role with Workload anyway isn't incorrect — it's general project consistency, useful for anyone manually auditing "what belongs to this AI workload" — but the automated policy, quite deliberately, only requires it on the resource that can actually generate spend.


Step 3 — A real terraform plan, and the complete library's first PASS

terraform validate -no-color
terraform plan -input=false -no-color -out=tfplan
terraform show -json tfplan > tfplan.json

What to expect (literal — executed to write this lesson):

Success! The configuration is valid.

Plan: 17 to add, 0 to change, 0 to destroy.

Seventeen resources, the same number this guide's Module 5 already confirmed — no new resource, just a tag change on two that already existed. Now, the policy that left the FAIL in lesson 3:

conftest test tfplan.json -p cost-policy/bedrock-budget.rego

What to expect (literal):

2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions

A real PASS — lesson 3's FAIL is closed. And, for the first time in this module, cost-policy/'s complete library, both files together:

conftest test tfplan.json -p cost-policy/

What to expect (literal — executed to write this lesson, against the complete project with CostCenter/Owner already complete):

3 tests, 3 passed, 0 warnings, 0 failures, 0 exceptions

Three tests, not two: one rule from required-cost-tags.rego (now passing clean, thanks to CostCenter/Owner added in Step 1) plus bedrock-budget.rego's two. All three pass, in a single run, over the same plan a real cost-tags job would evaluate before letting a merge proceed — this module's lesson 8 runs this exact command inside that job, with act pull_request, end to end.


The honest finding: why CostCenter/Owner were missing, and why closing it here is correct

It's worth stating this precisely, without hiding it: before this lesson, conftest test tfplan.json -p cost-policy/ (the complete directory) would have shown six FAILs — two missing tags (CostCenter, Owner) on each of the three classic billable resources (the S3 bucket, the DynamoDB table, the deterministic Lambda function). This isn't a problem specific to this guide, nor to the AI workload — it's an assumption common_tags, declared since this same guide's Module 1, never finished completing against the taxonomy finops-and-cost-guardrails-guide already fixed as mandatory. Since this lesson is the first time this module runs -p cost-policy/ (the whole directory, not just bedrock-budget.rego), it's also the first time that finding becomes visible — and, following the same honesty discipline every lesson in this ecosystem applies, it gets closed right here, not ignored or postponed. CostCenter=LOGISTICS-001 and Owner=platform-team@andescargo.io are the exact values, cited verbatim, from finops-and-cost-guardrails-guide Module 4, lesson 3 — never invented for this lesson.


Common mistakes

Adding Workload directly to common_tags, instead of creating ai_workload_tags (the mistake lesson 3's Exercise 3 already anticipated). What happens: someone, looking for the shortest path, writes Workload = "GenAIExtraction" inside common_tags. How to spot it: if the S3 bucket or the DynamoDB table show up with Workload=GenAIExtraction in the plan, even though they have nothing to do with Bedrock. How to fix it: Workload lives only in ai_workload_tags, a separate local, explicitly applied only to manifest_extractor_guardrail and bedrock_manifest_extractor_role — never merged into common_tags, which the whole project uses.

Fixing lesson 3's FAIL but not re-running the complete -p cost-policy/, and missing the CostCenter/Owner finding (partial verification). What happens: someone runs only conftest test tfplan.json -p cost-policy/bedrock-budget.rego, sees PASS, and calls the lesson done without running the complete library. How to spot it: if you never saw required-cost-tags.rego's six FAILs at any point in your own walkthrough. How to fix it: this lesson's Step 3 deliberately runs both commands — verifying only the new file hides a real finding that only appears when running the complete directory, exactly the kind of omission this entire module is designed to prevent.

Copying LOGISTICS-001/platform-team@andescargo.io from memory, without checking against the cited source, and introducing a casing or format inconsistency. What happens: someone writes Logistics-001 or platformteam@andescargo.io, with a subtle format difference. How to spot it: if your value doesn't match, character for character, finops-and-cost-guardrails-guide Module 4, lesson 3. How to fix it: copy the exact value from the cited source — the same consistency discipline that same lesson already warned about as a common mistake for CostCenter, now applied here, one module and one guide later.


Exercises

Exercise 1 — Predict whether BedrockManifestExtractorRole, with ai_workload_tags applied, would trigger any required-cost-tags.rego FAIL. Based on that policy's scope (finops-and-cost-guardrails-guide Module 4, lesson 3), justify your answer.

See solution

It wouldn't trigger any FAIL, for two independent reasons that reinforce the same conclusion: first, required-cost-tags.rego limits its billable_resource_types to aws_s3_bucket, aws_lambda_function, and aws_dynamodb_tableaws_iam_role/aws_iam_role_policy (the actual types BedrockManifestExtractorRole declares) never appear in that set, so the policy doesn't even evaluate that resource. Second, even if it did evaluate it, ai_workload_tags already includes all four complete generic tags (via merge(local.common_tags, ...)), so it would pass anyway. The result is the same for both reasons, but only the first is the real cause.

Exercise 2 — Calculate how many total tests you'd expect from conftest test tfplan.json -p cost-policy/ if, in the future, a third policy with two of its own rules were added to cost-policy/. Use this lesson's Step 3 count as a base.

See solution

Five: the current three (3 tests, 3 passed, confirmed in this lesson's Step 3) plus the two rules from the hypothetical new file. conftest -p <directory> sums the rules from all .rego files it finds, no matter how many there are — the same additive behavior this guide's Module 5, lesson 3, Exercise 1 already practiced counting rules across four files in policy/.

Exercise 3 — A colleague asks why CostCenter/Owner get closed in a lesson from this module (GenAI), instead of directly in a finops-and-cost-guardrails-guide lesson. Using the "the finding becomes visible wherever the command is run for the first time" argument, explain why this lesson is the right place to close it.

See solution

finops-and-cost-guardrails-guide wrote required-cost-tags.rego and, along its own arc, ran -p cost-policy/ against its own copy of the project — at that point, with no AI workload yet, it's reasonable to assume that guide already closed it correctly in its own context. This guide, building its own copy of the project since Module 1, inherited common_tags without checking it again against the complete four-tag taxonomy until this specific moment — the first time this guide runs the complete -p cost-policy/. The finding is real regardless of where it originated; closing it here, at the exact moment it first becomes visible in this project, is the same "never hide a real result" discipline every lesson in this ecosystem requires — postponing it to a different, already-closed guide wouldn't be a real option.


Summary and next step

This lesson closed lesson 3's FAIL: ai_workload_tags, a new local scoped to two resources, applied to manifest_extractor_guardrail and bedrock_manifest_extractor_role. You confirmed, with real terraform plan and conftest, that bedrock-budget.rego goes from FAIL to PASS (2 tests, 2 passed), and that cost-policy/'s complete library passes clean for the first time (3 tests, 3 passed) — after closing, along the way, a second, real, honest finding: CostCenter/Owner, absent from common_tags since Module 1, now complete with the exact values finops-and-cost-guardrails-guide already established.

Before moving on you should be able to: explain why ai_workload_tags is a separate local, never merged into common_tags; run conftest -p cost-policy/ and predict the exact number of tests; and explain why BedrockManifestExtractorRole gets Workload even though no automated policy requires it there.

Lesson 6 returns to lesson 4's calculator: a deliberately disproportionate volume change in GENAI-COST-PROFILE.md, stopped by the pipeline's step before it ever reaches an apply.

Resources

  1. finops-and-cost-guardrails-guide, Module 4, lesson 3 — the exact source of the LOGISTICS-001/platform-team@andescargo.io values, cited here with none invented.
  2. Terraform Docs — the merge() function — the function used to build ai_workload_tags without duplicating the four generic tags.
  3. This course, Module 5, lesson 3, Step 5 — the exact precedent for "run the complete library for the first time, not just the new file," reapplied here to cost-policy/.
  4. This module, lesson 3 — the origin of the FAIL this lesson closes.