Theoretical Foundation: Apply and Manage Tags on Resources
1. Initial Intuitionβ
Imagine you manage a physical datacenter with hundreds of servers. To know which servers belong to the development team, which are for production, which are for the e-commerce project, and which are for ERP, you put physical labels on each server. When the electricity bill arrives, you can calculate how much each project consumed because you know exactly which servers belong to each one.
In Azure, tags are exactly these labels, but digital ones. A tag is a key:value pair that you associate with any Azure resource. They are metadata that you freely define to categorize, organize, and track your resources according to your organization's needs.
Concrete example:
| Key | Value |
|---|---|
| Environment | Production |
| CostCenter | CC-2045 |
| Project | ecommerce-v2 |
| Owner | time-backend |
| CreatedBy | pipeline-devops |
With these five tags on each resource, you can answer questions like: "How much did the ecommerce-v2 project spend this month?" or "Which resources belong to the backend team?" or "Which production resources don't have a defined owner?"
2. Contextβ
Where tags fit in Azure governanceβ
Tags are a fundamental piece of any organization's governance strategy operating in Azure at scale. They are the mechanism that connects technical resources to business context.
Why tags existβ
Azure has no way to know that a specific VM belongs to project X from department Y. This is business information, not technical. Tags are the mechanism by which you inject business context into technical infrastructure.
Without tags, the Azure bill shows cost lines by resource type and subscription, with no correlation to projects, teams, or cost centers. With tags, you get granular cost visibility aligned with your organizational model.
3. Concept Buildingβ
3.1 What is a tag technicallyβ
A tag is a key:value pair stored as metadata in Azure Resource Manager (ARM) associated with a resource. Technically:
Technical limits per resource:
| Limit | Value |
|---|---|
| Tags per resource or Resource Group | 50 |
| Maximum key length | 512 characters |
| Maximum value length | 256 characters |
| Maximum key length (Storage Accounts) | 128 characters |
| Maximum value length (Storage Accounts) | 256 characters |
3.2 Case sensitivityβ
Tags are case-insensitive for keys from the perspective of a single resource, but case-sensitive in filters and queries. This means that Environment and environment are treated as the same key on the same resource, but when filtering resources through the portal or CLI, you need to use the exact capitalization that was used.
The recommended practice is to define a capitalization convention and follow it rigorously. The most common patterns are:
- PascalCase:
CostCenter,ProjectName,Environment - lowercase:
costcenter,projectname,environment - kebab-case:
cost-center,project-name,environment
Choose a pattern and use Azure Policy to enforce it.
3.3 Which resources support tagsβ
Most Azure resources support tags, but not all. Resources that do not support tags include:
- Azure AD (users, groups, service principals)
- Classic resources (old deployment model)
- Some types of sub-resources and system resources
To check if a resource type supports tags, consult the documentation for each resource provider or use:
az provider show --namespace "Microsoft.Compute" \
--query "resourceTypes[?resourceType=='virtualMachines'].capabilities"
3.4 Tags are not automatically inheritedβ
This is the most important and most frequently misunderstood point about tags in Azure:
Tags on a Resource Group are NOT automatically propagated to the resources within it.
If you tag the RG with Environment: Production, the VMs, storage accounts, and other resources within the RG do not receive this tag. Each resource has its own independent set of tags.
Similarly, tags on a Subscription are not propagated to RGs or resources within it.
VM-Web-01 and Key Vault are without tags despite being in a tagged RG. To ensure resources inherit tags from RG, it's necessary to use Azure Policy with Modify effect (covered in the integration section).
4. Structural Viewβ
Decision flow for tagging strategyβ
How tags integrate with other servicesβ
5. Practical Operationβ
Tag lifecycleβ
Important and non-obvious behaviorsβ
Tags in Resource Groups are independent from resources within.
An RG can have the tag Environment: Production and contain resources without any tags, or with different tags. The RG is a management scope, not a container that propagates its properties.
Tags are resource properties in ARM, not within the resource. Tags don't affect resource functionality. A VM with or without tags has identical behavior. Tags exist only in the management plane (ARM), not in the data plane.
Modifying resource tags doesn't cause downtime. Adding, modifying, or removing tags is a resource metadata operation. It doesn't restart services or affect availability.
Tags participate in Azure Resource Graph for complex queries. With consistent tags, you can make powerful queries like "all production resources without owner tag, across all subscriptions":
Resources
| where tags['Environment'] == 'Production'
| where isnull(tags['Owner'])
| project name, type, resourceGroup, subscriptionId
Some services propagate tags to child resources. For example, when creating a VM, Azure may propagate the VM's tags to associated disks and NICs, depending on configuration. But this is specific behavior of some resource providers, not a general rule.
6. Implementation Methodsβ
Azure Portalβ
When to use: point application on individual resources, visual tag review, manual correction
To apply tags on a resource:
- Navigate to the resource
- In the side menu, click Tags
- Type the key and value in the provided fields
- Click Apply
To apply tags on multiple resources via portal:
- Use Azure Resource Manager > Tags in the portal to filter and edit tags at scale (limited, less practical for many resources)
Limitation: doesn't scale, not reproducible, typos create inconsistencies.
Azure CLIβ
# Apply tags on a resource (replaces existing tags with specified set)
az resource tag \
--tags Environment=Production CostCenter=CC-2045 Project=ecommerce-v2 \
--resource-group "rg-producao" \
--name "vm-web-01" \
--resource-type "Microsoft.Compute/virtualMachines"
# Apply tags on a Resource Group
az group update \
--name "rg-producao" \
--tags Environment=Production CostCenter=CC-2045 Owner=time-backend
# IMPORTANT: The command above REPLACES all existing tags.
# To ADD tags without losing existing ones, use --set:
az resource update \
--resource-group "rg-producao" \
--name "vm-web-01" \
--resource-type "Microsoft.Compute/virtualMachines" \
--set tags.NewTag=NewValue
# Remove a specific tag without affecting others
az resource update \
--resource-group "rg-producao" \
--name "vm-web-01" \
--resource-type "Microsoft.Compute/virtualMachines" \
--remove tags.OldTag
# List resources with a specific tag
az resource list \
--tag Environment=Production \
--output table
# List resources WITHOUT a specific tag (requires Resource Graph)
az graph query \
-q "Resources | where isnull(tags['CostCenter']) | project name, type, resourceGroup"
# Apply tags on all resources in an RG (bash script)
for resource_id in $(az resource list --resource-group "rg-producao" --query "[].id" -o tsv); do
az resource tag \
--ids "$resource_id" \
--tags Environment=Production CostCenter=CC-2045
done
Critical attention:
az resource tagandaz group update --tagsreplace the complete set of tags. If you specify only one tag, all others are removed. To add tags while maintaining existing ones, useaz resource update --set tags.NewKey=Value.
Azure PowerShellβ
# Apply tags on a Resource Group (replaces all existing tags)
Set-AzResourceGroup `
-Name "rg-producao" `
-Tag @{
Environment = "Production"
CostCenter = "CC-2045"
Owner = "time-backend"
}
# Apply tags on a specific resource
$resource = Get-AzResource `
-ResourceGroupName "rg-producao" `
-ResourceName "vm-web-01" `
-ResourceType "Microsoft.Compute/virtualMachines"
$resource.Tags["Environment"] = "Production"
$resource.Tags["CostCenter"] = "CC-2045"
Set-AzResource -ResourceId $resource.ResourceId -Tag $resource.Tags -Force
# Add tag without losing existing ones
$resource = Get-AzResource -ResourceId "/subscriptions/<sub>/resourceGroups/rg-prod/providers/Microsoft.Compute/virtualMachines/vm-web-01"
$tags = $resource.Tags
$tags["NewTag"] = "NewValue"
Set-AzResource -ResourceId $resource.ResourceId -Tag $tags -Force
# Apply tags on all resources in an RG preserving existing tags
$resources = Get-AzResource -ResourceGroupName "rg-producao"
foreach ($resource in $resources) {
$existingTags = $resource.Tags ?? @{}
$existingTags["Environment"] = "Production"
$existingTags["CostCenter"] = "CC-2045"
Set-AzResource -ResourceId $resource.ResourceId -Tag $existingTags -Force
}
# List resources with a specific tag
Get-AzResource -Tag @{Environment = "Production"} | Select-Object Name, ResourceType, ResourceGroupName
# Remove a specific tag from a resource
$resource = Get-AzResource -ResourceId "<resource-id>"
$tags = $resource.Tags
$tags.Remove("OldTag")
Set-AzResource -ResourceId $resource.ResourceId -Tag $tags -Force
Bicep and ARM Templatesβ
In IaC, tags should be defined as reusable variables or parameters to ensure consistency:
// Define tags as reusable parameter
param resourceTags object = {
Environment: 'Production'
CostCenter: 'CC-2045'
Project: 'ecommerce-v2'
Owner: 'time-backend'
CreatedBy: 'pipeline-devops'
CreatedDate: '2026-03-24'
}
// Apply to Resource Group
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
name: 'rg-producao'
location: 'brazilsouth'
tags: resourceTags
}
// Apply to VM
resource vm 'Microsoft.Compute/virtualMachines@2023-09-01' = {
name: 'vm-web-01'
location: resourceGroup().location
tags: union(resourceTags, {
Role: 'web-server' // additional tag specific to this VM
})
properties: {
// ... VM configurations
}
}
The union() function in Bicep merges two tag objects, allowing each resource to inherit common tags and add its own.
Terraformβ
# Common tags variable (defined once, reused across all resources)
locals {
common_tags = {
Environment = "Production"
CostCenter = "CC-2045"
Project = "ecommerce-v2"
Owner = "time-backend"
ManagedBy = "terraform"
}
}
# Resource Group with tags
resource "azurerm_resource_group" "prod" {
name = "rg-producao"
location = "brazilsouth"
tags = local.common_tags
}
# VM with common tags + specific tags
resource "azurerm_linux_virtual_machine" "web" {
name = "vm-web-01"
resource_group_name = azurerm_resource_group.prod.name
location = azurerm_resource_group.prod.location
tags = merge(local.common_tags, {
Role = "web-server"
Tier = "frontend"
})
# ... rest of configurations
}
Using locals for common tags and merge() to combine with specific tags is the Terraform pattern for tag management at scale.
7. Control and Securityβ
Tag enforcement with Azure Policyβ
Tags only have value if they are applied consistently. Azure Policy is the mechanism to ensure this:
1. Policy to require specific tag (Deny or Audit):
Built-in policy: "Require a tag on resources"
- Deny effect: blocks resource creation without the mandatory tag
- Audit effect: logs resources without the tag as non-compliant, without blocking
2. Policy to inherit tag from Resource Group (Modify):
Built-in policy: "Inherit a tag from the resource group if missing"
This policy uses Modify effect to automatically copy a tag from RG to resource, if the tag is missing on the resource. This is the solution for "tags are not automatically inherited".
3. Policy for controlled values (Deny with allowedValues):
Custom policy that restricts accepted values for a tag:
{
"mode": "Indexed",
"policyRule": {
"if": {
"allOf": [
{
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
{
"not": {
"field": "tags['Environment']",
"in": ["Production", "Staging", "Development", "Testing"]
}
}
]
},
"then": {
"effect": "Deny"
}
}
}
This policy ensures that the Environment tag on VMs can only have defined values. Any other value is blocked.
Who can modify tagsβ
Tags are resource properties in ARM. To modify tags on a resource, you need write permission on that resource. This means that:
- Contributor can add, modify, and remove resource tags
- Reader cannot modify tags
- A tag cannot be used as access control; it's just metadata
To restrict who can modify tags specifically, without restricting other operations, would require a Custom Role that only allows */read and */write for tags, which is complex and rarely necessary.
8. Decision Makingβ
Tag taxonomy definitionβ
| Tag category | Key examples | Purpose | | Financial | CostCenter, BudgetCode, BusinessUnit | Chargeback, cost allocation | | Operational | Environment, Tier, Role, Criticality | Operational management, automation | | Owner | Owner, Team, Contact | Responsibility, onCall | | Lifecycle | CreatedDate, ExpiresOn, CreatedBy | Governance, automated cleanup | | Project | Project, Application, Version | Investment traceability | | Compliance | DataClassification, Regulation | Security, legal requirements |
When to use each application approachβ
| Situation | Approach | Reason |
|---|---|---|
| New environment being provisioned | IaC (Bicep/Terraform) with tags as variables | Ensure consistency from the start |
| Existing resources without tags | PowerShell/CLI mass script + Policy for new ones | Scale remediation + future prevention |
| Mandatory tags across organization | Azure Policy with Deny on Management Group | Centralized enforcement, doesn't depend on human discipline |
| Inherit tags from RG automatically | Azure Policy with Modify | Official solution for tag propagation |
| Tag with controlled values | Custom policy with allowedValues | Avoid inconsistent values like "prod", "Prod", "PROD" |
| Tag compliance audit | Azure Resource Graph + Policy Compliance | Scalable queries across organization |
9. Best Practicesβ
Define taxonomy before starting to tag. Decide which tags are mandatory, which are optional, which values are accepted, and what the capitalization convention is before starting. Changing taxonomy after thousands of resources are already tagged is extremely laborious.
Start with few mandatory tags and add gradually.
A set of 3 to 5 mandatory tags that the entire organization can fill correctly is much better than 20 tags where half remain empty or incorrect. Recommended tags to start: Environment, CostCenter, Owner, Project.
Use Policy with Deny for mandatory tags in production. In mature production environments, mandatory tags should be enforced with Deny. In new or transitional environments, start with Audit to understand the impact before blocking.
Use IaC as source of truth for tags. Tags should be defined in Terraform or Bicep templates, not applied manually in the portal. When a resource is recreated or redeployed, tags are automatically restored.
Implement tag inheritance from RG via Policy. Apply the built-in policy "Inherit a tag from the resource group if missing" for the most important tags. This way, if the RG has the correct tag, any resource created within it automatically receives it.
Use tags for lifecycle automation.
Tags like ExpiresOn: 2026-06-30 can be used by Azure Automation runbooks to identify and shut down (or alert about) resources that have passed their expiration date.
Never use tags for security control. Tags are metadata, not access controls. Don't implement security logic that depends on tags (like "resources with Confidential tag have restricted access"). Use RBAC and Azure Policy for security control.
10. Common Mistakesβ
| Mistake | Why it happens | How to avoid |
|---|---|---|
| Assuming RG tags propagate to resources | Expected behavior that doesn't exist in Azure | Use Policy with Modify for explicit inheritance |
Using az resource tag and losing existing tags | The command replaces all tags | Use az resource update --set tags.Key=Value to add without losing |
| Capitalization inconsistency: "prod", "Prod", "PROD" | Lack of defined and enforced standard | Define standard + Policy with allowedValues |
| Tags only on RG, not on individual resources | Mistaken belief in automatic inheritance | Apply tags to each resource, or use inheritance Policy |
| Using tags as only organization mechanism without proper RGs | Tags don't replace scope structure | Use RGs for isolation + tags for metadata |
| Not including tags in IaC templates | Rush, considered minor detail | Include common tag variable in all templates |
| Creating tags with many possible values without control | No allowedValues Policy | Define enum of accepted values and enforce via Policy |
| Tags with sensitive information like passwords or tokens | Tags are visible to any Reader | Never put credentials in tags; use Key Vault |
The most common mistake at scaleβ
In organizations with multiple teams, each team creates their own tags with different names and values to represent the same concept. Result: Env, env, Environment, ENVIRONMENT, environment, Ambiente all coexisting, making cost reports by environment impossible without massive cleanup.
The solution is to define a tag taxonomy document as official reference and enforce via Policy before proliferation begins.
11. Operation and Maintenanceβ
Query resources by tag in daily operationsβ
# List all resources with a specific tag
az resource list --tag Environment=Production --output table
# Count resources by tag value (how many per environment)
az resource list \
--query "[].tags.Environment" \
--output tsv | sort | uniq -c
# Resources without mandatory tag (via Resource Graph)
az graph query -q "
Resources
| where isnull(tags['CostCenter']) or tags['CostCenter'] == ''
| project name, type, resourceGroup, subscriptionId
| order by type asc"
# All unique values of a tag (to detect inconsistencies)
az graph query -q "
Resources
| where isnotnull(tags['Environment'])
| summarize count() by tostring(tags['Environment'])
| order by count_ desc"
Cost reports by tag in Cost Managementβ
- Portal > Cost Management + Billing > Cost analysis
- In Group by, select Tag and choose the desired key (ex: CostCenter)
- The report shows cost grouped by tag value
Attention: resources without the tag appear in a separate category (usually "untagged" or with empty value). This is a visual indication of tagging non-compliance.
Automatic cost report exports by tagβ
Configure automatic exports in Cost Management to a Storage Account, with tag grouping, generating monthly CSV files that can be automatically processed for chargeback.
Monitor tag compliance via Policyβ
The Policy > Compliance dashboard shows the percentage of compliant resources for each tag policy. This allows tracking tag coverage evolution over time.
Limits to monitorβ
| Limit | Value | Impact if reached |
|---|---|---|
| Tags per resource | 50 | Cannot add more tags; review and remove unnecessary tags |
| Key length | 512 characters | Error when trying to create tag with too long key |
| Value length | 256 characters | Error when trying to create tag with too long value |
The 50 tags per resource limit is rarely reached with well-designed taxonomy. If you're approaching this limit, it's a sign that taxonomy needs to be reviewed and simplified.
12. Integration and Automationβ
Tag-based automation with Azure Automationβ
A powerful pattern is using tags to control automatic resource behaviors:
Example PowerShell runbook that shuts down VMs based on tag:
# Runbook: Auto-Shutdown based on tag
$vms = Get-AzResource `
-ResourceType "Microsoft.Compute/virtualMachines" `
-Tag @{AutoShutdown = "true"}
$currentHour = (Get-Date).ToString("HH:mm")
foreach ($vm in $vms) {
$shutdownTime = $vm.Tags["ShutdownTime"]
if ($shutdownTime -eq $currentHour) {
Write-Output "Shutting down $($vm.Name)..."
Stop-AzVM -ResourceGroupName $vm.ResourceGroupName -Name $vm.Name -Force
}
}
Integration with Azure Cost Management for chargebackβ
Integration with Azure Monitor for tag-segmented alertsβ
It's possible to create alerts that filter by tag to notify only the team responsible for a set of resources:
# Create action group for backend team
az monitor action-group create \
--name "ag-backend-team" \
--resource-group "rg-monitoring" \
--short-name "backend" \
--email backend-team@company.com
# Create high CPU alert only for VMs with tag Owner=backend-team
az monitor metrics alert create \
--name "high-cpu-backend-vms" \
--resource-group "rg-monitoring" \
--scopes "/subscriptions/<sub-id>/resourceGroups/rg-prod" \
--condition "avg Percentage CPU > 90" \
--action "ag-backend-team" \
--description "High CPU on backend team VMs"
Automated cleanup of resources with expiration tagβ
# Runbook: Identify and report expired resources
$today = Get-Date
$expiredResources = Get-AzResource | Where-Object {
$_.Tags.ContainsKey("ExpiresOn") -and
[DateTime]::Parse($_.Tags["ExpiresOn"]) -lt $today
}
foreach ($resource in $expiredResources) {
Write-Output "EXPIRED RESOURCE: $($resource.Name) - Expired on: $($resource.Tags['ExpiresOn'])"
# Optional: Send-MailMessage or New-AzMonitorAlertRule
# Optional: Remove-AzResource -ResourceId $resource.ResourceId -Force
}
13. Final Summaryβ
Essential points:
- Tags are key:value pairs associated with resources as metadata in ARM
- No automatic inheritance of tags: RG tags don't propagate to resources within
- Limit is 50 tags per resource, with keys up to 512 characters and values up to 256
- Tags exist only in the management plane (ARM) and don't affect resource functionality
- To ensure RG tag inheritance, use Azure Policy with Modify effect ("Inherit tag from resource group if missing")
Critical differences:
az resource tagreplaces the entire tag set;az resource update --set tags.Key=Valueadds without losing existing ones- RG vs. resource tags: RG and its resources have completely independent tag sets
- Policy Deny vs. Modify for tags: Deny blocks resources without tag; Modify adds the tag automatically. Both are complementary
- Audit vs. Deny for enforcement: Audit logs non-compliance without blocking; Deny blocks at creation
What needs to be remembered for AZ-104:
- Tags are not inherited automatically from parent to child scopes
- Replacement vs. merge: attention when using commands that replace all tags versus those that add incrementally
- Azure Resource Graph is the tool for complex queries of resources by tags at scale
- Tags appear in Cost Management as grouping dimension for cost reports
- The built-in policy "Inherit a tag from the resource group if missing" uses Modify effect and is the official solution for tag propagation
- Tags can be used in automation (Runbooks, Logic Apps) to control resource behaviors
- Tag keys are case-insensitive on the same resource but case-sensitive in external filters and queries