Hello, Chris Wallen here and in this post, I’m going to show how you can use an Azure Automation runbook to deploy and configure the Log Analytics extension to a group of virtual machines running either Windows or Linux.
Before we get started with the code portion, there are a couple of important things to note.
- This script requires the virtual machines to be powered on and will skip any VMs that are deallocated. The output will show any skipped VMs.
- The virtual machine guest agent must be in a good state. Check this by looking at the Agent Status under the Properties blade of the Virtual Machine
- This script uses the AzureRM modules, but can easily be converted to use the new Az modules
Now, on to the fun part: The code.
Below are the required parameters for the runbook:
- azureSubscriptionId – The unique identifier of the subscription you want to use
- azureEnvironment – The Azure cloud environment to use. e.g, AzureCloud, AzureUSGovernment
- LogAnalyticsWorkspaceName – The name of the Log Analytics workspace to connect the virtual machines
- LAResourceGroup – The resource group that contains the Log Analytics workspace
The following parameters are not required. If specified, they limit the scope of the runbook to only the resource groups or virtual machines that you want to configure.
- ResourceGroupNames – If this parameter is specified the Log Analytics extension will be deployed to all virtual machines in the resource group. The list of resource groups should be specified in JSON format – [‘rg1′,’rg2’]
- VMNames – If this is specified, the Log Analytics extension will only be deployed to the provided virtual machines. This variable should be provided in JSON format – [‘vm1′,’vm2’]
Note: You can visit my Github repo to download the full script.
First, we’ll define our parameters
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | Param ( [parameter(mandatory)] [string] $azureSubscriptionID, [parameter(mandatory)] [string] $azureEnvironment, [parameter(mandatory)] [string] $WorkspaceName, [parameter(mandatory)] [string] $LAResourceGroup, [string[]] $ResourceGroupNames, [string[]] $VMNames ) |
In the next section, we need to configure our runbook to use our AzureRunAsAccount. This will use the credentials that are automatically created when you first create an automation account.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | $connectionName = "AzureRunAsConnection" # Get the connection "AzureRunAsConnection " $servicePrincipalConnection = Get-AutomationConnection ` -Name $connectionName -ErrorAction Stop Write-Output "Logging in to Azure..." Add-AzureRmAccount ` -ServicePrincipal ` -TenantId $servicePrincipalConnection.TenantId ` -ApplicationId $servicePrincipalConnection.ApplicationId ` -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint ` -EnvironmentName $azureEnvironment ` -ErrorAction Stop $azContext = Select-AzureRmSubscription ` -subscriptionId $azureSubscriptionID -ErrorAction Stop |
Next, we’ll build the list of Virtual Machine objects using the provided resource groups or virtual machine names to which we want to deploy the extension. This will look for unique virtual machine names. If objects with identical names are found, they will be skipped to ensure we’re only installing the extension on the desired machines.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | $vms = @() if (-not $ResourceGroupNames -and -not $VMNames) { Write-Output "No resource groups or VMs specified. Collecting all VMs" $vms = Get-AzureRmVM } elseif ($ResourceGroupNames -and -not $VMNames) { foreach ($rg in $ResourceGroupNames) { Write-Output "Collecting VM facts from resource group $rg" $vms += Get-AzureRmVM -ResourceGroupName $rg } } else { foreach ($VMName in $VMNames) { $azureResource = Get-AzureRmResource -Name $VMName ` -ResourceType 'Microsoft.Compute/virtualMachines' if ($azureResource.Count -lt 1) { Write-Error -Message "Failed to find $VMName" } elseif ($azureResource.Count -gt 1) { Write-Error -Message "Found multiple VMs with the name $VMName. Unable to configure extension" } $vms += Get-AzureRmVM -Name $VMName -ResourceGroupName $azureResource.ResourceGroupName } } |
Now we need to add the code that deploys and configures the extension. Inside the foreach loop, you’ll notice a call to Start-Job. This will create a new job for each VM in the array and allows for parallel processing, which significantly speeds up the runbook.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | $workspace = Get-AzureRmOperationalInsightsWorkspace -Name $WorkspaceName ` -ResourceGroupName $LAResourceGroup -ErrorAction Stop $key = (Get-AzureRmOperationalInsightsWorkspaceSharedKeys -ResourceGroupName $LAResourceGroup ` -Name $WorkspaceName).PrimarySharedKey $PublicSettings = @{"workspaceId" = $workspace.CustomerId } $ProtectedSettings = @{"workspaceKey" = $key } #Loop through each VM in the array and deploy the extension foreach ($vm in $vms) { Start-Job -ArgumentList $azContext, $vm, $workspace, $key, $PublicSettings, $ProtectedSettings -ScriptBlock { Param ( $azContext, $vm, $workspace, $key, $PublicSettings, $ProtectedSettings ) $vmStatus = (Get-AzureRmVM -ResourceGroupName $vm.ResourceGroupName ` -Name $vm.Name -Status).Statuses.DisplayStatus[-1] Write-Output "Processing VM: $($vm.Name)" if ($vmStatus -ne 'VM running') { Write-Warning -Message "Skipping VM as it is not currently powered on" } #Check to see if Linux or Windows if ($vm.StorageProfile.OsDisk.OsType -eq 'Windows') { $extensions = Get-AzureRmVMExtension -ResourceGroupName $vm.ResourceGroupName ` -VMName $vm.Name -Name 'Microsoft.EnterpriseCloud.Monitoring' ` -ErrorAction SilentlyContinue #Make sure the extension is not already installed before attempting to install it if (-not $extensions) { Write-Output "Adding MicrosoftMonitoringAgent extension to VM: $($vm.Name)" $result = Set-AzureRmVMExtension -ExtensionName "Microsoft.EnterpriseCloud.Monitoring" ` -ResourceGroupName $vm.ResourceGroupName ` -VMName $vm.Name ` -Publisher "Microsoft.EnterpriseCloud.Monitoring" ` -ExtensionType "MicrosoftMonitoringAgent" ` -TypeHandlerVersion 1.0 ` -Settings $PublicSettings ` -ProtectedSettings $ProtectedSettings ` -Location $vm.Location } else { Write-Output "Skipping VM - Extension already installed" } } elseif($vm.StorageProfile.OsDisk.OsType -eq 'Linux') { $extensions = Get-AzureRmVMExtension -ResourceGroupName $vm.ResourceGroupName ` -VMName $vm.Name -Name 'OmsAgentForLinux' -ErrorAction SilentlyContinue #Make sure the extension is not already installed before attempting to install it if (-not $extensions) { Write-Output "Adding OmsAgentForLinux extension to VM: $($vm.Name)" $result = Set-AzureRmVMExtension -ExtensionName "OmsAgentForLinux" ` -ResourceGroupName $vm.ResourceGroupName ` -VMName $vm.Name ` -Publisher "Microsoft.EnterpriseCloud.Monitoring" ` -ExtensionType "OmsAgentForLinux" ` -TypeHandlerVersion 1.0 ` -Settings $PublicSettings ` -ProtectedSettings $ProtectedSettings ` -Location $vm.Location } else { Write-Output "Skipping VM - Extension already installed" } } } } |
Finally, we add a while loop at the end of the script to continue writing output until the final job has completed
1 2 3 4 5 6 7 8 9 | $runningJobs = Get-Job -State Running While ($runningJobs.Count -gt 0) { foreach ($job in $runningJobs) { Receive-Job $job.Id } $runningJobs = Get-Job -State Running } |
Now putting all of this together, the completed runbook looks like the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | <# .SYNOPSIS Installs the OMS Agent to Azure VMs with the Guest Agent .DESCRIPTION Traverses an entire subscription / resource group/ or list of VMs to install and configure the Log Analytics extension. If no ResourceGroupNames or VMNames are provided, all VMs will have the extension installed. Otherwise a superset of the 2 parameters is used to determine VM list. .PARAMETER azureSubscriptionID ID of Azure subscription to use .PARAMETER azureEnvironment The Azure Cloud environment to use, i.e. AzureCloud, AzureUSGovernment .PARAMETER LogAnalyticsWorkspaceName Log Analytic workspace name .PARAMETER LAResourceGroup Resource Group of Log Analytics workspace .PARAMETER ResourceGroupNames List of Resource Groups. VMs within these RGs will have the extension installed Should be specified in format ['rg1','rg2'] .PARAMETER VMNames List of VMs to install OMS extension to Specified in the format ['vmname1','vmname2'] .NOTES Version: 1.0 Author: Chris Wallen Creation Date: 09/10/2019 #> Param ( [parameter(mandatory)] [string] $azureSubscriptionID, [parameter(mandatory)] [string] $azureEnvironment, [parameter(mandatory)] [string] $WorkspaceName, [parameter(mandatory)] [string] $LAResourceGroup, [string[]] $ResourceGroupNames, [string[]] $VMNames ) $connectionName = "AzureRunAsConnection" # Get the connection "AzureRunAsConnection " $servicePrincipalConnection = Get-AutomationConnection ` -Name $connectionName -ErrorAction Stop Write-Output "Logging in to Azure..." Add-AzureRmAccount ` -ServicePrincipal ` -TenantId $servicePrincipalConnection.TenantId ` -ApplicationId $servicePrincipalConnection.ApplicationId ` -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint ` -EnvironmentName $azureEnvironment ` -ErrorAction Stop $azContext = Select-AzureRmSubscription ` -subscriptionId $azureSubscriptionID -ErrorAction Stop $vms = @() if (-not $ResourceGroupNames -and -not $VMNames) { Write-Output "No resource groups or VMs specified. Collecting all VMs" $vms = Get-AzureRmVM } elseif ($ResourceGroupNames -and -not $VMNames) { foreach ($rg in $ResourceGroupNames) { Write-Output "Collecting VM facts from resource group $rg" $vms += Get-AzureRmVM -ResourceGroupName $rg } } else { foreach ($VMName in $VMNames) { $azureResource = Get-AzureRmResource -Name $VMName ` -ResourceType 'Microsoft.Compute/virtualMachines' if ($azureResource.Count -lt 1) { Write-Error -Message "Failed to find $VMName" } elseif ($azureResource.Count -gt 1) { Write-Error -Message "Found multiple VMs with the name $VMName. Unable to configure extension" } $vms += Get-AzureRmVM -Name $VMName -ResourceGroupName $azureResource.ResourceGroupName } } $workspace = Get-AzureRmOperationalInsightsWorkspace -Name $WorkspaceName ` -ResourceGroupName $LAResourceGroup -ErrorAction Stop $key = (Get-AzureRmOperationalInsightsWorkspaceSharedKeys -ResourceGroupName $LAResourceGroup ` -Name $WorkspaceName).PrimarySharedKey $PublicSettings = @{"workspaceId" = $workspace.CustomerId } $ProtectedSettings = @{"workspaceKey" = $key } #Loop through each VM in the array and deploy the extension foreach ($vm in $vms) { Start-Job -ArgumentList $azContext, $vm, $workspace, $key, $PublicSettings, $ProtectedSettings -ScriptBlock { Param ( $azContext, $vm, $workspace, $key, $PublicSettings, $ProtectedSettings ) $vmStatus = (Get-AzureRmVM -ResourceGroupName $vm.ResourceGroupName ` -Name $vm.Name -Status).Statuses.DisplayStatus[-1] Write-Output "Processing VM: $($vm.Name)" if ($vmStatus -ne 'VM running') { Write-Warning -Message "Skipping VM as it is not currently powered on" } #Check to see if Linux or Windows if ($vm.StorageProfile.OsDisk.OsType -eq 'Windows') { $extensions = Get-AzureRmVMExtension -ResourceGroupName $vm.ResourceGroupName ` -VMName $vm.Name -Name 'Microsoft.EnterpriseCloud.Monitoring' ` -ErrorAction SilentlyContinue #Make sure the extension is not already installed before attempting to install it if (-not $extensions) { Write-Output "Adding MicrosoftMonitoringAgent extension to VM: $($vm.Name)" $result = Set-AzureRmVMExtension -ExtensionName "Microsoft.EnterpriseCloud.Monitoring" ` -ResourceGroupName $vm.ResourceGroupName ` -VMName $vm.Name ` -Publisher "Microsoft.EnterpriseCloud.Monitoring" ` -ExtensionType "MicrosoftMonitoringAgent" ` -TypeHandlerVersion 1.0 ` -Settings $PublicSettings ` -ProtectedSettings $ProtectedSettings ` -Location $vm.Location } else { Write-Output "Skipping VM - Extension already installed" } } elseif($vm.StorageProfile.OsDisk.OsType -eq 'Linux') { $extensions = Get-AzureRmVMExtension -ResourceGroupName $vm.ResourceGroupName ` -VMName $vm.Name -Name 'OmsAgentForLinux' -ErrorAction SilentlyContinue #Make sure the extension is not already installed before attempting to install it if (-not $extensions) { Write-Output "Adding OmsAgentForLinux extension to VM: $($vm.Name)" $result = Set-AzureRmVMExtension -ExtensionName "OmsAgentForLinux" ` -ResourceGroupName $vm.ResourceGroupName ` -VMName $vm.Name ` -Publisher "Microsoft.EnterpriseCloud.Monitoring" ` -ExtensionType "OmsAgentForLinux" ` -TypeHandlerVersion 1.0 ` -Settings $PublicSettings ` -ProtectedSettings $ProtectedSettings ` -Location $vm.Location } else { Write-Output "Skipping VM - Extension already installed" } } } } $runningJobs = Get-Job -State Running While ($runningJobs.Count -gt 0) { foreach ($job in $runningJobs) { Receive-Job $job.Id } $runningJobs = Get-Job -State Running } |
And that’s it! Now that you have the runbook created, I recommend running a few tests to ensure you’re seeing the expected behavior.
Once you’ve tested and verified the runbook, the only things left to do are to publish it and set a recurring schedule.
I hope you find this useful. Please comment if you find any issues!