Bulk tagging of Azure resources with PowerShell


Recently I came across a situation wherein we need to apply tags (multiple tags) to many resources. When I say many resources it means multiple subscriptions (for example 25 Azure subscriptions) and in each of these subscriptions, we have at least 500+ resources. The requirement becomes complex when we add multiple tags simultaneously. I want to achieve it in such a way that this becomes a configurable and extensible solution that avoids any code change. It should work by updating the configuration.

I wrote two scripts for it. The first one is the Discovery script which discovers all the resources from all the subscriptions. Now once you have discovered all the resources from Azure you can tweak it by adding or deleting the resources and this will become the input for the second script which actually tags the resources. Now let’s understand these scripts:

Azure Resource Discovery script

The discovery script discovers all the resources from all the subscriptions and dumps the info into a CSV File. The name of the spreadsheet contains the current date. Here is the script.

connect-azAccount
$date = Get-Date -UFormat("%m-%d-%y")
$currentDir = $(Get-Location).Path
$oFile = "$($currentDir)\List_Of_All_Azure_Resources_$($date).csv"

if(Test-Path $oFile){
    Remove-Item $oFile -Force
}

"SUBSCRIPTION_NAME,SUBSCRIPTION_ID, RESOURCE_GROUP_NAME,RESOURCE_NAME,RESOURCE_TYPE,TAGS" | Out-File $oFile -Append -Encoding ascii

Get-AzSubscription | ForEach-Object{
    $subscriptionId = $_.Id
    $subscriptionName = $_.Name
    
    Set-AzContext -SubscriptionId $subscriptionId
    Get-AzResourceGroup | ForEach-Object{
        $resourceGroupName = $_.ResourceGroupName
        Get-AzResource -ResourceGroupName $resourceGroupName | ForEach-Object{
            $resourceName = $_.Name
             
            $resourceType = $_.ResourceType
            
            if(!([string]::IsNullOrEmpty($_.Tags))){
                $tags = @()
                $_.Tags.GetEnumerator() |ForEach-Object {
                    [string]$tags += $_.key+ "=" + $_.value+ ";"
                }
            }
            else{
                $tags = ""
            }
            
            "$subscriptionName,$subscriptionId,$resourceGroupName,$resourceName,$resourceType,$tags" | Out-File $oFile -Append -Encoding ascii
        }
    }
}

This script will create the CSV file containing the list of all the resources. Here is the sample:

Azure Resource Tagging script

This script takes the inputs from the CSV file generated by the discovery script and you can add /delete or update the values in the spreadsheet based on your requirement. Once the spreadsheet is ready, you can create a CSV file that contains the list of tags. This script will take this CSV file as another input. Here is the sample tag CSV file:

Another sample tag file:

Here is the script we have used to apply the tags provided in the CSV to the discovered resources from the earlier script.

Connect-AzAccount 
# This can be modified based on your requirement.
$TagFilePath="$($currentDir)\tags.csv"

#You can modify the path here based on your requirements.
$ResourceToTagFilePath="$($currentDir)\List_Of_All_Azure_Resources_$($date).csv"
 
function convertCsvToHashTable($csvFile){
    $csv=import-csv $csvFile
    $headers=$csv[0].psobject.properties.name
    $key=$headers[0]
    $value=$headers[1]
    $hashTable = @{}
    $csv | % {$hashTable[$_."$key"] = $_."$value"}
    return $hashTable
}
 $TagsHashTable=@{}
$TagsHashTable=convertCsvToHashTable $TagFilePath
  $csv = import-csv $ResourceToTagFilePath

$csv | ForEach-Object {
    Write-Host " $($_.RESOURCE_NAME), $($_. RESOURCE_GROUP_NAME), $($_.SUBSCRIPTION_ID ) "
   Set-AzContext $_.SUBSCRIPTION_ID

   $resource = Get-AzResource -Name $_.RESOURCE_NAME -ResourceGroup $_.RESOURCE_GROUP_NAME
   Update-AzTag -ResourceId $resource.id -Tag $TagsHashTable -Operation Merge
 }

Here is how it looks when you applied the tags to the resource.

What if I want to tag all the Resource groups?

Sometimes you have to tag only Resource Groups instead of tagging individual resources inside Resource Groups. There may be certain scenarios where you would like to tag empty Resource Groups. In order to achieve it, I have created a separate Discovery script and tagging script. Here is the Discovery script. First, run this discovery script. It will create the CSV file List_Of_All_Azure_Resources_Today_Date.csv. This CSV file will contain all the subscriptions along with Resource Groups and you can update it.

Here is the discovery script.

connect-azAccount
$date = Get-Date -UFormat("%m-%d-%y")
$currentDir = $(Get-Location).Path
$oFile = "$($currentDir)\List_Of_All_Azure_Resources_$($date).csv"
 
if(Test-Path $oFile){
    Remove-Item $oFile -Force
}
 
"SUBSCRIPTION_NAME,SUBSCRIPTION_ID, RESOURCE_GROUP_NAME" | Out-File $oFile -Append -Encoding ascii
 
Get-AzSubscription | ForEach-Object{
    $subscriptionId = $_.Id
    $subscriptionName = $_.Name
     
    Set-AzContext -SubscriptionId $subscriptionId
    Get-AzResourceGroup | ForEach-Object{
        $resourceGroupName = $_.ResourceGroupName
                    
             
            "$subscriptionName,$subscriptionId,$resourceGroupName" | Out-File $oFile -Append -Encoding ascii
        }
    }

Now once you have the CSV file ready you can run the following tag script to tag the Resource Groups.

Connect-AzAccount 
# This can be modified based on your requirement.
$TagFilePath="$($currentDir)\tags.csv"
 
#You can modify the path here based on your requirements.
$ResourceToTagFilePath="$($currentDir)\List_Of_All_Azure_Resources_$($date).csv"
  
function convertCsvToHashTable($csvFile){
    $csv=import-csv $csvFile
    $headers=$csv[0].psobject.properties.name
    $key=$headers[0]
    $value=$headers[1]
    $hashTable = @{}
    $csv | % {$hashTable[$_."$key"] = $_."$value"}
    return $hashTable
}
 $TagsHashTable=@{}
$TagsHashTable=convertCsvToHashTable $TagFilePath
  $csv = import-csv $ResourceToTagFilePath
 
$csv | ForEach-Object {
    Write-Host "  $($_.RESOURCE_GROUP_NAME), $($_.SUBSCRIPTION_ID ) "
   Set-AzContext $_.SUBSCRIPTION_ID
 
   $resource = Get-AzResourceGroup  -Name  $_.RESOURCE_GROUP_NAME
     

   write-Host "$resource.ResourceId"
   Update-AzTag -ResourceId $resource.ResourceId -Tag $TagsHashTable -Operation Merge
 }

Hope this helps.

31 Comments

Add yours
  1. 2
    Jacques

    Hi Rajaniesh. Thanks for this. This has saved me heaps of time.
    It seems to work about 95% of the time. Certain resources like Application Insights seem to throw this error:
    “Update-AzTag : Cannot convert ‘System.Object[]’ to the type ‘System.String’ required by parameter ‘ResourceId’. Specified method is not supported.
    At line:26 char:29
    + Update-AzTag -ResourceId $resource.id -Tag $TagsHashTable -Operati …
    + ~~~~~~~~~~~~
    + CategoryInfo : InvalidArgument: (:) [Update-AzTag], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgument,Microsoft.Azure.Commands.Tags.Tag.UpdateAzureTagCommand”
    Did you run into this issue?
    Thanks

  2. 5
    mkh

    Hi rajaniesh, Thank you so much for your article and script. I have been tasked to tag many resources! Trying to get this working but bit confused. I have success exporting the existing tag csv file but not sure how to create tags.csv file and what should I modify the exported csv. please help. if possible please share a sample file in the email. Thank you in advance _/\_

  3. 8
    swingpips

    Hi, Thank you so much for the script and the article. I have managed to get this working to export csv file but not sure how to create tags.csv and how to get the tag updated. if you have any sample csv file please share. Thank you so much in advance. Tx

  4. 11
    swingpips

    Excellent. Thank you so much for your help. you are a savior! It worked!! I owe you a drink. please send me your paypal id (you can email it to me). I will sponsor you a drink! Thank you bro again.

  5. 13
    Jacques

    Hi Rajaniesh.

    I think I found the issue. It looks to be when you have resources that were badly named, 2 resources being of different types but having the same name. It seems to occur when it attempts the calling of “Update-AzTag…” on line 26. The root cause seems to be that “$resource = Get-AzResource…” on line 26 seems to pick up multiple resources as an array if there is more than 1 resource of different types but with the same name. When it then tries to pass the array object into “Update-AzTag…” on line 26 it blows up.

    I assume a way to handle this in the script would probably be to output the Resource id’s and then when calling “$resource = Get-AzResource…” on line 26 looking up via resource Id instead of Name.

    Thanks for this post. It was extremely helpful.

    • 14
      js

      Hii Rajneesh, thank you for the article, i was able to export the csv with associated tags, some tag field were blank, so i modified the file with only blank filled tags, then i create tags.csv with key value pair, and instead of Update-AzTag i used New-AzTag unfortunately i end up having an error.
      I also tried to use -ResourceName but of no use.
      Update-AzTag : The term ‘Update-AzTag’ is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of
      the name, or if a path was included, verify that the path is correct and try again.
      New-AzTag : A parameter cannot be found that matches parameter name ‘ResourceName’.
      New-AzTag : A parameter cannot be found that matches parameter name ‘Tag’.
      New-AzTag : A parameter cannot be found that matches parameter name ‘ResourceId’.

  6. 15
    JD

    Hi Rajneesh, Thanks for the script & article, I have exported the List_Of_All_Azure_Resources_01-25-2022.csv with associated tags, i found that many resources were not tagged, in order to apply missing tags i filtered resouces with blank or missing tags, then i created tags.csv file with tags to apply, unfortunately i end up in having error term update-AzTag not found, i also tried to use New-AzTag as shown in line 26
    Update-AzTag -ResourceId $resource.id
    Uate-AzTag -Name $resource.name
    New-AzTag -ResourceId $resource.id
    New-AzTag -Name $resource.name nothing worked for me, any help here much appreciated

  7. 18
    karthick

    Hi Rajaniesh,
    Thanks a lot for supporting. You are really great. The above script is working fine as expected. Keep posting. Thanks again 🙂😇

  8. 22
    Aaryaman Katoch

    Hi Rajaniesh, can we decrease the no. of columns in the discovery csv and keep there just resource group or server name.

  9. 25
    Balaji

    What if we applied tags using azure policy? Will this script will work?

    Requirement: add multiple tags to existing all resources.

    1. Environment tag name – untagged value – update the untagged tag value to prod/non-prod
    2. Application tag name – untagged value – update the untagged tag value to app1
    3. Create a product tag name – add new values

  10. 27
    Surendar

    Hi,
    Script working fine..
    In my case I’m having 50 virtual machine need to update the tag with each their original names
    Resource_name,vm1
    Resource_name,vm2
    Resource_name,vm3
    It’s possible in this script??
    Thanks
    Surendar

  11. 30
    Benjamin

    Hi Rajaniesh
    Thank you for providing this solution as this is one important task faced by most Azure Admins.

    I would like to edit the script for a single subscription. I am working on tagging resource groups per subscription.

    I would appreciate it if you can provided some insights.

    Thank you.

Leave a Reply