Skip to content

Instantly share code, notes, and snippets.

@JustinGrote
Last active April 24, 2024 16:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JustinGrote/b7fac2b239420b4befd753d21952c3ec to your computer and use it in GitHub Desktop.
Save JustinGrote/b7fac2b239420b4befd753d21952c3ec to your computer and use it in GitHub Desktop.
Better Azure Arc Agent Onboarding Script
#Requires -Version 5.1 -RunAsAdministrator
using namespace System.IO
using namespace System.Net
#NOTE: The core code flow in is in the Main region at the bottom of the script.
<#
.SYNOPSIS
This is a script that unifies the Azure Arc and AKS Edge Essentials installation process. You can use this script to connect a server to Azure Arc as well as AKS Edge Essentials.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
#Tenant ID to deploy Azure Arc. Can be specified as a GUID or a domain name.
[Parameter(Mandatory)]
[string]$TenantId,
#Subscription ID where Azure Arc should be deployed.
[Parameter(Mandatory)]
[string]$SubscriptionId,
#Resource group where Arc and Kubernetes Arc will be created. Default is 'Arc'
[ValidateNotNullOrEmpty()]
[string]$ResourceGroup = 'arc',
#Resource group to create Kubernetes Arc resources. If not provided will use the same as ResourceGroup
[ValidateNotNullOrEmpty()]
[string]$K3sResourceGroup = $ResourceGroup,
# Azure Location where Arc resources will be created. Default is 'westus3'
[ValidateNotNullOrEmpty()]
[string]$Location = 'westus3',
# Azure Cloud to use. Default is 'AzureCloud'
[ValidateNotNullOrEmpty()]
[string]$Cloud = 'AzureCloud',
# Path where temporary files will be stored. It will be created if not existing
[ValidateNotNullOrEmpty()]
[string]$TempPath = $(Join-Path ([Path]::GetTempPath()) 'Arcify')
)
#region Variables
#Azure Connected Machine Agent Path
$SCRIPT:AzCMAgentPath = Join-Path ([Environment]::GetFolderPath('ProgramFiles')) 'AzureConnectedMachineAgent\azcmagent.exe'
#endregion Variables
function Assert-IsElevated {
if (-not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Error 'This script must be run as an Administrator.'
}
}
function Initialize-TempPath ($TempPath) {
if (-not (Test-Path $TempPath)) {
Write-Verbose "Creating $TempPath for deployment."
New-Item -ItemType Directory -Path $TempPath -Force | Out-Null
}
$TempPath = Resolve-Path $TempPath
Write-Verbose "Verified Temporary Storage Path $TempPath exists."
return $TempPath
}
function Assert-64Bit {
if (-not [Environment]::Is64BitProcess) {
Write-Error 'This script is not supported with 32-bit Windows PowerShell. Please start the 64 bit Windows PowerShell or PowerShell 7+ and try again.'
}
if (-not [Environment]::Is64BitOperatingSystem) {
Write-Error 'The agent is not supported on 32 bit operating systems. Please run this script on a 64-bit operating system.'
}
}
function Get-WebFile {
<#
.SYNOPSIS
Download a file from the web
#>
param(
[ValidateNotNullOrEmpty()]
[string]$Uri,
[ValidateNotNullOrEmpty()]
[string]$OutFile
)
if (-not $IsCoreCLR) {
#TLS1.2 is not enabled by default on WinPS, enable it
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
# Disable progress for Invoke-WebRequest, it slows things down on 5.1
$CurrentProgressPreference = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
}
try {
Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 30 -OutFile $OutFile
} finally {
if ($CurrentProgressPreference) {
$ProgressPreference = $CurrentProgressPreference
}
}
}
<#
.SYNOPSIS
This function will install the Azure Connected Machine Agent if not already installed
.OUTPUTS
It will return the path to the agent executable. If the agent was already installed, it will return $null. If an error occurs, the error will be returned as a terminating error.
#>
function Install-AzConnectedMachineAgent {
[CmdletBinding()]
param(
[ValidateNotNullOrEmpty()]
[string]$AzCMAgentDownloadUri = 'https://aka.ms/azcmagent-windows',
[ValidateNotNullOrEmpty()]
[string]$TempPath = $SCRIPT:TempPath,
#This is currently not supported to change from the default.
[ValidateNotNullOrEmpty()]
[string]$InstallPath = $SCRIPT:AzCMAgentPath,
[string]$InstallLogPath = $(Join-Path $TempPath 'azcmagent-install.log'),
#Specify this to not configure the agent to automatically update via Microsoft Update
[switch]$NoAutoUpdate,
#Dont delete the MSI after installation. Useful for minimizing downloads during testing/troubleshooting
[switch]$NoCleanup
)
#TODO: Add Reinstall Support
$ErrorActionPreference = 'Stop'
$AzCMAgentPath = Join-Path $InstallPath 'azcmagent.exe'
try {
#Will Error if not present
Resolve-Path $AzCMAgentPath
Write-Verbose "Azure Connected Machine Agent already installed at $AzCMAgentPath."
return $AzCMAgentPath
} catch {
Write-Verbose 'Azure Connected Machine Agent not found. Installing...'
}
$AzCmMsiPath = Join-Path $TempPath 'AzureConnectedMachineAgent.msi'
try {
# Download the installation package
Write-Debug "Downloading Azure Connected Machine Agent from $AzCMAgentDownloadUri to $InstallPath"
try {
Get-WebFile -Uri $AzCMAgentDownloadUri -OutFile $AzCmMsiPath
} catch {
$PSItem.ErrorDetails = "Error downloading Azure Connected Machine Agent from $AzCMAgentDownloadUri to $AzCmMsiPath`: $PSItem"
Write-Error $PSItem
}
# Install the Azure Arc Agent
Write-Debug "Installing Azure Connected Machine Agent from $AzCmMsiPath"
$installArgs = @(
'/i'
$AzCmMsiPath
'/qn'
'/l*v'
$InstallLogPath
'REBOOT=ReallySuppress'
)
$msiOutput = & $AzCmMsiPath @installArgs
$msiResultCode = $LASTEXITCODE
if ($msiOutput) {
Write-Debug "MSI Output: $msiOutput"
}
#Configure Automatic Updates
#Ref: https://learn.microsoft.com/en-us/azure/azure-arc/servers/manage-agent?tabs=windows
if (-not $NoAutoUpdate) {
$ServiceManager = (New-Object -com 'Microsoft.Update.ServiceManager')
$ServiceID = '7971f918-a847-4430-9279-4a52d1efe18d'
$ServiceManager.AddService2($ServiceId, 7, '')
}
switch ($msiResultCode) {
0 {
try {
Resolve-Path $AzCMAgentPath
} catch {
Write-Error "Error installing Azure Connected Machine Agent from $AzCmMsiPath`: Installer Reported Success but Azure Connected Machine Agent not found at $AzCMAgentPath. See log at $InstallLogPath for details."
}
Write-Verbose 'Azure Connected Machine Agent installed successfully.'
return $AzCMAgentPath
}
1641 {
Write-Warning 'Azure Connected Machine Agent installation initiated. Reboot required.'
}
3010 {
Write-Warning 'Azure Connected Machine Agent installation initiated. Reboot required.'
}
default {
Write-Error "Error installing Azure Connected Machine Agent from $AzCmMsiPath`: $msiOutput. See log at $InstallLogPath for details." -Category InvalidOperation -TargetObject $AzCmMsiPath -RecommendedAction 'Check the log at $InstallLogPath for more details.' -ErrorId 'AzCmAgentInstallError' -
}
}
} catch {
$PSItem.ErrorDetails = "Error installing Azure Connected Machine Agent from $AzCmMsiPath`: $PSItem"
Write-Error $PSItem
} finally {
#Clean up the MSI
if ((Test-Path $AzCmMsiPath) -and -not $NoCleanup) {
Remove-Item $AzCmMsiPath -Force -Confirm:$False
}
}
}
function Assert-AzConnectedMachineAgentConnectivity {
param(
[ValidateNotNullOrEmpty()]
[string]$Location = $SCRIPT:Location,
[ValidateNotNullOrEmpty()]
[string]$Cloud = $SCRIPT:Cloud,
[ValidateNotNullOrEmpty()]
[string]$AzCMAgentPath = $SCRIPT:AzCMAgentPath
)
$AzCMParams = @(
'check'
'--json'
'--location'
$Location
'--cloud'
$Cloud
)
Write-Verbose "Testing Azure Connected Machine Agent connectivity to $Location in $Cloud."
Write-Debug "Executing Command: $AzCMAgentPath $AzCMParams"
$connectivityResult = & $AzCMAgentPath @AzCMParams | ConvertFrom-Json
foreach ($url in $connectivityResult.PSObject.Properties.Name) {
$urlStatus = $connectivityResult.$url
if ($true -eq $urlStatus.reachable) {
Write-Debug "Azure Connected Machine Agent can reach $url with $($url.tls)"
continue
}
#EA Continue is used so we can report all errors at once
Write-Error -ErrorAction Continue "Azure Connected Machine Agent cannot reach $url with $($url.tls): $($urlStatus | ConvertTo-Json)"
$errorDetected = $true
}
if ($errorDetected) { Write-Error "Connectivity Errors trying to reach $url" }
}
function Connect-AzConnectedMachineAgent {
<#
.SYNOPSIS
Connect the Azure Connected Machine Agent to Azure Arc
#>
[CmdletBinding(DefaultParameterSetName = 'Interactive')]
param(
[ValidateNotNullOrEmpty()]
[string]$AzCMAgentPath = $SCRIPT:AzCMAgentPath,
[ValidateNotNullOrEmpty()]
[string]$TenantId,
[ValidateNotNullOrEmpty()]
[string]$SubscriptionId,
[ValidateNotNullOrEmpty()]
[string]$Location,
[ValidateNotNullOrEmpty()]
[string]$ResourceGroupName,
[ValidateNotNullOrEmpty()]
[string]$Cloud = 'AzureCloud',
[ValidateNotNullOrEmpty()]
[string]$CorrelationId,
[ValidateNotNullOrEmpty()]
[Parameter(Mandatory, ParameterSetName = 'DeviceCode')]
[switch]$UseDeviceCode,
[Parameter(Mandatory, ParameterSetName = 'ServicePrincipal')]
[PSCredential]$ServicePrincipalCredential
)
$ErrorActionPreference = 'Stop'
$azcmAgentArgs = @(
'connect'
'--tenant-id'
$TenantId
'--subscription-id'
$SubscriptionId
'--location'
$Location
'--resource-group'
$ResourceGroupName
'--cloud'
$Cloud
'--correlation-id'
$CorrelationId
)
if ($ServicePrincipalCredential) {
$azcmAgentArgs += '--service-principal-id'
$azcmAgentArgs += $ServicePrincipalCredential.UserName
$azcmAgentArgs += '--service-principal-secret'
$azcmAgentArgs += $ServicePrincipalCredential.GetNetworkCredential().Password
} elseif ($UseDeviceCode) {
$azcmAgentArgs += '--use-device-code'
}
Write-Verbose "Running $AzCMAgentPath $azcmAgentArgs"
# Run connect command
& $AzCMAgentPath @azcmAgentArgs
}
#region Main
$ErrorActionPreference = 'Stop'
#Override Debug Inquire to avoid prompts (usually 5.1 only)
if ($DebugPreference -eq 'Inquire') {$DebugPreference = 'Continue'}
Assert-IsElevated
Assert-64Bit
$SCRIPT:TempPath = Initialize-TempPath $TempPath
Install-AzConnectedMachineAgent
Assert-AzConnectedMachineAgentConnectivity
#endregion Main
using namespace System.Net
using namespace System.IO
#requires -version 5.1
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateNotNullOrEmpty()]
[string]$TenantId = $env:TENANT_ID,
[ValidateNotNullOrEmpty()]
[string]$SubscriptionId = $env:SUBSCRIPTION_ID,
[ValidateNotNullOrEmpty()]
[string]$Location = $env:LOCATION,
[ValidateNotNullOrEmpty()]
[string]$ResourceGroupName = $env:RESOURCE_GROUP,
[ValidateNotNullOrEmpty()]
[string]$Cloud = $( if ($env:CLOUD) { $env:CLOUD } else { 'AzureCloud' }),
[ValidateSet('Interactive','DeviceCode','ServicePrincipal')]
[string]$AuthType = 'DeviceCode',
[string]$CorrelationId = $(New-GUID),
[PSCredential]$ServicePrincipalCredential
)
if ($env:SERVICE_PRINCIPAL_ID -and $env:SERVICE_PRINCIPAL_SECRET) {
$ServicePrincipalCredential = [PSCredential]::new($env:SERVICE_PRINCIPAL_ID, (ConvertTo-SecureString $env:SERVICE_PRINCIPAL_SECRET -AsPlainText -Force))
}
$ErrorActionPreference = 'stop'
$IsWinPS = $PSEdition -eq 'Desktop'
$ProgParams = @{
ID = (Get-Random)
Activity = "Onboarding $($env:COMPUTERNAME) to Azure Arc"
}
if (-not $PSCmdlet.ShouldProcess($ENV:ComputerName, "Onboard to Azure Arc to TenantId: $TenantId, SubscriptionId: $SubscriptionId, Location: $Location, ResourceGroupName: $ResourceGroupName, Cloud: $Cloud, CorrelationId: $CorrelationId")) {
Write-Verbose "Onboarding to Azure Arc was skipped by user"
return
}
try {
Write-Progress @ProgParams -Status 'Downloading Azure Connected Machine Agent' -PercentComplete 0
if ($IsWinPS) {
#Enable TLS 1.2 for 5.1 compatibility
[ServicePointManager]::SecurityProtocol = [ServicePointManager]::SecurityProtocol -bor [SecurityProtocolType]::Tls12
#Disable Progress for Invoke-WebRequest, it slows things down on 5.1
$ExistingProgressPreference = $ProgressPreference;
$ProgressPreference = 'SilentlyContinue';
}
Write-Verbose "Downloading Azure Connected Machine Agent from https://aka.ms/azcmagent-windows"
# We save the installation script locally because it is signed and Invoke-Expression does not check signatures
$installScriptPath = Join-Path ([Path]::GetTempPath()) 'install_windows_azcmagent.ps1'
Invoke-WebRequest -UseBasicParsing -Uri 'https://aka.ms/azcmagent-windows' -TimeoutSec 30 -OutFile $installScriptPath
# Install the hybrid agent
Write-Verbose "Invoking $installScriptPath"
& $installScriptPath
if ($ExistingProgressPreference) {
$ProgressPreference = $ExistingProgressPreference
}
# Verify the agent setup was indeed installed. This will error if not present
$azcmAgentPath = Resolve-Path "$env:ProgramW6432\AzureConnectedMachineAgent\azcmagent.exe"
Write-Progress @ProgParams -Status 'Activating Azure Connected Machine Agent' -PercentComplete 50
$azcmAgentArgs = @(
'connect'
'--tenant-id'
$TenantId
'--subscription-id'
$SubscriptionId
'--location'
$Location
'--resource-group'
$ResourceGroupName
'--cloud'
$Cloud
'--correlation-id'
$CorrelationId
)
if ($AuthType -eq 'ServicePrincipal') {
if (-not $ServicePrincipalCredential) {
$ServicePrincipalCredential = Get-Credential -Message 'Enter the Service Principal credentials'
}
$azcmAgentArgs += '--service-principal-id'
$azcmAgentArgs += $ServicePrincipalCredential.UserName
$azcmAgentArgs += '--service-principal-secret'
$azcmAgentArgs += $ServicePrincipalCredential.GetNetworkCredential().Password
} elseif ($AuthType -eq 'DeviceCode') {
$azcmAgentArgs += '--use-device-code'
}
Write-Verbose "Running $azcmAgentPath $azcmAgentArgs"
# Run connect command
& $azcmAgentPath @azcmAgentArgs
} catch {
$logBody = @{
subscriptionId = $SubscriptionId
resourceGroup = $ResourceGroupName
tenantId = $TenantId
location = $Location
correlationId = $CorrelationId
authType = "$env:AUTH_TYPE"
operation = 'onboarding'
messageType = $_.FullyQualifiedErrorId
message = "$_";
};
Invoke-WebRequest -UseBasicParsing -Uri 'https://gbl.his.arc.azure.com/log' -Method 'PUT' -Body ($logBody | ConvertTo-Json) | Out-Null;
$PSItem.ErrorDetails.Message = "Failed to onboard the machine to Azure Arc subscription: $($PSItem.Exception). Details: $($logBody | ConvertTo-Json)"
$PSItem.ErrorDetails.RecommendedAction = 'See https://learn.microsoft.com/en-us/azure/azure-arc/servers/troubleshoot-agent-onboard for more information'
$PSCmdlet.ThrowTerminatingError($PSItem)
}
Write-Progress @ProgParams -Status 'Azure Connected Machine Agent Activated' -Completed

Better Azure Arc Onboarding

Rather than have to go to the portal to gather all the onboarding info, you can now simply do this:

Environment Variable Method

$env:SUBSCRIPTION_ID = '48759ba7-c496-43ac-xxxx-03abf5ef2092';
$env:RESOURCE_GROUP = 'ArcRG';
$env:TENANT_ID = '2f46c040-48e3-4eb8-xxxx-418417f64401';

#Optional
$env:LOCATION = 'westus2';
$env:CORRELATION_ID = 'b9e96ec9-322f-4d34-xxxx-ee115a6e38c4';
$env:CLOUD = 'AzureCloud';
& ([ScriptBlock]::Create((irm bit.ly/ArcOnboard))) -AuthType DeviceCode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment