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