Introduction

It’s been a while since I’ve blogged about naming conventions. For most it’s not an interesting topic, and for many, it’s the last thing on their mind. While I would never say I get joy out of creating a good naming convention, I do get a sense of satisfaction and pride. For this blog post, I want to document how I’ve tackled getting consistent naming out of an inconsistent Azure location name.

The problem

Azure’s location naming scheme leaves a bit to be desired. It looks like they kind of changed their scheme part of the way through. Or maybe they thought they were making it easier to understand. Honestly, I’m a little lost on why they picked what they picked, and why they shifted directions.

Let’s run this PowerShell commands below.

$AllMicrosoftAzureLocations = Get-AzLocation | Where-Object {$PSItem.RegionType -eq "Physical"}
$AllMicrosoftAzureLocations | Sort-Object -Property Location | Select-Object -Property Location

Your output should looks something like this. And depending on when you’re reading this article, there may be more locations.

australiacentral
australiacentral2
australiaeast
australiasoutheast
brazilsouth
brazilsoutheast
canadacentral
canadaeast
centralindia
centralus
centraluseuap
eastasia
eastus
eastus2
eastus2euap
francecentral
francesouth
germanynorth
germanywestcentral
japaneast
japanwest
koreacentral
koreasouth
northcentralus
northeurope
norwayeast
norwaywest
qatarcentral
southafricanorth
southafricawest
southcentralus
southeastasia
southindia
swedencentral
switzerlandnorth
switzerlandwest
uaecentral
uaenorth
uksouth
ukwest
westcentralus
westeurope
westindia
westus
westus2
westus3

I see three different naming patterns, how many do you see?

Example 1, Cardinal Direction + Country + Region Number (or not)

Here are a few examples of this naming scheme

eastus
westus3
southindia

Example 2, Cardinal Direction + Continent + Region Number (or not)

eastasia
westeurope

Example 3, Country + Cardinal Direction + Region Number (or not)

southafricanorth
brazilsouth
uaenorth

What is the problem with this

For me, a naming scheme should be first and foremost consistent. Bonus points if it’s intuitive. In this case, I don’t find it to be either. You could argue the names are self explanatory from a human eye, but programmatically speaking, it’s inconsistent.

Think of it like this, if you were going to write a scripted (automated) process to read the location, and dynamically make a unique location name from the data above could you?

Another problem to consider is the length of the location names. Azure has a number of resources that have very short naming requirements. In a case like that, it would be problematic to use the full location name, regardless of it’s consistency. Just to give you an example, the longest location name is 18 characters long.

ISO3166

If you are not familiar with ISO3166, it is a naming scheme for countries and their subdivisions. This standard ensures there is a unique identifier for each country and in turn each countries subdivisions. There are two useful (for us) versions of the ISO3166.

ISO3166-1

This standard contains the following three basic properties.

  1. Numeric: This is a unique number assigned to a country
  2. Alpha 2: This an abbreviated two character identifier for each country in the world.
  3. Alpha 3: Similar to above, but allowing one more character.

ISO3166-2

This standard contains unique identifiers for localites within a given country. In the US, that would be states. The name is comprised of the alpha 2 of the country, and then a unique two digit alpha for the given locality in that country.

Hope is not lost

While Microsoft has made it difficult to get this information, they have provided a way for us to hunt it down ourselves. Let’s take a look at the results of a Get-AzLocation.

Location         : eastus
DisplayName      : East US
Type             : Region
Longitude        : -79.8164
Latitude         : 37.3719
PhysicalLocation : Virginia
RegionType       : Physical
RegionCategory   : Recommended
GeographyGroup   : US
PairedRegion     : [
                     {
                       "Name": "westus",
                       "Id": "/subscriptions/REDACTED GUID/locations/westus",
                       "SubscriptionId": null
                     }
                   ]
Providers        : {microsoft.insights, Microsoft.OperationsManagement, Microsoft.ContainerRegistry, Microsoft.Cache…}

Do you see the two interesting data points I do? It’s not anything that’s name related. If you haven’t guessed it, it’s the Latitude and Longitude. Check out this link here and we can see the exact location of where the EastUS data center is located.

What can we do with GPS

What we need, is a geolocation based API. The first place I checked was Google Maps. And while the API does exist, I had zero desire to enter a credit card in to tap their database. I gave Bing Maps API a try, but didn’t really like the information I was getting out of them (no surprise). I stumbled on this slick host called Position Stack, located (here)[https://positionstack.com/documentation].

Other locational information

While I was on the hunt for ISO3166 and GPS based data, I grabbed one more dataset from IP2Location.

SCRIPT

Requirements

  1. You will need the file “AllLocationInformationJoinedHashTable.json” located here in my Gists repo.
    1. Place this file in the same path as this script below.
  2. You will need an access key of your own for PositionStack. At this point in time, you can sign up here.
    1. The credential object is looking for just the access key. You can enter whatever you want for the username, the password is where the access key goes.
  3. You will need the AzModules and you will need to be connected to Azure.

allMicrosoftAzureLocations.ps1

[CmdletBinding()]
param 
    (
    [Parameter(Mandatory = $true)]
    [System.Management.Automation.PSCredential]$Credential,

    [Parameter(Mandatory = $true)]
    [string]$ExportFilePath
    )

##################################################
#START: Variables

$basGeoURL = $null
$basGeoURL = "http://api.positionstack.com/v1"

$regexPatternAllCardinalDirections = $null
$regexPatternAllCardinalDirections =  'north|east|south|west|central'

$allMicrosoftAzureLocationsEnhanced = $null
$allMicrosoftAzureLocationsEnhanced = [ordered]@{}

$AccessKey = $Credential.GetNetworkCredential().Password

#END: Variables
##################################################

##################################################
#START: Import additional location information

$AllAdditionalLocationInformation = $null
$AllAdditionalLocationInformation = Get-Content -Path '.\AllLocationInformationJoinedHashTable.json' | ConvertFrom-Json -Depth 100 -AsHashtable

#START: Import additional location information
##################################################

##################################################
#START: All Az Locations

$allAzLocations = $null
$allAzLocations = Get-AzLocation | Where-Object {$PSItem.RegionType -eq "Physical"}

#START: All Az Locations
##################################################

##################################################
#START: Reverse Lookup

foreach ($azLocation in $allAzLocations)
    {

    #If you want to explore data of a particular location, uncomment and adjust accordingly.
    <#
    If ($azLocation.DisplayName -like "")
        {
        #;break
        }
    #>
    Write-Host "Working on location $($azLocation.Location)"
    
    ##################################################
    #START: Reverse Lookup
    $ReverseLookup = $null
    $ReverseLookup = Invoke-RestMethod -Uri "$($basGeoURL)/reverse?access_key=$($AccessKey)&query=$($azLocation.Latitude),$($azLocation.Longitude)&limit=1&timezone_module=1" | Select-Object -ExpandProperty Data
    
    #If you want to see all objects within the vicinity
    #$ReverseLookup = Invoke-RestMethod -Uri "$($basGeoURL)/reverse?access_key=$($AccessKey)&query=$($azLocation.Latitude),$($azLocation.Longitude)&timezone_module=1" | Select-Object -ExpandProperty Data
    #START: Reverse Lookup
    ##################################################

    ##################################################
    #START: Analyze if country name has cardinal directions in name
    
    $AllCountryCardinalDirectionMatches = $null
    $AllCountryCardinalDirectionMatches = $ReverseLookup.country | Select-String -Pattern $regexPatternAllCardinalDirections -AllMatches | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty value

    $AllCountryCardinalDirectionMatchesCount = 0
    $AllCountryCardinalDirectionMatchesCount = $AllCountryCardinalDirectionMatches | Measure-Object | Select-Object -ExpandProperty Count
    #START: Analyze if country name has cardinal directions in name
    ##################################################

    ##################################################
    #START: Determine Cardinal Direction in Azure
    
    $allAzureLocationCardinalDirectionsPreCompare = $null
    $allAzureLocationCardinalDirectionsPreCompare = $azLocation.Location | Select-String -Pattern $regexPatternAllCardinalDirections -AllMatches | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty value
    
    $allAzureLocationCardinalDirectionsPreCompareCount = $null
    $allAzureLocationCardinalDirectionsPreCompareCount = $allAzureLocationCardinalDirectionsPreCompare | Measure-Object | Select-Object -ExpandProperty Count
    #END: Determine Cardinal Direction in Azure
    ##################################################

    ##################################################
    #START: If country name has a cardinal direction, exclude direction from analysis
    $allAzureLocationCardinalDirections = $null
    If ($AllCountryCardinalDirectionMatchesCount -ge 1 -and $allAzureLocationCardinalDirectionsPreCompareCount -ge 2)
        {
        $CompareCountryNameToAzureLocation = $null
        $CompareCountryNameToAzureLocation = Compare-Object -ReferenceObject $AllCountryCardinalDirectionMatches -DifferenceObject $allAzureLocationCardinalDirectionsPreCompare
        
        $allAzureLocationCardinalDirections = $CompareCountryNameToAzureLocation | Where-Object {$_.SideIndicator -eq '=>'} | Select-Object -ExpandProperty InputObject
        }
    else 
        {
        $allAzureLocationCardinalDirections = $allAzureLocationCardinalDirectionsPreCompare
        }

    #END: If country name has a cardinal direction, exclude direction from analysis
    ##################################################

    ##################################################
    #START: Azure Location Cardinal Direction
    If ($null -ne (Get-Variable -Name 'cardinalDirectionPrimary' -ErrorAction SilentlyContinue))
        {
        Remove-Variable -Name cardinalDirectionPrimary -Force -Confirm:$false
        $cardinalDirectionPrimary = $null
        }
    Else 
        {
        $cardinalDirectionPrimary = $null
        }

    If ($null -ne (Get-Variable -Name 'cardinalDirectionSecondary' -ErrorAction SilentlyContinue))
        {
        Remove-Variable -Name cardinalDirectionSecondary -Force -Confirm:$false
        $cardinalDirectionSecondary = $null
        }
    else 
        {
        $cardinalDirectionSecondary = $null
        }
    
    :SwitchAllAzureLocationCardinalDirectionsCount switch ($allAzureLocationCardinalDirections.Count)
        {
        {$PSItem -eq 0} {
            Write-Warning "No cardinal direction found"
            break SwitchAllAzureLocationCardinalDirectionsCount
            }
        {$PSItem -eq 1} {
            $cardinalDirectionPrimary = $allAzureLocationCardinalDirections | Select-Object -First 1
            $cardinalDirectionSecondary = $allAzureLocationCardinalDirections | Select-Object -First 1
            break SwitchAllAzureLocationCardinalDirectionsCount
            }
        {$PSItem -eq 2} {
            $cardinalDirectionPrimary = $allAzureLocationCardinalDirections | Select-Object -First 1
            $cardinalDirectionSecondary = $allAzureLocationCardinalDirections | Select-Object -Skip 1 -First 1
            break SwitchAllAzureLocationCardinalDirectionsCount
            }
        {$PSItem -gt 2} {
            Throw "More than 2 matches"
            }
        }

    #START: Azure Location Cardinal Direction
    ##################################################

    ##################################################
    #START: Determine datacenter Region Number
    
    $AzureLocationNumberExists = $null
    $AzureLocationNumberExists = $azLocation.Location -match '\d+'

    $AzureLocationNumber = $null
    If ($AzureLocationNumberExists -eq $true)
        {
        $AzureLocationNumber = $azLocation.Location | Select-String -Pattern "\d+" | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty value
        }
    else 
        {
        $AzureLocationNumber = 0
        }


    #START: Determine Region Number
    ##################################################

    ##################################################
    #START:Find additional location info
    
    $LocationInfo = $null
    $LocationInfo = $AllAdditionalLocationInformation.$($ReverseLookup.country_code.ToLower())

    $LocationInfoSubDivisions = $null
    $LocationInfoSubDivisions = $LocationInfo.allSubdivisions.keys | ForEach-Object {[pscustomobject]$LocationInfo.allSubdivisions.$PSItem}

    try 
        {
        $ISO3166TwoCode = $null
        $ISO3166TwoCode = If (([string]::IsNullOrWhiteSpace($ReverseLookup.region_code) -eq $false))
            {
            "$($LocationInfo.'iso3166-1_alpha2'.ToLower())-$($ReverseLookup.region_code.ToLower())"
            }
        
        
        $LocationSubDivision = $null
        $LocationSubDivision = $LocationInfoSubDivisions | Where-Object {
            $PSItem.'iso3166-2_name' -eq $ReverseLookup.region -or 
            $PSItem.'iso3166-2_name' -eq $ReverseLookup.locality -or 
            $PSItem.'iso3166-2_name' -eq $ReverseLookup.county -or 
            $PSItem.'iso3166-2_name' -eq $ReverseLookup.neighbourhood -or 
            $PSItem.'iso3166-2_name' -eq $azLocation.PhysicalLocation } | Select-Object -First 1

        #Only using as last resort
        If ($null -eq $LocationSubDivision)
            {
            $LocationSubDivision = $LocationInfoSubDivisions | Where-Object {$PSItem.'iso3166-2_code' -eq $ISO3166TwoCode}
            }
    
        }
    catch 
        {
        Throw $($_.Exception.Message)
        }
    

    if ($null -eq $LocationSubDivision) {Write-Warning -Message "Could not find subdivision for $($azLocation.Location)"}

    #START: Find additional location info
    ##################################################

    ##################################################
    #START: Create the object
    
    $microsoftAzureLocationHashTable = $null
    $microsoftAzureLocationHashTable = [ordered]@{
        azureCardinalDirectionPrimary = $cardinalDirectionPrimary
        azureCardinalDirectionSecondary = $cardinalDirectionSecondary
        azureDisplayName = $azLocation.DisplayName
        azureGeographyGroup = $azLocation.GeographyGroup
        azureLatitude = $azLocation.Latitude
        azureLocation = $azLocation.Location
        azureLocationNumber = $AzureLocationNumber
        azureLongitude = $azLocation.Longitude
        azurePairedRegion = $azLocation.PairedRegion.name
        azurePhysicalLocation = $azLocation.PhysicalLocation
        azureRegionType = $azLocation.RegionType
        reverseLookupContinent = $ReverseLookup.continent
        reverseLookupCountry = $ReverseLookup.country
        reverseLookupCountryCode = $ReverseLookup.country_code

        locationInfoISO3166OneAlpha2 =  $LocationInfo.'iso3166-1_alpha2'
        locationInfoISO3166OneAlpha3 =  $LocationInfo.'iso3166-1_alpha3'
        locationInfoISO3166OneName =  $LocationInfo.'iso3166-1_name'
        locationInfoISO3166OneNumeric =  $LocationInfo.'iso3166-1_numeric'
        locationInfoISO3166TwoCode = $LocationSubDivision.'iso3166-2_code'
        locationInfoISO3166TwoName = $LocationSubDivision.'iso3166-2_Name'
        locationInfoISO3166TwoType = $LocationSubDivision.'iso3166-2_Type'
        locationInfoCapital =  $LocationInfo.capital

        reverseLookupLabel = $ReverseLookup.label
        reverseLookupLocality = $ReverseLookup.locality
        reverseLookupName = $ReverseLookup.name
        reverseLookupNeighborhood = $ReverseLookup.neighbourhood
        reverseLookupPostalCode = $ReverseLookup.postal_code
        reverseLookupRegion = $ReverseLookup.region
        reverseLookupRegionCode = $ReverseLookup.region_code
        reverseLookupTimeZoneName = $ReverseLookup.timezone_module.name
        reverseLookupTimeZoneOffsetSec = $ReverseLookup.timezone_module.offset_sec
        reverseLookupTimeZoneOffsetString = $ReverseLookup.timezone_module.offset_string
        }
    $allMicrosoftAzureLocationsEnhanced.add($microsoftAzureLocationHashTable.azureLocation, $microsoftAzureLocationHashTable)

    #END: Create the object
    ##################################################
    }

#END: Reverse Lookup
##################################################

##################################################
#START: Output
$allMicrosoftAzureLocationsEnhanced | ConvertTo-Json -Depth 100 | Out-File -FilePath $ExportFilePath

#END: Output
##################################################

Output Example (EastUS)

azureCardinalDirectionPrimary  east
azureCardinalDirectionSeconda… east
azureDisplayName               East US
azureGeographyGroup            US
azureLatitude                  37.3719
azureLocation                  eastus
azureLocationNumber            0
azureLongitude                 -79.8164
azurePairedRegion              westus
azurePhysicalLocation          Virginia
azureRegionType                Physical
reverseLookupContinent         North America
reverseLookupCountry           United States
reverseLookupCountryCode       USA
locationInfoISO3166OneAlpha2   us
locationInfoISO3166OneAlpha3   usa
locationInfoISO3166OneName     united states
locationInfoISO3166OneNumeric  840
locationInfoISO3166TwoCode     us-va
locationInfoISO3166TwoName     Virginia
locationInfoISO3166TwoType     State
locationInfoCapital            Washington, D.C.
reverseLookupLabel             5 Silverbirch Dr, Blue Ridge, VA, USA
reverseLookupLocality          Blue Ridge
reverseLookupName              5 Silverbirch Dr
reverseLookupNeighborhood
reverseLookupPostalCode        24064
reverseLookupRegion            Virginia
reverseLookupRegionCode        VA
reverseLookupTimeZoneName      America/Kentucky/Monticello
reverseLookupTimeZoneOffsetSec -14400
reverseLookupTimeZoneOffsetSt… -04:00

Explanation of output

I could walk through the script, but that’s not really what this post is about. The script is there for your use, and you can do with it as you want. However, the output of the script, that is what this post is primarily about.

There are three main outputs.

  1. Data directly from the Get-AzLocation command. These properties start with “azure”.
  2. Data from PositionStack. Those properties start with “reverseLookup”.
  3. Other data from ISO3166, and IP2Location. Those properties start with “locationInfo”

I want to highlight some of the properties that were proprietary to myself. Meaning, these were properties I had extracted from the azure properties.

azureCardinalDirectionPrimary: east
azureCardinalDirectionSecondary: east
azureLocationNumber: 0

Cardinal Direction

  1. Some of Azures locations use a country name. And some some country names include cardinal directions. For example, South Africa. In those cases, I excluded the first “south” from any cardinal direction hits.
  2. Many of the Azure locations only contain one cardinal direction. In those cases I doubled set the secondary as the primary for consistency. That is, EastUS, will have a primary of “east” and a secondary of “east” as well.
  3. Something like SouthCentralUS, will have “South” as primary and a secondary of “Central”.

Location Number

  1. Some of Azures locations have a trailing number after the name. Like EastUS2, but some don’t. In the spirit of “consistency” I looked for a number in the name, and if I did not find one, I utilized a zero in it’s place. So EastUS is “EastUS 0”

Fun with the data

Now that we have this data, what can we do with it?

Import the data we exported

This puts the data into a couple of formats to help us with access it in different ways.

###################################
#START: Import data

$AllMicrosoftAzureLocationsEnhancedHashTable = Get-Content -Path ".\AllLocationInformationJoinedHashTable.json" -Raw | ConvertFrom-Json -AsHashtable -Depth 100

$AllMicrosoftAzureLocationsEnhancedPSObject = Get-Content -Path ".\AllLocationInformationJoinedHashTable.json" -Raw | ConvertFrom-Json -Depth 100 

$AllMicrosoftAzureLocationsEnhancedArray = Foreach ($Location in $AllMicrosoftAzureLocationsEnhancedHashTable.Keys)
    {
    $AllMicrosoftAzureLocationsEnhancedPSObject.$($Location)

    }

#END: Import data
###################################


$AllOptions = Foreach ($Location in $AllMicrosoftAzureLocationsEnhancedArray)
    {
    $SubdivisionCode = $null
    $SubdivisionCode = If (([string]::IsNullOrWhiteSpace($Location.locationInfoISO3166TwoCode) -eq $true))
        {
        If (([string]::IsNullOrWhiteSpace($Location.reverseLookupRegionCode) -eq $true))
            {
            Write-Warning "Location $($Location.azureLocation) has no identifier for subdivision"
            }
        else 
            {
            $Location.reverseLookupRegionCode.ToLower()
            }
        }
    else 
        {
        ($Location.locationInfoISO3166TwoCode -replace "$($location.locationInfoISO3166OneAlpha2)-","").ToLower()
        }
    
    [pscustomobject]@{
        Location = $location.azureLocation
        Option1 = "$($location.reverseLookupCountry -replace '\s')$($Location.azureCardinalDirectionPrimary)$($Location.azureCardinalDirectionSecondary)$($location.azureLocationNumber)".ToLower()
        Option2 = "$($location.reverseLookupCountry -replace '\s')$($Location.azureCardinalDirectionPrimary.Substring(0,1))$($Location.azureCardinalDirectionSecondary.Substring(0,1))$($location.azureLocationNumber)".ToLower()
        Option3 = "$($Location.reverseLookupContinent)_$($location.reverseLookupCountry)_$($Location.azureCardinalDirectionPrimary)_$($Location.azureCardinalDirectionSecondary)_$($location.azureLocationNumber)"
        Option4 = "$($location.locationInfoISO3166OneAlpha3)$($Location.azureCardinalDirectionPrimary.Substring(0,1))$($Location.azureCardinalDirectionSecondary.substring(0,1))$($location.azureLocationNumber)".ToLower()
        Option5 = "$($location.locationInfoISO3166OneAlpha2)$($Location.azureCardinalDirectionPrimary.Substring(0,1))$($Location.azureCardinalDirectionSecondary.substring(0,1))$($location.azureLocationNumber)".ToLower()
        Option6 = "$($location.locationInfoISO3166OneNumeric)$($Location.azureCardinalDirectionPrimary.Substring(0,1))$($Location.azureCardinalDirectionSecondary.substring(0,1))$($location.azureLocationNumber)".ToLower()
        
        Option7 = "$($location.locationInfoISO3166OneAlpha2)$($SubdivisionCode)$($location.azureLocationNumber)".ToLower()
        }
    }

$AllOptions | Sort-Object -Property Option6 | FT

Example output of EastUS

$AllOptions | Where-Object {$_.Location -eq 'EastUS'}

Location : eastus
Option1  : unitedstateseasteast0
Option2  : unitedstatesee0
Option3  : North America_United States_east_east_0
Option4  : usaee0
Option5  : usee0 
Option6  : 840ee0
Option7  : usva0 

Explaining the output options

  1. Location: This is the default location name provided by Microsoft.
  2. Option1: This what a location name would look like if it was created with the following standard properties in a predictable order
    1. Country Name (no space)
    2. Cardinal Direction Primary
    3. Cardinal Direction Secondary
    4. Location Number
  3. Option2: This is the same as Option1, except we abbreviate the cardinal directions. Instead of “easteast” we do “ee”
  4. Option3: If we take option1 and prepend it with the continent. Now you can really get a geographic understanding of where the Azure location is.
  5. Option4: This is an abbreviated version of the location name. This is what I use for identifying the location in most of my resource names
    1. iso3166 alpha3
    2. abbreviated cardinal directions
    3. location number
  6. Option5: Same as above, but instead utilizing the ISO3166 alpha2
  7. Option6: Same as option4, but using the ISO numeric3. This is a little to cryptic for me.
  8. Option7: This one would be the most ideal, and it works for all but one instances. Keeping uniqueness in mind, it unfortunately fails the uniqueness test for Microsoft’s India locations, as MS has two locations in the same subdivision.
    1. iso3166 alpha2
    2. subdivision code
    3. location number.

To summarize the outputs, option1 - option3 are friendly location names, and option4 - option7 are examples of something you could use in a resource naming convention.

Final thoughts

This is probably one of those blog posts that most folks will find boring. For me, I enjoyed the ability of exploring what data we could join together to enhance what we know about a given Azure location. More so, I found that being able to programmatically name a given Azure location consistently, was a big win. Utilizing these techniques, you can not only prepopulate your location names, but also ensure they follow a consistent predictable standard.

While written in the context of Microsoft Azure, ultimately these techniques could be used for pretty much any location information.