Exam Room · Advanced Solutions Architect

Lab: Enforce a Team Tag and Read the Cost Back

· 15 min read

Advanced Cloud Architecture · part of The Exam Room

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 organisation with all features enabled, the same policy document attached to an OU as a Service Control Policy caps every member account under it at once. Two exemptions survive that move, and they matter more at OU scale than they do here: an SCP never restricts the management account, and it never restricts a service-linked role. Anything a service does on your behalf through one of those roles stays outside the mandate and needs its own arrangement. Prove the rule on one role, widen its blast radius, then re-check what falls outside.

The resource being provisioned here is an SQS queue. An idle queue carries no charge, the first million SQS requests a month are free for every account, and sqs:CreateQueue accepts tags in the request, which is what makes tag-on-create enforcement possible at all. That support is per-action rather than universal: sqs:CreateQueue exposes aws:RequestTag/${TagKey} alongside aws:TagKeys and aws:ResourceTag/${TagKey}, while sqs:UntagQueue exposes aws:TagKeys and aws:ResourceTag/${TagKey} but no aws:RequestTag. The two statements you are about to write are shaped by that difference.

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. sqs:TagQueue is in that allow list on purpose. Tagging a queue on create requires both sqs:CreateQueue and sqs:TagQueue, so a guardrail that denies the second locks out the happy path along with the loophole. 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 key is absent at authorisation time” is the Null condition operator set to true against 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: ForAnyValue:StringEquals 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.

An explicit Deny beats an Allow wherever the two live. The role’s allow policy says what the provisioner may do; the deny caps it. SCPs cap rather than grant in exactly the same way, which is why the document moves up a level unchanged.

Both statements together still leave two gaps. A tag key is not a tag value: team=tbd passes the Null test, and pinning the acceptable values means a StringNotEquals list on aws:RequestTag/team, or a tag policy from the management account, which standardises keys and values across the organisation but cannot require a tag that is absent. And sqs:TagQueue stays allowed, so a principal who cannot remove the team tag can still overwrite it: in SQS a new tag with an identical key replaces the existing one.

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, and it is far slower than the guardrail. Only a management account, or a standalone account that belongs to no organisation, has the Cost allocation tags page, so that is where team gets activated. Allow up to 24 hours for a newly used key to appear there for activation, and up to another 24 for the activation to take. Activation applies forward from there, though the history is not lost: the management account can backfill activation status across the previous twelve months, one request every 24 hours, and spend comes back tagged only where the resource already carried the tag at the time. Don’t expect this lab on its own to put team on that page, either. Its queue lives for seconds and meters nothing, so run the activation step against a resource you actually keep. After that Cost Explorer and the Cost and Usage Report group spend by team, and the guardrail you proved is what makes the 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, not aws:TagKeys, for the create statement.
  • The tag can still be removed. The second statement is missing or its operator isn’t ForAnyValue:StringEquals on aws:TagKeys.
  • AccessDenied on sts:AssumeRole. Your CLI identity isn’t allowed to assume roles in this account; run the test as a principal that can.
  • Both creates are refused after you changed Region. SQS’s tag condition keys are not available everywhere. The ABAC documentation names Asia Pacific (Hyderabad), Asia Pacific (Melbourne), Europe (Spain) and Europe (Zurich) as exceptions, and where aws:RequestTag/team never reaches the request context the Null test matches every create, tagged or not. The lab’s default ap-southeast-2 is unaffected.

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:RequestTag conditions evaluate at create time, the only moment the resource can be refused before it exists. 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, and the SCP still reaches neither the management account nor a service-linked role.
  • Enforcement lands in seconds; attribution takes up to 48 hours, and the tag can only be activated from a management account or a standalone one. Plan the reporting timeline around the slower half.

Next

All six SA Pro builds, with their domains and scaffolding levels, are listed in the track’s README. The one that hands you the least is the DMS change-data-capture migration.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.