Tagging Strategy and Resource Organization
Why this matters
You've structured the org (management groups), governed it (initiatives), and provisioned it (stacks/Terraform) — but you still can't answer the question finance will ask on day one: "which team, project, and cost centre does this $40,000 belong to?" Subscriptions give a coarse billing boundary (module 00); tags give the fine-grained, queryable metadata that turns a cloud bill into an allocatable, filterable, ownable inventory. A real tagging taxonomy — agreed names, required keys, enforced by policy, inherited down resource groups — is the difference between tags as decoration and tags as the backbone of cost management (track 21), automation, and ownership. This module builds that taxonomy and enforces it with the exact policy machinery from module 03.
Concepts
Tags are queryable metadata, and the taxonomy is the point
A tag is a key = value pair you attach to a subscription, resource group,
or resource. Individually trivial; collectively they're the metadata layer the
whole org queries — "show me all Environment=prod resources," "sum cost by
CostCenter," "who owns this?" The value is entirely in consistency: if
half the org writes env=prod, some Environment=Production, and some
ENV=PRD, no query works. So the deliverable of tagging is not "we use tags"
but a taxonomy: a documented, agreed set of required tag keys, their
allowed values, and their meaning. The taxonomy is a governance artifact the
platform team owns, exactly like the policy baseline (module 03) — and, like a
baseline, it's worthless unless enforced, not merely recommended.
A real starter taxonomy (the four every org needs)
There are dozens of possible tags; a defensible minimum taxonomy that earns its keep is four required keys:
CostCenter— the financial allocation code (e.g.CC-1001). This is the one finance lives on; it maps spend to a budget/team. It's the tag that makes the cloud bill allocatable (track 21).Environment—dev/test/prod(fixed allowed values). Drives filtering, automation ("stop alldevat night"), and correlates resources to the environment strictness you set at the MG level (module 01).Owner— a team or distribution list (e.g.team-payments@contoso.com), not an individual person (people leave; teams persist). Answers "who do I page about this resource?"DataClassification—public/internal/confidential/restricted. Drives security and compliance handling (module 07) — e.g.restricteddata implies stricter policy and audit scope.
Beyond these, common optional keys are Project, Application, ManagedBy
(terraform/portal), and ExpiryDate (for sandbox cleanup). Start with the
four required, keep the list short, and define allowed values for the
categorical ones — an unconstrained free-text tag drifts as badly as no tag.
Enforcing and defaulting tags with policy (reuse of module 03)
A taxonomy is only real if resources can't be created without it and inherit it where sensible. Azure Policy — the exact machinery from module 03 — provides the built-ins:
Require a tag on resources(effectDeny) — blocks creating a resource (or RG) that lacks a required tag. This is the track 11 module 05Denyeffect, now enforcing metadata instead of security config. Put these in your baseline initiative (module 03).Inherit a tag from the resource group(effectModify) — auto-adds a tag to resources from their parent resource group if missing, so you tag the RG once and resources inherit it. Because it writes, it's aModifypolicy needing a managed identity and a remediation task for existing resources (the DINE/Modify lesson from track 11 module 05 and module 03).Require a tag and its value— enforce not just presence but an allowed value (pairs well with the categorical keys).
The pattern: Deny missing required tags at create time, Modify/inherit to
propagate and backfill, and remediate existing resources. This is why the
taxonomy belongs in the governance baseline, not in a wiki — a wiki tag rule
is forgotten; a policy tag rule is enforced.
Tag inheritance is NOT automatic — the mental-model trap
The single biggest tagging misconception: tags do not inherit down the
resource hierarchy by default. A tag on a subscription does not automatically
appear on its resource groups, and a tag on a resource group does not
automatically appear on the resources inside it. (This is the opposite of
policy/RBAC inheritance from module 01, which is automatic — so it's an easy
mental-model collision.) Cost reports can group by a resource's own tags, and
some tools can roll up by RG, but the tag value itself only lands on a
resource if it was set there or an Inherit a tag/Modify policy copied it
down. Assume nothing inherits; enforce inheritance explicitly with policy. This
trap is the module's diagnose-and-fix.
How tags feed cost management (the track 21 handoff)
The payoff for finance: Azure Cost Management can group, filter, and budget
by tag. With a consistent CostCenter/Environment/Owner taxonomy you can
produce "spend by cost centre," "dev vs prod spend," and per-team budgets and
alerts — turning the single subscription invoice (module 00) into an allocatable
breakdown without needing a subscription per team. Track 21 (cost management &
FinOps) builds its entire practice on this: right-sizing, showback/chargeback,
budgets, and forecasts all key off the tags you standardize here. A crucial
caveat to know now: untagged resources at creation time are the permanent gap
— Cost Management shows historical cost against the tags a resource had at the
time, and backfilling a tag later doesn't retroactively re-tag past usage. So
enforcing tags at create time (the Deny) is what makes cost data trustworthy;
this module is really cost management's foundation.
Command reference
| Command | What it does | Example |
|---|---|---|
az tag list / az resource show --query tags |
Reads tags on a resource/RG | az resource show --ids <id> --query tags -o json |
az resource tag |
Sets/replaces tags on a resource | az resource tag --tags CostCenter=CC-1001 Environment=prod --ids <id> |
az group update --set tags.<k>=<v> |
Adds/updates a tag on a resource group | az group update -n rg-app --set tags.Owner=team-web@contoso.com |
az policy assignment create (Require a tag) |
Enforces required tags via Deny (module 03 machinery) |
see breakdown below |
az policy remediation create |
Backfills tags for existing resources under a Modify/inherit policy |
az policy remediation create --name backfill --policy-assignment <id> |
az consumption usage list / Cost Management |
Groups/filters cost by tag (track 21) | az consumption usage list --query "[].{name:instanceName, tags:tags, cost:pretaxCost}" -o json |
az graph query (Resource Graph) |
Queries resources by tag across subscriptions at scale | `az graph query -q "Resources |
Flag breakdown — az resource tag --tags CostCenter=CC-1001 Environment=prod Owner=team-web@contoso.com DataClassification=internal --ids <resource-id> --is-incremental:
--tags k=v ...— the space-separated tag pairs to set. Without--is-incremental, this replaces the entire tag set (a common way to accidentally wipe existing tags).--is-incremental— merges these tags with the resource's existing ones instead of replacing them. Use this whenever you're adding a tag and want to keep the others — the flag that prevents clobbering.--ids <resource-id>— the target resource; can be repeated for bulk tagging.
Flag breakdown — az policy assignment create --name require-costcenter --policy <require-tag-def-id> --scope /providers/Microsoft.Management/managementGroups/mg-org --params '{"tagName":{"value":"CostCenter"}}':
--policy <require-tag-def-id>— the built-in "Require a tag on resources" definition (default effectDeny).--scope .../managementGroups/mg-org— assign at the MG so every subscription enforces the required tag (module 01 inheritance + module 03 scale). Better still, add this policy to yourcontoso-baselineinitiative from module 03 rather than as a standalone assignment.--params '{"tagName":{"value":"CostCenter"}}'— which tag key is required. You'd repeat/parameterize per required key (or use the multi-tag initiative).
Flag breakdown — Terraform tag defaults with provider-level tags + azurerm_resource_group:
provider "azurerm" {
features {}
}
locals {
common_tags = {
CostCenter = var.cost_center
Environment = var.environment
Owner = var.owner_team
DataClassification = var.data_classification
}
}
resource "azurerm_resource_group" "app" {
name = "rg-app-${var.environment}"
location = var.location
tags = local.common_tags
}
resource "azurerm_storage_account" "app" {
# ...
tags = local.common_tags # applied explicitly; tags do NOT auto-inherit from the RG
}
locals.common_tags— define the taxonomy once and spread it onto every resource (the track 9 DRY habit); this is how you keep tags consistent in code.tags = local.common_tagson each resource — note it's repeated because, as the concept says, tags don't inherit; thelocalkeeps that repetition DRY and consistent rather than error-prone.
Hands-on exercises
Tags, policy assignments, and Resource Graph queries are free; the storage
accounts used as tag targets are trivially cheap and cleaned up. Reuse the
mg-org hierarchy and the contoso-baseline initiative from modules 01-03.
(Written) Draft your taxonomy. In a scratch file, write your org's tag taxonomy: the four required keys (
CostCenter,Environment,Owner,DataClassification), the allowed values for the categorical ones (Environment∈ {dev,test,prod};DataClassification∈ {public,internal,confidential,restricted}), the format forCostCenter(CC-####) andOwner(a team DL, never a person), and two optional keys. This document is the deliverable — everything else enforces it.(Azure) Tag a resource group and a resource by hand, and feel the non-inheritance. Create an RG, tag it, create a resource in it without tags, and observe the resource is untagged:
az group create -n rg-tag-lab -l eastus az group update -n rg-tag-lab --set tags.CostCenter=CC-1001 tags.Environment=dev \ tags.Owner=team-web@contoso.com tags.DataClassification=internal SA=govtag$RANDOM az storage account create -n "$SA" -g rg-tag-lab --sku Standard_LRS az resource show --ids $(az storage account show -n "$SA" -g rg-tag-lab --query id -o tsv) --query tags -o jsonExpect the storage account's tags to be empty or null even though its RG is fully tagged — tags did not inherit. This is the mental-model trap made concrete.
(Azure) Tag the resource, using
--is-incrementalto avoid clobbering. Add the taxonomy to the resource, then add one more tag without wiping the rest:SAID=$(az storage account show -n "$SA" -g rg-tag-lab --query id -o tsv) az resource tag --ids "$SAID" --tags CostCenter=CC-1001 Environment=dev Owner=team-web@contoso.com DataClassification=internal az resource tag --ids "$SAID" --tags Project=billing --is-incremental az resource show --ids "$SAID" --query tags -o jsonExpect all five tags present. Now repeat the second command without
--is-incrementaland watch it replace the whole set with justProject— then re-add. This is the clobber footgun;--is-incrementalis the guard.(Azure) Enforce a required tag at scale with policy (module 03 reuse). Assign the "Require a tag on resources" built-in for
CostCenteratmg-orginDeny, then prove an untagged create is blocked:REQ=$(az policy definition list --query "[?displayName=='Require a tag on resources'].id" -o tsv) az policy assignment create --name require-costcenter \ --policy "$REQ" --scope /providers/Microsoft.Management/managementGroups/mg-org \ --params '{"tagName":{"value":"CostCenter"}}' # allow a few minutes for the assignment to take hold, then try an untagged resource: az storage account create -n govnotag$RANDOM -g rg-tag-lab --sku Standard_LRSExpect the create to be rejected for missing the
CostCentertag (a policy-violation error). Metadata is now mandatory org-wide — the sameDenydiscipline from track 11 module 05, applied to tags. (In production you'd add this to thecontoso-baselineinitiative rather than as a standalone assignment.)(Azure) Auto-inherit a tag from the RG with a
Modifypolicy + remediation. Assign the "Inherit a tag from the resource group" built-in forCostCenter, with a managed identity, and remediate existing resources:INH=$(az policy definition list --query "[?displayName=='Inherit a tag from the resource group if missing'].id" -o tsv) az policy assignment create --name inherit-costcenter \ --policy "$INH" --scope $(az group show -n rg-tag-lab --query id -o tsv) \ --params '{"tagName":{"value":"CostCenter"}}' \ --mi-system-assigned --location eastus --role Contributor \ --identity-scope $(az group show -n rg-tag-lab --query id -o tsv) # trigger backfill for resources that predate the policy: az policy remediation create --name backfill-costcenter \ --policy-assignment $(az policy assignment show --name inherit-costcenter --scope $(az group show -n rg-tag-lab --query id -o tsv) --query id -o tsv) \ --resource-group rg-tag-lab 2>/dev/null || echo "remediation may need a few minutes for the identity role to propagate"Expect (after the identity role propagates and remediation runs) resources in
rg-tag-labto gainCostCenterfrom the RG. This isModify+remediation — the write-and-backfill pattern from module 03 — enforcing inheritance that Azure doesn't do on its own.(Azure) Find untagged resources across the estate with Resource Graph. The audit query every platform team runs:
az graph query -q "Resources | where isnull(tags.CostCenter) or tags.CostCenter == '' | project name, type, resourceGroup, subscriptionId" -o table 2>/dev/null \ || echo "install the resource-graph extension: az extension add --name resource-graph"Expect a list of resources missing the required tag — your compliance gap. This is how you measure tagging adoption before and after enforcement, across every subscription at once.
(Azure) Connect tags to cost (track 21 preview). Inspect how cost data carries tags:
az consumption usage list --top 10 \ --query "[].{resource:instanceName, cost:pretaxCost, tags:tags}" -o json 2>/dev/null \ || echo "consumption API may be unavailable on some subscription types — the point stands"Note that each usage record can carry the resource's tags, which is what lets Cost Management group spend by
CostCenter/Environment. Write down the caveat: resources that were untagged at the time of usage can't be retroactively allocated — which is why the create-timeDenyin exercise 4 matters for trustworthy cost data.Diagnose and fix: "a tag-enforcement policy is blocking a legitimate deployment." A governance-real incident: you added a
DenyrequiringDataClassification, and a pipeline that deploys a legitimate resource suddenly fails because it doesn't set that tag. Reproduce:REQ=$(az policy definition list --query "[?displayName=='Require a tag on resources'].id" -o tsv) az policy assignment create --name require-dataclass \ --policy "$REQ" --scope $(az group show -n rg-tag-lab --query id -o tsv) \ --params '{"tagName":{"value":"DataClassification"}}' # a "legitimate" deployment that just forgot the tag now fails: az storage account create -n govdc$RANDOM -g rg-tag-lab --sku Standard_LRS --tags CostCenter=CC-1001Expect a
RequestDisallowedrejection for missingDataClassification. Diagnose: the deployment is legitimate but non-compliant — it isn't a broken policy, it's a real missing tag. Two correct fixes, and one wrong one to avoid: (a) fix the deployment to set the required tag (the right default — the policy is doing its job):az storage account create -n govdc$RANDOM -g rg-tag-lab --sku Standard_LRS \ --tags CostCenter=CC-1001 DataClassification=internal # now succeeds(b) if the resource type genuinely can't carry the tag yet, add a narrow, time-bound exemption (module 03) for just that scope — not a broadening of the policy. The wrong fix is deleting or loosening the org-wide
Deny, which removes the guardrail for everyone. Lesson: a tagDenyblocking a deploy usually means "add the tag," not "remove the policy" — and when a real exception exists, exempt narrowly (module 03), never weaken broadly.(Azure) Clean up.
az policy assignment delete --name require-dataclass --scope $(az group show -n rg-tag-lab --query id -o tsv) 2>/dev/null; true az policy assignment delete --name inherit-costcenter --scope $(az group show -n rg-tag-lab --query id -o tsv) 2>/dev/null; true az policy assignment delete --name require-costcenter --scope /providers/Microsoft.Management/managementGroups/mg-org 2>/dev/null; true az group delete -n rg-tag-lab --yes --no-waitConfirm with
az group show -n rg-tag-lab -o table(gone). Keep the MG hierarchy andcontoso-baselinefor module 07.
Independent challenge
No commands given — build it yourself, drawing on this module, module 03
(initiatives, Deny/Modify, exemptions), module 01 (MG scope/inheritance), and
track 9 (Terraform locals/tags). Design a complete tagging taxonomy for the
Payments/Web/Data org you've carried since module 00: document the required keys
(at least the four), their allowed values and formats, and which are enforced
Deny (required at create) vs. Modify/inherited vs. optional. Then enforce
it: add the required-tag policies to your contoso-baseline initiative (module
03) so they're assigned once at the org-root MG and inherited by every
subscription, and add an inherit/Modify policy so resources pick up
CostCenter from their resource group with a remediation task for existing
resources. Prove enforcement by attempting an untagged create (blocked) and a
tagged create (allowed), and run a Resource Graph query to measure how many
existing resources still lack a required tag. Finally, write two or three
sentences on how this taxonomy will feed Cost Management in track 21, including
why enforcing tags at create time (rather than backfilling later) is what makes
per-cost-centre spend reports trustworthy. Clean up all billable resources.
Stuck? One hint
The enforcement has two halves that map to two policy effects you already know
from module 03: Deny the presence of required tags at create time (the
"Require a tag on resources" built-in, one per required key or bundled in your
initiative) so nothing is born untagged, and Modify to inherit tags the
resource group already carries (the "Inherit a tag from the resource group if
missing" built-in) — which, because it writes, needs --mi-system-assigned --location <region> --role Contributor and a az policy remediation create to
backfill existing resources. In Terraform, keep the taxonomy DRY with a
locals.common_tags map spread onto every resource's tags (repeated per
resource because tags don't inherit). The create-time-Deny point for cost:
Cost Management allocates historical usage by the tags a resource had at the
time, so a tag added later never retroactively fixes past months — only
enforcing at create keeps the cost breakdown complete.
Common mistakes & troubleshooting
- Assuming tags inherit like policy/RBAC. They don't — a tag on an RG does
not appear on resources inside it automatically. Enforce inheritance
explicitly with a
Modify/"Inherit a tag" policy, or set tags on every resource (a Terraformlocalsmap keeps that DRY). This is the opposite of module 01's automatic policy inheritance — an easy collision. - Clobbering existing tags.
az resource tag --tags ...replaces the whole set unless you pass--is-incremental. Forgetting the flag wipes tags you didn't mean to touch. - Free-text categorical tags.
Environment=ProdvsproductionvsPRDbreaks every query. Define allowed values and enforce them ("Require a tag and its value"), especially forEnvironmentandDataClassification. Ownerset to an individual. People change teams and leave; tagOwnerto a team or distribution list so the tag survives staff churn.- Forgetting the managed identity/remediation for
Modify/inherit. Inherit and value-setting policies write, so they need a managed identity with a role and a remediation task to affect existing resources — the module 03 / track 11 module 05 DINE/Modify lesson. - Backfilling tags and expecting historical cost to fix itself. Cost
Management allocates by the tags a resource had at the time of usage;
enforcing tags at create time (a
Deny) is what makes cost data complete — late tags don't rewrite the past. - Loosening a tag
Denybecause it blocked a deploy. The block usually means the deployment forgot the tag — fix the deployment. For a genuine exception, add a narrow, time-bound exemption (module 03), never a broad weakening.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
Show questions
- What makes a set of tags valuable — the tags themselves, or something else? Name the four required keys of a starter taxonomy and what each is for.
- Do tags inherit down the resource hierarchy by default? How is this different from policy/RBAC inheritance, and how do you actually get inheritance?
- Which policy effect enforces a required tag at create time, and which propagates/backfills a tag — and what does the second one need to work?
- Why should
Ownerbe a team/DL rather than a person, and why should categorical tags have enforced allowed values? - What does
--is-incrementaldo onaz resource tag, and what goes wrong without it? - Why is enforcing tags at create time important for trustworthy cost reporting, rather than backfilling later?
- A
Denytag policy is blocking a legitimate deployment. What's the usual correct fix, and what's the correct fix if the resource genuinely can't carry the tag — and what should you not do?
Show answers
- The consistency (the agreed taxonomy), not the tags themselves —
inconsistent tags break every query. Four required keys:
CostCenter(financial allocation code, feeds cost management),Environment(dev/test/prod, drives filtering/automation),Owner(team/DL for contact/paging),DataClassification(public→restricted, drives security/compliance handling). - No — tags do not inherit down the hierarchy by default (an RG's tag
doesn't land on its resources). This is the opposite of policy/RBAC
inheritance (module 01), which is automatic. To get tag inheritance you set
tags explicitly on each resource or use a
Modify/"Inherit a tag from the resource group" policy. Deny("Require a tag on resources") enforces a required tag at create time;Modify("Inherit a tag from the resource group" / set value) propagates or backfills a tag. TheModifypolicy writes, so it needs a managed identity with a role and a remediation task to affect existing resources.Owneras a person breaks when they leave or change teams; a team/DL survives churn and stays a valid contact. Categorical tags need enforced allowed values because free-text drift (Prod/production/PRD) breaks grouping and filtering — the whole point of the tag.--is-incrementalmerges the given tags with the resource's existing ones; without it,az resource tag --tags ...replaces the entire tag set, silently wiping tags you didn't include.- Cost Management allocates historical usage by the tags a resource had at the
time of that usage; a tag added later doesn't retroactively re-tag past
months. Enforcing at create time (a
Deny) ensures resources are never billed while untagged, keeping per-cost-centre reports complete and trustworthy. - Usual fix: add the missing tag to the deployment — the policy is doing its
job. If the resource genuinely can't carry the tag, add a narrow, time-bound
exemption (module 03) for just that scope. Do not delete or loosen the
org-wide
Deny, which would remove the guardrail for everyone.
Next
Continue to 07-compliance-and-regulatory-as-code — you have structure, policy, provisioning, and metadata; now tie them into regulatory compliance: the built-in compliance initiatives, what "compliance as code" means in practice, and how to be audit-ready.