├── LICENSE ├── README.md ├── bicep ├── ModuleVM.bicep └── main.bicep ├── docs ├── quickstart-infrastructure.png ├── reporting-mail.png ├── tag_syntax.png └── update-management-big-picture.png ├── runbooks ├── UM-CleanUp-Schedules.ps1 ├── UM-CleanUp-Snapshots.ps1 ├── UM-PostTasks.ps1 ├── UM-PreTasks.ps1 └── UM-ScheduleUpdatesWithVmsTags.ps1 └── terraform └── baseInfra.tf /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Santiago / Vincent Misson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Update Management - Schedule VM patching with tags 2 | 3 | This repo is a set of Runbooks that allows you to schedule Azure Virtual Machines patching by simply applying the `POLICY_UPDATE` tag on machines. 4 | 5 | - [Azure Update Management - Schedule VM patching with tags](#azure-update-management---schedule-vm-patching-with-tags) 6 | - [Global Picture](#global-picture) 7 | - [`POLICY_UPDATE` syntax](#policy_update-syntax) 8 | - [Features](#features) 9 | - [Unsupported scenarios](#unsupported-scenarios) 10 | - [Runbooks description](#runbooks-description) 11 | - [Limitations](#limitations) 12 | - [Getting started](#getting-started) 13 | - [Quick deployment (for testing purpose)](#quick-deployment-for-testing-purpose) 14 | - [Production Deployment & Recommendations](#production-deployment--recommendations) 15 | - [Authors](#authors) 16 | 17 | # Global Picture 18 | 19 | ![BigPicture](docs/update-management-big-picture.png) 20 | 21 | Shared Runbooks need the following initial bricks to work: 22 | * A Log Analytics Workspace 23 | * An Automation Account linked to the Log Analytics Workspace 24 | * The Automation account must use a System-assigned Managed Identity 25 | * The System-assigned Managed Identity must have **Contributor** role 26 | * assigned on all subscriptions where machines (Azure VMs or Azure Arc Servers) can be found 27 | * assigned to the Resource Group where the Automation Account is located 28 | * Machines that need to be patched must have: 29 | * Log Analytics Agent Extension installed, and linked to the Log Analytics Workspace 30 | * `POLICY_UPDATE` key tag declared with appropriated value 31 | 32 | ## `POLICY_UPDATE` syntax 33 | 34 | Here is the syntax to follow for the `POLICY_UPDATE` tag: 35 | 36 | ![POLICY_UPDATE Syntax](docs/tag_syntax.png) 37 | 38 | **Examples**: 39 | * VM1 - `POLICY_UPDATE=Friday;10:00 PM;Never;*java*;` will be patched every Friday, at 10:00 PM. Even if updates require reboot, the VM will not be rebooted. Packages containing `java` string will be excluded. 40 | * VM2 - `POLICY_UPDATE=Tuesday,Sunday;08:00 AM;IfRequired;;TeamA@abc.com` will be patched every Tuesday and Sunday, at 08:00 AM. The VM will be rebooted only if a patch needs the machine to be reboot to be taken into account. No excluded packages. When patching is done, TeamA@abc.com will receive the list of updated packages by mail. 41 | * VM3 - `POLICY_UPDATE=Sunday;07:00 PM;Always;;` will be patched every Synday at 07:00 PM. The VM will be rebooted after applying patches, even if it is not required. No excluded packages. 42 | * VM4 - `POLICY_UPDATE=Monday;01:00 PM;Always;*java*,*oracle*;TeamB@abc.com` will be patched every Monday at 01:00 PM. The VM will be rebooted after applying patches. Packages containing `java` or `oracle` string will be excluded. When patching is done, TeamB@abc.com will receive the list of updated packages by mail. 43 | 44 | ## Features 45 | 46 | Shared Runbooks allows you to: 47 | * Patch **Azure Virtual Machines** and **Azure Arc Servers** with [Supported OS](https://docs.microsoft.com/en-us/azure/automation/update-management/operating-system-requirements#supported-operating-systems) 48 | * Patch in a multi-subscriptions context: the system-assigned managed identity must have **Contributor** role assigned on each subscription. 49 | * Perform several pre and post patching tasks: 50 | * Pre scripts, before patching: 51 | * [OPTIONAL] Snapshot VM OS disk 52 | * [OPTIONAL] Start VM if it is stopped 53 | * Post scripts, after patching: 54 | * [OPTIONAL] Shutdown VM if it was started by pre-script. 55 | * [OPTIONAL] Send a patching report email 56 | * Support Azure Arc Server 57 | * Pre-scripts and post scripts are not supported for Azure Arc Servers 58 | * You can schedule Azure Arc Servers restarts using `POLICY_RESTART` tag with Runbooks available in [this repo](https://github.com/dawlysd/schedule-azure-arc-servers-restarts-with-tags) 59 | 60 | ## Unsupported scenarios 61 | 62 | Below Azure Resource cannot be patched by Azure Update Management: 63 | * [VMSS](https://docs.microsoft.com/en-us/azure/virtual-machine-scale-sets/overview): Leverage [automatic OS image upgrade](https://docs.microsoft.com/en-us/azure/virtual-machine-scale-sets/virtual-machine-scale-sets-automatic-upgrade) instead. 64 | * [AVD](https://docs.microsoft.com/en-us/azure/virtual-desktop/): Leverage [automatic updates](https://docs.microsoft.com/en-us/azure/virtual-desktop/configure-automatic-updates) instead. 65 | * [AKS](https://docs.microsoft.com/en-us/azure/aks/): Leverage [auto-upgrade channel](https://docs.microsoft.com/en-US/azure/aks/upgrade-cluster#set-auto-upgrade-channel) and [planned-maintenance](https://docs.microsoft.com/en-us/azure/aks/planned-maintenance) instead. 66 | 67 | ## Runbooks description 68 | 69 | There is a set of 5 Runbooks that must be deployed in the Automation Account: 70 | * **UM-ScheduleUpdatesWithVmsTags**: Must be scheduled (at least) daily. Searches for all machines with the `POLICY_UPDATE` tag and configures the Update Management schedules. 71 | * **UM-PreTasks**: Triggered before patching, it can perform several optional actions like OS disk snapshot, start VM if stopped, etc.. 72 | * **UM-PostTasks**: Triggered after patching, it can perform several optional actions like stop VM if it was started, send patching report mail, etc.. 73 | * **UM-CleanUp-Snapshots**: Must be scheduled daily, to delete snapshots that are X days older. 74 | * **UM-CleanUp-Schedules**: Must be schedule (at least) daily. It removes Update Management schedules for VM machines that not longer have the `POLICY_UPDATE` tag 75 | 76 | ## Prerequisites 77 | 78 | Automation Account must have the following modules installed: 79 | * Az.ResourceGraph, >= 0.11.0 80 | * Az.ConnectedMachine >= 0.2.0 81 | * Az.Automation >= 1.7.1 82 | * Az.Compute >= 4.17.1 83 | 84 | **Note**: Runbooks must be deployed using Powershell Runtime v5.1 85 | 86 | ## Limitations 87 | 88 | * VMs must have a unique name. It is not possible to have several VMs with the same name using `POLICY_UPDATE` tag. 89 | * A VM must be part of a single schedule. If it is not the case, *UM-CleanUp-Schedules* has side effects. 90 | * When the `POLICY_UPDATE` tag is applied, the first patching can be done the day after the execution of *UM-ScheduleUpdatesWithVmsTags*. 91 | 92 | # Getting started 93 | 94 | ## Quick deployment (for testing purpose) 95 | 96 | To quickly test provided Runbooks, use the provided bicep script to deploy a complete testing infrastructure that will: 97 | * Deploy a Log Analytics Workspace 98 | * Deploy an Automation Account using a System-assigned Magaged Identity 99 | * Install Update Management solution 100 | * Assign *Contributor* role on the System-assigned Managed Identity to the Resource Group 101 | * Deploy 5 Runbooks to the Automation Accounts and schedule few of them 102 | * Deploy 1 VNet 103 | * Deploy 6 VMs (3 Windows, 3 Linux) in the VNet 104 | * with Log Analytics agent installed and plugged to the Log Analytics Workspace 105 | * with `POLICY_UPDATE` tag examples 106 | * If you want to receive a **mail report**, 2 variables in the Automation Account needs to be defined : 107 | * SendGridAPIKey (type: secure string): API access key provided by SendGrid to use your account 108 | * SendGridSender (type: string): Sender email address (from) configured on SendGrid 109 | 110 | Here is a *partial* screenshot of deployed resources: 111 | ![Infrastructure](docs/quickstart-infrastructure.png) 112 | 113 | **Quick start:** 114 | 115 | * Prerequisites: 116 | ```bash 117 | # Create a resource group 118 | $ az group create --location westeurope --name MyRg 119 | 120 | $ git clone https://github.com/dawlysd/azure-update-management-with-tags.git 121 | ... 122 | $ cd azure-update-management-with-tags/bicep 123 | ``` 124 | 125 | * Deploy **without email feature**: 126 | ```bash 127 | $ az deployment group create --resource-group MyRg --template-file main.bicep 128 | ``` 129 | If you want to enable patching report email feature later, just update `SendGridSender` and `SendGridAPIKey` Automation Account variables. 130 | 131 | * Deploy **with email feature**: 132 | ```bash 133 | $ az deployment group create --resource-group MyRg --template-file main.bicep --parameters SendGridSender="no-reply@mydomain.fr" SendGridAPIKey="SG.XXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXX" 134 | ``` 135 | Infrastructure deployment will take around 5 minutes and it can take until 20 minutes to have update agent ready and first patching assessment. 136 | 137 | 138 | ## Production Deployment & Recommendations 139 | 140 | For scaling up of this model in production, we recommend: 141 | * Minimize the number of Log analytics workspace and Automation Account. 142 | * Option 1: Using a single log analytics workspace and a single automation account. Provided runbooks support this option. 143 | * Option 2: Having one log analytics workspace and one automation account per geo. Provided runbooks must be updated to adopt this option. 144 | * Leverage Policies to install Log Analytics agent on VMs 145 | * [Built-in Deploy Log Analytics agent for Linux VMs](https://portal.azure.com/#blade/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F053d3325-282c-4e5c-b944-24faffd30d77) 146 | * [Built-in Deploy - Configure Log Analytics agent to be enabled on Windows virtual machines](https://portal.azure.com/#blade/Microsoft_Azure_Policy/PolicyDetailBlade/definitionId/%2Fproviders%2FMicrosoft.Authorization%2FpolicyDefinitions%2F0868462e-646c-4fe3-9ced-a733534b6a2c) 147 | * Track all unpatched VMs with UM (due to incompatible OS…) 148 | 149 | # Authors 150 | 151 | [Vincent Misson](https://github.com/vmisson) & [David Santiago](https://github.com/dawlysd) 152 | -------------------------------------------------------------------------------- /bicep/ModuleVM.bicep: -------------------------------------------------------------------------------- 1 | param VmName string 2 | param VmLocation string 3 | param VmSize string 4 | param VmOsType string 5 | param VmOsPublisher string 6 | param VmOsOffer string 7 | param VmOsSku string 8 | param VmOsVersion string 9 | param VmNicSubnetId string 10 | param WorkspaceId string 11 | param WorkspaceKey string 12 | 13 | var VmOsDiskName = '${VmName}od01' 14 | var VmNicName = '${VmName}ni01' 15 | 16 | param tags_policy_update string 17 | param adminUsername string 18 | param adminPassword string 19 | 20 | resource Nic 'Microsoft.Network/networkInterfaces@2020-08-01' = { 21 | name: VmNicName 22 | location: VmLocation 23 | properties: { 24 | ipConfigurations: [ 25 | { 26 | name: 'ipconfig1' 27 | properties: { 28 | privateIPAllocationMethod: 'Dynamic' 29 | subnet: { 30 | id: VmNicSubnetId 31 | } 32 | primary: true 33 | } 34 | } 35 | ] 36 | dnsSettings: { 37 | dnsServers: [] 38 | } 39 | enableAcceleratedNetworking: false 40 | enableIPForwarding: false 41 | } 42 | } 43 | 44 | resource VirtualMachine 'Microsoft.Compute/virtualMachines@2019-07-01' = { 45 | name: VmName 46 | location: VmLocation 47 | tags: { 48 | POLICY_UPDATE: tags_policy_update 49 | } 50 | properties: { 51 | hardwareProfile: { 52 | vmSize: VmSize 53 | } 54 | storageProfile: { 55 | osDisk: { 56 | name: VmOsDiskName 57 | createOption: 'FromImage' 58 | osType: VmOsType 59 | } 60 | imageReference: { 61 | publisher: VmOsPublisher 62 | offer: VmOsOffer 63 | sku: VmOsSku 64 | version: VmOsVersion 65 | } 66 | } 67 | osProfile: { 68 | computerName: VmName 69 | adminUsername: adminUsername 70 | adminPassword: adminPassword 71 | } 72 | networkProfile: { 73 | networkInterfaces: [ 74 | { 75 | id: Nic.id 76 | } 77 | ] 78 | } 79 | } 80 | } 81 | 82 | output VirtualMachineId string = VirtualMachine.id 83 | 84 | resource VmLinuxLaw 'Microsoft.Compute/virtualMachines/extensions@2021-04-01' = if (VmOsType == 'Linux') { 85 | name: '${VmName}/OmsAgentForLinux' 86 | location: VmLocation 87 | properties:{ 88 | publisher: 'Microsoft.EnterpriseCloud.Monitoring' 89 | type: 'OmsAgentForLinux' 90 | typeHandlerVersion: '1.13' 91 | autoUpgradeMinorVersion: true 92 | settings:{ 93 | workspaceId: WorkspaceId 94 | } 95 | protectedSettings:{ 96 | workspaceKey: WorkspaceKey 97 | } 98 | } 99 | dependsOn:[ 100 | VirtualMachine 101 | Nic 102 | ] 103 | } 104 | 105 | 106 | resource VmWindowsLaw 'Microsoft.Compute/virtualMachines/extensions@2021-04-01' = if (VmOsType == 'Windows') { 107 | name: '${VmName}/MicrosoftMonitoringAgent' 108 | location: VmLocation 109 | properties:{ 110 | publisher: 'Microsoft.EnterpriseCloud.Monitoring' 111 | type: 'MicrosoftMonitoringAgent' 112 | typeHandlerVersion: '1.0' 113 | autoUpgradeMinorVersion: true 114 | settings:{ 115 | workspaceId: WorkspaceId 116 | } 117 | protectedSettings:{ 118 | workspaceKey: WorkspaceKey 119 | } 120 | } 121 | dependsOn:[ 122 | VirtualMachine 123 | Nic 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /bicep/main.bicep: -------------------------------------------------------------------------------- 1 | // This script is given for testing purpose 2 | // 3 | // For a given RG, it will 4 | // - Deploy a Log Analytics Workspace 5 | // - Deploy an Automation account using a System-assigned Managed Identity, linked to the Log Analytics Workspace, with Update Management solution installed 6 | // - Give Contributor role to System-assigned Managed Identity to the RG 7 | // - Deploy Runbooks to Automation Account 8 | // - Deploy 1 VNet 9 | // - Deploy 6 VMs (3 Windows, 3 Linux) in the VNet, with Log analytics agent plugged to the Log Analytics Workspace 10 | // 11 | // Example of execution: 12 | // az deployment group create --resource-group MyRg --template-file main.bicep 13 | 14 | // Location 15 | param Location string = 'West Europe' 16 | param LocationShort string = 'weu' 17 | 18 | // Vnet 19 | param VNetName string = '${LocationShort}-spoke-vnet' 20 | param VNetAddressSpace array = [ 21 | '10.0.0.0/16' 22 | ] 23 | param VnetSubnetWorkloadAddressSpace string = '10.0.0.0/24' 24 | 25 | // Log Analytic Workspace 26 | param LawName string = 'poc-updatemanamgenet-lag03' 27 | param LawSku string = 'pergb2018' 28 | 29 | param AutomationAccountName string = 'automationaccount03' 30 | 31 | // VMs 32 | param VmSize string = 'Standard_B2s' 33 | param VmWindows_2019DC bool = true 34 | param VmWindows_2016 bool = true 35 | param VmWindows_2012R2 bool = true 36 | param VmUbuntu_20_04 bool = true 37 | param VmUbuntu_18_04 bool = true 38 | param VmCentOS_7_4 bool = true 39 | param adminUsername string = 'User01' 40 | param adminPassword string = 'HelloFromPatchingManager!)1=&' 41 | 42 | // Mail 43 | param SendGridAPIKey string = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' 44 | param SendGridSender string = 'noreplay@mydomain.com' 45 | 46 | resource Lag 'Microsoft.OperationalInsights/workspaces@2021-06-01' = { 47 | name: LawName 48 | location: Location 49 | properties:{ 50 | sku:{ 51 | name: LawSku 52 | } 53 | } 54 | } 55 | 56 | resource AutomationAccount 'Microsoft.Automation/automationAccounts@2020-01-13-preview' = { 57 | name: AutomationAccountName 58 | location: Location 59 | identity: { 60 | type: 'SystemAssigned' 61 | } 62 | properties:{ 63 | sku: { 64 | name: 'Basic' 65 | } 66 | } 67 | } 68 | 69 | 70 | resource Az_ResourceGraph 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = { 71 | name: '${AutomationAccountName}/Az.ResourceGraph' 72 | dependsOn: [ 73 | AutomationAccount 74 | ] 75 | properties: { 76 | contentLink: { 77 | uri: 'https://www.powershellgallery.com/api/v2/package/Az.ResourceGraph/0.11.0' 78 | version: '0.11.0' 79 | } 80 | } 81 | } 82 | 83 | resource LinkAutomationAccount 'Microsoft.OperationalInsights/workspaces/linkedServices@2020-08-01' = { 84 | name: '${Lag.name}/Automation' 85 | dependsOn:[ 86 | Lag 87 | AutomationAccount 88 | ] 89 | properties: { 90 | resourceId: AutomationAccount.id 91 | } 92 | } 93 | 94 | resource Updates 'Microsoft.OperationsManagement/solutions@2015-11-01-preview' = { 95 | name: 'Updates(${LawName})' 96 | location: Location 97 | plan: { 98 | name: 'Updates(${LawName})' 99 | product: 'OMSGallery/Updates' 100 | promotionCode: '' 101 | publisher: 'Microsoft' 102 | } 103 | properties: { 104 | workspaceResourceId: Lag.id 105 | } 106 | } 107 | 108 | resource Runbook_ScheduleUpdatesWithVmsTags 'Microsoft.Automation/automationAccounts/runbooks@2019-06-01' = { 109 | name: '${AutomationAccount.name}/UM-ScheduleUpdatesWithVmsTags' 110 | location: Location 111 | properties:{ 112 | runbookType: 'PowerShell' 113 | logProgress: false 114 | logVerbose: false 115 | logActivityTrace: 0 116 | publishContentLink: { 117 | uri: 'https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-ScheduleUpdatesWithVmsTags.ps1' 118 | } 119 | } 120 | } 121 | 122 | resource Runbook_PreTasks 'Microsoft.Automation/automationAccounts/runbooks@2019-06-01' = { 123 | name: '${AutomationAccount.name}/UM-PreTasks' 124 | location: Location 125 | properties:{ 126 | runbookType: 'PowerShell' 127 | logProgress: false 128 | logVerbose: false 129 | logActivityTrace: 0 130 | publishContentLink: { 131 | uri: 'https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-PreTasks.ps1' 132 | } 133 | } 134 | } 135 | 136 | resource Runbook_PostTasks 'Microsoft.Automation/automationAccounts/runbooks@2019-06-01' = { 137 | name: '${AutomationAccount.name}/UM-PostTasks' 138 | location: Location 139 | properties:{ 140 | runbookType: 'PowerShell' 141 | logProgress: false 142 | logVerbose: false 143 | logActivityTrace: 0 144 | publishContentLink: { 145 | uri: 'https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-PostTasks.ps1' 146 | } 147 | } 148 | } 149 | 150 | resource Runbook_CleanUpSchedules 'Microsoft.Automation/automationAccounts/runbooks@2019-06-01' = { 151 | name: '${AutomationAccount.name}/UM-CleanUp-Schedules' 152 | location: Location 153 | properties:{ 154 | runbookType: 'PowerShell' 155 | logProgress: false 156 | logVerbose: false 157 | logActivityTrace: 0 158 | publishContentLink: { 159 | uri: 'https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-CleanUp-Schedules.ps1' 160 | } 161 | } 162 | } 163 | 164 | resource Runbook_CleanUpSnapshots 'Microsoft.Automation/automationAccounts/runbooks@2019-06-01' = { 165 | name: '${AutomationAccount.name}/UM-CleanUp-Snapshots' 166 | location: Location 167 | properties:{ 168 | runbookType: 'PowerShell' 169 | logProgress: false 170 | logVerbose: false 171 | logActivityTrace: 0 172 | publishContentLink: { 173 | uri: 'https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-CleanUp-Snapshots.ps1' 174 | } 175 | } 176 | } 177 | 178 | resource DailySchedule 'Microsoft.Automation/automationAccounts/schedules@2020-01-13-preview' = { 179 | name: '${AutomationAccount.name}/Schedules-ScheduleVmsWithTags' 180 | properties:{ 181 | description: 'Schedule daily' 182 | startTime: '' 183 | frequency: 'Day' 184 | interval: 1 185 | } 186 | } 187 | 188 | param Sched1Guid string = newGuid() 189 | resource ScheduleRunbook_ScheduleUpdatesWithVmsTags 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = { 190 | name: '${AutomationAccount.name}/${Sched1Guid}' 191 | properties:{ 192 | schedule:{ 193 | name: split(DailySchedule.name, '/')[1] 194 | } 195 | runbook:{ 196 | name: split(Runbook_ScheduleUpdatesWithVmsTags.name, '/')[1] 197 | } 198 | } 199 | } 200 | 201 | param Sched2Guid string = newGuid() 202 | resource ScheduleRunbook_CleanUpSnapshots 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = { 203 | name: '${AutomationAccount.name}/${Sched2Guid}' 204 | properties:{ 205 | schedule:{ 206 | name: split(DailySchedule.name, '/')[1] 207 | } 208 | runbook:{ 209 | name: split(Runbook_CleanUpSnapshots.name, '/')[1] 210 | } 211 | } 212 | } 213 | 214 | param Sched3Guid string = newGuid() 215 | resource ScheduleRunbook_CleanUpSchedules 'Microsoft.Automation/automationAccounts/jobSchedules@2020-01-13-preview' = { 216 | name: '${AutomationAccount.name}/${Sched3Guid}' 217 | properties:{ 218 | schedule:{ 219 | name: split(DailySchedule.name, '/')[1] 220 | } 221 | runbook:{ 222 | name: split(Runbook_CleanUpSchedules.name, '/')[1] 223 | } 224 | } 225 | } 226 | 227 | resource Variable_SendGridAPIKey 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = { 228 | name: '${AutomationAccount.name}/SendGridAPIKey' 229 | properties: { 230 | isEncrypted: true 231 | value: '"${SendGridAPIKey}"' 232 | } 233 | } 234 | 235 | resource Variable_SendGridSender 'Microsoft.Automation/automationAccounts/variables@2020-01-13-preview' = { 236 | name: '${AutomationAccount.name}/SendGridSender' 237 | properties: { 238 | isEncrypted: false 239 | value: '"${SendGridSender}"' 240 | } 241 | } 242 | 243 | param assignmentName string = guid(resourceGroup().id) 244 | resource SystemAssignedManagedIdentityRgContributor 'Microsoft.Authorization/roleAssignments@2020-03-01-preview' = { 245 | name: assignmentName 246 | properties: { 247 | roleDefinitionId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c' 248 | principalId: AutomationAccount.identity.principalId 249 | } 250 | dependsOn:[ 251 | AutomationAccount 252 | // Workaround because AutomationAccount.identity.principalId takes time to be available ... 253 | // This workaround avoid the PrincipalNotFound error message 254 | Win01 255 | ] 256 | } 257 | 258 | resource Vnet 'Microsoft.Network/virtualNetworks@2020-08-01' = { 259 | name: VNetName 260 | location: Location 261 | properties: { 262 | addressSpace: { 263 | addressPrefixes: VNetAddressSpace 264 | } 265 | subnets: [ 266 | { 267 | name: 'workload' 268 | properties: { 269 | addressPrefix: VnetSubnetWorkloadAddressSpace 270 | } 271 | } 272 | ] 273 | virtualNetworkPeerings: [] 274 | enableDdosProtection: false 275 | } 276 | } 277 | 278 | module Win01 'ModuleVM.bicep' = if (VmWindows_2019DC) { 279 | name: 'Win01' 280 | params:{ 281 | VmName: 'VmWin2019DC' 282 | VmLocation: Location 283 | VmSize: VmSize 284 | VmOsType: 'Windows' 285 | VmOsPublisher: 'MicrosoftWindowsServer' 286 | VmOsOffer: 'WindowsServer' 287 | VmOsSku: '2019-Datacenter' 288 | VmOsVersion: 'latest' 289 | VmNicSubnetId: Vnet.properties.subnets[0].id 290 | WorkspaceId: Lag.properties.customerId 291 | WorkspaceKey: listKeys(Lag.id, '2015-03-20').primarySharedKey 292 | adminUsername: adminUsername 293 | adminPassword: adminPassword 294 | tags_policy_update: 'Friday;10:00 PM;Never;*java*;' 295 | } 296 | dependsOn:[ 297 | Lag 298 | Vnet 299 | ] 300 | } 301 | 302 | module Win02 'ModuleVM.bicep' = if (VmWindows_2016) { 303 | name: 'Win02' 304 | params:{ 305 | VmName: 'VmWin2016' 306 | VmLocation: Location 307 | VmSize: VmSize 308 | VmOsType: 'Windows' 309 | VmOsPublisher: 'MicrosoftWindowsServer' 310 | VmOsOffer: 'WindowsServer' 311 | VmOsSku: '2016-datacenter-gensecond' 312 | VmOsVersion: 'latest' 313 | VmNicSubnetId: Vnet.properties.subnets[0].id 314 | WorkspaceId: Lag.properties.customerId 315 | WorkspaceKey: listKeys(Lag.id, '2015-03-20').primarySharedKey 316 | adminUsername: adminUsername 317 | adminPassword: adminPassword 318 | tags_policy_update: 'Tuesday,Sunday;08:00 AM;IfRequired;;TeamA@abc.com' 319 | } 320 | dependsOn:[ 321 | Lag 322 | Vnet 323 | ] 324 | } 325 | 326 | module Win03 'ModuleVM.bicep' = if (VmWindows_2012R2) { 327 | name: 'Win03' 328 | params:{ 329 | VmName: 'VmWin2012R2' 330 | VmLocation: Location 331 | VmSize: VmSize 332 | VmOsType: 'Windows' 333 | VmOsPublisher: 'MicrosoftWindowsServer' 334 | VmOsOffer: 'WindowsServer' 335 | VmOsSku: '2012-r2-datacenter-gensecond' 336 | VmOsVersion: 'latest' 337 | VmNicSubnetId: Vnet.properties.subnets[0].id 338 | WorkspaceId: Lag.properties.customerId 339 | WorkspaceKey: listKeys(Lag.id, '2015-03-20').primarySharedKey 340 | adminUsername: adminUsername 341 | adminPassword: adminPassword 342 | tags_policy_update: '' 343 | } 344 | dependsOn:[ 345 | Lag 346 | Vnet 347 | ] 348 | } 349 | 350 | module Lnx01 'ModuleVM.bicep' = if (VmUbuntu_20_04) { 351 | name: 'Lnx01' 352 | params:{ 353 | VmName: 'VmUbuntu2004' 354 | VmLocation: Location 355 | VmSize: VmSize 356 | VmOsType: 'Linux' 357 | VmOsPublisher: 'canonical' 358 | VmOsOffer: '0001-com-ubuntu-server-focal' 359 | VmOsSku: '20_04-lts' 360 | VmOsVersion: 'latest' 361 | VmNicSubnetId: Vnet.properties.subnets[0].id 362 | WorkspaceId: Lag.properties.customerId 363 | WorkspaceKey: listKeys(Lag.id, '2015-03-20').primarySharedKey 364 | adminUsername: adminUsername 365 | adminPassword: adminPassword 366 | tags_policy_update: 'Sunday;07:00 PM;Always;;' 367 | } 368 | dependsOn:[ 369 | Lag 370 | Vnet 371 | ] 372 | } 373 | 374 | module Lnx02 'ModuleVM.bicep' = if (VmUbuntu_18_04) { 375 | name: 'Lnx02' 376 | params:{ 377 | VmName: 'VmUbuntu1804' 378 | VmLocation: Location 379 | VmSize: VmSize 380 | VmOsType: 'Linux' 381 | VmOsPublisher: 'canonical' 382 | VmOsOffer: 'UbuntuServer' 383 | VmOsSku: '18_04-lts-gen2' 384 | VmOsVersion: 'latest' 385 | VmNicSubnetId: Vnet.properties.subnets[0].id 386 | WorkspaceId: Lag.properties.customerId 387 | WorkspaceKey: listKeys(Lag.id, '2015-03-20').primarySharedKey 388 | adminUsername: adminUsername 389 | adminPassword: adminPassword 390 | tags_policy_update: 'Monday;01:00 PM;Always;*java*,*oracle*;TeamB@abc.com' 391 | } 392 | dependsOn:[ 393 | Lag 394 | Vnet 395 | ] 396 | } 397 | 398 | module Lnx03 'ModuleVM.bicep' = if (VmCentOS_7_4) { 399 | name: 'Lnx03' 400 | params:{ 401 | VmName: 'VmCentOS74' 402 | VmLocation: Location 403 | VmSize: VmSize 404 | VmOsType: 'Linux' 405 | VmOsPublisher: 'OpenLogic' 406 | VmOsOffer: 'CentOS' 407 | VmOsSku: '7_9-gen2' 408 | VmOsVersion: 'latest' 409 | VmNicSubnetId: Vnet.properties.subnets[0].id 410 | WorkspaceId: Lag.properties.customerId 411 | WorkspaceKey: listKeys(Lag.id, '2015-03-20').primarySharedKey 412 | adminUsername: adminUsername 413 | adminPassword: adminPassword 414 | tags_policy_update: '' 415 | } 416 | dependsOn:[ 417 | Lag 418 | Vnet 419 | ] 420 | } 421 | -------------------------------------------------------------------------------- /docs/quickstart-infrastructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidsntg/azure-update-management-with-tags/2f833251ba58a1ae1d3bf7937a734c4bfefa6841/docs/quickstart-infrastructure.png -------------------------------------------------------------------------------- /docs/reporting-mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidsntg/azure-update-management-with-tags/2f833251ba58a1ae1d3bf7937a734c4bfefa6841/docs/reporting-mail.png -------------------------------------------------------------------------------- /docs/tag_syntax.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidsntg/azure-update-management-with-tags/2f833251ba58a1ae1d3bf7937a734c4bfefa6841/docs/tag_syntax.png -------------------------------------------------------------------------------- /docs/update-management-big-picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidsntg/azure-update-management-with-tags/2f833251ba58a1ae1d3bf7937a734c4bfefa6841/docs/update-management-big-picture.png -------------------------------------------------------------------------------- /runbooks/UM-CleanUp-Schedules.ps1: -------------------------------------------------------------------------------- 1 | ################# 2 | # CONFIGURATION # 3 | ################# 4 | 5 | # Deployment Schedule name Prefix. Must be the same used on Updatemanagement-CleanUpSchedules Runbook. 6 | $schedulePrefix = "ScheduledByTags-" 7 | 8 | ########## 9 | # SCRIPT # 10 | ########## 11 | 12 | Import-Module Az.Automation 13 | Import-Module Az.Compute 14 | 15 | # Connect to Azure with Automation Account system-assigned managed identity 16 | Disable-AzContextAutosave -Scope Process 17 | $AzureContext = (Connect-AzAccount -Identity -WarningAction Ignore).context 18 | $AzureContext = Set-AzContext -SubscriptionName $AzureContext.Subscription -DefaultProfile $AzureContext 19 | 20 | # Get current automation account 21 | $automationAccountsQuery = @{ 22 | Query = "resources 23 | | where type == 'microsoft.automation/automationaccounts'" 24 | } 25 | $automationAccounts = Search-AzGraph @automationAccountsQuery 26 | 27 | foreach ($automationAccount in $automationAccounts) 28 | { 29 | Select-AzSubscription -SubscriptionId $automationAccount.subscriptionId 30 | $Job = Get-AzAutomationJob -ResourceGroupName $automationAccount.resourceGroup -AutomationAccountName $automationAccount.name -Id $PSPrivateMetadata.JobId.Guid -ErrorAction SilentlyContinue 31 | if (!([string]::IsNullOrEmpty($Job))) 32 | { 33 | $automationAccountRg = $Job.ResourceGroupName 34 | $automationAccountName = $Job.AutomationAccountName 35 | break; 36 | } 37 | } 38 | 39 | # Get Update Deployment Schedules 40 | $updateSchedules = Get-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $automationAccountRg -AutomationAccountName $automationAccountName | Where-Object { $_.Name -like "$($schedulePrefix)*" } 41 | 42 | foreach ($updateSchedule in $updateSchedules) 43 | { 44 | $schedule = Get-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $automationAccountRg -AutomationAccountName $automationAccountName -Name $updateSchedule.Name 45 | $vmScheduledId = $schedule.UpdateConfiguration.AzureVirtualMachines[0] 46 | $vmScheduledName = $vmScheduledId.Split("/")[8] 47 | 48 | # Quick naming check 49 | if ($schedule.Name.Replace($schedulePrefix,"") -ne $vmScheduledName) { 50 | Write-Output "Error ! Schedule Name & VM Name in schedule mismatch, continue..." 51 | continue 52 | } 53 | 54 | $vmScheduledTags = Get-AzTag -ResourceId $vmScheduledId 55 | $vmScheduledTags = $vmScheduledTags.PropertiesTable 56 | $deleteSchedule = $false 57 | 58 | if ($null -eq $vmScheduledTags) 59 | { 60 | $deleteSchedule = $true 61 | } 62 | else { 63 | if (! $vmScheduledTags.Contains("POLICY_UPDATE")) { 64 | $deleteSchedule = $true 65 | } 66 | } 67 | 68 | # Delete Schedule 69 | if ($deleteSchedule) 70 | { 71 | Write-Output "Removing $($updateSchedule.Name) ..." 72 | Remove-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $automationAccountRg -AutomationAccountName $automationAccountName -Name $updateSchedule.Name 73 | } 74 | else { 75 | Write-Output "Nothing to do!" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /runbooks/UM-CleanUp-Snapshots.ps1: -------------------------------------------------------------------------------- 1 | ################# 2 | # CONFIGURATION # 3 | ################# 4 | 5 | # Snapshot prefix to use. Must be the same used on UpdatementManagement-CleanUpSnapshots.ps1 Runbook 6 | $snapshotPrefix = "UpdateMngmnt_snapshot_" 7 | 8 | # Number of days to keep snapshot before deletion 9 | $keepSnapshotDays = 8 10 | 11 | ########## 12 | # SCRIPT # 13 | ########## 14 | 15 | Import-Module Az.Automation 16 | 17 | # Connect to Azure with Automation Account system-assigned managed identity 18 | Disable-AzContextAutosave -Scope Process 19 | $AzureContext = (Connect-AzAccount -Identity -WarningAction Ignore).context 20 | $AzureContext = Set-AzContext -SubscriptionName $AzureContext.Subscription -DefaultProfile $AzureContext 21 | 22 | # Get Snapshots 23 | $snapshotsQuery = @{ 24 | Query = "resources 25 | | where type == 'microsoft.compute/snapshots' 26 | | where name startswith '$($snapshotPrefix)'" 27 | } 28 | $snapshots = Search-AzGraph @snapshotsQuery 29 | 30 | foreach ($snapshot in $snapshots) 31 | { 32 | $snapDate = $snapshot.properties.timeCreated 33 | $currentDate = Get-Date 34 | $dateDiff = (New-TimeSpan -Start $snapDate -End $currentDate).Days 35 | 36 | if ($dateDiff -gt $keepSnapshotDays) 37 | { 38 | # Delete Snapshot 39 | Select-AzSubscription -SubscriptionId $snapshot.subscriptionId 40 | Write-Output "$($snapshot.name) will be deleted." 41 | Remove-AzSnapshot -ResourceGroupName $snapshot.resourceGroup -SnapshotName $snapshot.name -Force 42 | } 43 | else { 44 | # Keep Snapshot 45 | Write-Output "$($snapshot.name) will not be deleted. Creation date is less or equal to $($keepSnapshotDays) days." 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /runbooks/UM-PostTasks.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .PARAMETER SoftwareUpdateConfigurationRunContext 4 | This is a system variable which is automatically passed in by Update Management during a deployment. 5 | 6 | #> 7 | 8 | param( 9 | [string]$SoftwareUpdateConfigurationRunContext, 10 | 11 | [Parameter(Mandatory = $true)] 12 | [string]$DestinationMail = "", 13 | 14 | [Parameter(Mandatory = $true)] 15 | [string]$ScheduleName = "" 16 | ) 17 | 18 | ################# 19 | # CONFIGURATION # 20 | ################# 21 | 22 | # Stop VM that were started after patching? Possible values: $true or $false. Value must be the same used in UM-PreTasks runbook by $startStopppedVmEnabled variable 23 | $stopStartedVmEnable = $true 24 | 25 | ########## 26 | # SCRIPT # 27 | ########## 28 | 29 | Import-Module Az.Automation 30 | Import-Module Az.Compute 31 | 32 | # Connect to Azure with Automation Account system-assigned managed identity 33 | Disable-AzContextAutosave -Scope Process 34 | $AzureContext = (Connect-AzAccount -Identity -WarningAction Ignore).context 35 | $AzureContext = Set-AzContext -SubscriptionName $AzureContext.Subscription -DefaultProfile $AzureContext 36 | 37 | # Get current automation account 38 | $automationAccountsQuery = @{ 39 | Query = "resources 40 | | where type == 'microsoft.automation/automationaccounts'" 41 | } 42 | $automationAccounts = Search-AzGraph @automationAccountsQuery 43 | 44 | foreach ($automationAccount in $automationAccounts) 45 | { 46 | Select-AzSubscription -SubscriptionId $automationAccount.subscriptionId 47 | $Job = Get-AzAutomationJob -ResourceGroupName $automationAccount.resourceGroup -AutomationAccountName $automationAccount.name -Id $PSPrivateMetadata.JobId.Guid -ErrorAction SilentlyContinue 48 | if (!([string]::IsNullOrEmpty($Job))) 49 | { 50 | $automationAccountSubscriptionId = $automationAccount.subscriptionId 51 | $automationAccountRg = $Job.ResourceGroupName 52 | $automationAccountName = $Job.AutomationAccountName 53 | break; 54 | } 55 | } 56 | 57 | $context = ConvertFrom-Json $SoftwareUpdateConfigurationRunContext 58 | $vmIds = $context.SoftwareUpdateConfigurationSettings.AzureVirtualMachines 59 | 60 | $vmIds | ForEach-Object { 61 | $vmId = $_ 62 | 63 | $split = $vmId -split "/"; 64 | 65 | if ($split.Length -eq 1) 66 | { 67 | # Azure Arc Server 68 | } 69 | else { 70 | # Azure VM 71 | $subscriptionId = $split[2]; 72 | $rg = $split[4]; 73 | $name = $split[8]; 74 | 75 | $mute = Select-AzSubscription -Subscription $subscriptionId 76 | 77 | #Hack to get VM log analytics ID 78 | $vmExtension = Get-AzVMExtension -ResourceGroupName $rg -VMName $name -DefaultProfile $mute 79 | $workspaceID = ($vmExtension | Select-String -InputObject {$_.PublicSettings} -Pattern "(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})" -All).Matches.Value 80 | } 81 | } 82 | 83 | if ($DestinationMail -and $ScheduleName -and $DestinationMail -Match "^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$") 84 | { 85 | Write-Output "Create reporting mail" 86 | #Log Analytics Query Informations 87 | $query = 'UpdateRunProgress 88 | | where TimeGenerated > now(-4h) 89 | | where InstallationStatus <> ''NotStarted'' 90 | | where UpdateRunName == ''' + $scheduleName + ''' 91 | | project Computer, strcat(Product, KBID), InstallationStatus, ResourceId 92 | | project-rename Product=Column1 93 | | order by InstallationStatus asc, Computer asc' 94 | 95 | #Log Analytics Request 96 | $queryResults = Invoke-AzOperationalInsightsQuery -WorkspaceId $workspaceID -Query $query 97 | 98 | $rows = $queryResults.Results 99 | $rows | ConvertTo-Json 100 | 101 | #Data Modeling for Mail Report 102 | if ($rows){ 103 | $mailContent = "" 104 | $mailContent += "" 105 | 106 | foreach ($row in $rows){ 107 | $mailContent += "" 108 | } 109 | 110 | $mailContent += "
VMSoftwareStatus
" + $row.Computer + "" + $row.Product + "" + $row.InstallationStatus + "

---
Cloud Team" 111 | 112 | #Get Automation Account variables 113 | $SendGridAPIKey = Get-AutomationVariable -Name SendGridAPIKey 114 | $fromEmailAddress = Get-AutomationVariable -Name SendGridSender 115 | 116 | # Create the headers for the API call 117 | $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" 118 | $headers.Add("Authorization", "Bearer " + $SendGridAPIKey) 119 | $headers.Add("Content-Type", "application/json") 120 | 121 | # Parameters for sending the email 122 | $subject = "Update Management - Automatic patching report : " + $scheduleName 123 | 124 | $emailTo = @() 125 | $DestinationMail.Split(';') | ForEach-Object { 126 | $emailTo += @{email=$_} 127 | } 128 | 129 | # Create a JSON message with the parameters from above 130 | $body = @{ 131 | personalizations = @( 132 | @{ 133 | to = $emailTo 134 | } 135 | ) 136 | from = @{ 137 | email = $fromEmailAddress 138 | } 139 | subject = $subject 140 | content = @( 141 | @{ 142 | type = "text/html" 143 | value = $mailContent 144 | } 145 | ) 146 | } 147 | 148 | # Convert the string into a real JSON-formatted string 149 | # Depth specifies how many levels of contained objects 150 | # are included in the JSON representation. The default 151 | # value is 2 152 | $bodyJson = $body | ConvertTo-Json -Depth 4 153 | 154 | # Call the SendGrid RESTful web service and pass the 155 | # headers and json message. More details about the 156 | # webservice and the format of the JSON message go to 157 | # https://sendgrid.com/docs/api-reference/ 158 | $response = Invoke-RestMethod -Uri https://api.sendgrid.com/v3/mail/send -Method Post -Headers $headers -Body $bodyJson 159 | } 160 | } 161 | 162 | if ($stopStartedVmEnable) 163 | { 164 | # Get VMs from $SoftwareUpdateConfigurationRunContext 165 | $context = ConvertFrom-Json $SoftwareUpdateConfigurationRunContext 166 | $runId = "PrescriptContext" + $context.SoftwareUpdateConfigurationRunId 167 | 168 | #Retrieve the automation variable, which we named using the runID from our run context. 169 | #See: https://docs.microsoft.com/en-us/azure/automation/automation-variables#activities 170 | $variable = Get-AutomationVariable -Name $runId 171 | if (!$variable) 172 | { 173 | Write-Output "No machines to turn off" 174 | return 175 | } 176 | 177 | $vmIds = $variable -split "," 178 | $stoppableStates = "starting", "running" 179 | 180 | $vmIds | ForEach-Object { 181 | $vmId = $_ 182 | 183 | $split = $vmId -split "/"; 184 | $subscriptionId = $split[2]; 185 | $rg = $split[4]; 186 | $name = $split[8]; 187 | 188 | $mute = Select-AzSubscription -Subscription $subscriptionId 189 | $vm = Get-AzVM -ResourceGroupName $rg -Name $name -Status -DefaultProfile $mute 190 | 191 | ########### 192 | # VM STOP # 193 | ########### 194 | 195 | # Get VM state 196 | $state = ($vm.Statuses[1].DisplayStatus -split " ")[1] 197 | if($state -in $stoppableStates) { 198 | Write-Output "$($name) - Stopping ..." 199 | Stop-AzVM -Id $vmId -Force -DefaultProfile $mute 200 | 201 | }else { 202 | Write-Output ($name + ": already stopped. State: " + $state) 203 | } 204 | } 205 | 206 | # Clean up automation account variable: 207 | Select-AzSubscription -SubscriptionId $automationAccountSubscriptionId 208 | Remove-AzAutomationVariable -AutomationAccountName $automationAccountName -ResourceGroupName $automationAccountRg -name $runID 209 | } 210 | 211 | Write-Output "Done" 212 | 213 | -------------------------------------------------------------------------------- /runbooks/UM-PreTasks.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .PARAMETER SoftwareUpdateConfigurationRunContext 3 | This is a system variable which is automatically passed in by Update Management during a deployment. 4 | #> 5 | 6 | param( 7 | [string]$SoftwareUpdateConfigurationRunContext 8 | ) 9 | 10 | 11 | ################# 12 | # CONFIGURATION # 13 | ################# 14 | 15 | # Start stopped VM before patching ? Possible values: $true or $false. Value must be the same used in UM-PostTasks runbook by $stopStartedVmEnable variable 16 | $startStopppedVmEnabled = $true 17 | 18 | # Snapshot before patching ? Possible values: $true or $false. 19 | $snapshotEnabled = $true 20 | 21 | # Snapshot prefix to use. Must be the same used on UM-CleanUp-Snapshots.ps1 Runbook 22 | $snapshotPrefix = "UpdateMngmnt_snapshot_" 23 | 24 | # VM status that can be started 25 | $startableStates = "stopped" , "stopping", "deallocated", "deallocating" 26 | 27 | ########## 28 | # SCRIPT # 29 | ########## 30 | 31 | Import-Module Az.Automation 32 | Import-Module Az.Compute 33 | 34 | # Connect to Azure with Automation Account system-assigned managed identity 35 | Disable-AzContextAutosave -Scope Process 36 | $AzureContext = (Connect-AzAccount -Identity -WarningAction Ignore).context 37 | $AzureContext = Set-AzContext -SubscriptionName $AzureContext.Subscription -DefaultProfile $AzureContext 38 | 39 | # Get current automation account 40 | $automationAccountsQuery = @{ 41 | Query = "resources 42 | | where type == 'microsoft.automation/automationaccounts'" 43 | } 44 | $automationAccounts = Search-AzGraph @automationAccountsQuery 45 | 46 | foreach ($automationAccount in $automationAccounts) 47 | { 48 | Select-AzSubscription -SubscriptionId $automationAccount.subscriptionId 49 | $Job = Get-AzAutomationJob -ResourceGroupName $automationAccount.resourceGroup -AutomationAccountName $automationAccount.name -Id $PSPrivateMetadata.JobId.Guid -ErrorAction SilentlyContinue 50 | if (!([string]::IsNullOrEmpty($Job))) 51 | { 52 | $automationAccountRg = $Job.ResourceGroupName 53 | $automationAccountName = $Job.AutomationAccountName 54 | break; 55 | } 56 | } 57 | 58 | 59 | # Get Azure VMs or Azure Arc Servers from $SoftwareUpdateConfigurationRunContext 60 | $context = ConvertFrom-Json $SoftwareUpdateConfigurationRunContext 61 | $vmIds = $context.SoftwareUpdateConfigurationSettings.AzureVirtualMachines 62 | $runId = "PrescriptContext" + $context.SoftwareUpdateConfigurationRunId 63 | 64 | if (!$vmIds) 65 | { 66 | Write-Output "No Azure VMs found, checking Azure Arc Servers..." 67 | $vmIds = $context.SoftwareUpdateConfigurationSettings.NonAzureComputerNames 68 | if (!$vmIds){ 69 | if (!$vmIds) 70 | { 71 | Write-Output "No Azure Arc Servers found!" 72 | return 73 | } 74 | } 75 | } 76 | 77 | $vmIds | ForEach-Object { 78 | $vmId = $_ 79 | 80 | $split = $vmId -split "/"; 81 | 82 | if ($split.Length -eq 1) 83 | { 84 | # Azure Arc Server 85 | $vmType = "Arc" 86 | } 87 | else { 88 | # Azure VM 89 | $vmType = "Azure" 90 | $subscriptionId = $split[2]; 91 | $rg = $split[4]; 92 | $name = $split[8]; 93 | 94 | $mute = Select-AzSubscription -Subscription $subscriptionId 95 | $vm = Get-AzVM -Name $name -resourceGroupName $rg -DefaultProfile $mute 96 | 97 | $vmOS = $vm.StorageProfile.osDisk.osType 98 | 99 | } 100 | 101 | 102 | #################### 103 | # SNAPSHOT OS DISK # 104 | #################### 105 | 106 | if ($snapshotEnabled -and $vmType -eq "Azure") 107 | { 108 | Write-Output "$($vm.name) - OS Disk Snapshot Begin" 109 | $snapshotdisk = $vm.StorageProfile 110 | $OSDiskSnapshotConfig = New-AzSnapshotConfig -SourceUri $snapshotdisk.OsDisk.ManagedDisk.id -CreateOption Copy -Location $vm.Location -OsType $vmOS 111 | $snapshotNameOS = "$($snapshotPrefix)$($snapshotdisk.OsDisk.Name)_$(Get-Date -Format yyyyMMdd_HHmm)" 112 | 113 | try { 114 | New-AzSnapshot -ResourceGroupName $rg -SnapshotName $snapshotNameOS -Snapshot $OSDiskSnapshotConfig -ErrorAction Stop 115 | } 116 | catch { 117 | $_ 118 | } 119 | Write-Output "$($vm.name) - OS Disk Snapshot End" 120 | } 121 | 122 | ############ 123 | # VM START # 124 | ############ 125 | 126 | if ($startStopppedVmEnabled -and $vmType -eq "Azure") 127 | { 128 | # Create Automation Account Variable - used to store the state of VMs 129 | New-AzAutomationVariable -ResourceGroupName $automationAccountRg -AutomationAccountName $automationAccountName -Name $runId -Value "" -Encrypted $false 130 | 131 | $updatedMachines = @() 132 | 133 | # Get VM state 134 | $vm = Get-AzVM -Name $name -resourceGroupName $rg -Status -DefaultProfile $mute 135 | #Query the state of the VM to see if it's already running or if it's already started 136 | $state = ($vm.Statuses[1].DisplayStatus -split " ")[1] 137 | if($state -in $startableStates) { 138 | Write-Output "$($name) - Starting ..." 139 | $updatedMachines += $vmId 140 | Start-AzVM -Id $vmId -NoWait -DefaultProfile $mute 141 | } else { 142 | Write-Output ($name + ": no action taken. State: " + $state) 143 | } 144 | } 145 | 146 | } 147 | 148 | if ($startStopppedVmEnabled -and $null -ne $updatedMachines) 149 | { 150 | $updatedMachinesCommaSeperated = $updatedMachines -join "," 151 | 152 | # Store output in the automation variable 153 | Set-AutomationVariable -Name $runId -Value $updatedMachinesCommaSeperated 154 | } 155 | 156 | Write-Output "Done" 157 | -------------------------------------------------------------------------------- /runbooks/UM-ScheduleUpdatesWithVmsTags.ps1: -------------------------------------------------------------------------------- 1 | ############### 2 | # DESCRIPTION # 3 | ############### 4 | 5 | # This script searchs VMs with key tag "POLICY_UPDATE" 6 | # If a VM has a "POLICY_UPDATE" key tag, an update deployment will be created on the current automation account. 7 | # The VM will be patched weekly based on its "POLICY_UPDATE" tag value 8 | 9 | # Syntax of "POLICY_UPDATE" key tag: 10 | # DaysOfWeek;startTime;rebootPolicy;excludedPackages;reportingMail 11 | 12 | # Example #1 - POLICY_UPDATE: Sunday;05h20 PM;Always;*java*,*nagios*; 13 | # Example #2 - POLICY_UPDATE: Friday;07h00 PM;IfRequired;;TeamA@abc.com 14 | 15 | # rebootPolicy possible values: Always, Never, IfRequired 16 | # excludedPackages: optional parameter, comma separated if multiple. 17 | # reportingMail: optional parameter 18 | 19 | # Prerequisites: 20 | # 1) Automation Account must have a system-managed identity 21 | # 2) System-managed identity must be Contributor on VM's subscriptions scopes 22 | # 3) Virtual Machines must be connected to Log Analytics Workspace linked to the Automation Account. 23 | 24 | ################# 25 | # CONFIGURATION # 26 | ################# 27 | 28 | # Pre-Task Runbook to execute before the patching. The runbook must exists in the automation account. 29 | $PreTaskRunbookName = "UM-PreTasks" 30 | 31 | # Post-Task Runbook to execute after the patching. The runbook must exists in the automation account. 32 | $PostTaskRunbookName = "UM-PostTasks" 33 | 34 | # Onboard Azure Arc Servers ? Possible values: $true or $false. 35 | $onboardAzureArcServersEnabled = $true 36 | 37 | # Maintenance window (minutes) to perform patching. Minimum: 30 minutes. Maximum: 6 hours 38 | $duration = New-TimeSpan -Hours 2 39 | 40 | # TimeZone - Can be the IANA ID or the Windows Time Zone ID 41 | $timezone = "Romance Standard Time" # France - Central European Time 42 | 43 | # Deployment Schedule name Prefix. Must be the same used on Updatemanagement-CleanUpSchedules Runbook. 44 | $schedulePrefix = "ScheduledByTags-" 45 | 46 | # Valid days of week - used to check tags values. 47 | $validDaysOfWeek = @("Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday") 48 | 49 | # Valid reboot settings - used to check tags values. 50 | $validRebootSettings = @("Always","Never","IfRequired") 51 | 52 | ########## 53 | # SCRIPT # 54 | ########## 55 | 56 | Import-Module Az.ResourceGraph 57 | 58 | # Connect to Azure with Automation Account system-assigned managed identity 59 | Disable-AzContextAutosave -Scope Process 60 | $AzureContext = (Connect-AzAccount -Identity -WarningAction Ignore).context 61 | $AzureContext = Set-AzContext -SubscriptionName $AzureContext.Subscription -DefaultProfile $AzureContext 62 | 63 | # Get current automation account 64 | $automationAccountsQuery = @{ 65 | Query = "resources 66 | | where type == 'microsoft.automation/automationaccounts'" 67 | } 68 | $automationAccounts = Search-AzGraph @automationAccountsQuery 69 | 70 | foreach ($automationAccount in $automationAccounts) 71 | { 72 | Select-AzSubscription -SubscriptionId $automationAccount.subscriptionId 73 | $Job = Get-AzAutomationJob -ResourceGroupName $automationAccount.resourceGroup -AutomationAccountName $automationAccount.name -Id $PSPrivateMetadata.JobId.Guid -ErrorAction SilentlyContinue 74 | if (!([string]::IsNullOrEmpty($Job))) 75 | { 76 | $automationAccountSubscriptionId = $automationAccount.subscriptionId 77 | $automationAccountRg = $Job.ResourceGroupName 78 | $automationAccountName = $Job.AutomationAccountName 79 | break; 80 | } 81 | } 82 | 83 | # Search all Azure VMs with key tags "POLICY_UPDATE" 84 | $azureVmsQueryParams = @{ 85 | Query = "resources 86 | | where type == 'microsoft.compute/virtualmachines' 87 | | where isnotnull(tags['POLICY_UPDATE']) 88 | | project id, name, policy_update=tags['POLICY_UPDATE'], osType=properties.storageProfile.osDisk.osType, vmType='Azure'" 89 | } 90 | 91 | $azureVmsWithBackupPolicy = Search-AzGraph @azureVmsQueryParams 92 | 93 | # Search all Azure Arc Servers with key tags "POLICY_UPDATE" 94 | if ($onboardAzureArcServersEnabled) 95 | { 96 | $azureArcServersQueryParam = @{ 97 | Query = "resources 98 | | where type == 'microsoft.hybridcompute/machines' 99 | | where isnotnull(tags['POLICY_UPDATE']) 100 | | project id, name, policy_update=tags['POLICY_UPDATE'], osType=properties.osType, vmType='Arc'" 101 | } 102 | 103 | $azureArcServersBackupPolicy = Search-AzGraph @azureArcServersQueryParam 104 | } 105 | else { 106 | $azureArcServersBackupPolicy = $null 107 | } 108 | 109 | # Concatain $azureVmsQueryParams & $azureArcServersQueryParam 110 | $vmsWithBackupPolicy = $azureVmsWithBackupPolicy + $azureArcServersBackupPolicy 111 | 112 | foreach ($vm in $vmsWithBackupPolicy) { 113 | Write-Output "==========" 114 | Write-Output "VM Name: $($vm.name)" 115 | Write-Output "VM Policy: $($vm.policy_update)" 116 | 117 | # Check inputs 118 | if ($vm.policy_update.Split(";").Length -ne 4 -And $vm.policy_update.Split(";").Length -ne 5) 119 | { 120 | Write-Error "/!\ Error! Wrong number of parameters given. POLICY_UPDATE syntax is: DaysOfWeek;startTime;rebootPolicy;excludedPackages;reportingMail" 121 | continue 122 | } 123 | $DaysOfWeek = $vm.policy_update.Split(";")[0].Split(',') 124 | foreach($day in $DaysOfWeek) 125 | { 126 | if (!$validDaysOfWeek.contains($day)) 127 | { 128 | Write-Error "/!\ Error! DaysOfWeek is not valid. It should be Monday, Tuesday, Wednesday, Thursday, Friday, Saturday or Sunday. Current value for VM $($vm.name): $($DaysOfWeek)" 129 | continue 130 | } 131 | } 132 | 133 | $startTime = $vm.policy_update.Split(";")[1] 134 | try { 135 | $startTime = (Get-Date $startTime).AddDays(1) 136 | } 137 | catch { 138 | Write-Error "/!\ Error! startTime is not valid. It should be 'hh:mm AM' or 'hh:mm PM' formatted. Current value for VM: $($startTime)" 139 | continue 140 | } 141 | 142 | $rebootPolicy = $vm.policy_update.Split(";")[2] 143 | if (!$validRebootSettings.contains($rebootPolicy)) 144 | { 145 | Write-Error "/!\ Error! rebootSetting is not valid. It should be Always, Never or IfRequired. Current value for VM $($vm.name): $($rebootPolicy)" 146 | continue 147 | } 148 | 149 | $excludedPackages = $vm.policy_update.Split(";")[3] 150 | if ($excludedPackages) 151 | { 152 | $excludedPackages = $excludedPackages.Split(",") 153 | } 154 | else 155 | { 156 | $excludedPackages = $null 157 | } 158 | 159 | $reportingMail = $vm.policy_update.Split(";")[4] 160 | $reportingMailHash = $null 161 | if ($reportingMail) 162 | { 163 | if ($reportingMail -NotMatch "^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$") 164 | { 165 | Write-Error "/!\ Error! reportingMail is not valid. It should a valid email address. Current value for VM $($vm.name): $($reportingMail)" 166 | continue 167 | } 168 | $reportingMailHash = @{"DestinationMail" = $reportingMail ; "ScheduleName" = "$($schedulePrefix)$($vm.name)" } 169 | } 170 | 171 | if (!$PreTaskRunbookName) { $PreTaskRunbookName = $null } 172 | if (!$PostTaskRunbookName) { $PostTaskRunbookName = $null } 173 | 174 | # Checking osType 175 | if ($vm.osType -ne "Windows" -and $vm.osType -ne "Linux") 176 | { 177 | Write-Error "/!\ Error! VM osType not supported. Supported OS are: Linux or Windows. Current value for VM $($vm.name): $($vm.osType)" 178 | continue 179 | } 180 | 181 | # Check is a schedule for the VM already exists 182 | $schedules = Get-AzAutomationSchedule -ResourceGroupName $automationAccountRg -AutomationAccountName $automationAccountName | Where-Object {($_.Name -like "$($schedulePrefix)$($vm.name)*")} 183 | $schedule = $null 184 | $createOrUpdateSchedule = $false 185 | 186 | # If a schedule for the VM already exists, check that Days&Hour defined in tags = Days&Hours defined in schedule. Update Schedule otherwise. 187 | if ($schedules.Length -eq 1 -And !([string]::IsNullOrEmpty($schedules.Name))) 188 | { 189 | # Get schedule details 190 | $schedule = Get-AzAutomationSchedule -ResourceGroupName $automationAccountRg -AutomationAccountName $automationAccountName -Name $schedules.Name 191 | 192 | # Check if Days between those defined in existing schedule are equal to those defined in vm tags 193 | $automationScheduleDaysOfWeek = $schedule.WeeklyScheduleOptions.DaysOfWeek | Sort-Object 194 | $automationScheduleDaysOfWeek = $automationScheduleDaysOfWeek -join ", " 195 | 196 | $DaysOfWeekSorted = $DaysOfWeek | Sort-Object 197 | $DaysOfWeekSorted = $DaysOfWeekSorted -join ', ' 198 | 199 | if ($automationScheduleDaysOfWeek -ne $DaysOfWeekSorted) 200 | { 201 | Write-Output "Days between those defined in existing schedule and days defined tags are different. Existing schedule will be updated." 202 | $createOrUpdateSchedule = $true 203 | } 204 | 205 | # Check if hour:minute between those defined in existing schedule are equal to those defined in vm tags 206 | $automationScheduleNextRunDateTime = $schedule.NextRun.DateTime 207 | $diff = New-TimeSpan -Start (Get-Date $startTime) -End $automationScheduleNextRunDateTime 208 | 209 | if ($diff.Hours -ne 0 -Or $diff.Minutes -ne 0) 210 | { 211 | Write-Output "hour:minute between those defined in existing schedule are different to those defined in vm tags. Existing schedule will be updated." 212 | $createOrUpdateSchedule = $true 213 | } 214 | } 215 | else 216 | { 217 | # New VM detected : a schedule must be created 218 | $createOrUpdateSchedule = $true 219 | } 220 | 221 | 222 | if ($createOrUpdateSchedule) 223 | { 224 | $schedule = New-AzAutomationSchedule -ResourceGroupName $automationAccountRg ` 225 | -AutomationAccountName $automationAccountName ` 226 | -Name "$($schedulePrefix)$($vm.name)" ` 227 | -StartTime $startTime ` 228 | -TimeZone $timezone ` 229 | -DaysOfWeek $DaysOfWeek ` 230 | -WeekInterval 1 ` 231 | -ForUpdateConfiguration 232 | } else { 233 | $schedule.Name = "$($schedulePrefix)$($vm.name)" 234 | } 235 | # Azure VM 236 | if ($vm.vmType -eq "Azure") 237 | { 238 | if ($vm.osType -eq "Windows") 239 | { 240 | New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $automationAccountRg ` 241 | -AutomationAccountName $automationAccountName ` 242 | -Schedule $schedule ` 243 | -Windows ` 244 | -AzureVMResourceId $vm.id ` 245 | -IncludedUpdateClassification Unclassified,Critical,Security,UpdateRollup,FeaturePack,ServicePack,Definition,Tools,Updates ` 246 | -ExcludedKbNumber $excludedPackages ` 247 | -RebootSetting $rebootPolicy ` 248 | -Duration $duration ` 249 | -PreTaskRunbookName $PreTaskRunbookName ` 250 | -PostTaskRunbookName $PostTaskRunbookName ` 251 | -PostTaskRunbookParameter $reportingMailHash 252 | } 253 | elseif ($vm.osType -eq "Linux") 254 | { 255 | New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $automationAccountRg ` 256 | -AutomationAccountName $automationAccountName ` 257 | -Schedule $schedule ` 258 | -Linux ` 259 | -AzureVMResourceId $vm.id ` 260 | -IncludedPackageClassification Critical, Security, Other, Unclassified ` 261 | -ExcludedPackageNameMask $excludedPackages ` 262 | -RebootSetting $rebootPolicy ` 263 | -Duration $duration ` 264 | -PreTaskRunbookName $PreTaskRunbookName ` 265 | -PostTaskRunbookName $PostTaskRunbookName ` 266 | -PostTaskRunbookParameter $reportingMailHash 267 | } 268 | } 269 | # Azure Arc Server 270 | elseif ($vm.vmType -eq "Arc") { 271 | if ($vm.osType -eq "Windows") 272 | { 273 | New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $automationAccountRg ` 274 | -AutomationAccountName $automationAccountName ` 275 | -Schedule $schedule ` 276 | -Windows ` 277 | -NonAzureComputer $vm.name ` 278 | -IncludedUpdateClassification Unclassified,Critical,Security,UpdateRollup,FeaturePack,ServicePack,Definition,Tools,Updates ` 279 | -ExcludedKbNumber $excludedPackages ` 280 | -RebootSetting $rebootPolicy ` 281 | -Duration $duration ` 282 | -PreTaskRunbookName $PreTaskRunbookName ` 283 | -PostTaskRunbookName $PostTaskRunbookName ` 284 | -PostTaskRunbookParameter $reportingMailHash 285 | } 286 | elseif ($vm.osType -eq "Linux") 287 | { 288 | New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $automationAccountRg ` 289 | -AutomationAccountName $automationAccountName ` 290 | -Schedule $schedule ` 291 | -Linux ` 292 | -NonAzureComputer $vm.name ` 293 | -IncludedPackageClassification Critical, Security, Other, Unclassified ` 294 | -ExcludedPackageNameMask $excludedPackages ` 295 | -RebootSetting $rebootPolicy ` 296 | -Duration $duration ` 297 | -PreTaskRunbookName $PreTaskRunbookName ` 298 | -PostTaskRunbookName $PostTaskRunbookName ` 299 | -PostTaskRunbookParameter $reportingMailHash 300 | } 301 | } 302 | else { 303 | Write-Error "VM Type Unknown!" 304 | continue 305 | } 306 | } 307 | 308 | Write-Output "Done" 309 | -------------------------------------------------------------------------------- /terraform/baseInfra.tf: -------------------------------------------------------------------------------- 1 | 2 | provider "azurerm" { 3 | features {} 4 | 5 | } 6 | 7 | # Deploy demo resource group 8 | resource "azurerm_resource_group" "baseInfraUM_rg" { 9 | name = "baseInfra-UpdateManagement-rg2" 10 | location = "westeurope" 11 | 12 | } 13 | 14 | # Deploy automation account 15 | resource "azurerm_automation_account" "automationAccount" { 16 | name = "automationAccount-01" 17 | location = azurerm_resource_group.baseInfraUM_rg.location 18 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 19 | sku_name = "Basic" 20 | 21 | identity { 22 | type = "SystemAssigned" 23 | } 24 | 25 | } 26 | 27 | # Deploy log analytics workspace 28 | resource "azurerm_log_analytics_workspace" "demosg_law" { 29 | name = "demosglaw" 30 | location = azurerm_resource_group.baseInfraUM_rg.location 31 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 32 | sku = "PerGB2018" 33 | retention_in_days = 30 34 | } 35 | 36 | # Link the automation account to the log analytics workspace 37 | resource "azurerm_log_analytics_linked_service" "link_law_automation" { 38 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 39 | workspace_id = azurerm_log_analytics_workspace.demosg_law.id 40 | read_access_id = azurerm_automation_account.automationAccount.id 41 | } 42 | 43 | # Install Update Management solution on Log Analytics Workspace 44 | resource "azurerm_log_analytics_solution" "automation_account_solutions_updates" { 45 | solution_name = "Updates" 46 | location = azurerm_resource_group.baseInfraUM_rg.location 47 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 48 | workspace_resource_id = azurerm_log_analytics_workspace.demosg_law.id 49 | workspace_name = azurerm_log_analytics_workspace.demosg_law.name 50 | 51 | plan { 52 | publisher = "Microsoft" 53 | product = "OMSGallery/Updates" 54 | } 55 | } 56 | 57 | # Add Az.Account module to Automation Account 58 | resource "azurerm_automation_module" "automation_account_module_accounts" { 59 | name = "Az.Accounts" 60 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 61 | automation_account_name = azurerm_automation_account.automationAccount.name 62 | module_link { 63 | uri = "https://www.powershellgallery.com/api/v2/package/az.accounts/2.10.3" 64 | } 65 | } 66 | 67 | # Add Az.ResourceGraph module to Automation Account 68 | resource "azurerm_automation_module" "automation_account_module_resourcegraph" { 69 | name = "Az.ResourceGraph" 70 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 71 | automation_account_name = azurerm_automation_account.automationAccount.name 72 | module_link { 73 | uri = "https://www.powershellgallery.com/api/v2/package/az.resourcegraph/0.13.0" 74 | } 75 | depends_on = [azurerm_automation_module.automation_account_module_accounts] 76 | } 77 | 78 | # (Optional: for Azure Arc for Servers) Add Az.ConnectedMachine module to Automation Account 79 | resource "azurerm_automation_module" "automation_account_module_connectedmachine" { 80 | name = "Az.ConnectedMachine" 81 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 82 | automation_account_name = azurerm_automation_account.automationAccount.name 83 | module_link { 84 | uri = "https://www.powershellgallery.com/api/v2/package/az.connectedmachine/0.2.0" 85 | } 86 | depends_on = [azurerm_automation_module.automation_account_module_accounts] 87 | } 88 | 89 | # Deploy Automation Account Runbook - UM-ScheduleUpdatesWithVmsTags 90 | resource "azurerm_automation_runbook" "UM-ScheduleUpdatesWithVmsTags" { 91 | name = "UM-ScheduleUpdatesWithVmsTags" 92 | location = azurerm_resource_group.baseInfraUM_rg.location 93 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 94 | automation_account_name = azurerm_automation_account.automationAccount.name 95 | log_verbose = "true" 96 | log_progress = "true" 97 | description = "Updatemanagement-schedule updates VMs with tags Runbook" 98 | runbook_type = "PowerShell" 99 | publish_content_link { 100 | uri = "https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-ScheduleUpdatesWithVmsTags.ps1" 101 | } 102 | } 103 | 104 | # Deploy Automation Account Runbook - UM-PreTasks 105 | resource "azurerm_automation_runbook" "UM-PreTasks" { 106 | name = "UM-PreTasks" 107 | location = azurerm_resource_group.baseInfraUM_rg.location 108 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 109 | automation_account_name = azurerm_automation_account.automationAccount.name 110 | log_verbose = "true" 111 | log_progress = "true" 112 | description = "Updatemanagement-pretask Runbook" 113 | runbook_type = "PowerShell" 114 | publish_content_link { 115 | uri = "https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-PreTasks.ps1" 116 | } 117 | } 118 | 119 | # Deploy Automation Account Runbook - UM-PostTasks 120 | resource "azurerm_automation_runbook" "UM-PostTasks" { 121 | name = "UM-PostTasks" 122 | location = azurerm_resource_group.baseInfraUM_rg.location 123 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 124 | automation_account_name = azurerm_automation_account.automationAccount.name 125 | log_verbose = "true" 126 | log_progress = "true" 127 | description = "Updatemanagement-post task Runbook" 128 | runbook_type = "PowerShell" 129 | publish_content_link { 130 | uri = "https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-PostTasks.ps1" 131 | } 132 | } 133 | 134 | # Deploy Automation Account Runbook - UM-CleanUp-Schedules 135 | resource "azurerm_automation_runbook" "UM-CleanUp-Schedules" { 136 | name = "UM-CleanUp-Schedules" 137 | location = azurerm_resource_group.baseInfraUM_rg.location 138 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 139 | automation_account_name = azurerm_automation_account.automationAccount.name 140 | log_verbose = "true" 141 | log_progress = "true" 142 | description = "Updatemanagement-CleanUpSchedules Runbook" 143 | runbook_type = "PowerShell" 144 | publish_content_link { 145 | uri = "https://raw.githubusercontent.com/dawlysd/azure-update-management-with-tags/main/runbooks/UM-CleanUp-Schedules.ps1" 146 | } 147 | } 148 | 149 | # Create daily schedule - will be used by runbooks 150 | resource "azurerm_automation_schedule" "UM-Schedule-daily" { 151 | name = "UM-Schedule-daily" 152 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 153 | automation_account_name = azurerm_automation_account.automationAccount.name 154 | frequency = "Day" 155 | interval = 1 156 | #timezone = "Australia/Perth" 157 | #start_time = "2014-04-15T18:00:15+02:00" 158 | description = "Schedule daily" 159 | #week_days = ["Friday"] 160 | } 161 | 162 | # Schedule "UM-ScheduleUpdatesWithVmsTags" Runbook with "UM-Schedule-daily" schedule 163 | resource "azurerm_automation_job_schedule" "UM-ScheduleRunbook-ScheduleUpdatesWithVmsTags" { 164 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 165 | automation_account_name = azurerm_automation_account.automationAccount.name 166 | schedule_name = azurerm_automation_schedule.UM-Schedule-daily.name 167 | runbook_name = azurerm_automation_runbook.UM-ScheduleUpdatesWithVmsTags.name 168 | } 169 | 170 | # Schedule "UM-CleanUp-Schedules" Runbook with "UM-Schedule-daily" schedule 171 | resource "azurerm_automation_job_schedule" "UM-CleanUp-Schedules" { 172 | resource_group_name = azurerm_resource_group.baseInfraUM_rg.name 173 | automation_account_name = azurerm_automation_account.automationAccount.name 174 | schedule_name = azurerm_automation_schedule.UM-Schedule-daily.name 175 | runbook_name = azurerm_automation_runbook.UM-CleanUp-Schedules.name 176 | } --------------------------------------------------------------------------------