Registering Azure Local (formerly known as Azure Stack HCI) with Azure has come leaps and bounds from the early days that I remember all too well. The process has now been simplified to a one-liner that will prompt you for Azure authentication (via Device Code) and will then start registering the node with Azure Arc to your desired resource group on your Azure subscription. Although that is great, the one thing I’m constantly asked for is guidance on how to do this via a Service Principal (app registration), or SPN for short, with the required permissions.

There are many reasons why someone would want to do this. Some of the common reasons have to deal with a company’s Azure security policy preventing them from using a device code to authenticate to Azure from an unmanaged host. Another reason may be simply because they are wanting to automate end-to-end a deployment of an Azure Local cluster (or multiple clusters).

If you find yourself in that scenario, the below will aim to provide you with some helpful code blocks that my team leverages to perform end-to-end Azure Local deployments for our validation efforts.

Prerequisites

  • The Azure Local machines you intend to connect to Azure Arc must be running release 2505 or later.
  • The Azure Local machines have some initial / temporary assigned management network connectivity.
  • The hostname (Computer Name) has been set on each Azure Local machine you wish to register with Azure Arc.
  • You have assigned to appropriate permissions to either the SPN or a resource group has been created where the needed role assignments have been granted to the SPN.
  • The Azure region you intend to use for Azure Arc is supported for use with Azure Local.

Define Parameters

PowerShell
# Azure Details
$AzureEnvironment = "AzureCloud"
$AzureRegion = "eastus"
$AzureResourceGroup = "<MyAzureResourceGroup>"
$AzureSubscriptionId = "<MyAzureSubscriptionId>"
$AzureTenantId = "<MyAzureTenantId>"
$AzureSPNId = "<MyAzureAppRegistrationId>"
$AzureSPNSecret = Read-Host -Prompt "Azure SPN Secret" -AsSecureString
# Create Credential Objects
$AzureSPNCredential = New-Object System.Management.Automation.PSCredential ($AzureSPNId, $AzureSPNSecret)

Connect to Azure

PowerShell
# Connect to Azure
Connect-AzAccount `
-ServicePrincipal `
-TenantId $AzureTenantId `
-Environment $AzureEnvironment `
-Credential $AzureSPNCredential `
-InformationAction Ignore

As of this writing, the latest Dell Golden Image for Azure Local is version 2512 which is based on solution version 12.2510.1002.*. With that being the case, it has been observed that there are multiple versions of the Az.Accounts module installed. It’s important to know this and understand the version that was imported by default when using the Connect-AzAccount cmdlet. As of version Az.Accounts version 5.3.0, when you run the Get-AzAccessToken cmdlet, the object returned will be a SecureString for the Token value.

Start the Azure Arc Registration Script

The command to start Azure Arc registration (Invoke-AzStackHciArcInitialization) expects the -ArmAccessToken parameter to be a String object rather an a SecureString. This can be seen by running the following command:

PowerShell
Get-Command -Name "Invoke-AzStackHciArcInitialization" -Syntax

The below code block can be used to account for either scenario. If version 5.3.0 or greater is detected as the loaded module, the object will be converted to a String when passed into Invoke-AzStackHciArcInitialization cmdlet.

PowerShell
$AzAccountsLoaded = Get-Module -Name "Az.Accounts" | Select-Object Name, Version, Path
if ([Version]$AzAccountsLoaded.Version -ge [Version]"5.3.0") {
$AzureAccessToken = (Get-AzAccessToken -WarningAction SilentlyContinue).Token
Invoke-AzStackHciArcInitialization `
-SubscriptionID $AzureSubscriptionId `
-ResourceGroup $AzureResourceGroup `
-TenantID $AzureTenantId `
-Region $AzureRegion `
-Cloud $AzureEnvironment `
-ArmAccessToken $([System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($AzureAccessToken))) `
-AccountID ((Get-AzContext).Account.Id)
} else {
Invoke-AzStackHciArcInitialization `
-SubscriptionID $AzureSubscriptionId `
-ResourceGroup $AzureResourceGroup `
-TenantID $AzureTenantId `
-Region $AzureRegion `
-Cloud $AzureEnvironment `
-ArmAccessToken ((Get-AzAccessToken -WarningAction SilentlyContinue).Token) `
-AccountID ((Get-AzContext).Account.Id)
}

During the Azure Arc registration process, the node may be rebooted to update to a targeted solution version (latest available) if there is one newer than provided with the Dell Golden Image. If that is the case, simply log back into the node with the local administrator account and re-run the above code blocks to log into Azure and run the Azure Arc registration script.

Bonus Material

With the Azure Local golden images, there are various modules that are installed that can be used to remotely query the machine while it is undergoing the Azure Arc initialization / registration process. One of those commands is Get-ArcBootstrapStatus. The below can be run on each node individually or you can write a loop to check until a desired status is reached.

PowerShell
# Verify that Azure Arc Bootstrap is 'Succeeded'
(Get-ArcBootstrapStatus).Response
PowerShell
# Define variables
$OSCredential = Get-Credential -Message "Azure Local Administrator OS Credential"
$NodeIpAddress = <NodeIpAddress>
$Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
:MonitorArcBootStrapStatus while ($true) {
if ($OSSession.State -ne 'Opened') {
$OSSession = New-PSSession -ComputerName $NodeIpAddress -Credential $OSCredential
}
$ArcBootStrapResponse = Invoke-Command -Session $OSSession {
return (Get-ArcBootStrapStatus).Response
}
$ArcBootStrapStatus = $ArcBootStrapResponse.Status
switch ($ArcBootStrapStatus) {
'InProgress' {
Write-Host "Arc Bootstrap Status: $($ArcBootStrapStatus) (Sleeping 5 minutes)"
Start-Sleep -Seconds 300
}
'Succeeded' {
Write-Host "Arc Bootstrap Status: $($ArcBootStrapStatus)"
break MonitorArcBootStrapStatus
}
'Failed' {
Write-Host "Arc Bootstrap Status: $($ArcBootStrapStatus)"
throw "Arc Bootstrap Failed - $($ArcBootStrapResponse.Message)"
}
}
if ($Stopwatch.Elapsed.TotalSeconds -gt 1800) {
throw "Timeout Exceeded"
}
}

Once the nodes are Azure Arc registered, the next steps when following the Azure Portal UI based deployment process will be to install the required Azure Arc machine extensions on each node. The below can be used to trigger this so you can roll right into performing an Azure Local deployment using an ARM template.

Install Azure Arc Machine Extensions

To run the below, it will require the Az.ConnectedMachine PowerShell module. In the context of how we run this for our validation efforts, we have a Windows-based Jenkins agent that has the required PowerShell module installed along with others. This Jenkins agent then either invokes commands either by a PSSession connecting to the node or by calling Azure API endpoints directly. In the case of the below, this can be performed from a separate machine that has internet access and the additional Az.ConnectedMachine PowerShell module installed.

The below code block will trigger the required Azure Arc machine extensions to be installed as required for an Azure Local deployment. Although the below code block is specific to the node you are running it on, you could create a $MyNodes array object and then pass it as a function into to a ForEach loop which connects to each node via a PSSession.

PowerShell
# Node Details
$NodeHostname = "<MyNodeHostname>"
$AzConnectedMachineExtensions = (Get-AzConnectedMachineExtension `
-ResourceGroupName $AzureResourceGroup `
-MachineName $NodeHostname
| Where-Object { $_.ProvisioningState -eq 'Succeeded' }
| Measure-Object).Count
if ($AzConnectedMachineExtensions -ge 4) {
Write-Host "Machine Extensions Status - Total : $($AzConnectedMachineExtensions) (Skipping Install)"
} else {
$Response = Invoke-AzRestMethod `
-Method PUT `
-Uri "https://management.azure.com/subscriptions/$AzureSubscriptionId/resourceGroups/$AzureResourceGroup/providers/Microsoft.HybridCompute/machines/$NodeHostname/providers/microsoft.azurestackhci/edgeDevices/default?api-version=2024-09-01-preview" `
-Payload (@{
kind = 'HCI'
} | ConvertTo-Json)
if ($Response.StatusCode -ne 201) {
throw "Response '$($Response.StatusCode)' is not 201 (Created) - Aborting - $($Response.Content)"
}
}

Monitor Azure Arc Machine Extension Install

PowerShell
$Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
while ($true) {
$AzConnectedMachineExtensions = Get-AzConnectedMachineExtension -ResourceGroupName $AzureResourceGroup -MachineName $NodeHostname
$AzConnectedMachineExtensionsTotal = ($AzConnectedMachineExtensions | Measure-Object).Count
$AzConnectedMachineExtensionsSucceeded = ($AzConnectedMachineExtensions | Where-Object { $_.ProvisioningState -eq 'Succeeded' } | Measure-Object).Count
Write-Host "Machine Extensions Status - Total : $($AzConnectedMachineExtensionsTotal) | Succeeded : $($AzConnectedMachineExtensionsSucceeded)" -NoRepeat:$true
if ($AzConnectedMachineExtensionsTotal -ge 4 -and $AzConnectedMachineExtensionsTotal -eq $AzConnectedMachineExtensionsSucceeded) {
break
}
if ($Stopwatch.Elapsed.TotalSeconds -gt 900) {
throw "Timeout Exceeded"
}
Start-Sleep -Seconds 30
}

Leave a comment