First published on TECHNET on Jul 31, 2017
Hello all
Neil Bird here from the Cloud & Infrastructure team in the UK. I am a Premier Field Engineer (PFE) specialising in helping customers get the most out of Microsoft Azure and Windows Server technologies.
One of my customers recently asked me if there was an easy way to install the BGInfo Extension on all of the Azure Resource Manager (ARM) VMs running in their Azure subscription. They had found a previous Azure Blog on VM Extensions , but this was from back in 2014 which meant the PowerShell cmdlets and example code was for Azure Service Manager (ASM), aka - "Classic VMs" and therefore is NOT valid for ARM VMs.
I hope everyone is using ARM for their Azure workloads, or if not, that you have plans to migrate from ASM to ARM in the near future. If you require more information on the difference between ASM and ARM, the following article provides useful information: Azure Resource Manager vs. Classic Deployment: Understand deployment models and the state of your r...
Azure virtual machine extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines. For example, if a virtual machine requires software installation, anti-virus protection, or Docker configuration, a VM extension can be used to complete these tasks.
The script framework I created and that I am sharing with you today is a PowerShell script that can be used to automate the installation of Any VM Extension . I have added lots of comments in green to make it easier to understand what actions the code is performing. I am aware that it is possible to install VM Extensions using a PowerShell " One-Liner ". However, I prefer to include prerequisite checks ( logic ) and an output showing the overall results as part of the installation process, this is useful when processing a large number of VMs and/or multiple subscriptions. The pre-install checks ensure the VMs are in the "correct state" to install an Extension, for example: in the BGInfo Extension scenario the checks ensure the following conditions are true :
If you use a PowerShell One-Liner to install VM Extensions, the installation could throw an error for any VM(s) that fail these tests. For example: if a VM is NOT running, it is NOT possible to install an Extension.
The reason I have called the PowerShell script a "framework" is that it can be used to install any VM Extension. Although the original request was for BGInfo, I used Parameters for the " Extension Type " ( i.e. Name ) and " Publisher Name ". This means you can easily install any other Extension using the same script, including Extensions that require additional "Settings". I will provide more information on how to specify Extension settings later in the post.
Updating AzureRM Module:
# Install the Azure Resource Manager modules from the PowerShell Gallery
Install-Module AzureRM
To make the script simple to use and familiarise yourself with its use, I have left the Default Extension as BGInfo . This means if you would like to install the BGInfo Extension on the VMs running in your subscription, you only need to specify the "-
SubscriptionName "<Name of your subscription>
" parameter ( or alternatively you could edit the parameter in the Params section of the code, Line 144 ). This parameter is a string value that stores the name of the subscription you would like the script to process.
Optionally also include the "-
ProcessAllVMs
" switch when executing the script. This switch instructs the script to "process ALL of the VMs in the subscription". Note - If this is NOT specified the script will only process the first 3 x VMs in the subscription.
For full details of the Script Parameters, including Syntax and a few Examples, once you have downloaded the script ( URL link below ), open a PowerShell prompt, change directory to the location you have saved the script to and run:
get-help
.\azure-install-vm-extension.ps1 -full
The "
get-help -full"
command will output the Script Synopsis, Description, Parameters, Examples, Links and Notes.
Recommended: Download the script from The Microsoft TechNet Gallery .
Without further ado, find below a sample PowerShell script that can be used to " Install an Extension on ALL Powered On, ARM VMs in a Subscription ".
Important Note - The Warranty statement shown in the
.NOTES
section of the script is standard wording for "sample scripts". As with anything that makes changes to IT systems, it is always highly recommended to perform acceptance tests against a "non-production or test subscription" prior to implementing in a live / production subscription(s). In addition to following your organisation's Change Control / Release Management / Pipeline processes and procedures. ( Of course, you already know this, but I wanted to call this out. )
Once you have downloaded the script , you can make changes or review the code in your favourite Script Editor, my "coding weapon of choice" is the awesome ( and free ) Visual Studio Code with the optional PowerShell Extension Installed :)
The code from the script " azure-install-vm-extension.ps1 " is shown below, so you can review it whilst reading this blog ( if you wish to ). To skip reviewing the code / continue reading the the blog, click here .
##########################################################################################################
<#
.SYNOPSIS
Automates the installation of Extensions on VMs in an Azure Subscription.
Performs checks to ensure VM is in the correct state to install an extension.
.DESCRIPTION
Script framework to install VM Extensions on All of the VMs in a subscription.
The Extension "Type" and "Publisher" are specified as parameters.
If these parameters are NOT specified at execution, the script defaults to installing the "BGInfo" Extension on Windows VMs.
The script performs the following checks to ensure a VM is in the "correct state" to install the extension:
1) VM is Running (NOT deallocated / stopped).
2) VM is using an Operating System that is compatible with the Extension (configured as parameters).
3) VM does not currently have the extension installed.
.PARAMETER SubscriptionName
Mandatory parameter. This can be configured in the script Params section or by passing as a pipeline parameter.
The Name of the Azure Subscription you wish to process.
.PARAMETER VMExtensionName
Mandatory parameter with default of "BGInfo".
The Name (Type) of the VM Extension you wish to install.
This parameter is Case Sensitive, due to the comparison used to test if the Extension is already installed
The PowerShell code below can be used to obtain a full list of the VM Extensions that are available in an Azure region.
# Edit the $location variable with your target region
[string]$location = "uksouth"
Get-AzureRmVmImagePublisher -Location $location | `
Get-AzureRmVMExtensionImageType | `
Get-AzureRmVMExtensionImage | Select Type, PublisherName | ft *
.PARAMETER VMExtensionPublisher
Mandatory parameter with default of "Microsoft.Compute".
The Publisher of the VM Extension you wish to install.
.PARAMETER ProcessAllVMs
Include this parameter if you would like to "Process ALL VMs in the subscription".
If this parameter is NOT included the script will only process the first 3 x VMs in the subscription.
This is a safety measure to prevent unintentionally installing the Extension on ALL VMs.
.LINK
.EXAMPLE
.\azure-install-vm-extension.ps1 -SubscriptionName "Visual Studio Enterprise"
Installs the default VM Extension configured in the script parameters (BGInfo) on first 3 x Windows VMs in the "Visual Studio Enterprise" subscription.
.EXAMPLE
.\azure-install-vm-extension.ps1 -SubscriptionName "Visual Studio Enterprise" -ProcessAllVMs
Installs the default VM Extension configured in the script parameters (BGInfo) on ALL of the Windows VMs in the "Visual Studio Enterprise" subscription.
.EXAMPLE
.\azure-install-vm-extension.ps1 -SubscriptionName "Visual Studio Enterprise" `
-VMExtensionName "IaaSAntimalware" `
-VMExtensionPublisher "Microsoft.Azure.Security" `
-VMExtensionWindowsCompatible $true `
-VMExtensionLinuxCompatible $false `
-VMExtensionSettingsFilePath "C:\scripts\IaaSAntimalware-Config.json" `
-ProcessAllVMs
Installs the "IaaSAntimalware" VM Extension on ALL of the Windows VMs in the "Visual Studio Enterprise" subscription.
Configures the "IaaSAntimalware" Extension settings using the configuration in the "C:\scripts\IaaSAntimalware-Config.json" file.
Example JSON Schema for "Microsoft Antimalware" Extension, "C:\scripts\IaaSAntimalware-Config.json" file:
{
"AntimalwareEnabled": true,
"RealtimeProtectionEnabled": true,
"ScheduledScanSettings": {
"isEnabled": true,
"day": "7",
"time": "120",
"scanType": "Quick"
},
"Exclusions": {
"Extensions": "",
"Paths": "%windir%\\SoftwareDistribution\\Datastore\\DataStore.edb;%windir%\\SoftwareDistribution\\Datastore\\Logs\\Edb.chk",
"Processes": ""
}
}
For full details on how to configure the "Microsoft Antimalware Extension" settings including file exclusions, see the following article:
"Microsoft Antimalware for Azure Cloud Services and Virtual Machines"
.NOTES
THIS CODE-SAMPLE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR
FITNESS FOR A PARTICULAR PURPOSE.
This sample is not supported under any Microsoft standard support program or service.
The script is provided AS IS without warranty of any kind. Microsoft further disclaims all
implied warranties including, without limitation, any implied warranties of merchantability
or of fitness for a particular purpose. The entire risk arising out of the use or performance
of the sample and documentation remains with you. In no event shall Microsoft, its authors,
or anyone else involved in the creation, production, or delivery of the script be liable for
any damages whatsoever (including, without limitation, damages for loss of business profits,
business interruption, loss of business information, or other pecuniary loss) arising out of
the use of or inability to use the sample or documentation, even if Microsoft has been advised
of the possibility of such damages, rising out of the use of or inability to use the sample script,
even if Microsoft has been advised of the possibility of such damages.
#>
##########################################################################################################
###############################
## SCRIPT OPTIONS & PARAMETERS
###############################
#Requires -Version 3
#Requires -Modules AzureRM
# Version: 1.1
<# - 28/07/2017
* added progress bar and confirmation prompt.
* added "-ProcessAllVMs" switch, without this script only processes 3 x VMs by default.
* added parameter to specify a "SettingString" configuration file for Extension settings.
* added counters to provide an "installation results" report when the script completes.
- 14/07/2017
* initial script creation.
#>
# Define and validate mandatory parameters
[CmdletBinding
()
]
Param
(
# Azure Subscription Name
[parameter
(
Position
=1)
]
[string]
$
SubscriptionName =
"SUBSCRIPTION NAME"
,
# ***** EDIT ABOVE WITH YOUR SUBSCRIPTION NAME, OR PASS AS SCRIPT PARAMETER *****
# VM Extension Name (Case sensitive for "Extensions.id.Contains" comparison)
[parameter
(
Position
=2)
]
[string]
$
VMExtensionName =
"BGInfo"
,
# VM Extension Publisher
[parameter
(
Position
=3)
]
[string]
$
VMExtensionPublisher =
"Microsoft.Compute"
,
# VM Extension Windows OS Compatible
[parameter
(
Position
=4)
]
[bool]
$
VMExtensionWindowsCompatible =
$true
,
# VM Extension Linux OS Compatible
[parameter
(
Position
=5)
]
[bool]
$
VMExtensionLinuxCompatible =
$false
,
# VM Extension JSON Settings File Path
[parameter
(
Position
=6)
]
[string]
$
VMExtensionSettingsFilePath =
""
,
# Process All VMs in Subscription Switch, if not present script only processes first 3 VMs
[parameter
(
Position
=7)
]
[switch]
$
ProcessAllVMs
)
# Set strict mode to identify typographical errors
Set-StrictMode
-Version Latest
# Make the script verbose by default
$VerbosePreference =
"Continue"
##########################################################################################################
#####################################
## FUNCTION 1 - Install-VMExtension
#####################################
Function
Install-VMExtension {
# Connect to Azure
Write-Host
"`nPrompting for Azure Credentials and Authenticating..."
# Login to Azure Resource Manager (ARM), if this fails, stop script.
Login-AzureRmAccount -SubscriptionName
$
SubscriptionName
-ErrorAction Stop
# Get all ARM VMs in Subscription
[array]
$
VMs
= Get-AzureRMVM -Status -ErrorAction Stop
# Counter for Progress bar and $ProcessAllVMs switch
$
VMsProcessed
= 0
# Loop through all VMs in the Subscription
ForEach
($
VM
in
$
VMs
)
{
# Check if the ProcessAllVMs switch has NOT been set
if
(
!
$
ProcessAllVMs
.IsPresent
)
{
# We are NOT Processing All VMs (switch NOT present), stop after first 3 x VMs
if
(
$
VMsProcessed
-eq
3
)
{
# Write informational message about use of the -ProcessAllVMs switch
Write-Host
"`nINFO: Script Stopping."
Write-Host
'INFO: To process more than the first 3 x VMs in a subscription, Set the -ProcessAllVMs parameter when executing the script.'
# Break out of the ForEach Loop to stop processing
Break
}
}
# Show the Progress bar for number of VMs Processed...
$
VMsProcessed
++
Write-Progress -Activity
"Processing VMs in ""
$($
SubscriptionName
)
>""..."
`
-Status
"Processed:
$
VMsProcessed
of
$($
VMs
.count
)
"
`
-PercentComplete
(($
VMsProcessed
/
$
VMs
.Count
)
*
100
)
# Ensure the VM OS is Compatible with Extension
if
(($
VM
.OSProfile.WindowsConfiguration -and
$
VMExtensionWindowsCompatible
)
`
-or
($
VM
.OSProfile.LinuxConfiguration -and
$
VMExtensionLinuxCompatible
))
{
# Ensure the Extension is NOT already installed
if
(($
VM
.Extensions.count -eq 0
)
-or
(
!
(
Split-Path -Leaf
$
VM
.Extensions.id
)
.Contains
($
VMExtensionName
)))
{
# If VM is Running
if
(
$
VM
.PowerState -eq
'VM running'
)
{
# Output the VM Name
Write-Host
"
$($
VM
.Name
)
: requires
$($
VMExtensionName
)
, installing..."
# Get the latest version of the Extension in the VM's Location:
[version]
$
ExtensionVersion
=
(
Get-AzureRmVMExtensionImage -Location
$
VM
.Location
`
-PublisherName
$
VMExtensionPublisher
-Type
$
VMExtensionName
)
.Version `
|
ForEach-Object
{
New-Object System.Version
($
PSItem
)
} | `
Sort-Object -Descending | Select-Object -First 1
[string]
$
ExtensionVersionMajorMinor
=
"{0}.{1}"
-F
$
ExtensionVersion
.Major,
$
ExtensionVersion
.Minor
# If the $VMExtensionSettingFilePath parameter has been specified and the file exists
if
(($
VMExtensionSettingsFilePath
-ne
""
)
-and
(
Test-Path
$
VMExtensionSettingsFilePath
))
{
# Import Extension Config File
$
VMExtensionConfigfile
= Get-Content
$
VMExtensionSettingsFilePath
-Raw
# Install the Extension with SettingString parameter
$
ExtensionInstallResult
= Set-AzureRmVMExtension -ExtensionName
$
VMExtensionName
`
-Publisher
$
VMExtensionPublisher
-TypeHandlerVersion
$
ExtensionVersionMajorMinor
-
ExtensionType
$
VMExtensionName
`
-Location
$
VM
.Location -ResourceGroupName
$
VM
.ResourceGroupName
`
-SettingString
$
VMExtensionConfigfile
-VMName
$
VM
.Name
}
else
{
# $VMExtensionSettingFilePath does NOT exist
# Install the Extension WITHOUT SettingString parameter
$
ExtensionInstallResult
= Set-AzureRmVMExtension -ExtensionName
$
VMExtensionName
`
-Publisher
$
VMExtensionPublisher
-TypeHandlerVersion
$
ExtensionVersionMajorMinor
-ExtensionType
$
VMExtensionName
`
-Location
$
VM
.Location -ResourceGroupName
$
VM
.ResourceGroupName
`
-VMName
$
VM
.Name
}
# Install Extension with SettingString parameter if file specified and exists
# Installation finished, check the return status code
if
(
$
ExtensionInstallResult
.IsSuccessStatusCode -eq
$true)
{
# Installation Succeeded
Write-Host
"SUCCESS: "
-ForegroundColor Green -nonewline; `
Write-Host
"
$($
VM
.Name
)
: Extension installed successfully"
$Global
:
SuccessCount
++
}
else
{
# Installation Failed
Write-Host
"ERROR: "
-ForegroundColor Red -nonewline; `
Write-Host
"
$($
VM
.Name
)
: Failed - Status Code:
$($
ExtensionInstallResult
.StatusCode
)
"
$Global
:
FailedCount
++
}
}
else
{
# VM is NOT Running
Write-Host
"WARN: "
-ForegroundColor Yellow -nonewline; `
Write-Host
"
$($
VM
.Name
)
: Unable to install
$($
VMExtensionName
)
- VM is NOT Running"
$Global
:
VMsNotRunningCount
++
# Could use "Start-AzureRmVM -ResourceGroupName $vm.ResourceGroupName -Name $VM.Name",
# wait for VM to start and Install extension, possible improvement for future version.
}
}
else
{
# VM already has the Extension installed.
Write-Host
"INFO:
$($
VM
.Name
)
: Already has the
$($
VMExtensionName
)
Extension Installed"
$Global
:
AlreadyInstalledCount
++
}
# Extension NOT Compatible with VM OS, as defined in Script Parameters boolean values
}
else
{
# Linux
if
($
VM
.OSProfile.LinuxConfiguration
-and
(
!
$
VMExtensionLinuxCompatible
))
{
# VM is running Linux distro and $VMExtensionLinuxCompatible = $false
Write-Host
"INFO:
$($
VM
.Name
)
: Is running a Linux OS, extension
$($
VMExtensionName
)
is not compatible, skipping..."
$Global
:
OSNotCompatibleCount
++
# Windows
}
elseif
($
VM
.OSProfile.WindowsConfiguration
-and
(
!
$
VMExtensionWindowsCompatible
))
{
# VM is running Windows $VMExtensionWindowsCompatible = $false
Write-Host
"INFO:
$($
VM
.Name
)
: Is running a Windows OS, extension
$($
VMExtensionName
)
is not compatible, skipping..."
$Global
:
OSNotCompatibleCount
++
# Error VM does NOT have a Windows or Linux Configuration
}
else
{
# Unexpected condition, VM does not have a Windows or Linux Configuration
Write-Host
"ERROR: "
-ForegroundColor Red -nonewline; `
Write-Host
"
$($
VM
.Name
)
: Does NOT have a Windows or Linux OSProfile!?"
}
# Extension OS Compatibility
}
# ForEach VM Loop
}
# end of Function Install-VMExtension
}
# end of Function Install-VMExtension
# Setup counters for Extension installation results
[double]
$Global
:
SuccessCount
=
0
[double]
$Global
:
FailedCount
=
0
[double]
$Global
:
AlreadyInstalledCount
=
0
[double]
$Global
:
VMsNotRunningCount
=
0
[double]
$Global
:
OSNotCompatibleCount
=
0
[string]
$
DateTimeNow
= get-date -Format
"dd/MM/yyyy - HH:mm:ss"
Write-Host
"`n========================================================================`n"
Write-Host
"
$($
DateTimeNow
)
- Install VM Extension Script Starting...`n"
Write-Host
"========================================================================`n"
# Prompt for confirmation...
if
($
ProcessAllVMs
.IsPresent
)
{
[string]
$
VMTargetCount
=
"ALL of the"
}
else
{
[string]
$
VMTargetCount
=
"the first 3 x"
}
# User prompt confirmation before processing
[string]
$
UserPromptMessage
=
"Do you want to install the ""
$($
VMExtensionName
)
"" Extension on
$($
VMTargetCount
)
VMs in the ""
$($
SubscriptionName
)
"" Subscription?"
if
(
!
$
ProcessAllVMs
.IsPresent
)
{
$
UserPromptMessage
=
$
UserPromptMessage
+
"`n`nNote: use the ""-ProcessAllVMs"" switch to install the Extension on ALL VMs."
}
$
UserPromptMessage
=
$
UserPromptMessage
+
"`n`nType ""yes"" to confirm....`n`n`t"
[string]
$
UserConfirmation
=
Read-Host
-Prompt
$
UserPromptMessage
if
($
UserConfirmation
.ToLower
()
-ne
'yes'
)
{
# Abort script, user reponse was NOT "yes"
Write-Host
"`nUser typed ""
$($
UserConfirmation
)
"", Aborting script...`n`n"
-ForegroundColor Red
Exit
}
else
{
# Continue, user responded "yes" to confirm
Write-Host
"`nUser typed 'yes' to confirm...."
-
ForegroundColor Green
Write-Host
"Processing...`n"
# Call Function to Install Extension on VMs
Install-VMExtension
}
# Add up all of the counters
[double]
$
TotalVMsProcessed
=
$Global
:
SuccessCount
+
$Global
:
FailedCount
+
$Global
:
AlreadyInstalledCount
`
+
$Global
:
VMsNotRunningCount
+
$Global
:
OSNotCompatibleCount
# Output Extension Installation Results
Write-Host
"`n"
Write-Host
"========================================================================"
Write-Host
"`tExtension
$($
VMExtensionName
)
- Installation Results`n"
Write-Host
"Installation Successful:`t`t
$($Global
:
SuccessCount
)
"
Write-Host
"Already Installed:`t`t`t
$($Global
:
AlreadyInstalledCount
)
"
Write-Host
"Installation Failed:`t`t`t
$($Global
:
FailedCount
)
"
Write-Host
"VMs Not Running:`t`t`t
$($Global
:
VMsNotRunningCount
)
"
Write-Host
"Extension Not Compatible with OS:`t
$($Global
:
OSNotCompatibleCount
)
`n"
Write-Host
"Total VMs Processed:`t`t`t
$($
TotalVMsProcessed
)
"
Write-Host
"========================================================================`n`n"
[string]
$
DateTimeNow
= get-date -Format
"dd/MM/yyyy - HH:mm:ss"
Write-Host
"`n========================================================================`n"
Write-Host
"
$($
DateTimeNow
)
- Install VM Extension Script Complete.`n"
Write-Host
"========================================================================`n"
.\azure-install-vm-extension.ps1
-SubscriptionName
"Name of your subscription"
-ProcessAllVMs
# Edit below location variable with your target region
[string]
$
location
=
"uksouth"
Get-AzureRmVmImagePublisher
-Location
$
location
| `
Get-AzureRmVMExtensionImageType
| `
Get-AzureRmVMExtensionImage
| Select Type, PublisherName | ft *
-VMExtensionName
= "
IaaSAntimalware
"
-VMExtensionPublisher
= "
Microsoft.Azure.Security
"
{
"AntimalwareEnabled"
:
true
,
"RealtimeProtectionEnabled"
:
true
,
"ScheduledScanSettings"
: {
"isEnabled"
:
true
,
"day"
:
"7"
,
"time"
:
"120"
,
"scanType"
:
"Quick"
},
"Exclusions"
: {
"Extensions"
:
""
,
"Paths"
: "
"
,
"Processes"
:
""
}
}
Extensions
", "
Paths
" and "
Processes
" sections of the JSON file, the syntax is a " List of Semi-Colon Delimited Values ". "
Paths
" also require an escape character for the backslash ( i.e - double backslashes ). For example:
"Paths": "%windir%\\SoftwareDistribution\\Datastore\\DataStore.edb
;
%windir%\\SoftwareDistribution\\Datastore\\Logs\\Edb.chk",
-VMExtensionSettingsFilePath
parameter to pass the File Path of the JSON file to the script. An example PowerShell command line to install the Microsoft Antimalware Extension is shown below:
.\azure-install-vm-extension.ps1
-SubscriptionName
"Visual Studio Enterprise"
-VMExtensionName
"IaaSAntimalware"
-VMExtensionPublisher
"Microsoft.Azure.Security"
-VMExtensionWindowsCompatible
$true
-VMExtensionLinuxCompatible
$false
-VMExtensionSettingsFilePath
"C:\scripts\IaaSAntimalware-Config.json"
-ProcessAllVMs
The example above could be used to " Automate the installation of the Microsoft Antimalware Extension on All Windows VMs running in a Subscription. "
Important : As mentioned earlier regarding Warranty of sample scripts. Please ensure that you Test Automation Scripts against a "Test / Dev Subscription" or against a "Subset of VMs" prior to implementing against your Production Subscription(s). You are responsible for testing and validating the desired outcomes are achieved by the code you execute.
{
"type": "Microsoft.Compute/virtualMachines/extensions",
"name": "BGInfo",
"apiVersion": "2016-04-30-preview",
"scale": null,
"location": "[resourceGroup().location]",
"properties": {
"publisher": "Microsoft.Compute",
"type": "BGInfo",
"typeHandlerVersion": "2.1",
"autoUpgradeMinorVersion": true
}
}
Name
" property can be customised if you want to include the VM Name, but this is optional. And you would also require a "
dependsOn
" property specifying
"[resourceId('Microsoft.Compute/virtualMachines', parameters('virtualMachineName'))]"
or similar, depending on the parameter name you use for the VMs Name. You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.