SA Pro Lab 05 — Enforce a team tag, then read the cost back

Scaffold: 2/5. The provisioner role and its allow policy are built. You write the guardrail from a requirement: a managed policy whose two Deny statements make the team tag unavoidable.

The scenario

An estate’s bill is one number, and finance wants it to be a number per team. The mechanism is boring and well-known: every resource carries a team tag, the tag is activated as a cost allocation tag, and Cost Explorer groups by it. The hard part is not the mechanism, it’s the discipline: the day one team’s pipeline creates resources untagged, the “unallocated” bucket starts growing and never stops.

So the discipline gets automated. A guardrail refuses to create a resource unless the request carries the tag, and refuses to remove the tag afterwards. In this lab the guardrail is an IAM deny policy on a provisioner role, which works in any standalone account. In an AWS Organization the same policy document, attached to an OU as a Service Control Policy, enforces the rule across every account and every principal at once — that’s the promotion path, not a different design.

The requirement

Assuming the provisioner role: creating an SQS queue without a team tag is refused; creating one tagged team=checkout succeeds; removing the team tag from that queue is refused.

What’s provided

Your task

Add an AWS::IAM::ManagedPolicy attached to the role, with two Deny statements:

  1. Deny sqs:CreateQueue when the request carries no team tag. “No team tag in the request” is the Null condition operator on aws:RequestTag/team (quote 'Null' in YAML, or it parses as an actual null).
  2. Deny sqs:UntagQueue when the keys being removed include team (ForAnyValue:StringEquals on aws:TagKeys). Without this the first statement is theatre: create tagged, untag a second later.

An explicit Deny always beats the role’s Allow — the same evaluation logic that makes SCPs work.

Run it

# Defaults: stack sa-pro-lab-05, region ap-southeast-2.
./scripts/deploy.sh          # deploys src/template.yaml
./scripts/test.sh            # assumes the role and pushes on the guardrail
./scripts/teardown.sh        # deletes everything

Nothing in this lab bills meaningfully (a role, a policy, and an empty queue that lives for seconds), but teardown keeps the account clean and the deploy tags the stack for the lab reaper as a backstop.

What success looks like

./scripts/test.sh prints:

1. Creating sa-pro-lab-05-t12345 with NO tags (this should be refused) ...
   refused. No team tag, no queue.
2. Creating sa-pro-lab-05-t12345 tagged team=checkout (this should succeed) ...
   created: https://sqs.../sa-pro-lab-05-t12345
3. Removing the team tag (this should be refused) ...
   refused. The tag stays.

PASS: untagged create refused, tagged create allowed, tag removal refused.

Reveal the solution

SRC=solution ./scripts/deploy.sh && ./scripts/test.sh

The cost half

The guardrail proves in seconds; the payoff lands on the bill, on billing’s timetable. In the payer account, activate team as a cost allocation tag (Billing console → Cost allocation tags). From the next day’s data onward, Cost Explorer and the Cost and Usage Report can group spend by team, and the guardrail you built is what makes that grouping trustworthy: nothing exists that isn’t attributed.

What you just learned

Next

The rest of the SA Pro lab track is listed in labs/README-sa-pro.md.