Automating deployments to Azure IaaS with custom actions

Firstly the final scripts of all the content discussed are available here. A video walkthrough is available at and included below.

In this post I want to document the results of a POC (proof of concept) I was engaged in for a very large customer. The customer wanted to create single/multi VM environments in Azure for dev/test/QA purposes. The goal was a single command execution that would create the VM and in this case make it a domain controller, install SQL Server 2012 then install SharePoint 2010. For this scenario I decided to use PowerShell rather than JSON just to demonstrate the PowerShell approach since there are already many JSON templates in the gallery around SharePoint deployment.

To enable this solution the high level workflow would be:

  1. Create a new resource group and in that create a new storage account and virtual network (since each environment was to be isolated and by placing in their own resource group the lifecycle management, i.e. deletion, would be simple)
  2. Create a new VM using the created resources
  3. Execute PowerShell inside the VM via the Azure VM Agent to promote the VM to a domain controller then reboot it
  4. After the reboot execute additional PowerShell to create accounts, open firewall exceptions, install SQL Server 2012 then install SharePoint 2010

The unattended installation of a domain controller via PowerShell is very simple. Below is an example that creates a pocdom.local domain.

Import-Module "Servermanager" #For Add-WindowsFeature
Add-WindowsFeature AD-Domain-Services, DNS -IncludeManagementTools

$netbiosname = 'POCDom'
$fqdomname = 'pocdom.local'
$NTDSPath = 'e:ntds'
$NTDSLogPath = 'e:ntdslogs'
$SYSVOLPath = 'e:sysvol'

$SafePassPlain = 'Pa55word'
$SafePass = ConvertTo-SecureString -string $SafePassPlain `
    -AsPlainText -force
Install-ADDSForest -DomainName $fqdomname -DomainNetBIOSName $netbiosname `
	-SafemodeAdministratorPassword $SafePass -SkipPreChecks `
	-InstallDNS:$true -SYSVOLPath $SysvolPath -DatabasePath $NTDSPath -LogPath $NTDSLogpath `

You will notice in the code I write the AD to the E: drive. This is because in Azure the OS disk by default is read/write cache enabled which is not desirable for databases. Therefore for the VM I add two data disks with no caching; one for AD and one for SQL and SharePoint. The code below is what I use to change the drive letter of the DVD device then initialize and format the two data disks.

#Bring data disks online and initialize them
Get-Disk | Where-Object PartitionStyle –Eq "RAW"| Initialize-Disk -PartitionStyle GPT   
#Change CD drive letter
$drv = Get-WmiObject win32_volume -filter 'DriveLetter = "E:"'
$drv.DriveLetter = "L:"
$drv.Put() | out-null
Get-Disk -Number 2 | New-Partition -UseMaximumSize -DriveLetter E | Format-Volume -FileSystem NTFS -NewFileSystemLabel "Data1" -Confirm:$False      
Get-Disk -Number 3 | New-Partition -UseMaximumSize -DriveLetter F | Format-Volume -FileSystem NTFS -NewFileSystemLabel "Data2" -Confirm:$False

The two pieces of code above would be combined into the first boot PowerShell code (with the disk initialization block before the DC promotion code). Once the reboot has completed firewall exceptions for SQL and SharePoint need to be enabled.

New-NetFirewallRule -DisplayName "MSSQL ENGINE TCP" -Direction Inbound -LocalPort 1433 -Protocol TCP -Action Allow
New-NetFirewallRule -DisplayName "SharePoint TCP 2013" -Direction Inbound -LocalPort 2013 -Protocol TCP -Action Allow

Next I need the SQL Server and SharePoint media along with unattended commands to install. I decided to use Azure Files as the store for the installation media. Azure Files presents an SMB file share to the VMs with only the storage account key and name required to access. In my example I place this in the PowerShell script however it could also be injected in at runtime or stored more securely if required. Create a storage account then create an Azure Files share through the portal and take a note of the access key and storage account name.


Into this share I will copy the SQL Server and SharePoint installation files. The easiest way to upload content is using the free Azure Storage Explorer tool from

Now the details of performing unattended installations of SQL and SharePoint are outside the scope of this write-up as the goal for this is more how to install applications through Azure IaaS PowerShell however at a very high level:

  • To install SQL Server unattended simply requires a configuration file which can be generated by running through the graphical SQL Server setup and on the last page it will show you the location of the configuration file it will use for installation. Simply copy this file and cancel the installation. Copy the SQL Setup structure and the configuration file to the Azure Files share. I place the ConfigurationFile.ini in a separate Assets folder on the share. Then use that setup file with the SQL setup.exe, for example
    .'X:SQLServer2012SP3Setup.exe' /ConfigurationFile="C:AssetsConfigurationFile.ini"
  • For the SharePoint unattended installation I used the autospinstaller solution which is fully documented at and includes a web based site to create the unattended answer file used by the program. Follow the instructions on the site and copy the resulting structure to the Azure Files share.

My resulting Azure Files share consists therefore of 3 folders:

  • AutoSPInstaller – The SharePoint installation media and AutoSPInstaller solution
  • POCAzureScripts – The SQL configuration script
  • SQLServer2012SP3 – SQL Server installation media

To map to the share, copy the content, trigger the SQL Server installation from the share, dismount the share then trigger the SharePoint installation I use the following (which also adds an account named Administrator as that was a requirement). I would add the firewall exception creation to this code as the secondboot PowerShell file. You will notice I wait for 40 minutes at the end for the SharePoint installation to complete. I run the SharePoint install as a separate, asynchronous job as at the end it asks for key presses to continue so this avoids trying to handle that and after a reboot that will all get cleared up.

#Add domain admin called Administrator
New-ADUser -Name 'administrator' -GivenName 'admin' -Surname 'istrator' `
    -SamAccountName 'administrator' -UserPrincipalName 'administrator@pocdom.local' `
    -AccountPassword (ConvertTo-SecureString -AsPlainText 'Pa55word' -Force) `
    -Enabled $true

Add-ADGroupMember 'Domain Admins' administrator

$storkey = '<storage key>'
New-SmbMapping -LocalPath X: -RemotePath \<storage account name> -username 'sascuspocstdstor' -Password $storkey

#SharePoint Setup files
Copy-Item -Recurse -Path X:AutoSPInstaller -Destination C:Assets
#Configuration files that make up SQL and SharePoint install including the SharePoint backup
Copy-Item -Recurse -Path X:POCAzureScripts* -Destination C:Assets

#SQL Install
.'X:SQLServer2012SP3Setup.exe' /ConfigurationFile="C:AssetsConfigurationFile.ini"

Remove-SmbMapping -LocalPath X: -Force

#SharePoint Install
$AccountsToCreate = @("SP_CacheSuperUser","SP_CacheSuperReader","SP_Services","SP_PortalAppPool","SP_ProfilesAppPool","SP_SearchService","SP_SearchContent","SP_ProfileSync")

foreach($account in $AccountsToCreate)
  New-ADUser -Name $account -GivenName $account -Surname $account `
    -SamAccountName $account -UserPrincipalName $account@pocdom.local `
    -AccountPassword (ConvertTo-SecureString -AsPlainText 'Pa55word' -Force) `
    -Enabled $true

#Perform the actual install
$SPInstallJob = Start-Job -ScriptBlock {C:AssetsSPAutoSPInstallerAutoSPInstallerLaunch.bat}
Start-Sleep -Seconds 2400 #wait for 40 minutes for above to complete

At this point I have a firstboot.ps1 and a secondboot.ps1 file. Upload those files into blobs in a container named scripts in the same storage account as the Azure Files. These files will be used as part of the total VM provisioning process.

The final part is to create the VM and use the PowerShell created. In the example code below I create all the resources and use premium storage accounts to maximum performance however any of these parameters can be changed to meet requirements. In the code replace the <storage account name for assets> with the storage account created holding the Azure Files and blob content along with its key. Also change the VM name to something unique since a public IP name will be generated based on this name. If you will deploy this many times add some logic to include some random sequence or perhaps the requesting username. Also include that as part of the resource group, storage account etc name.

#Setup a filter to timestamp logs
filter timestamp {"$(Get-Date -Format G): $_"}

#Create Resources for new deployment
Write-Output "Setting up VM resources and variables" | timestamp
$SAName = 'saussclrspremstor1'
$SASKU = 'Premium_LRS'
$Location = 'southcentralus'

$VirtNetName = 'VNUSSCVNPOC1'

$VMName = '<something unique>'
$VMSize ="Standard_DS2"
#Get latest image
$AzureImageSku = Get-AzureRmVMImage -Location $Location -PublisherName "MicrosoftWindowsServer" -Offer "WindowsServer" -Skus "2012-R2-Datacenter"
$AzureImageSku = $AzureImageSku | Sort-Object Version -Descending #put the newest first which is the highest patched version
$AzureImage = $AzureImageSku[0] #Newest

#Create Resource Group
New-AzureRmResourceGroup -Name $RGName -Location $Location

#Create Storage Account
New-AzureRmStorageAccount -ResourceGroupName $RGName -StorageAccountName $SAName -Location $Location -Type $SASKU

#Create a Virtual Network
$subnet = New-AzureRmVirtualNetworkSubnetConfig -Name 'StaticSub' -AddressPrefix ""
$vnet = New-AzureRmVirtualNetwork -Force -Name $VirtNetName -ResourceGroupName $RGName `
    -Location $Location -AddressPrefix "" -Subnet $subnet # -DnsServer "" don't set yet
#If VM points to itself and not offering DNS yet the agents will hang during install

#Create VM
$vm = New-AzureRmVMConfig -VMName $VMName -VMSize $VMSize
#Create NIC
#For demo for easy access give a public IP
$pip = New-AzureRmPublicIpAddress -ResourceGroupName $RGName -Name ('PubIP' + $VMName) `
    -Location $Location -AllocationMethod Dynamic -DomainNameLabel $vmname.ToLower()
$nic = New-AzureRmNetworkInterface -Force -Name ('nic' + $VMName) -ResourceGroupName $RGName `
    -Location $Location -SubnetId $vnet.Subnets[0].Id -PrivateIpAddress `
    -PublicIpAddressId $pip.Id
$vm = Add-AzureRmVMNetworkInterface -VM $vm -Id $nic.Id

$osDiskName = $VMName+'-OSDisk'
$osDiskCaching = 'ReadWrite'
$osDiskVhdUri = "https://$"+$VMName+"-OS.vhd"

# Setup OS & Image
$user = "localadmin"
$password = 'Pa55word'
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential ($user, $securePassword)  
$vm = Set-AzureRmVMOperatingSystem -VM $vm -Windows -ComputerName $VMName -Credential $cred
$vm = Set-AzureRmVMSourceImage -VM $vm -PublisherName $AzureImage.PublisherName -Offer $AzureImage.Offer -Skus $AzureImage.Skus -Version $AzureImage.Version
$vm = Set-AzureRmVMOSDisk -VM $vm -VhdUri $osDiskVhdUri -name $osDiskName -CreateOption fromImage -Caching $osDiskCaching

$vm = Set-AzureRmVMBootDiagnostics -VM $vm -Disable

#Add two data disks
$dataDisk1VhdUri = "https://$"+$VMName+"-Data1.vhd"
$dataDisk1Name = $VMName+'-data1Disk'
$vm = Add-AzureRmVMDataDisk -VM $vm -Name $dataDisk1Name -Caching None -CreateOption Empty -DiskSizeInGB 127 -VhdUri $dataDisk1VhdUri -Lun 1
$dataDisk2VhdUri = "https://$"+$VMName+"-Data2.vhd"
$dataDisk2Name = $VMName+'-data2Disk'
$vm = Add-AzureRmVMDataDisk -VM $vm -Name $dataDisk2Name -Caching None -CreateOption Empty -DiskSizeInGB 512 -VhdUri $dataDisk2VhdUri -Lun 2

# Create Virtual Machine
Write-Output "Creating the VM" | timestamp
$NewVM = New-AzureRmVM -ResourceGroupName $RGName -Location $Location -VM $vm 
Write-Output "VM creation complete" | timestamp

#Now make a DC by running the first boot script
$ScriptBlobAccount = "<storage account name for assets>"
$ScriptBlobKey = "<storage key>"
$ScriptBlobURL = "https://<storage account name for assets>"
$ScriptName = "FirstBoot.ps1"
$ExtensionName = 'FirstBootScript'
$ExtensionType = 'CustomScriptExtension' 
$Publisher = 'Microsoft.Compute'&nbsp;&nbsp;
$Version = '1.8'
$timestamp = (Get-Date).Ticks
$ScriptLocation = $ScriptBlobURL + $ScriptName
$ScriptExe = ".$ScriptName"
$PrivateConfiguration = @{"storageAccountName" = "$ScriptBlobAccount";"storageAccountKey" = "$ScriptBlobKey"} 
$PublicConfiguration = @{"fileUris" = [Object[]]"$ScriptLocation";"timestamp" = "$timestamp";"commandToExecute" = "powershell.exe -ExecutionPolicy Unrestricted -Command $ScriptExe"}
Write-Output "Injecting First Boot PowerShell" | timestamp
Set-AzureRmVMExtension -ResourceGroupName $RGName -VMName $VMName -Location $Location `
 -Name $ExtensionName -Publisher $Publisher -ExtensionType $ExtensionType -TypeHandlerVersion $Version `
 -Settings $PublicConfiguration -ProtectedSettings $PrivateConfiguration
((Get-AzureRmVM -Name $VMName -ResourceGroupName $RGName -Status).Extensions | Where-Object {$_.Name -eq $ExtensionName}).Substatuses

Write-Output "Waiting 5 minutes for reboot to complete" | timestamp
Start-Sleep -Seconds 300 #Wait 5 minutes

#Have to remove the previous before creating a new one
Remove-AzureRmVMExtension -ResourceGroupName $RGName -VMName $VMName -Name FirstBootScript -Force

#Now run the second boot script to install SQL and SharePoint
$ScriptName = "SecondBoot.ps1"
$ExtensionName = 'SecondBootScript'
$timestamp = (Get-Date).Ticks
$ScriptLocation = $ScriptBlobURL + $ScriptName
$ScriptExe = ".$ScriptName"
$PrivateConfiguration = @{"storageAccountName" = "$ScriptBlobAccount";"storageAccountKey" = "$ScriptBlobKey"} 
$PublicConfiguration = @{"fileUris" = [Object[]]"$ScriptLocation";"timestamp" = "$timestamp";"commandToExecute" = "powershell.exe -ExecutionPolicy Unrestricted -Command $ScriptExe"}
Write-Output "Injecting Second Boot PowerShell" | timestamp
Set-AzureRmVMExtension -ResourceGroupName $RGName -VMName $VMName -Location $Location `
 -Name $ExtensionName -Publisher $Publisher -ExtensionType $ExtensionType -TypeHandlerVersion $Version `
 -Settings $PublicConfiguration -ProtectedSettings $PrivateConfiguration
((Get-AzureRmVM -Name $VMName -ResourceGroupName $RGName -Status).Extensions | Where-Object {$_.Name -eq $ExtensionName}).Substatuses

Remove-AzureRmVMExtension -ResourceGroupName $RGName -VMName $VMName -Name SecondBootScript -Force

Write-Output "Installation complete" | timestamp

In this example I give the VM a public IP so it can be accessed externally and has no NSG to lock down traffic. In reality you may not want the public IP and may add the environment to existing networks with connectivity to on-premises so would connect via private IP but I added public IP to handle worst case connectivity. If you do add a public IP like this example don’t use administrator account and don’t set simple passwords and make sure you configure NSGs to at least lock down traffic. I talk about NSGs at and below is example ARM PowerShell to create and add an NSG to a NIC.

$Location = 'southcentralus'

#Create a new rule to allow traffic from the Internet to port 443
$NSGRule1 = New-AzureRmNetworkSecurityRuleConfig -Name 'WEB' -Direction Inbound -Priority 100 `
    -Access Allow -SourceAddressPrefix 'INTERNET'  -SourcePortRange '*' `
    -DestinationAddressPrefix '*' -DestinationPortRange '443' -Protocol TCP

#Create a new NSG using the Rule created
New-AzureRmNetworkSecurityGroup -Name "NSGFrontEnd" -Location $Location -ResourceGroupName $RGName -SecurityRules $NSGRule1 #could use array of rules or separate by comma, e.g. $Rule1, $Rule2

$NSG = Get-AzureRmNetworkSecurityGroup -Name "NSGFrontEnd" -ResourceGroupName $RGName

#Add rule to existing to allow RDP 
Add-AzureRmNetworkSecurityRuleConfig -NetworkSecurityGroup $NSG -Name 'RDP' -Direction Inbound -Priority 101 `
    -Access Allow -SourceAddressPrefix 'INTERNET'  -SourcePortRange '*' `
    -DestinationAddressPrefix '*' -DestinationPortRange '3389' -Protocol TCP
Set-AzureRmNetworkSecurityGroup -NetworkSecurityGroup $NSG #Apply the change to the in memory object

#Remove a rule
Get-AzurermNetworkSecurityGroup -Name "NSGFrontEnd" -ResourceGroupName $RGName | Remove-AzureRmNetworkSecurityRuleConfig -Name 'RDP' |

#NSG must be same region as the resource
#Associate a NSG to a Virtual machine NIC
$NICName = 'dummyvm292'
$NIC = Get-AzureRmNetworkInterface -Name $NICName -ResourceGroupName $RGname
$NIC.NetworkSecurityGroup = $NSG
Set-AzureRmNetworkInterface -NetworkInterface $NIC

Finally if you want to delete the entire environment just run:

Remove-AzureRmResourceGroup -ResourceGroupName $RGName -Force


90 minute Azure Infrastructure Whiteboard Overview

Just finished a brand new 90 minute whiteboarding overview of Azure Infrastructure services so grab your popcorn, kick back and enjoy. Available at up to 1080 to see all the screen detail :-).

Websites I show in the video include:

Creating a Minecraft server using the new Azure portal

A couple of years ago I wrote a basic set of instructions on creating a Minecraft server in Azure. I felt it was time to create a new set of instructions based on the new Azure portal. You need an Azure subscription to follow this tutorial, this could be a subscription you pay for, Azure benefits that are part of an MSDN subscription or even an Azure trial subscription which can be signed up for from

Once you have a subscription follow the steps outlined below:

Login to the new Azure portal at When you first login you will be at the Azure Startboard. Also notice the hub menu on the left hand side which enables access to various resources within Azure. At the bottom of the hub menu is a New button which enables all the different types of Azure service to be created.


Click the New button and under Browse select Compute which opens up the Compute blade with a list of images. Select the Windows Server 2012 R2 Datacenter image.


The Create VM blade will open which enables the configuration for the new VM to be selected. Notice in the Create VM blade there are some basic properties to be completed such as the name for the VM, a username and a password. Additionally there are options for the size of the VM, optional configurations and location.  The hostname can be anything you wish, for example Minecraft. Enter a username, it cannot be Administrator as this is reserved but could use localadmin. Enter a complex password which needs to be at least 8 characters with a mix of three of the following; lowercase, uppercase, numbers and symbols.

Select the Pricing Tier part to open the pricing tier lens which enables the size of the VM to be selected. The A1 Basic is sufficient for basic testing however if you wanted to host more players the Standard A2 may be a better fit. Standard tier VMs have higher storage IOPS and network performance in addition to load balancing and scale capabilities (which we don’t use for our single instance Minecraft server). Notice there is a View all link to show all the available VM sizes. Select a VM size and click Select.


By default a new virtual network is created for the VM along with a new cloud service which owns a Virtual IP that is accessible from the Internet and a random name is selected for the cloud service DNS name. Select Optional Configuration – Network – Domain Name – Create new domain name and type in a name. The name must be unique across all of Azure. You could try Minecraft-<your name> and click OK. As you type the name it will be checked to ensure its unique. This name will be how you can connect to your server, <name> Click OK to the Network blade. Notice by default a new Storage Account will be created to store the virtual hard disks for the new VM however you could change this if desired.


On the main Create VM blade select the Location part. This enables the Azure region to be selected where the new VM will be created. Notice there are Azure regions throughout the world so pick one closest to you (or your players!). Close the blade. By default a checkbox is selected “Add to Startboard” that would add the new VM to the Startboard of your subscription. Click Create to create the VM.

The VM will now be created and will take around 5 minutes to be fully provisioned and ready for use. By default the VM will have an OS disk that is 127 GB in size and a temporary disk, the D: drive. Never put any data you care about on the D: drive as this is not persistent and by default will only be used for the pagefile. The OS disk has read and write caching enabled. You can also add data disks which have configurable caching options including no caching which is what is needed for databases and other types of workload that need writes to be persisted directly to disk. We will add a data disk to our Minecraft server for our Minecraft binaries and data files. You will need to wait for the VM to be created before adding the data disk. Select the VM which will open the VMs build and under the Essential lens (a lens is a group of parts that share a common theme) select the All settings link which opens the Settings blade.

Select Disks in the Settings blade and select the Attach New action. By default the maximum size of 1023 GB for a disk is selected with caching disabled. Select the Storage Container part – Choose Storage Account and select the storage account that you used for the Minecraft server. For the Container select the default vhds container. Click OK to create the new data disk. You may wonder why 1023 GB since you have to pay for storage in Azure and your Minecraft world may only be 50 MB which means it would seem you are paying for a lot of wasted space. This is not the case as Azure actually uses sparse storage which means even though you are creating a 1023 GB VHD file in Azure Storage behind the scenes storage is only actually allocated for the data written which is what you pay for.


Your VM is now created and has a data disk added to it. The next step is to connect to the new VM. In the VMs blade select the Connect action. This will download an RDP file which can either be opened or saved to disk so the exact options such as display size etc can be changed. The RDP file is populated with the DNS name of the cloud service that contains the Minecraft VM and the port for the RDP endpoint for the specific VM.


You are now connected to your Azure VM. Open Explorer and you will see your OS C: drive and the temporary storage drive D: but the data disk we added is not shown because it has not yet been initialized or formatted. Open the Disk Management MMC snap-in (Start – Run – diskmgmt.msc). When the snap-in opens it will inform you of a new disk and offer to initialize. Click OK. Once the disk is online right click on the disk and select New Simple Volume. Except all the defaults. Notice on the Format Partition dialog enter a label for the Volume label such as Data and make sure “Perform a quick format” is selected. This is critical in Azure as remember that sparse storage? If you don’t perform a quick format every block of the disk will be written to which means you would then pay for the full 1023 GB size. Complete the dialogs to create the new data disk.


Navigate back to Explorer and the data disk will now be visible. Select the data disk and create a folder called Minecraft.

Open Internet Explorer and navigate to In the Multiplayer Server area download the latest server binary and save to the Minecraft folder that you created on the data disk. I normally rename this download to minecraft_server.exe and remove the version number from the name. You also need to download and install Java. This can be downloaded from and select the 64-bit version. During the Java installation you likely want to unselect the options to install and set Ask as the default!

You are now ready to get Minecraft running. I recommend creating a batch file to launch the Minecraft server which will configure it to use more memory. I save the following to a file (minecraft.bat) and place in my Minecraft folder. This launches Minecraft server and set it to use 2 GB of memory:

“C:Program FilesJavajre1.8.0_45binjavaw.exe” -Xms2048m -Xmx2048m -jar “Minecraft_Server.exe”

Run the batch file. Once it has run open the eula.txt and change the false to true then rerun the minecraft.bat file which will now launch the Minecraft server service however it is not usable yet.

Minecraft clients communicate to the server on port 25565 which by default is blocked by the Windows Firewall. You need to create a firewall exception. Click Start and type firewall. This will find the Windows Firewall with Advanced Security application. Launch it. Select Inbound Rules and select the New Rule action. Select a type of Port in the New Inbound Rule Wizard and click Next. In the next page select TCP and type in port 25565 then click Next. Accept the defaults to Allow and for all types of profile and on the final page enter a name of Minecraft Server. Click Finish.


There is one final action. The VM created sits within a cloud service and the cloud service has the publically accessible IP address. Endpoints are created on that IP address which enable communications on specific ports to be forwarded to specific ports on VMs in the cloud service. You already used one of these endpoints when you RDP’d to the VM earlier, that is using an automatic endpoint that was created to enable RDP access to the VM from the Internet. We will add a new endpoint for the Minecraft port. Open the VMs blade in the Azure portal and select All settings. Select Endpoints where you will see the existing endpoints created. Click Add. Enter a name of Minecraft and set the public and private port to 25565 then click OK.


You are now ready to use your new Minecraft server. Launch the Minecraft client and select Multiplayer. Click Add Server and for the Server Address use your cloud server DNS name, e.g. and click Done.


Select your new server and click Join Server.


And play!


As an optional step you probably want to make yourself an operator for your server. Add to the ops.json file. walks through this process and links to to find your UUID for your account.

For more information on Azure check out my new book and free Windows application.

AzureBookCover AzureAppPicture




Mastering Azure IaaS Windows 8.1 App Available!

In preparation for my new book, Mastering Microsoft Azure Infrastructure Services ( which comes out end of this month I have completed the companion application.

The application contains all the links and code from the book in addition to videos to help understand the concepts. The app checks for updated content each time it launches and I’ll be continually adding new content to the app so check it often.

You can download the application at or search the Windows Store for SavillTech.


I also updated the Hyper-V application with a bug fix related to scrolling at