This is the fifth lab in the SA Pro hands-on track. The reference posts argue
the decisions; this stands one up. It pairs with the cost-visibility post,
which builds the reporting pipeline from cost allocation tags to Cost
Categories to a CUR you can query. This lab builds the thing that makes any of
that trustworthy: the control that stops untagged resources existing at all.
The full lab is in lab-sa-pro-05-tag-cost.zip.
Before your first lab, do the one-time, once-per-account setup: run the zip’s
preflight.sh to confirm your account is ready, then deploy the lab reaper,
a standing backstop that auto-deletes any lab you forget to tear down after 24
hours. Every SA Pro lab tags its stack for the reaper on deploy.
The scenario
An estate’s bill is one number, and finance wants a number per team. The
mechanism is well worn: every resource carries a team tag, the tag is
activated for cost allocation, and Cost Explorer groups by it. The mechanism is
not the problem. The problem is the day one team’s pipeline ships resources
untagged, the “unallocated” bucket starts growing, and every report after that
carries an asterisk.
So the tag requirement gets enforced by machine. 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 is a promotion path, not a different design, and it is exactly the kind of layering a Professional paper probes: prove the rule small, then widen its blast radius.
The resource being provisioned here is an SQS queue, because queues are free, instant, and support tag-on-create conditions; the pattern is identical for any service that does.
What you’re given
CloudFormation builds the provisioner role: the identity a platform team’s
tooling would use to create queues. Its allow policy covers creating, tagging,
inspecting, and deleting queues within the lab’s name prefix. Note that
sqs:TagQueue is in the allow list; tagging on create needs both it and
sqs:CreateQueue, and a guardrail that denies it locks out the happy path
too. What is missing is the guardrail itself. The gap in src/template.yaml
is a TODO block where a managed policy goes: two Deny statements, one per
loophole.
Your task
Write that managed policy and attach it to the role. The first statement
denies sqs:CreateQueue when the request carries no team tag; the IAM idiom
for “this tag is missing” is the Null condition operator on
aws:RequestTag/team (quote 'Null' in YAML so it stays a string). The
second denies sqs:UntagQueue when the keys being removed include team,
which is a set-membership test on aws:TagKeys. The second closes the
loophole the first leaves open. Without it the guardrail is theatre: create
tagged, untag a second later, and the spend is orphaned again with nothing in
the report to flag it.
The evaluation logic doing the work is the one worth carrying into the Professional paper: an explicit Deny always beats an Allow, whichever policy it lives in. The role’s allow policy says what the provisioner may do; the deny caps it. That cap-not-grant relationship is precisely how SCPs behave.
Run it
./scripts/deploy.sh # deploys src/template.yaml
./scripts/test.sh # assumes the role and pushes on the guardrail
./scripts/teardown.sh # deletes everything
test.sh assumes the provisioner role, so every check runs the way real
tooling would hit the control, then pushes three times: an untagged create
(must be refused), a tagged create (must succeed), and an attempt to strip the
tag (must be refused). With the unedited template the untagged create sails
through and the test tells you why. With the guardrail in place it 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.
The cost half happens on the bill, on billing’s timetable. In the payer
account, activate team as a cost allocation tag (Billing console, Cost
allocation tags), and from the next day’s data onward Cost Explorer and the
CUR can group spend by team. The guardrail you just proved is what makes that
grouping mean something: nothing exists that isn’t attributed.
If it fails
- The untagged create succeeds. The managed policy is missing, not
attached to the role, or its first condition is wrong. Check
'Null'is quoted; unquoted, YAML turns the operator into an actual null and the condition evaporates. - The tagged create is refused too. The deny is over-matching. The
condition must test
aws:RequestTag/team, notaws:TagKeys, for the create statement. - The tag can still be removed. The second statement is missing or its
operator isn’t
ForAnyValue:StringEqualsonaws:TagKeys. AccessDeniedonsts:AssumeRole. Your CLI identity isn’t allowed to assume roles in this account; run the test as a principal that can.
Reveal the solution
The two statements are short, and writing them from the operators named above is the exercise. When you want the reference answer, deploy it without editing anything:
SRC=solution ./scripts/deploy.sh && ./scripts/test.sh
Or unfold it here:
Show the answer
The managed policy’s two Deny statements:
- Sid: DenyCreateWithoutTeamTag
Effect: Deny
Action: sqs:CreateQueue
Resource: '*'
Condition:
'Null':
aws:RequestTag/team: 'true'
- Sid: DenyRemovingTeamTag
Effect: Deny
Action: sqs:UntagQueue
Resource: '*'
Condition:
ForAnyValue:StringEquals:
aws:TagKeys: team
What you just learned
- Cost allocation is a tagging problem, and tagging is an enforcement problem. A report is only as good as the worst-tagged resource in it.
aws:RequestTagconditions evaluate at create time, the only moment a resource can be refused cheaply. Everything after that is detective controls and cleanup toil.- A guardrail needs both halves: require the tag on create, and protect it from removal. One without the other does not survive contact with a deadline.
- An IAM deny on a role and an SCP on an OU are the same document with different blast radius. The deny caps what the allows grant, wherever it sits in the evaluation.
Next
The rest of the SA Pro lab arc is in the track’s README: a DMS change-data-capture migration closes it out.