Use Azure Automation to Install and Configure the Log Analytics Extension

Hello, Chris Wallen here and in this post, I'm going to show how you can use an Azure runbook to deploy and configure the Log Analytics extension to a group of virtual machines running either Windows or .

Before we get started with the code portion, there are a couple of important things to note.

  1. 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.
  2. 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
  3. 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, AzureCloudAzureUSGovernment
  • 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

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 account.

$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.

$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.

$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

$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:

<#
    .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!

 

This article was originally published by Microsoft's Azure Blog. You can find the original article here.