Sitecore Development Azure VM Setup – A Scripted Approach

In this episode, we will be using Azure PowerShell, Chocolatey, the Sitecore Installation Framework to set up a virtual machine. This post is not an end-to-end tutorial but points out some of the main concepts needed to get there. A full implementation can be found in my Azure Virtual Machine for Sitecore Development repository on GitHub.

Getting Started

Make sure you have the following software installed on your local development environment:

If you are unaware of why you need Visual Studio Code instead of just using the PowerShell ISE, read this article about PowerShell Core 6.0.

Make sure you have installed everything correctly by connecting to azure in the Visual Studio Code’s built-in terminal using Connect-AzAccount.


Create the Virtual Machine

You can manually create this virtual machine in the Azure portal, or you can read my previous post on how to Create an Azure VM with ports actually working via Azure PowerShell. Once created, you should see these resources in your resource group:

Resources created after VM creation.

Installing Software

Now that we have the VM setup, we will use Chocolatey to install the software. A full list of packages can be found on the Chocolatey Packages page.

Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString(''))

$ChocoPackages = "googlechrome", "visualstudio2017community", "git", "notepadplusplus.install" , "scala"

ForEach ($PackageName in $ChocoPackages) {
    choco install $PackageName -y

Now you might be wondering, “Won’t executing this install the software on my local machine?” The answer to that is YES. To get the choco install to run on the Azure VM, we will leverage Set-AzVMCustomScriptExtension.

#Make this object as you will use it A LOT
$Vm = Get-AzVM -Name $VmName -ResourceGroupName $ResourceGroup

Set-AzVMCustomScriptExtension  `
        -ResourceGroupName $Vm.ResourceGroupName `
        -VmName $Vm.Name `
        -Location $Vm.Location `
        -FileUri "$BaseFileUri/InstallSoftware.ps1" `
        -Run "InstallSoftware.ps1" `
        -Name "VmInstallSoftware" `
        -Argument "-VmDownloadFolder $VmDownloadFolder"

The Set-AzVMCustomScriptExtension will download the PowerShell scripts from an accessible URL, such as a public blob storage. You will likely want to pass any secure variables via the Argument parameter.

It is important to note that AzVMCustomScriptExtension creates a string of the PowerShell script it will execute remotely. You may want to encode/decode the variables you are passing.

Enable PowerShell Remoting and Using the Custom Script Extension

Before we can install Sitecore, we will need to get PowerShell remoting enabled so that we can copy the license.xml file to the server. To do this, you can call Set-AzCustomScriptExtension again. Note that you need to pass the same script extension Name parameter as you can only have one.

Set-AzVMCustomScriptExtension  `
     -ResourceGroupName $Vm.ResourceGroupName `
     -VmName $Vm.Name `
     -Location $Vm.Location `
     -FileUri "$BaseFileUri/InstallPsRemoting.ps1" `
     -Run "InstallPsRemoting.ps1" `
     -Name "VmInstallSoftware"

The contents of InstallPsRemoting.ps1 would Enable-PSRemoting and open the Windows firewall. Again, this file would be stored in an accessible location such as a public Azure container. I am also taking this execution opportunity to install IIS.

Enable-PSRemoting -Force
netsh advfirewall firewall add rule name="WinRM-HTTP" dir=in localport=5985 protocol=TCP action=allow

if ((Get-WindowsFeature Web-Server).InstallState -ne "Installed") {
    Install-WindowsFeature -Name "Web-Server" -IncludeAllSubFeature -IncludeManagementTools

While this opens up connectivity on the virtual machine, you’ll also need to set this up on your local instance with the public IP of the virtual machine.

#Get the public IP of your VM
$VmPublicIpAddress = Get-AzPublicIpAddress -Name $Vm.Name

#Update your trustedhosts
Set-Item WSMan:localhost\client\trustedhosts -value $VmPublicIpAddress.IpAddress -Force

#Update your local firewall for WinRM"
netsh advfirewall firewall show rule name="WinRM-HTTP" | netsh advfirewall firewall add rule name="WinRM-HTTP" dir=in localport=5985 protocol=TCP action=allow

#Enable-PSRemoting on your local
Enable-PSRemoting -SkipNetworkProfileCheck -Force

Once this is configured, you can simply use PSSession to copy the file from your local to the VM.

$Cred = New-Object System.Management.Automation.PSCredential ($VmUsername, $VmUserPassword)

$Session = New-PSSession -ComputerName $VmPublicIpAddress.IpAddress -Port 5985 -Credential $Cred

Copy-Item -Path "license.xml" -Destination $SCInstallRoot -ToSession $Session

While you can use Invoke-Command with the $Session to run local scripts remotely, consider the reasons to use AzVMCustomScriptExtension instead.

For my purposes, this script potentially could be run by a number of developers globally. It made more sense for my implementation that these remotely executed PowerShell scripts remained consistent. Using AzVMCustomScriptExtension, most of the execution functions will live in a managed public storage account reducing the risk of them being changed as well as providing less complexity upfront to the users executing the scripts.

Installing Prerequisites and Sitecore

For this specific implementation, I am installing Sitecore 9.1 Update-1, which requires SQL 2016 SP2/2017, and Solr 7.2.1.

This means we will also need to install the Sitecore Install Framework and install the prerequisites on the virtual machine:

$SitecorePSRepository = ""

$SifModule = Get-InstalledModule SitecoreInstallFramework -AllVersions -ErrorAction SilentlyContinue
if([string]::IsNullOrEmpty($SifModule)) {
    Register-PSRepository –Name SitecoreRepo –SourceLocation $SitecorePSRepository -InstallationPolicy Trusted

    Install-Module SitecoreInstallFramework -Force

Import-Module SitecoreInstallFramework

Install-SitecoreConfiguration 'C:\VmSoftware\Sitecore\prerequisites.json' -Verbose

For Solr, let’s make life easy and use Jeremy Davis‘ Low Effort Solr Install.
For SQL, we will use Brad Christie’s SQL SIF Installer.

Installing SQL may take a while this way as it downloads it from Microsoft. I think it might be worthwhile to create a private repository in Azure, place the download there, and provide access to the VM to increase the speed. However, I haven’t had the opportunity to validate the improvement.

$SqlInstallParams = @{
    SqlExpressDownload = ""
    SqlAdminPassword = $SqlAdminPassword
    TempLocation = $VmDownloadPath
    Path = "sqlexpress.json"
try {
    Install-SitecoreConfiguration @SqlInstallParams
} catch [System.Data.SqlClient.SqlException] {
#The SQL installer produces a "needs restart" error which stops the script.  We want to continue
    if( $_.Exception.Number -eq 3021) {

Sitecore can be installed as usual with the XP0-SingleDeveloper.ps1. I have modified mine to decode all the variables. As a reminder, AzVMCustomScriptExtension creates a string of the PowerShell script it will execute remotely. I found it easiest to go ahead and encode all my variables passed to this specific file and decode them before execution.

function Get-Decoded {
    return [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Str))

$DeSCInstallRoot = Get-Decoded -Str $SCInstallRoot

# Install XP0 via combined partials file.
$singleDeveloperParams = @{
    Path = "$DeSCInstallRoot\XP0-SingleDeveloper.json"
    SqlServer = Get-Decoded -Str $SqlServer
    SqlAdminUser = Get-Decoded -Str $SqlAdminUser
    SqlAdminPassword = Get-Decoded -Str $SqlAdminPassword
    SitecoreAdminPassword = Get-Decoded -Str $SitecoreAdminPassword
    SolrUrl = Get-Decoded -Str $SolrUrl
    SolrRoot = Get-Decoded -Str $SolrRoot
    SolrService = Get-Decoded -Str $SolrService
    Prefix = Get-Decoded -Str $Prefix
    XConnectCertificateName = Get-Decoded -Str $XConnectSiteName
    IdentityServerCertificateName = Get-Decoded -Str $IdentityServerSiteName
    IdentityServerSiteName = Get-Decoded -Str $IdentityServerSiteName
    LicenseFile = Get-Decoded -Str $LicenseFile
    XConnectPackage = Get-Decoded -Str $XConnectPackage
    SitecorePackage = Get-Decoded -Str $SitecorePackage
    IdentityServerPackage = Get-Decoded -Str $IdentityServerPackage
    XConnectSiteName = Get-Decoded -Str $XConnectSiteName
    SitecoreSitename = Get-Decoded -Str $SitecoreSiteName
    PasswordRecoveryUrl = Get-Decoded -Str $PasswordRecoveryUrl
    SitecoreIdentityAuthority = Get-Decoded -Str $SitecoreIdentityAuthority
    XConnectCollectionService = Get-Decoded -Str $XConnectCollectionService
    ClientSecret = Get-Decoded -Str $ClientSecret
    AllowedCorsOrigins = Get-Decoded -Str $AllowedCorsOrigins

Push-Location $DeSCInstallRoot

Install-SitecoreConfiguration @singleDeveloperParams *>&1 | Tee-Object XP0-SingleDeveloper.log


Restarting the Virtual Machine

There are at least two times I needed to restart the machine during the install. One is after installing the Software via Chocolatey because of the .NET installs required for Visual Studio. The other is after installing Solr and MSSQL as the Solr script updates the Windows Environment Variables and MSSQL’s dependencies for a restart. To do this in your local script, you can simply execute the following:

Restart-AzVM -Name $Vm.Name -ResourceGroupName $Vm.ResourceGroupName

Final Thoughts

Again, this has not shown a full implementation, but a sampling of things you might need to do. A full implementation can be found at Azure Virtual Machine for Sitecore Development on GitHub.

You can absolutely do most, if not all of this, with an ARM template and Azure Automation. In fact, those might be a better option for you and your organization.

If you decide to put things on a public container, please do not put your license file or any other sensitive information on the public cloud.

By Greg Coffman

Technical strategist, agile evangelist, and all-around web nerd. Spends the day as Solution Architect at Sitecore. Thoughts and ideas are my own and do not represent Sitecore.

Leave a Reply

Your email address will not be published. Required fields are marked *

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