Azure Automation: update them modules
I have been working a lot with Azure Automation lately. It’s a great product, helping organize the use of Powershell making an awesome language even better.
With AA as any other of the Azure services there are always some challenges, one is modules and how some keep ever changing…I’m looking at you AzureRM. This is why I wanted to get some automation into this process. And as it is in the product name this is only fitting.
Browsing the AA teams github I found the following Runbook that looked promising. Though in testing it has some issues with modules on gallery that did not use the same title as module name, like you know AzureAD. Therefore I did some fast triaging to get it into shape. As with everything, check to see if somebody else already have figured it out before creating the wheel anew. Browsing the web this guy had it all figured out, so I borrowed the code.
Update:
there is now an improved version of this available here:
https://github.com/mortenlerudjordet/runbooks/blob/master/Utility/ARM/Update-PSGalleryModulesInAA.ps1And one for installing the modules in the first place:
https://github.com/mortenlerudjordet/runbooks/blob/master/Utility/ARM/Import-PSGalleryModulesToAA.ps1
Here is the updated version that updates the modules in AA, just add it as a Runbook and schedule it to run as often as needed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 | <#PSScriptInfo .VERSION 1.03 .GUID fa658952-8f94-45ac-9c94-f5fe23d0fcf9 .AUTHOR Automation Team .CONTRIBUTOR Morten Lerudjordet .COMPANYNAME Microsoft .COPYRIGHT .TAGS AzureAutomation OMS Module Utility .LICENSEURI .PROJECTURI https://github.com/azureautomation/runbooks/blob/master/Utility/Update-ModulesInAutomationToLatestVersion.ps1 .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES #> #Requires -Module AzureRM.Profile, AzureRM.Automation,AzureRM.Resources <# .SYNOPSIS This Azure/OMS Automation runbook imports the latest version on PowerShell Gallery of all modules in an Automation account.If a new module to import is specified, it will import that module from the PowerShell Gallery after all other modules are updated from the gallery. .DESCRIPTION This Azure/OMS Automation runbook imports the latest version on PowerShell Gallery of all modules in an Automation account. By connecting the runbook to an Automation schedule, you can ensure all modules in your Automation account stay up to date. If a new module to import is specified, it will import that module from the PowerShell Gallery after all other modules are updated from the gallery. .PARAMETER ResourceGroupName Optional. The name of the Azure Resource Group containing the Automation account to update all modules for. If a resource group is not specified, then it will use the current one for the automation account if it is run from the automation service .PARAMETER AutomationAccountName Optional. The name of the Automation account to update all modules for. If an automation account is not specified, then it will use the current one for the automation account if it is run from the automation service .PARAMETER NewModuleName Optional. The name of a module in the PowerShell gallery to import after all existing modules are updated .EXAMPLE Update-ModulesInAutomationToLatestVersion -ResourceGroupName "MyResourceGroup" -AutomationAccountName "MyAutomationAccount" -NewModuleName "AzureRM.Batch" .NOTES AUTHOR: Automation Team LASTEDIT: September 2nd, 2016 #> param( [Parameter(Mandatory=$false)] [String] $ResourceGroupName, [Parameter(Mandatory=$false)] [String] $AutomationAccountName, [Parameter(Mandatory=$false)] [String] $NewModuleName ) $VerbosePreference = "silentlycontinue" Write-Output -InputObject "Starting Runbook at time: $(get-Date -format r). Running PS version: $($PSVersionTable.PSVersion)" Import-Module -Name AzureRM.Profile, AzureRM.Automation,AzureRM.Resources -ErrorAction Continue -ErrorVariable oErr If($oErr) { Write-Error -Message "Failed to load needed modules for Runbook. Error: $($oErr.Message)" -ErrorAction Stop } #$VerbosePreference = "continue" $ErrorActionPreference = 'stop' $ModulesImported = @() function _doImport { param( [Parameter(Mandatory=$true)] [String] $ResourceGroupName, [Parameter(Mandatory=$true)] [String] $AutomationAccountName, [Parameter(Mandatory=$true)] [String] $ModuleName, # if not specified latest version will be imported [Parameter(Mandatory=$false)] [String] $ModuleVersion ) $Filter = @($ModuleName.Trim('*').Split('*') | ForEach-Object { "substringof('$_',Id)" }) -join " and " $Url = "https://www.powershellgallery.com/api/v2/Packages?`$filter=$Filter and IsLatestVersion" # Fetch results and filter them with -like, and then shape the output $SearchResult = Invoke-RestMethod -Method Get -Uri $Url -ErrorAction Continue -ErrorVariable oErr | Where-Object { $_.title.'#text' -like $ModuleName } | Select-Object @{n='Name';ex={$_.title.'#text'}}, @{n='Version';ex={$_.properties.version}}, @{n='Url';ex={$_.Content.src}}, @{n='Dependencies';ex={$_.properties.Dependencies}} If($oErr) { # Will stop runbook, though message will not be logged Write-Error -Message "Stopping runbook" -ErrorAction Stop } # Should not be needed as filter will only return one hit, though will keep the code to strip away if search ever get multiple hits if($SearchResult.Length -and $SearchResult.Length -gt 1) { $SearchResult = $SearchResult | Where-Object -FilterScript { return $_.Name -eq $ModuleName } } if(!$SearchResult) { Write-Warning "Could not find module '$ModuleName' on PowerShell Gallery. This may be a module you imported from a different location" } else { $ModuleName = $SearchResult.Name # get correct casing for the module name if(!$ModuleVersion) { # get latest version $ModuleContentUrl = $SearchResult.Url } Else { $ModuleContentUrl = "https://www.powershellgallery.com/api/v2/package/$ModuleName/$ModuleVersion" } # Make sure module dependencies are imported $Dependencies = $SearchResult.Dependencies if($Dependencies -and $Dependencies.Length -gt 0) { $Dependencies = $Dependencies.Split("|") # parse depencencies, which are in the format: module1name:module1version:|module2name:module2version: $Dependencies | ForEach-Object { if($_ -and $_.Length -gt 0) { $Parts = $_.Split(":") $DependencyName = $Parts[0] $DependencyVersion = ($Parts[1] -replace '\[', '') -replace '\]', '' # check if we already imported this dependency module during execution of this script if(!$ModulesImported.Contains($DependencyName)) { # Logg errors if occures $AutomationModule = Get-AzureRmAutomationModule ` -ResourceGroupName $ResourceGroupName ` -AutomationAccountName $AutomationAccountName ` -Name $DependencyName ` -ErrorAction Continue # check if Automation account already contains this dependency module of the right version if((!$AutomationModule) -or $AutomationModule.Version -ne $DependencyVersion) { Write-Output -InputObject "Importing dependency module $DependencyName of version $DependencyVersion first." # this dependency module has not been imported, import it first _doImport ` -ResourceGroupName $ResourceGroupName ` -AutomationAccountName $AutomationAccountName ` -ModuleName $DependencyName ` -ModuleVersion $DependencyVersion -ErrorAction Stop $ModulesImported += $DependencyName } } } } } # Find the actual blob storage location of the module do { $ActualUrl = $ModuleContentUrl $ModuleContentUrl = (Invoke-WebRequest -Uri $ModuleContentUrl -MaximumRedirection 0 -UseBasicParsing -ErrorAction Ignore).Headers.Location } while(!$ModuleContentUrl.Contains(".nupkg")) $ActualUrl = $ModuleContentUrl If($ModuleVersion) { Write-Output -InputObject "Importing $ModuleName module of version $ModuleVersion to Automation" } Else { Write-Output -InputObject "Importing $ModuleName module of version $($SearchResult.Version) to Automation" } $AutomationModule = New-AzureRmAutomationModule ` -ResourceGroupName $ResourceGroupName ` -AutomationAccountName $AutomationAccountName ` -Name $ModuleName ` -ContentLink $ActualUrl while( (!([string]::IsNullOrEmpty($AutomationModule))) -and $AutomationModule.ProvisioningState -ne "Created" -and $AutomationModule.ProvisioningState -ne "Succeeded" -and $AutomationModule.ProvisioningState -ne "Failed" ) { Write-Verbose -Message "Polling for module import completion" Start-Sleep -Seconds 10 $AutomationModule = $AutomationModule | Get-AzureRmAutomationModule } if($AutomationModule.ProvisioningState -eq "Failed") { Write-Error "Importing $ModuleName module to Automation failed." } else { Write-Output -InputObject "Importing $ModuleName module to Automation succeeded." } } } try { $RunAsConnection = Get-AutomationConnection -Name "AzureRunAsConnection" Write-Output -InputObject ("Logging in to Azure...") Add-AzureRmAccount ` -ServicePrincipal ` -TenantId $RunAsConnection.TenantId ` -ApplicationId $RunAsConnection.ApplicationId ` -CertificateThumbprint $RunAsConnection.CertificateThumbprint -ErrorAction Continue -ErrorVariable oErr If($oErr) { Throw "Failed to connect to Azure. Error: $($oErr.Message)" } Select-AzureRmSubscription -SubscriptionId $RunAsConnection.SubscriptionID -ErrorAction Continue -ErrorVariable oErr If($oErr) { Throw "Failed to select Azure subscription. Error: $($oErr.Message)" } # Find the automation account or resource group is not specified if (([string]::IsNullOrEmpty($ResourceGroupName)) -or ([string]::IsNullOrEmpty($AutomationAccountName))) { Write-Verbose -Message ("Finding the ResourceGroup and AutomationAccount that this job is running in ...") if ([string]::IsNullOrEmpty($PSPrivateMetadata.JobId.Guid)) { throw "This is not running from the automation service. Please specify ResourceGroupName and AutomationAccountName as parameters" } # Breaking change in version 6 of AzureRM.Resources, Find-AzureRmResource is deprecated If((Get-Module -Name AzureRM.Resources).Version.Major -lt 6) { $AutomationResource = Find-AzureRmResource -ResourceType Microsoft.Automation/AutomationAccounts } Else { $AutomationResource = Get-AzureRmResource -ResourceType Microsoft.Automation/AutomationAccounts } foreach ($Automation in $AutomationResource) { $Job = Get-AzureRmAutomationJob -ResourceGroupName $Automation.ResourceGroupName -AutomationAccountName $Automation.Name -Id $PSPrivateMetadata.JobId.Guid -ErrorAction SilentlyContinue if (!([string]::IsNullOrEmpty($Job))) { $ResourceGroupName = $Job.ResourceGroupName $AutomationAccountName = $Job.AutomationAccountName break; } } } } catch { if(!$RunAsConnection) { throw "Connection AzureRunAsConnection not found. Please create one" } else { throw $_.Exception } } $Modules = Get-AzureRmAutomationModule ` -ResourceGroupName $ResourceGroupName ` -AutomationAccountName $AutomationAccountName foreach($Module in $Modules) { $Module = Get-AzureRmAutomationModule ` -ResourceGroupName $ResourceGroupName ` -AutomationAccountName $AutomationAccountName ` -Name $Module.Name $ModuleName = $Module.Name $ModuleVersionInAutomation = $Module.Version Write-Output -InputObject "Checking if module '$ModuleName' is up to date in your automation account" $Filter = @($ModuleName.Trim('*').Split('*') | ForEach-Object { "substringof('$_',Id)" }) -join " and " $Url = "https://www.powershellgallery.com/api/v2/Packages?`$filter=$Filter and IsLatestVersion" # Fetch results and filter them with -like, and then shape the output $SearchResult = Invoke-RestMethod -Method Get -Uri $Url -ErrorAction Continue -ErrorVariable oErr | Where-Object { $_.title.'#text' -like $ModuleName } | Select-Object @{n='Name';ex={$_.title.'#text'}}, @{n='Version';ex={$_.properties.version}}, @{n='Url';ex={$_.Content.src}}, @{n='Dependencies';ex={$_.properties.Dependencies}} If($oErr) { # Will stop runbook, though message will not be logged Write-Error -Message "Stopping runbook" -ErrorAction Stop } # Should not be needed anymore, though in the event of the search returning more than one hit this will strip it down if($SearchResult.Length -and $SearchResult.Length -gt 1) { $SearchResult = $SearchResult | Where-Object -FilterScript { return $_.Name -eq $ModuleName } } if(!$SearchResult) { Write-Warning "Could not find module '$ModuleName' on PowerShell Gallery. This may be a module you imported from a different location" } else { $LatestModuleVersionOnPSGallery = $SearchResult.Version if($ModuleVersionInAutomation -ne $LatestModuleVersionOnPSGallery) { Write-Output -InputObject "Module '$ModuleName' is not up to date. Latest version on PS Gallery is '$LatestModuleVersionOnPSGallery' but this automation account has version '$ModuleVersionInAutomation'" Write-Output -InputObject "Importing latest version of '$ModuleName' into your automation account" _doImport ` -ResourceGroupName $ResourceGroupName ` -AutomationAccountName $AutomationAccountName ` -ModuleName $ModuleName } else { Write-Output -InputObject "Module '$ModuleName' is up to date." } } } # Import module if specified if (!([string]::IsNullOrEmpty($NewModuleName))) { # Check if module exists in the gallery $Filter = @($ModuleName.Trim('*').Split('*') | ForEach-Object { "substringof('$_',Id)" }) -join " and " $Url = "https://www.powershellgallery.com/api/v2/Packages?`$filter=$Filter and IsLatestVersion" # Fetch results and filter them with -like, and then shape the output $SearchResult = Invoke-RestMethod -Method Get -Uri $Url -ErrorAction Continue -ErrorVariable oErr | Where-Object { $_.title.'#text' -like $ModuleName } | Select-Object @{n='Name';ex={$_.title.'#text'}}, @{n='Version';ex={$_.properties.version}}, @{n='Url';ex={$_.Content.src}}, @{n='Dependencies';ex={$_.properties.Dependencies}} If($oErr) { # Will stop runbook, though message will not be logged Write-Error -Message "Stopping runbook" -ErrorAction Stop } if($SearchResult.Length -and $SearchResult.Length -gt 1) { $SearchResult = $SearchResult | Where-Object -FilterScript { return $_.Name -eq $NewModuleName } } if(!$SearchResult) { throw "Could not find module '$NewModuleName' on PowerShell Gallery." } if ($NewModuleName -notin $Modules.Name) { Write-Output -InputObject "Importing latest version of '$NewModuleName' into your automation account" _doImport ` -ResourceGroupName $ResourceGroupName ` -AutomationAccountName $AutomationAccountName ` -ModuleName $NewModuleName } else { Write-Output -InputObject ("Module $NewModuleName is already in the automation account") } } Write-Output -InputObject "Runbook ended at time: $(get-Date -format r)" |
Now that should have been it, though as there currently is no connection between AA and the Hybrid Workers, modules deployed there are forever in the twilight zone. Therefore I needed to address this also. Note, you will still have to manually install the modules you want the first time, though the below Runbook should help keep them up to date. Though you must remember to install these modules in the first place using “Install-Module”, else the logic will not keep them updated for you.
A couple of things to be aware of, you will need to create a couple of assets in AA.
Namly:
AAResourceGroupName as a string variable with the name of the AA resourcegroup,
AAAccountName as a string variable with the name of the AA account,
AAHybridWorkerName as a string variable with the name of the hybrid worker group,
AAWorkerAdminCredential as a credential object with a user that is local admin on all the workers.
Also the script is set up to use the default RunAs service principal, so the AA account will need to have this set up. You will also need to target the Runbook to run on the hybrid worker group.
Update:
Improvements to the script can be found here:
AzureRM
https://github.com/mortenlerudjordet/runbooks/blob/master/Utility/ARM/Update-AAHybridWorkerModules.ps1Az
https://github.com/mortenlerudjordet/runbooks/blob/master/Utility/ARM/Update-AAHybridWorkerModulesAz.ps1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | <# .SYNOPSIS Triggers update of installed modules on hybrid workers .DESCRIPTION .EXAMPLE .INPUTS .OUTPUTS No output .NOTES NAME: Update-AzureAHybridWorkerModules AUTHOR: Morten Lerudjordet VERSION: 1.0 #> #Requires -Version 5.0 #Requires -Module AzureRM.Profile, AzureRM.Automation Write-Output -InputObject "Starting Runbook at time: $(get-Date -format r). Running PS version: $($PSVersionTable.PSVersion)" $VerbosePreference = "silentlycontinue" Import-Module -Name AzureRM.Profile, AzureRM.Automation -ErrorAction Continue -ErrorVariable oErr If($oErr) { Write-Error -Message "Failed to load needed modules for Runbook. Error: $($oErr.Message)" -ErrorAction Stop } $AutomationResourceGroupName = Get-AutomationVariable -Name "AAResourceGroupName" $AutomationAccountName = Get-AutomationVariable -Name "AAAccountName" $AutomationHybridWorkerName = Get-AutomationVariable -Name "AAHybridWorkerName" $AAworkerCredential = Get-AutomationPSCredential -Name "AAWorkerAdminCredential" # Azure Automation Login for Resource Manager $AzureConnection = Get-AutomationConnection -Name "AzureRunAsConnection" $AzureRunAsCertificate = Get-AutomationCertificate -Name "AzureRunAsCertificate" Try { # ADD certificate if it is not in the cert store of the user if ((Test-Path Cert:\CurrentUser\My\$($AzureConnection.CertificateThumbprint)) -eq $false) { Write-Verbose -Message "Installing the Service Principal's certificate..." $store = new-object System.Security.Cryptography.X509Certificates.X509Store("My", "CurrentUser") $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::MaxAllowed) $store.Add($AzureRunAsCertificate) $store.Close() } $cert = Get-ChildItem -Path Cert:\CurrentUser\my | Where-Object {$_.Thumbprint -eq $($AzureConnection.CertificateThumbprint)} If($($cert.PrivateKey.CspKeyContainerInfo.Accessible) -eq $True) { Write-Verbose -Message "Private key of login certificate is accessible" } Else { Write-Error -Message "Private key of login certificate is NOT accessible, check you user certificate store if the private key is missing or damaged" -ErrorAction Stop } # User must have SP certificate in local cert store Login-AzureRmAccount -ServicePrincipal -ApplicationId $AzureConnection.ApplicationId -CertificateThumbprint $AzureConnection.CertificateThumbprint -TenantId $AzureConnection.TenantId -SubscriptionId $AzureConnection.SubscriptionId # Get names of hybrid workers $AAworkers = (Get-AzureRMAutomationHybridWorkerGroup -Name $AutomationHybridWorkerName -ResourceGroupName $AutomationResourceGroupName -AutomationAccountName $AutomationAccountName -ErrorAction Continue -ErrorVariable oErr).RunbookWorker.Name If($oErr) { Write-Error -Message "Failed to fetch hybrid worker group" -ErrorAction Continue Write-Error -ErrorAction Stop } Write-Output -InputObject "Unloading modules on local worker" Remove-Module -Name AzureRM.Profile, AzureRM.Automation -Force -ErrorAction Continue -ErrorVariable oErr If($oErr) { Write-Error -Message "Failed to unload modules. Error: $($oErr.Message)" -ErrorAction Contine } $CurrentHostName = ([System.Net.Dns]::GetHostByName(($env:computerName))).HostName Write-Output -InputObject "Runbook is currently running on worker: $CurrentHostName" $ScriptBlock = { Import-Module -Name Packagemanagement -ErrorAction Continue -ErrorVariable oErr If($oErr) { Write-Error -Message "Failed to load needed module. Error: $($oErr.Message)" -ErrorAction Stop } $InstalledModules = Get-InstalledModule $VerbosePreference = "Continue" # Redirect Verbose log stream to output stream ForEach($InstalledModule in $InstalledModules) { # Only update modules instelled from gallery If($InstalledModule.Repository -eq "PSGallery") { # Redirecting Verbose stream to Output stream $VerboseLog = Update-Module -Name $InstalledModule.Name -ErrorAction SilentlyContinue -ErrorVariable oErr -Verbose:$True -Confirm:$False 4>&1 If($oErr) { Write-Error -Message "Failed to update module: $($InstalledModule.Name)" -ErrorAction Continue $oErr = $Null } If($VerboseLog) { If($VerboseLog -like "*Skipping installed module*") { Write-Output -InputObject "Module: $($InstalledModule.Name) is up to date running version: $($InstalledModule.Version)" } Else { Write-Output -InputObject "Updating Module: $($InstalledModule.Name)" # Outputting the whole verbose log $VerboseLog } } } Else { Write-Output -InputObject "Module: $($InstalledModule.Name) not in PSGallery, therefore will not autoupdate" } } } ForEach($AAworker in $AAworkers) { Write-Output -InputObject "Processing worker: $AAworker" Write-Output -InputObject "Invoking module update" Invoke-Command -ComputerName $AAworker -Credential $AAworkerCredential -ScriptBlock $ScriptBlock -HideComputerName -ErrorAction SilentlyContinue -ErrorVariable oErr If($oErr) { Write-Error -Message "Error executing remote command against: $AAworker $oErr" -ErrorAction Continue Write-Error -ErrorAction Stop } } } Catch { Throw "An unhandled exception occured. Error Message: $($_.Exception.Message)" } Finally { Write-Output -InputObject "Runbook ended at time: $(get-Date -format r)" } |
Happy tinkering!