When starting to use any cost management or reporting solution, one always stumbles on Azure ‘feature’ of non-inhering tags.
All analysis software assume that resources have tags and admins tend to find it easy to set tags on resource groups. Sounds familiar?
Searching the internet I found couple of solutions for this problem. There are scripts that copy tags from resource groups to the resources inside the group. However, some of the scripts that I found didn’t work at all and other had limitations rendering those useless for my use cases. I have three needs:
- Copy and update all resource group level tags to the resources in the group
- Maintain all resource level tags if they are not conflicting with the resource group tags
- Be able to run on Azure Automation
I need to solve this by myself. Couple iterations later I have minimum viable product that fulfills all of the needs. There is still room for speed optimization and better handing of errors. There are some resources that don’t allow writing tags to those and they cause error in current solution (although that error is ignored). Maybe in next version they are detected and skipped.
Prerequisites and installation
Script is written to be run in Azure Automation Account using default Azure Run as Connection “AzureRunAsConnection”. That connection needs permissions to read and write tags in resources. If you don’t want to make state-of-the-art minimum-permission-policy RBAC, contributor role works just fine. Creation of Azure Automation Account creates that Run As Connection and grants Contributor role to the subscription holding the automation account. If you have multiple subscriptions, just add that service principal to other subscriptions as contributor.
Installation is quite simple after prerequisites are met. Just create new PowerShell runbook in Azure Automation, read and understand the code below, paste it in, schedule daily or weekly runs and enjoy the magic.
The Code
# Copy RG tags to Resources, keep existing tags, overwrite if same Tag.
# Mika Vilpo, mika@vilpo.fi
# V1.2 2019-02-07
# Ideas from:
# https://docs.microsoft.com/en-us/azure/azure-resource-manager/resource-group-using-tags
function Add-ResourceGroupTagsToResources()
{
param (
[Parameter(Mandatory = $true)]
[string] $resourceGroupName
)
$group = Get-AzureRmResourceGroup $resourceGroupName
if ($null -ne $group.Tags)
{
$resources = Get-AzureRmResource -ResourceGroupName $group.ResourceGroupName
foreach ($r in $resources)
{
$tagChanges = $false
$resourcetags = (Get-AzureRmResource -ResourceId $r.ResourceId).Tags
if ($resourcetags)
{
foreach ($key in $group.Tags.Keys)
{
if (-not($resourcetags.ContainsKey($key)))
{
Write-Output "ADD: $($r.Name) - $key"
$resourcetags.Add($key, $group.Tags[$key])
$tagChanges = $True
}
else
{
if ($resourcetags[$key] -eq $group.Tags[$key])
{
# Key is up-to-date
}
else
{
Write-Output "UPD: $($r.Name) - $key"
$null = $resourcetags.Remove($key)
$resourcetags.Add($key, $group.Tags[$key])
$tagChanges = $True
}
}
}
$tagsToWrite = $resourcetags
}
else
{
# All tags missing
Write-Output "ADD: $($r.Name) - All tags from RG"
$tagsToWrite = $group.Tags
$tagChanges = $True
}
if ($tagChanges)
{
try
{
$rUPD = Set-AzureRmResource -Tag $tagsToWrite -ResourceId $r.ResourceId -Force -ErrorAction Stop
}
catch
{
# Write-Error "$($r.Name) - $($group.ResourceID) : $_.Exception"
}
}
}
}
else
{
Write-Warning "$resourceGroupName has no tags set."
}
}
$connectionName = "AzureRunAsConnection"
try
{
$servicePrincipalConnection = Get-AutomationConnection -Name $connectionName
Add-AzureRmAccount `
-ServicePrincipal `
-TenantId $servicePrincipalConnection.TenantId `
-ApplicationId $servicePrincipalConnection.ApplicationId `
-CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint
}
catch
{
if (!$servicePrincipalConnection)
{
$ErrorMessage = "Connection $connectionName not found."
Write-Error -Message $ErrorMessage
throw $ErrorMessage
}
else
{
Write-Error -Message $_.Exception
throw $_.Exception
}
}
$subscriptions = Get-AzureRMSubscription
ForEach ($sub in $subscriptions)
{
$subscription = Select-AzureRmSubscription -SubscriptionId $sub.SubscriptionId
Write-Output "Processing $($sub.Name) ($($sub.SubscriptionId))"
$allResourceGroups = Get-AzureRmResourceGroup
ForEach ($resourceGroup in $allResourceGroups)
{
Write-Output "Processing $($resourceGroup.ResourceGroupName) ($($sub.Name))"
Add-ResourceGroupTagsToResources -resourceGroupName $resourceGroup.ResourceGroupName
}
}
Future discussion
I will post later what tags to use and where, how to monitor your tags and tools that I have used for the cost management.
If you happen to make this script better, I’m happy to hear about it and share it with the rest of the world. Send me an email or post modifications into the comments.
1 Comment
Chris · 22.12.2020 at 18.07
Thank you, exactly what is was needing too. Had to change to Az cmdlets though.