Use Azure Automation to install and configure the Log Analytics extension

In this post, I’ll 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.

In case you’re not familiar with creating a runbook, you can get started using the instructions here.

Before we get started with the code portion, there are a couple 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 Virtual Machine

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’]

To save yourself from copy-pasting each section, you can visit my Github repo to download the full script or skip to the bottom to see the completed script.

#Define the 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 automation account.

 $connectionName = "AzureRunAsConnection"
    try
    {
        # Get the connection "AzureRunAsConnection "
        $servicePrincipalConnection = Get-AutomationConnection -Name $connectionName

        "Logging in to Azure..."
        Add-AzureRmAccount `
            -ServicePrincipal `
            -TenantId $servicePrincipalConnection.TenantId `
            -ApplicationId $servicePrincipalConnection.ApplicationId `
            -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint `
            -EnvironmentName AzureUSGovernment
    }
    catch
    {
        if (!$servicePrincipalConnection)
        {
            $ErrorMessage = "Connection $connectionName not found."
            throw $ErrorMessage
        } 
        else
        {
            throw $_.Exception
        }
    }    
    Set-AzureRmContext -susbscriptionID $subscriptionID

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.

#Define an array to hold the list of virtual machines
$vms = @()

#If no resource group names or VM names are specified
#grab all of the VMs in the subscription
if (-not $ResourceGroupNames -and -not $VMNames)
{
    Write-Output "No resource groups or VMs specified. Collecting all VMs"
    $vms = Get-AzureRMVM
}
#If a resource group but no VMs are specified, grab all VMs in RG
elseif ($ResourceGroupNames -and -not $VMNames)
{
    foreach ($rg in $ResourceGroupNames)
    {
        Write-Output "Collecting VM facts from resource group $rg"
        $vms += Get-AzureRmVM -ResourceGroupName $rg
    }
}
#Grab all VMs in the list
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.

#Configure the workspace information
$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.OsProfile.LinuxConfiguration -eq $null)
        {
            $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 Windows 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"
            }
        }
        else
        {
            $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 Linux 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
}

Putting it all together, the full runbook should resemble 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

"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.OsProfile.LinuxConfiguration -eq $null)
        {
            $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 Windows 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"
            }
        }
        else
        {
            $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 Linux 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 right behavior. To get started with running a test, see this article

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 all find this useful!

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.