To enforce mandatory tags such as CostCenter and Owner in Azure, assign the built-in policy Require a tag on resource groups (deny effect) once per tag, then assign Inherit a tag from the resource group if missing (modify effect, with a managed identity) so every resource picks the value up from its resource group. Existing resources aren't changed by the modify effect until you run a remediation task, which uses the assignment's managed identity to add the missing tags. Tag your existing resource groups before remediating, because the inherit policy only acts where the resource group already has a value.
Who this is for and what you will have at the end
This guide is for platform and FinOps teams who need every cost to land against a cost centre and an accountable owner, and who already have thousands of resources with inconsistent tags.
At the end you will have:
- New resource groups blocked unless they carry
CostCenterandOwner. - New and updated resources tagged automatically from their resource group.
- Existing resources remediated in bulk with tracked remediation tasks.
- A view of compliance per assignment, and an optional billing-side safety net in Cost Management.
How the pieces fit
Three facts drive the design:
- Tags don't flow down by themselves. Resources don't inherit resource group or subscription tags.
- Modify runs before deny. For a create or update request, Azure Policy evaluates
disabled, thenappendandmodify, thendeny, thenaudit. A modify policy can add a tag before a deny policy checks for it. - Modify doesn't touch existing resources during scans. It marks them non-compliant. A remediation task fixes them.
The built-in definitions you need:
| Built-in definition | Definition ID | Effect | Mode |
|---|---|---|---|
| Require a tag on resource groups | 96670d01-0a4d-4649-9c89-2d3abc0a5025 | deny | All |
| Inherit a tag from the resource group if missing | ea3f2387-9b95-492a-a190-fcdc54f7b070 | modify (add) | Indexed |
| Inherit a tag from the resource group | cd3aa116-8754-49c9-a813-ad46512ece54 | modify (addOrReplace) | Indexed |
| Require a tag on resources | 871b6d14-10aa-478d-b590-94f262ecfa99 | deny | Indexed |
| Add a tag to resources | 4f9dc7db-30c1-420c-b61a-e1d640128d26 | modify (add) | Indexed |
Choose the inherit variant per tag. If missing uses the add operation, so a resource that already has a different CostCenter keeps it; that suits shared resources billed elsewhere. The plain Inherit variant uses addOrReplace and overwrites any value that differs from the resource group's, which suits tags that must never diverge. Both built-in inherit definitions only act when the resource group's tag isn't empty, and both use Indexed mode, so they skip resource groups and resource types that don't support tags.
Prerequisites
- Rights at the target scope (management group or subscription) to create policy assignments and role assignments, for example Owner.
- Azure CLI signed in to the right tenant.
- An agreed tag taxonomy. Tag names are case-insensitive for operations, but tag values are case-sensitive, so
Financeandfinanceare different cost centres. Each resource, resource group and subscription supports up to 50 tags, names up to 512 characters and values up to 256 characters. - A list of resource groups created by platform services, which you may need to exclude later.
Step 1: Assign the resource group requirement in audit-only mode
Start with DoNotEnforce. The assignment evaluates compliance but doesn't block requests or write deny entries to the Activity log, so you see the size of the problem first.
SUB="/subscriptions/00000000-0000-0000-0000-000000000000"
az policy assignment create \
--name require-rg-costcenter \
--display-name "Require CostCenter tag on resource groups" \
--policy 96670d01-0a4d-4649-9c89-2d3abc0a5025 \
--scope "$SUB" \
--params '{ "tagName": { "value": "CostCenter" } }' \
--enforcement-mode DoNotEnforce
az policy assignment create \
--name require-rg-owner \
--display-name "Require Owner tag on resource groups" \
--policy 96670d01-0a4d-4649-9c89-2d3abc0a5025 \
--scope "$SUB" \
--params '{ "tagName": { "value": "Owner" } }' \
--enforcement-mode DoNotEnforceA new assignment takes about five minutes to apply, then the evaluation cycle runs; on a large scope there is no fixed completion time. To start a scan yourself:
az policy state trigger-scan --no-wait
az policy state summarize -a require-rg-costcenterIf you plan to group several tag policies, Microsoft's tag governance tutorial recommends combining them into an initiative and assigning that once. The individual assignments here keep each step easy to follow.
Step 2: Tag the existing resource groups
The inherit policies have nothing to copy until resource groups carry values. Get the non-compliant list and tag each group with a merge operation, which keeps existing tags:
az policy state list -a require-rg-costcenter \
--filter "ComplianceState eq 'NonCompliant'" \
--query "[].resourceId" -o tsv
az tag update \
--resource-id "$SUB/resourcegroups/rg-payments-prod" \
--operation Merge \
--tags CostCenter=CC-4100 Owner=payments-team@contoso.comUse Merge, not Replace; Replace overwrites the whole tag set. Tags are stored as plain text and appear in cost reports, exports and logs, so don't put personal or sensitive data in them; a team mailbox is a better owner value than a person.
Step 3: Assign the inherit policies with a managed identity
Modify assignments need a managed identity, and that identity needs the roles listed in the definition's roleDefinitionIds. The built-in inherit definitions list Contributor. The portal grants the role automatically when it creates a system-assigned identity; from the CLI you pass --role and --identity-scope, otherwise remediation fails for lack of permission.
az policy assignment create \
--name inherit-costcenter \
--display-name "Inherit CostCenter from resource group if missing" \
--policy ea3f2387-9b95-492a-a190-fcdc54f7b070 \
--scope "$SUB" \
--params '{ "tagName": { "value": "CostCenter" } }' \
--mi-system-assigned \
--identity-scope "$SUB" \
--role Contributor \
--location westeurope
az policy assignment create \
--name inherit-owner \
--display-name "Inherit Owner from resource group if missing" \
--policy ea3f2387-9b95-492a-a190-fcdc54f7b070 \
--scope "$SUB" \
--params '{ "tagName": { "value": "Owner" } }' \
--mi-system-assigned \
--identity-scope "$SUB" \
--role Contributor \
--location westeuropeThe --location sets where the system-assigned identity lives; it can't be global and can't be changed later, but it doesn't affect how the identity works. Each assignment has one identity, which can hold several roles. If you later edit roleDefinitionIds in a custom definition, grant the new roles manually, even in the portal.
From this point, any resource created or updated in a tagged resource group gets the missing tags in the same request.
Writing your own definition instead
If you need behaviour the built-ins don't offer, such as setting an Env tag from a resource group naming pattern, write a custom modify definition. The tag governance tutorial's CostCenter rule shows the shape of the then block:
"then": {
"effect": "modify",
"details": {
"roleDefinitionIds": [
"/providers/microsoft.authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
],
"operations": [
{
"operation": "add",
"field": "tags['CostCenter']",
"value": "[resourcegroup().tags['CostCenter']]"
}
]
}
}Use Indexed mode for rules that target resources and All for rules that target resource groups. conflictEffect defaults to deny; it decides what happens when two modify definitions change the same property.
Step 4: Remediate existing resources
Wait for the inherit assignments to finish evaluating, then create one remediation task per assignment. A task remediates one modify policy at a time.
In the portal:
- Open Policy and select Remediation.
- On Policies to remediate, select the inherit assignment to open New remediation task.
- Optionally narrow the Scope to child resource groups or resources and filter by Locations.
- Select Remediate and follow progress on the Remediation tasks tab. For a failed resource, select Related events to see the error.
From the CLI, using the assignment's resource ID:
az policy remediation create \
--name remediate-costcenter \
--policy-assignment "$SUB/providers/Microsoft.Authorization/policyAssignments/inherit-costcenter"From PowerShell you can tune throughput. -ResourceCount defaults to 500 and accepts up to 50,000; -ParallelDeploymentCount accepts 1 to 30, default 10; -FailureThreshold is a percentage, default 100.
Start-AzPolicyRemediation -Name 'remediate-owner' `
-PolicyAssignmentId '/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/policyAssignments/inherit-owner' `
-ResourceCount 50000 -ParallelDeploymentCount 30For assignments made at management group scope, create remediation tasks after evaluation has run, from Remediation or from the assignment's compliance page with Create Remediation Task.
Step 5: Switch on enforcement
When the resource group compliance report is clean, move the deny assignments to enforced mode:
az policy assignment update --name require-rg-costcenter --enforcement-mode Default
az policy assignment update --name require-rg-owner --enforcement-mode DefaultAssignments also accept a custom non-compliance message. Use it to tell people which tag values are valid and where to get a cost centre code; it is shown in addition to the default deny error.
Should you also require tags on every resource?
Require a tag on resources adds a hard stop at resource level. Because modify runs first, new resources in tagged resource groups pass. The risks are elsewhere: deny also blocks updates to existing resources that are still untagged, and some platform services create their own resource groups and resources. Azure Backup, for example, creates a resource group for VM restore point collections, and a deny tag policy causes UserErrorRequestDisallowedByPolicy on snapshot jobs. If you add resource-level deny, remediate first and exclude such scopes with --not-scopes or exemptions. See Azure VM backup with Recovery Services vaults for that failure in context.
Step 6: Add Cost Management tag inheritance
Cost Management has its own tag inheritance setting that applies billing, subscription and resource group tags to child resource usage records, not to the resources. Because it works on cost data rather than on resources, it catches spend on resources that were missed or can't be tagged. It is available for Enterprise Agreement, Microsoft Customer Agreement and Microsoft Partner Agreement (Azure plan) accounts.
For a subscription: open Cost Management, select the subscription scope, then Settings > Manage subscription. Under Tag inheritance, select Edit and enable Automatically apply subscription and resource group tags to new data. You can also decide whether a resource's own tag or the inherited tag wins when both exist. Usage records update within about 8 to 24 hours and the setting applies to the current month. Group by the tag in Cost Analysis to check results.
Verification
- Creating a resource group without
CostCenterfails withRequestDisallowedByPolicy. - A new storage account or VM in a tagged resource group shows both tags without the deployer supplying them.
az policy state summarize -a inherit-costcenterreports no non-compliant resources after remediation.- Remediation tasks show succeeded deployments, and Deployed Resources on the assignment lists what changed.
- Cost Analysis grouped by
CostCentershows little or no untagged spend.
Troubleshooting
RequestDisallowedByPolicy ... was disallowed by policy. The message names the policy assignment and definition. Check them with az policy assignment show --name <assignment-name> and az policy definition show --name <definition-name>, then add the tag or request an exemption.
Remediation task fails on every resource. The assignment's managed identity lacks the role from roleDefinitionIds, typically because the assignment was created with the CLI or a template without --role. Grant the role at the assignment scope (in Access control (IAM), search for the last segment of the assignment ID) and rerun the task.
Compliance shows 0 of 0 resources. The scope or mode is wrong. Tag rules that target resources need Indexed mode, rules that target resource groups need All, and the resources must not be excluded or exempt.
Results look stale. New assignments take about five minutes to apply, new or updated resources appear after about 15 minutes, and the full scan runs every 24 hours. Use az policy state trigger-scan or Start-AzPolicyComplianceScan.
Inherited tag never appears on a resource. The resource group has no value for that tag, or the resource type doesn't support tags. Tag the resource group, then remediate again.
Custom definition deployed through an ARM template loses resourceGroup(). Resource Manager evaluated the function at deployment time. Escape it as [[resourceGroup().tags['CostCenter']] so Azure Policy receives the expression.
Checklist
- Tag names and allowed values agreed, including case.
- Deny assignments for
CostCenterandOwneron resource groups, created inDoNotEnforce. - Existing resource groups tagged with
az tag update --operation Merge. - Inherit assignments with a system-assigned identity and the Contributor role at the right scope.
- Remediation tasks completed for each inherit assignment.
- Deny assignments switched to
Defaultwith a helpful non-compliance message. - Platform-created scopes excluded before adding any resource-level deny.
- Cost Management tag inheritance enabled for reporting.
For governance in a wider migration programme, see the enterprise Azure cloud migration playbook.
References
- Tutorial: Manage tag governance with Azure Policy
- Policy definitions for tagging resources
- Use tags to organize your Azure resources
- Azure Policy modify effect
- Azure Policy effect basics and order of evaluation
- Remediate non-compliant resources
- Policy assignment structure
- Get policy compliance data
- Troubleshoot common Azure Policy errors
- Request disallowed by policy error
- Group and allocate costs using tag inheritance
- az policy assignment
- az policy state
- az tag
- Azure Policy built-in tag definitions on GitHub