It's 3pm. Finance asks which team owns a chunk of the Azure bill, and you're staring
at a resource group full of things with no owner tag, no costCenter, no clue.
Every Azure estate drifts here eventually.
Tags are the fix, and they're the first governance job the Cloud Adoption Framework tells you to sort out. Without them you can't attribute cost, find owners, or tell production from a leftover experiment. The catch: asking people to remember to tag things never works.
So don't ask. Audit what's already there, then enforce it as code. Two moves, both runnable this week.
Move 1: find every untagged resource
Azure Resource Graph queries your whole estate in one shot. This finds every resource
missing an owner tag:
az graph query -q "Resources | where isnull(tags.owner) | project name, type, resourceGroup, location"
(Run az extension add --name resource-graph first if you don't have it.)
That's useful on its own. Run it on Monday morning and you have a worklist. But finding them is only half the job: tomorrow someone spins up ten more untagged resources and you're back where you started.
Move 2: enforce it, as code
This is where Azure Policy earns its keep. A policy watches your subscription and
reacts when a resource breaks a rule. The rule here is simple: a resource group with
no owner tag.
Every policy has an effect. Start with audit, which flags the offenders and
blocks nothing. Move to deny, which refuses to create anything without the tag, once
you trust it.
Here's the policy in Terraform, so it lives in a repo, gets reviewed in a pull request, and applies the same way every time:
data "azurerm_subscription" "current" {}
resource "azurerm_policy_definition" "audit_owner_tag" {
name = "audit-missing-owner-tag"
policy_type = "Custom"
mode = "All"
display_name = "Audit resource groups missing an owner tag"
policy_rule = jsonencode({
if = {
allOf = [
{ field = "type", equals = "Microsoft.Resources/subscriptions/resourceGroups" },
{ field = "tags['owner']", exists = "false" }
]
}
then = { effect = "audit" }
})
}
resource "azurerm_subscription_policy_assignment" "audit_owner_tag" {
name = "audit-missing-owner-tag"
subscription_id = data.azurerm_subscription.current.id
policy_definition_id = azurerm_policy_definition.audit_owner_tag.id
display_name = "Audit resource groups missing an owner tag"
}
Apply it:
terraform init && terraform apply
Read it top to bottom. The definition is the rule: if the thing is a resource group
and it has no owner tag, the effect is audit. The assignment switches that rule
on for your subscription. Because it's Terraform, the whole thing is versioned and
repeatable. No portal clicking, and no wondering whether you set it up the same way last
time.
The daily loop
Your routine changes. Instead of chasing tags by hand:
- The policy continuously reports which resource groups are non-compliant (Portal →
Policy → Compliance, or
az policy state summarize). - You remediate the list. Better still, flip the effect to
denyso new resources can't be created untagged in the first place.
A real, repeatable admin task, handed to the platform.
Where it goes from here
This is one tag, one effect, one subscription. At scale it grows into grouped initiatives (reusable policy sets applied across a management-group hierarchy), exemptions for the genuine edge cases, and remediation tasks that fix drift on their own. That's a governance discipline in its own right, and most of what I spend my days on.
You don't need any of that to start. One policy that stops untagged resources appearing is a useful thing to ship this week. It's the CAF and WAF instinct scaled down to something a single admin can run: make the safe path the default, and let the platform hold the line.
Next: turning audit into deny safely, without blocking your whole team on day one.