Cascading Resource Group Tags in Azure

Resource Manager Policies in Azure are the way to define and enforce a tagging system.
You can define in a json format rules that must be adhered to for new resources that are deployed.
eg.

{
  "if": {
    "allOf": [
      {
        "field": "tags",
        "exists": "true"
      },
      {
        "field": "tags.costCenter",
        "exists": "false"
      }
    ]

  },
  "then": {
    "effect": "append",
    "details": [
      {
        "field": "tags.costCenter",
        "value": "myDepartment"
      }
    ]
  }
}

For resources that you’ve already created, you’ll need to decide on the appropriate strategy. One that I’ve recently put together is a script that cascades the tags you define at the Resource Group level down to the individual resources (VM’s, vNETs, etc etc).

It doesn’t override any of the existing tags that a resource has, simply ensuring that each of the resources has at a minimum the tags that are defined at the Resource Group level.

This version isn’t optimised for running on a schedule in Azure Automation as it’s not a powershell workflow so doesn’t parallelise the foreach loops.

#Gordon.byers@microsoft.com
#Powershell script is provided as-is and without any warranty of any kind

function Add-ResourceGroupTagsToResources() 
{
    #V1.0 2016-06-29
    param (
        [Parameter(Mandatory=$true)]
        [string] $resourceGroupName
    )
    
    $taggedResourceGroups = $null

    $resourceGroup =  Get-AzureRmResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue

    if($resourceGroup -eq $null) {
        Write-Error "Resource Group : $resourceGroupName not found in subscription"
        return;
    }

    $taggedResourceGroups = $resourceGroup | where-object {$_.Tags.count -gt 0} 

    if ($taggedResourceGroups -eq $null)
    {
        Write-Warning "$resourceGroupName found - no tags defined to add to resources"
    }
    else {
        Write-Host "Finding Resources to tag for resourcegroup : $($resourceGroup.ResourceGroupName)"
        $rgTagsTable = $resourceGroup.TagsTable
        $rgTagCount = $resourceGroup.Tags.count
        $rgTagKeys = @()
        $resourceGroup.Tags | %{$rgTagKeys += $_["Name"]}

        $resoucesToTag = Get-AzureRmResource -ResourceGroupName $resourceGroup.ResourceGroupName -ResourceName " "

        ForEach($resource in $resoucesToTag) 
        { 
            If($resource.Tags.Count -eq 0) 
            {
                Write-Host "Resource $($resource.Name)  has no tags, adding full set of $rgTagCount tags"
                Set-AzureRmResource -ResourceId $resource.ResourceId -Tag $resourceGroup.Tags -Force
            }
            else {
                Write-Verbose "Resource $($resource.Name) ($($resource.ResourceType)) has existing tags found"
                
                $resourceTagKeys = @()
                ForEach($tag in $resource.tags)
                {
                    $resourceTagKeys += $tag["Name"]
                }

                $extraTags = Compare-Object -ReferenceObject $rgTagKeys -DifferenceObject $resourceTagKeys

                if($extraTags -eq $null)
                {
                    Write-Host "Resource $($resource.Name) ($($resource.ResourceType)) tags are up to date"
                }
                else {
                    Write-Verbose "Merging tags for $($resource.Name) ($($resource.ResourceType))"

                    $tagKeysToAdd = $($extraTags | Where-Object { $_.SideIndicator -eq "<="})

                    if ($tagKeysToAdd -eq $null) {
                        $resourceSpecificTags = $($extraTags | Select-Object -ExpandProperty InputObject) -join ', '
                        Write-Host "Resource $($resource.Name) ($($resource.ResourceType)) has extra tags from its Resource Group ($resourceSpecificTags). inherited tags are up to date"
                    }
                    else {
                        $tagsToUpdate = $resource.tags 

                        ForEach ($tagKeyToAdd in $tagKeysToAdd) {
                            Write-Verbose "For $($resource.Name) ($($resource.ResourceType)) tag $($tagKeyToAdd.InputObject) is missing."
                            $value = $($rg.Tags | Where-Object {$_["Name"] -eq $tagKeyToAdd.InputObject})["Value"]
                   
                            $tagsToUpdate += @{Name=$tagKeyToAdd.InputObject;Value=$value} 
                        }

                        Write-Host "Resource $($resource.Name) ($($resource.ResourceType)) : updating extra tags"
                        Set-AzureRmResource -ResourceId $resource.ResourceId -Tag $tagsToUpdate -Force
                    }
                }

            }

        }
    }
}

Login-AzureRmAccount

$allResourceGroups = Get-AzureRmResourceGroup

ForEach ($resourceGroup in $allResourceGroups) {
    Add-ResourceGroupTagsToResources -resourceGroupName $resourceGroup.ResourceGroupName
}

For the latest version, use the GitHub link.
https://github.com/Gordonby/PowershellSnippets/blob/master/Add-ResourceGroupTagsToResources.ps1

3 Replies to “Cascading Resource Group Tags in Azure”

  1. Hey Gordon, lovely script but I’ve noticed an issue with it and was hoping perhaps you had some insight. It appears that the compare-object on line 52 is failing, as it’s receiving $rgTagKeys as being Null. As a result, your script is applying tags to a resource that starts with no tags just fine, but it is failing to append tags that exist on the RG but do not exist on the resource itself. Any ideas on a quick fix?

  2. Hi Gordon, Can you please share a script to update tags & values to all resources groups without overwriting existing tags.

Leave a Reply

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