Gérez vos mises à jour avec Azure Update Management

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:

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…