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.


Mika Vilpo

I started my current employment as an Identity and Access Management Consultant building challenging automated solutions and have recently shifted more to public cloud as there the future of identity and automation lies. I have seen Office 365 evolve from Live@Edu to current strengths and love to see how to story evolves. Currently I am building customers' architecture and solutions based on Microsoft Azure PaaS and IaaS.

1 Comment

Chris · 22.12.2020 at 18.07

Thank you, exactly what is was needing too. Had to change to Az cmdlets though.

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.