Enforce Tagging Policies with Pulumi CrossGuard

Cost allocation and ownership fall apart without consistent tags. This guide, part of Pulumi policy as code under Pulumi patterns and provider management, writes a CrossGuard policy that requires every taggable resource to carry a mandatory set of tags before a deploy is allowed.

Context

Finance wants every resource tagged with owner, env, and cost-center. Enforcing that with a CrossGuard policy means untagged resources fail preview, so the tag set is guaranteed rather than hoped for.

Required tags Required tags: Tag baseline with 4 facets. Tag baseline owner team email env dev/stg/prod cost-center GL code Level mandatory
Every taggable resource must carry the full required set or the preview fails.

Prerequisites

Prerequisites Prerequisites: layered from Python interface / API down to Cloud runtime. Python interface / API Typed resource model Provider plugin State backend Cloud runtime
Prerequisites: the stack from the Python interface down to the cloud runtime.
  • pulumi-policy installed and a working policy pack skeleton
  • A list of resource types that support tags in your stacks
  • Agreement on the exact required tag keys
# CLI: confirm the pack runs before adding the tag rule
pulumi preview --policy-pack ./policy

Implementation

Match any resource whose props include a tags map and report the missing keys. Keeping the required set in one constant makes the rule easy to audit.

Tag enforcement Tag enforcement: resource planned then has tags? then compare to set then report missing resource planned has tags? compare to set report missing
Each taggable resource is checked against the required tag set during preview.
# policy/tags.py — require a fixed tag set on taggable resources
# CLI: pulumi preview --policy-pack ./policy
from pulumi_policy import ResourceValidationPolicy, ReportViolation

REQUIRED = {"owner", "env", "cost-center"}

def require_tags(args, report: ReportViolation) -> None:
    props = args.props
    if "tags" not in props:            # resource is not taggable — skip
        return
    present = set((props.get("tags") or {}).keys())
    missing = REQUIRED - present
    if missing:
        report(f"Missing required tags: {', '.join(sorted(missing))}.")

tag_policy = ResourceValidationPolicy(
    name="required-tags",
    description="Taggable resources must carry owner, env, cost-center.",
    validate=require_tags)

Verification

Deploy-preview a stack with a deliberately untagged bucket and confirm it fails listing the missing keys, then add the tags and confirm a clean run.

Verification Verification: Test → Program → Mock/Cloud. Test Program Mock/Cloud invoke declare resolve assert
Verification: the test drives the program and asserts on resolved values.
# CLI: expect a violation naming the missing tags
pulumi preview --policy-pack ./policy --stack dev

Gotchas & Edge Cases

Gotchas & Edge Cases Gotchas & Edge Cases: Where it breaks with 4 facets. Where it breaks defaultTags watch this boundary tags watch this boundary Owner watch this boundary Edge Cases watch this boundary
Gotchas & Edge Cases: the boundaries where things break and what to check.

Default tags hide gaps. If you set provider-level defaultTags, resources may inherit them and the rule sees them as present — decide whether that satisfies the policy.

Non-taggable resources. Skipping resources without a tags key avoids false positives, but some resources tag via a sub-block; handle those explicitly.

Case sensitivity. Owner and owner are different keys; normalise before comparing if your org is inconsistent.

FAQ

Can I require tag values, not just keys?

Yes — extend the rule to validate the value (e.g. env in a known set) and report when it is missing or invalid.

Does this cover existing resources?

Policies evaluate what a preview plans, so pre-existing untagged resources surface the next time they are updated. Pair with a one-off audit for full coverage.

Where should the required set live?

In one constant in the pack, ideally sourced from the same place your finance team maintains it, so the rule and the policy of record never diverge.