Bonne année 2021
Dans ce poste, nous allons aborder la mise à jour des VM à l’aide d’azure update management.
Les entreprises sont de plus en plus sujettes à des attaques informatiques.
Il est donc essentiel de suivre et maintenir les mises à jour sur votre parc.
Présentation:
Azure update management est un composant d’azure automation qui vous permet à l’instar d’SCCM de gérer vos mises à jour.
Pour suivre l’état des mises à jour, update management nécessite de disposer d’un Log Analytics.
Vous trouverez sur le lien suivant un template ARM vous permettant de déployer l’ensemble de la solution:
https://docs.microsoft.com/fr-fr/azure/automation/update-management/enable-from-template
Déployer les agents sur les machines:
Il vous faut déployer les agents sur vos machines avant de rattacher celles-ci à la solution.
Azure met à disposition une initiative (Enable Azure Monitor fo VMs) permettant de déployer les différents agents en fonction des types de machines:
Attention : avant d’exécuter cette police et de rattacher vos machines à votre Log Analytics, vérifiez au préalable si elles ne sont pas déjà rattachées à un autre.
Pour ce faire, vous pouvez utiliser la requête Azure graph suivante (remplacez ## Central Log Analytics Workspace ID ## par l’id de votre Log Analytics, ou supprimez cette ligne):
Resources | where type == "microsoft.compute/virtualmachines/extensions" | where properties.publisher in ("Microsoft.EnterpriseCloud.Monitoring","Microsoft.Azure.Monitor") | where properties.settings.workspaceId != "## Central Log Analytics Workspace ID ##" | extend vmid = split(id,'/') | project properties.type, RG = vmid[4], VMName = vmid[8], location
Vous pouvez vérifier que votre agent a bien été déployé à l’aide de la requête Log Analytics suivante:
Heartbeat | where Computer == "Ma machine virtuelle"
Si votre agent n’apparaît pas, vous pouvez vérifier si il est rattaché au bon Log Analytics à l’aide de la requête azure graph suivante (remplacez ## ENTREZ LE NOM DE VOTRE VM ICI ## par le nom de votre VM):
Resources | where type == "microsoft.compute/virtualmachines/extensions" | where properties.publisher in ("Microsoft.EnterpriseCloud.Monitoring","Microsoft.Azure.Monitor") | extend vmid = split(id,'/'), workspaceId = properties.settings.workspaceId | project properties.type, RG = vmid[4], VMName = vmid[8], location, workspaceId | where VMName == "## ENTREZ LE NOM DE VOTRE VM ICI ##"
Exemple:
Rattacher vos agents à update management:
Pour rattacher vos machines, allez dans la section ‘Update Management’ de votre Automation.
Une fois vos machines rattachées, elles doivent apparaître dans la console de l’update management au bout de 15 minutes.
Si ce n’est pas le cas, vérifiez que votre machine remonte dans la log Update de votre Log Analytics.
Update | where Computer == "ubuntu1804"
Si votre machine ne remonte pas même après redémarrage de votre agent, lancez le script de diagnostic:
- pour windows: https://www.powershellgallery.com/packages/Troubleshoot-WindowsUpdateAgentRegistration/1.1
- Pour linux: https://gallery.technet.microsoft.com/scriptcenter/Troubleshooting-utility-3bcbefe6
Attention, certains systèmes comme redhat8 ne sont pas compatibles (RedHat 8 devrait être rendu compatible mi-février de cette année).
La liste des OS compatibles se trouve ici:
https://docs.microsoft.com/fr-fr/azure/automation/update-management/overview
Si vos machines ont été arrêtées, elles ne seront plus visibles dans la console.
Vous pouvez toutefois vérifier qu’une machine a déjà été rattachée dans le menu hybrid worker groups.
Planifier les mises à jour
La planification des mises à jour se fait via l’icône ‘Schedule update deployment’.
Vous avez à votre disposition deux menus pour sélectionner vos machines ‘Groups to Update’ et ‘Machines to update’.
Le menu ‘Groups to Update’ vous permet de sélectionner vos machines Azure via des filtres (souscription, RG, localisation et tags).
Le menu ‘Machines to update’ vous permet de sélectionner vos machines individuellement ou via une requête.
Attention : la sélection de machines via ce dernier menu nécessite que vos machines soient démarrées.
Option intéressante, update management offre la possibilité d’exécuter des scripts avant et après la mise à jour.
Toutefois, il n’est possible d’exécuter que des scripts powershell.
Microsoft met à disposition des scripts permettant de gérer le démarrage puis l’arrêt de vos machines.
Ces scripts sont disponibles dans le runbooks galery:
- UpdateManagement-TurnOnVms
- UpdateManagement-TurnOffVms
Si vous souhaitez exécuter plusieurs scripts en pré ou post tâche, vous ne pourrez pas le faire via le menu.
Vous devrez soit :
- créer un runbook qui fait appel aux différents runbooks via la commande Start-AzAutomationRunbook ou un appel webhoob
- créer un script unique qui réalise les différentes actions désirées
Le client pour lequel je travaille souhaitait exécuter en préscript les fonctions suivantes
- créer un snapshot pour les machines n’en ayant pas eu de mises à jour depuis longtemps
- Démarrer les machines si elles sont arrêtées
- Ne pas lancer de mises à jour si toutes les machines d’un même AvailabilitySet sont planifiées en même temps
Vous trouverez ci-dessous les scripts réalisés à cet effet (basés sur ‘UpdateManagement-TurnOnVms’ et ‘UpdateManagement-TurnOffVms’):
UpdateManagement-AzVMSnapShot.ps1:
#source: UpdateManagement-TurnOnVms # https://docs.microsoft.com/fr-fr/azure/virtual-machines/windows/snapshot-copy-managed-disk <# .SYNOPSIS Start VMs as part of an Update Management deployment and create a snapshot .DESCRIPTION This script is intended to be run as a part of Update Management Pre/Post scripts. It requires a RunAs account. This script will ensure all Azure VMs in the Update Deployment are running so they recieve updates. This script will store the names of machines that were started in an Automation variable so only those machines are turned back off when the deployment is finished (UpdateManagement-AzTurnOffVMs.ps1) .PARAMETER SoftwareUpdateConfigurationRunContext This is a system variable which is automatically passed in by Update Management during a deployment. #> [CmdletBinding()] Param ( #This is a system variable which is automatically passed in by Update Management during a deployment. [String]$SoftwareUpdateConfigurationRunContext, #this parameter define the percentage of vm in an AvSet who can't be launched in the same update. [double]$avset_purcentage_check = 66.7, #this parameter define if you want to snapshot all vm before the update [ValidateSet('True','False')] [String]$createsnapshot = 'True' ) #region ---modules--- #endregion ---modules--- #region ---Functions--- #endregion ---Functions--- #region ---variables--- #Requires -version 5 #requires -Modules ThreadJob $ErrorActionPreference = 'Stop' #endregion ---variables--- #region ---body--- Try { #region BoilerplateAuthentication #This requires a RunAs account $ServicePrincipalConnection = Get-AutomationConnection -Name 'AzureRunAsConnection' Add-AzAccount ` -ServicePrincipal ` -TenantId $ServicePrincipalConnection.TenantId ` -ApplicationId $ServicePrincipalConnection.ApplicationId ` -CertificateThumbprint $ServicePrincipalConnection.CertificateThumbprint #endregion BoilerplateAuthentication #If you wish to use the run context, it must be converted from JSON $context = ConvertFrom-Json $SoftwareUpdateConfigurationRunContext $vmIds = $context.SoftwareUpdateConfigurationSettings.AzureVirtualMachines $runId = "PrescriptContext" + $context.SoftwareUpdateConfigurationRunId if (!$vmIds) { #Workaround: Had to change JSON formatting $Settings = ConvertFrom-Json $context.SoftwareUpdateConfigurationSettings $vmIds = $Settings.AzureVirtualMachines if (!$vmIds) { throw "No Azure VMs found" } } #region create avset variable $avsetlist = @() foreach ($sub in (Get-AzSubscription).SubscriptionID) { Select-AzSubscription $sub $avsetlist += Get-AzAvailabilitySet | where {$_.VirtualMachinesReferences.id.count -gt 1} } foreach ($avset in $avsetlist) { #check for each AvSet if (the number of vm plan for this update / the number of the VM in the AvSet) is greater than ($avset_purcentage_check / 100) if ( (($avset.VirtualMachinesReferences.id | foreach {if ($_ -in $vmIds) {$_}}).count / $avset.VirtualMachinesReferences.id.count) -gt ($avset_purcentage_check/100)) { throw "AvSet $($avset.Name) in ResourceGroup $($avset.ResourceGroupName) contain more than $avset_purcentage_check% of its VM" } } #endregion create avset variable #https://github.com/azureautomation/runbooks/blob/master/Utility/ARM/Find-WhoAmI # In order to prevent asking for an Automation Account name and the resource group of that AA, # search through all the automation accounts in the subscription # to find the one with a job which matches our job ID Select-AzSubscription -SubscriptionId $ServicePrincipalConnection.SubscriptionID $AutomationResource = Get-AzResource -ResourceType Microsoft.Automation/AutomationAccounts foreach ($Automation in $AutomationResource) { $Job = Get-AzAutomationJob -ResourceGroupName $Automation.ResourceGroupName -AutomationAccountName $Automation.Name -Id $PSPrivateMetadata.JobId.Guid -ErrorAction SilentlyContinue if (!([string]::IsNullOrEmpty($Job))) { $ResourceGroup = $Job.ResourceGroupName $AutomationAccount = $Job.AutomationAccountName break; } } #This is used to store the state of VMs New-AzAutomationVariable -ResourceGroupName $ResourceGroup -AutomationAccountName $AutomationAccount -Name $runId -Value "" -Encrypted $false $updatedMachines = @() $startableStates = "stopped" , "stopping", "deallocated", "deallocating" $jobIDs= New-Object System.Collections.Generic.List[System.Object] #create scriptblock var (this script block create a snapshot and start the VM) $ScriptBlock = { param ( $resource, $vmname, $vmlistid, $vmId, $AutomationJobId ) $vm = Get-AzVM -ResourceGroupName $resource -Name $vmname $snapshot = New-AzSnapshotConfig ` -SourceUri $vm.StorageProfile.OsDisk.ManagedDisk.Id ` -Location $vm.location ` -CreateOption copy New-AzSnapshot ` -Snapshot $snapshot ` -SnapshotName "BeforeUpdate_${vmname}_${AutomationJobId}" ` -ResourceGroupName $resource if ($vmId -in $vmlistid) { Start-AzVM -ResourceGroupName $resource -Name $vmname } } #Parse the list of VMs and start those which are stopped #Azure VMs are expressed by: # subscription/$subscriptionID/resourcegroups/$resourceGroup/providers/microsoft.compute/virtualmachines/$name $vmIds | ForEach-Object { $vmId = $_ $split = $vmId -split "/"; $subscriptionId = $split[2]; $rg = $split[4]; $name = $split[8]; Write-Output ("Subscription Id: " + $subscriptionId) Select-AzSubscription -Subscription $subscriptionId | Out-Null #Query the state of the VM to see if it's already running or if it's already started $state = ($(Get-AzVM -ResourceGroupName $rg -Name $name -Status).Statuses[1].DisplayStatus -split " ")[1] if($state -in $startableStates) { Write-Output "Starting '$($name)' ..." #Store the VM we started so we remember to shut it down later $updatedMachines += $vmId if ($createsnapshot -eq 'False') { $newJob = Start-ThreadJob -ScriptBlock { param($resource, $vmname) Start-AzVM -ResourceGroupName $resource -Name $vmname} -ArgumentList $rg,$name $jobIDs.Add($newJob.Id) } } else { Write-Output ($name + ": no action taken. State: " + $state) } # create a snapshot and start the vm if $createsnapshot -eq $true if ($createsnapshot -eq 'True') { $newJob = Start-ThreadJob -ScriptBlock $ScriptBlock -ArgumentList $rg,$name,$updatedMachines,$vmId,$($context.SoftwareUpdateConfigurationRunId) $jobIDs.Add($newJob.Id) } } $updatedMachinesCommaSeperated = $updatedMachines -join "," #Wait until all machines have finished starting before proceeding to the Update Deployment $jobsList = $jobIDs.ToArray() if ($jobsList) { Write-Output "Waiting for machines to finish starting..." Wait-Job -Id $jobsList } foreach($id in $jobsList) { $job = Get-Job -Id $id if ($job.Error) { Write-Output $job.Error } } Write-output $updatedMachinesCommaSeperated #Store output in the automation variable Set-AzAutomationVariable -Name $runId -Value $updatedMachinesCommaSeperated -ResourceGroupName $ResourceGroup -AutomationAccountName $AutomationAccount -Encrypted $false } Catch { $ErrorMessage = $_.Exception.Message $ErrorLine = $_.InvocationInfo.ScriptLineNumber Write-error "UpdateManagement-AzVMSnapShot : Error on line $ErrorLine. The error message was: $ErrorMessage" } #endregion ---body---
UpdateManagement-AzTurnOffVms.ps1:
#source: UpdateManagement-TurnOffVms <#PSScriptInfo .VERSION 1.1 .GUID 9606f2a1-49f8-4a67-91d6-23fc6ebf5b3b .AUTHOR zachal .COMPANYNAME Microsoft .COPYRIGHT .TAGS UpdateManagement, Automation .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES ThreadJob .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES Removed parameters AutomationAccount, ResourceGroup .PRIVATEDATA #> <# .DESCRIPTION This script is intended to be run as a part of Update Management Pre/Post scripts. It requires a RunAs account. This script will ensure all Azure VMs in the Update Deployment are running so they recieve updates. This script works with the Turn Off VMs script. It will store the names of machines that were started in an Automation variable so only those machines are turned back off when the deployment is finished. #> <# .SYNOPSIS Stop VMs that were started as part of an Update Management deployment .DESCRIPTION This script is intended to be run as a part of Update Management Pre/Post scripts. It requires a RunAs account. This script will turn off all Azure VMs that were started as part of TurnOnVMs.ps1. It retrieves the list of VMs that were started from an Automation Account variable. .PARAMETER SoftwareUpdateConfigurationRunContext This is a system variable which is automatically passed in by Update Management during a deployment. #> param( [string]$SoftwareUpdateConfigurationRunContext ) #region ---variables--- #Requires -version 5 #requires -Modules ThreadJob $ErrorActionPreference = 'Stop' #endregion ---variables--- #region ---body--- Try { #region BoilerplateAuthentication #This requires a RunAs account $ServicePrincipalConnection = Get-AutomationConnection -Name 'AzureRunAsConnection' Add-AzAccount ` -ServicePrincipal ` -TenantId $ServicePrincipalConnection.TenantId ` -ApplicationId $ServicePrincipalConnection.ApplicationId ` -CertificateThumbprint $ServicePrincipalConnection.CertificateThumbprint Select-AzSubscription -SubscriptionId $ServicePrincipalConnection.SubscriptionID | Out-Null #endregion BoilerplateAuthentication #If you wish to use the run context, it must be converted from JSON $context = ConvertFrom-Json $SoftwareUpdateConfigurationRunContext $runId = "PrescriptContext" + $context.SoftwareUpdateConfigurationRunId #https:/github.com/azureautomation/runbooks/blob/master/Utility/ARM/Find-WhoAmI # In order to prevent asking for an Automation Account name and the resource group of that AA, # search through all the automation accounts in the subscription # to find the one with a job which matches our job ID $AutomationResource = Get-AzResource -ResourceType Microsoft.Automation/AutomationAccounts foreach ($Automation in $AutomationResource) { $Job = Get-AzAutomationJob -ResourceGroupName $Automation.ResourceGroupName -AutomationAccountName $Automation.Name -Id $PSPrivateMetadata.JobId.Guid -ErrorAction SilentlyContinue if (!([string]::IsNullOrEmpty($Job))) { $ResourceGroup = $Job.ResourceGroupName $AutomationAccount = $Job.AutomationAccountName break; } } #Retrieve the automation variable, which we named using the runID from our run context. #See: https://docs.microsoft.com/en-us/azure/automation/automation-variables#activities $variable = Get-AutomationVariable -Name $runId if (!$variable) { Write-Output "No machines to turn off" return } $vmIds = $variable -split "," $stoppableStates = "starting", "running" $jobIDs= New-Object System.Collections.Generic.List[System.Object] #This script can run across subscriptions, so we need unique identifiers for each VMs #Azure VMs are expressed by: # subscription/$subscriptionID/resourcegroups/$resourceGroup/providers/microsoft.compute/virtualmachines/$name $vmIds | ForEach-Object { $vmId = $_ $split = $vmId -split "/"; $subscriptionId = $split[2]; $rg = $split[4]; $name = $split[8]; Write-Output ("Subscription Id: " + $subscriptionId) Select-AzSubscription -Subscription $subscriptionId | Out-Null $vm = Get-AzVM -ResourceGroupName $rg -Name $name -Status $state = ($vm.Statuses[1].DisplayStatus -split " ")[1] if($state -in $stoppableStates) { Write-Output "Stopping '$($name)' ..." $newJob = Start-ThreadJob -ScriptBlock { param($resource, $vmname) Stop-AzVM -ResourceGroupName $resource -Name $vmname -Force} -ArgumentList $rg,$name $jobIDs.Add($newJob.Id) }else { Write-Output ($name + ": already stopped. State: " + $state) } } #Wait for all machines to finish stopping so we can include the results as part of the Update Deployment $jobsList = $jobIDs.ToArray() if ($jobsList) { Write-Output "Waiting for machines to finish stopping..." Wait-Job -Id $jobsList } foreach($id in $jobsList) { $job = Get-Job -Id $id if ($job.Error) { Write-Output $job.Error } } } Catch { $ErrorMessage = $_.Exception.Message $ErrorLine = $_.InvocationInfo.ScriptLineNumber Write-error "New-VMSnapShot : Error on line $ErrorLine. The error message was: $ErrorMessage" } finally { #Clean up our variables: Remove-AzAutomationVariable -AutomationAccountName $AutomationAccount -ResourceGroupName $ResourceGroup -name $runID } #endregion ---body---
Conclusion:
Update management est assez simple à prendre en main.
Il a toutefois quelques limitations:
– il ne prend pas en charge tous les types d’OS (par exemple redhat8 pour le moment)
– il ne supporte le rattachement que d’un seul Log Analytics par machine
– l’exécution de pré et post script ne supporte que powershell 5.1 (les versions supérieures n’étant pas prises en charge pour le moment par azure automation).
A propos de l’auteur
Article écrit par Vincent Fléchier
15 ans d’expertise dans l’IT dont 3 dans le Cloud Azure, Vincent est certifié Azure Cloud Architect.
Ses spécialistés: powershell, ansible, azure…