Easily configure Remote Desktop Gateway firewall rules

When you install Remote Desktop Gateway which enables RDP to be encapsulated in HTTPS a number of firewall exceptions are required which are enabled automatically. This also means the RDG has a public and private IP address.

There are many other firewall exceptions that are normal for Windows functionality that by default are enabled for the Any profile which when you have a public IP address on a NIC means they are also enabled to the Internet. What you really need is for those exceptions to be bound to the domain profile, i.e. the internal NIC. This is easy to do with PowerShell. Firstly you can list all the exceptions that are enabled for Any.

Get-NetFirewallRule -Enabled True | where {$_.Profile -eq "Any"} | ft name, displayname -AutoSize

This will list a lot of exceptions. Next we want to change them to a profile of Domain except for the two required for RDG, the RDG UDP and HTTPS rules. This can be done with the following:

Get-NetFirewallRule -Enabled True | where {$_.Profile -eq "Any" -and ($_.Name -ne "IIS-WebServerRole-HTTPS-In-TCP" -and $_.Name -ne "TSG-UDP-Transport-In-UDP")} | Set-NetFirewallRule -Profile Domain


Note, you could change the rules to be excluded for other requirements you may have for other types of server.

Deploying Operating Systems in Azure using Windows PE

In this article I want to walk-through deploying operating systems in Azure using a custom Windows PE environment and along the way cover some basics around PE and OS deployment. Before going any further I would stress I don’t recommend this. The best way to deploy in Azure is using templates, have generic images and then inject configuration into them using declarative technologies such as PowerShell DSC, Chef or Puppet however there are organizations that have multiple years of custom image development at their core that at least in the short term need to be maintained which was my goal for this investigation. Is it even possible to use your own Windows PE based deployment.

My starting point was to get a deployment working on-premises on Hyper-V. Azure uses Hyper-V and at this level there really is nothing special about what Azure does so my thinking is if I got a process running on-premises I should be able to take that VHD, upload it to Azure, make an image out of it and create VMs from it (and this proved to be true!). The benefit of this approach was speed of testing and the ability to interact with the Windows PE environment during the development and testing phase. Something that is much harder in Azure as there is no console access.

The first step was to create a VHD (not VHDX for Azure compatibility) that contained Windows PE which I would boot to. I downloaded the latest Windows ADK from https://developer.microsoft.com/en-us/windows/hardware/windows-assessment-deployment-kit (1709) and installed on a machine. Once installed I created my own Windows PE (x64) instance via the Deployment and Imaging Tools Environment. I used the following commands:

copype amd64 C:WinPE_amd64

I then wanted to add PowerShell and other components to it including imagex.exe:

Dism /Mount-Image /ImageFile:"C:WinPE_amd64mediasourcesboot.wim" /Index:1 /MountDir:"C:WinPE_amd64mount"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsWinPE-WMI.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsen-usWinPE-WMI_en-us.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsWinPE-NetFX.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsen-usWinPE-NetFX_en-us.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsWinPE-Scripting.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsen-usWinPE-Scripting_en-us.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsWinPE-PowerShell.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsen-usWinPE-PowerShell_en-us.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsWinPE-StorageWMI.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsen-usWinPE-StorageWMI_en-us.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsWinPE-DismCmdlets.cab"

Dism /Add-Package /Image:"C:WinPE_amd64mount" /PackagePath:"C:Program Files (x86)Windows Kits10Assessment and Deployment KitWindows Preinstallation Environmentamd64WinPE_OCsen-usWinPE-DismCmdlets_en-us.cab"

copy "C:Program Files (x86)Windows Kits10Assessment and Deployment KitDeployment Toolsamd64DISMimagex.exe" c:winpe_amd64mount

Dism /Unmount-Image /MountDir:C:WinPE_amd64mount /Commit

Notice in the code above when I’m adding packages I do this my mounting the boot.wim file that is part of my copied PE environment, performing actions against it then committing those changes when I unmounts it. I’m modifying that boot.wim. This is an important point.

Once the PE was ready I wanted to quickly test so I built a VHD based on that PE environment.

create vdisk file="C:WinPEPS.vhd" maximum=100000 type=expandable
attach vdisk
create partition primary size=1000
assign letter=V
format fs=ntfs quick

MakeWinPEMedia /UFD C:WinPE_amd64 V:

select vdisk file="C:WinPEPS.vhd"
detach vdisk

This creates a new VHD file and attaches it to the current OS as drive V:. I then make bootable media of my PE folder to the V: folder then detach. I then copied this VHD file to a Hyper-V box and created a VM that used it as its boot disk. Sure enough it booted and I was facing a PE environment. The next step was to format the disks and apply an image automatically. My initial though was “how can I format the disk and apply an OS to the disk if booted from it (PE)?” however quickly it became obvious that the PE I was booted into wasn’t really running from the local disk. Instead what happens is on boot the boot.wim file on the PE media is read into a writable RAM disk which is where the PE actually runs from (the X drive). Therefore even though the C: drive contained that boot.wim it’s not actually being used and do it can be wiped. Therefore I created a script that did three things

  1. Wiped the disk and create the system and windows partitions
  2. Applied a Windows Server image (1709 Server Core)
  3. Make the disk bootable

To partition the disk I created a text file, parts.txt which contained:

Select disk 0
create partition primary size=350
format quick fs=ntfs label="System"
assign letter="S"
create partition primary
format quick fs=ntfs label="Windows"
assign letter="W"

I could then call this with (I would copy this to my Windows PE environment as well):

diskpart /s x:parts.txt

The WIM file I placed on a file share (this would be an Azure Files share once in Azure) so I had to map to the network drive and apply so the complete file became:

Diskpart /s x:parts.txt

Net use z: \savdalfssoftware /user:savilltechadmin password

x:Imagex /apply "z:OS ImagesWindows Server 2016 1709Expandedsourcesinstall.wim" 2 w:

W:WindowsSystem32bcdboot W:Windows /l en-US


I saved this as autolaunch.bat and added to the root of my Windows PE boot.wim (by remounting it) along with the parts.txt. I also modified the startnet.cmd found under the WindowsSystem32 folder of my mounted PE environment to call my autolaunch.bat file, e.g.



I then unmounted and created a new VHD. I made sure my install.wim was present in my file server as referenced, copied over the VHD to my Hyper-V server, changed the VM to use the new VHD and sure enough it booted, formatted the disks and laid down the image. Note you are putting a password in the file, this is not ideal. Also not if your password contains special characters you may have to escape them in the batch file or they won’t work correctly, for example if you password contained % you actually need %% in the string!

The next step was to try this in Azure. I created a storage account in Azure, added an Azure Files share and uploaded the install.wim file to it. I changed the autolaunch.bat to map to the Azure Files share instead of the local file share (along with the path to the WIM file). It therefore became:

net use Z: \savpoc.file.core.windows.netpocfs /u:AZUREsavpoc Lbk0C7HVG8X1P8UtMtw6irBGjSQ==

And execute to:

x:Imagex /apply "z:install.wim" 2 w:

To upload the VHD to Azure and create an image from that uploaded file I used the following PowerShell. This is important. Trying to upload from other tools or the Azure portal seems to leave the VHD in a strange state and unusable.



Select-AzureRmSubscription -SubscriptionId "subid"

$rgImgName = "RGSCUSPOC"
$urlOfUploadedImageVhd = "https://savpoc.blob.core.windows.net/poctemplates/WinPEPS.vhd"
Add-AzureRmVhd -ResourceGroupName $rgImgName -Destination $urlOfUploadedImageVhd `
 -LocalFilePath "S:OS ImagesPE1709WinPEPS.vhd"

#Create image
$location = "South Central US" 
$imageName = "WinPEImage"

$imageConfig = New-AzureRmImageConfig -Location $location
$imageConfig = Set-AzureRmImageOsDisk -Image $imageConfig -OsType Windows -OsState Generalized -BlobUri $urlOfUploadedImageVhd
$image = New-AzureRmImage -ImageName $imageName -ResourceGroupName $rgImgName -Image $imageConfig

From here I created a new VM from my image. I used PowerShell below to create my VM. Note I’m enabling boot diagnostics. This allowed me to view the console even if I couldn’t interact with it. Therefore I had some idea what was happening

$location = "South Central US" 
$rgname = "POCRG"
$vmName = "poc"
$computerName = "poc"
$vmSize = "Standard_DS1_v2"
$netRG = "RG_SCUS"
$vnetname = "RG_SCUS-vnet"
$subnetname = "default"
$stotype = 'Standard_LRS'
$stoname = "sasavpocdiag"
$user = "localadmin"
$password = 'passwordhere'
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential ($user, $securePassword) 
$imageName = "WinPEImage" 
$rgImgName = "RGSCUSPOC"
$image = get-azurermimage -ImageName $imageName -ResourceGroupName $rgImgName

$vnet = Get-AzureRmVirtualNetwork -ResourceGroupName $netrg -Name $vnetName

New-AzureRmResourceGroup -Name $rgname -Location $location

$ipName = "POCPip"
$pip = New-AzureRmPublicIpAddress -Name $ipName -ResourceGroupName $rgName -Location $location `
 -AllocationMethod Dynamic

$nicName = "pocNIC"
$nic = New-AzureRmNetworkInterface -Name $nicName -ResourceGroupName $rgName -Location $location `
 -SubnetId $vnet.Subnets[0].Id -PublicIpAddressId $pip.Id

$vm = New-AzureRmVMConfig -VMName $vmName -VMSize $vmSize
$vm = Set-AzureRmVMSourceImage -VM $vm -Id $image.Id

$vm = Set-AzureRmVMOSDisk -VM $vm -DiskSizeInGB 1024 `
-CreateOption FromImage -Caching ReadWrite

$vm = Set-AzureRmVMOperatingSystem -VM $vm -Windows -ComputerName $computerName `
-Credential $cred #-ProvisionVMAgent -EnableAutoUpdate

$vm = Add-AzureRmVMNetworkInterface -VM $vm -Id $nic.Id

New-AzureRmStorageAccount -ResourceGroupName $rgname -Name $stoname -Location $location -Type $stotype
$vm = Set-AzureRmVMBootDiagnostics -VM $vm -Enable -ResourceGroupName $rgName -StorageAccountName $stoname

New-AzureRmVM -VM $vm -ResourceGroupName $rgName -Location $location

I then jumped over to the portal and via the Support + Troubleshooting section – Boot diagnostics – Screenshot I could see it deploying in Azure (updated about every 30 seconds or so).

This worked! The OS installed and strangely I could RDP to it even though I never enabled this, it had the right name, it had the Azure agent installed. What trickery is this and then it hit me. I never added my own unattend.xml file. All I did was apply a 2016 image to a disk and it rebooted. Basically the same as if I had used a template with 2016. The ISO file that Azure automatically creates when deploying a VM that contains an unattend.xml file and other setup files still got created, still got attached and was therefore still used. This was good but also bad as I wanted to use my own unattend.xml file to further prove we could customize.

The next step was to generate my own unattend.xml file and use it. At this point I didn’t want to keep having to rebuild the VHD every time I made a script change and so I broke apart the logic so the autolaunch.bat just connected to the Azure Files, partitioned the disk then copied down an imageinstall.bat file and executed. This way I could change imageinstall.bat on the file share whenever I wanted to change the functionality. autolaunch.bat became:

Diskpart /s x:parts.txt
net use Z: \savpoc.file.core.windows.netpocfs /u:AZUREsavpoc Lbk0C7HVG..BGjSQ==
copy z:imageinstall.bat x:
call x:imageinstall.bat

And imageinstall.bat which was placed on the file share became:

x:Imagex /apply "z:install.wim" 2 w:
W:WindowsSystem32bcdboot W:Windows /l en-US
mkdir w:windowspanther
copy z:unattend.xml w:windowspanther
powershell -executionpolicy bypass -nologo -noprofile -file z:unattendupdate.ps1

I created a new VHD with the reduced autolaunch.bat and uploaded to Azure (and created a new image after deleting the old one with Remove-AzureRmImage -ImageName $imageName -ResourceGroupName $rgImgName).

Now I’m jumping over a few steps here but basically I created an unattend file to set a default password, have a placeholder for the computer name, enable auto mount of disks, move the pagefile to D:, enable RDP and required firewall rules and also launch the install.cmd that Azure normally runs. This would install the agent, register with Azure fabric etc. Because I place my unattend.xml in the windowspanther folder it overrides any found on removable media, i.e. the Azure one! My unattend file was:

<?xml version=”1.0″ encoding=”utf-8″?>
<unattend xmlns=”urn:schemas-microsoft-com:unattend”>
<settings pass=”specialize”>
<component name=”Microsoft-Windows-TerminalServices-LocalSessionManager” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State&#8221; xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”&gt;
<component name=”Networking-MPSSVC-Svc” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State&#8221; xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”&gt;
<FirewallGroup wcm:action=”add” wcm:keyValue=”RemoteDesktop”>
<Group>Remote Desktop</Group>
<component name=”Microsoft-Windows-TerminalServices-RDP-WinStationExtensions” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State&#8221; xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”&gt;
<component name=”Microsoft-Windows-Shell-Setup” processorArchitecture=”wow64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State&#8221; xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”&gt;
<component name=”Microsoft-Windows-Deployment” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State&#8221; xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”&gt;
<RunSynchronousCommand wcm:action=”add”>
<Description>Execute Install.cmd</Description>
<Path>cmd.exe /c “(for %1 in (z y x w v u t s r q p o n m l k j i h g f e d c b a) do @(DIR %1: 1&gt;NUL 2&gt;&amp;1 &amp;&amp; if exist %1:Install.cmd (%1:Install.cmd &amp; exit))) &amp; exit 3″</Path>
<settings pass=”windowsPE”>
<component name=”Microsoft-Windows-Setup” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State&#8221; xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”&gt;
<settings pass=”offlineServicing”>
<component name=”Microsoft-Windows-PartitionManager” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State&#8221; xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”&gt;
<settings pass=”oobeSystem”>
<component name=”Microsoft-Windows-Shell-Setup” processorArchitecture=”amd64″ publicKeyToken=”31bf3856ad364e35″ language=”neutral” versionScope=”nonSxS” xmlns:wcm=”http://schemas.microsoft.com/WMIConfig/2002/State&#8221; xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”&gt;
<cpi:offlineImage cpi:source=”wim://savdalfs/software/os%20images/windows%20server%202016%201709/expanded/sources/install.wim#Windows Server 2016 SERVERDATACENTERACORE” xmlns:cpi=”urn:schemas-microsoft-com:cpi” />

Now in this file I have a placeholder string for the computername, <ComputerName>TSCAEDPH</ComputerName> . I wanted to replace this with the computername specified on the Azure fabric. How would I get this from inside the guest? Well Azure has an endpoint at that can be called from within a VM and basic information can be found and so I created a PowerShell script that would find the computername and update the unattend.xml I had copied to the panther folder of the deployed image:

$url = ""
$data = Invoke-RestMethod -Uri $url -Headers @{'Metadata'='true'} -Method Get
$computedata = $data | select-object -ExpandProperty compute
$computename = $computedata.name

$attendfile = "w:windowspantherunattend.xml"
(Get-Content $attendfile).Replace('TSCAEDPH',$computename) | Set-Content $attendfile

This was saved as unattendupdate.ps1 on the Azure Files share as well which now contained the install.wim, unattend.xml, imageinstall.bat and this ps1 file. Fingers crossed I kicked off a new VM build. It worked. It used my unattend.xml file but still got the Azure agent etc installed. It also still renamed the local administrator account to that specified as part of the VM creation as that happens as part of the Azure install step process which I was now calling from my unattend.xml file.

Now there are some problems here. If Azure changes the structure of their ISO file with the install.cmd this will break so it would have to be re-investigated however this is still better than trying to duplicate everything they do manually which is far more likely to change far more often.

So there you go. You can use your own PE in Azure to customize and create deployments including unattend. You can still call the Azure agent install and finalize. But ideally, use images 😉



New video on publishing corporate apps to iOS with Intune and Configuration Manager

Decided to create a video that walked through the exact process to deploy corporate applications to iOS (and Android) through Intune while actually performing the management with Configuration Manager. Additionally I lock it down so corporate data cannot flow from corporate apps to personal apps. Available at https://youtu.be/wfWoLLx8WeA.


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 https://youtu.be/7bobbg91cQc 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 http://storageexplorer.com/.

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 https://autospinstaller.com/ 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>.file.core.windows.netpocassets -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://$SAName.blob.core.windows.net/vhds/"+$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://$SAName.blob.core.windows.net/vhds/"+$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://$SAName.blob.core.windows.net/vhds/"+$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>.blob.core.windows.net/scripts/"
$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 http://windowsitpro.com/azure/network-security-groups-defined 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