Building a Azure Webserver (IaaS) - Part 1
Steve Jennings
by Steve Jennings

This guide focuses on primarily utilizing the GUI and Portal.

Table of Contents


Moving to the “Cloud” represents quite a few challenges for IT teams that have mostly kept things on-premises. There’s a lot of pressure to learn a bunch of new technologies. New methods or processes are introduced and new ways to manage infrastructure keep operations on its toes. Infrastructure as Code, Configuration as Code, Continuous Integration and Continuous Deployment are all being incorporated into business processes.

This guide is an attempt to become a bit more practical.

The guide will attempt to increase in scope of difficulty and complexity as we move through, however. As such, I will try to keep the buzzwords to a minimum and focus on actionable content.

It is recommended to follow this guide from a Windows Workstation, and we will be focusing on IIS. However, all the tools outlined here work fine with a -nix-like environment. Refer to the individual tools and their requirements for more information.


  • We’ve been tasked to build a IIS system in “the cloud”.
  • We want a IIS system that utilizes Azure resources in its entirety, and we want to utilize services to distribute load amongst our systems.
  • We want to be able to communicate with the servers over sftp so we can upload data securely.
  • Furthermore, we do not want our IIS systems accessible from the Internet other than through a load balancer.

We want to learn how to do this task in a variety of ways to explore the capability of Azure to accomodate varying levels of skill. As such, we will accomplish this task in three different ways:

  • GUI Method: focuses on creating this solution with the Azure Portal and Dashboard, communicating with the server using RDP. Configuring the system with the mouse, and some light PowerShell scripting to get started.

  • Command-line Method: focuses on building resources using Azure CLI or the ‘Az’ module for PowerShell.

  • Infrastructure as Code: focuses on building resources with Packer and Terraform. Our first introduction to creating resources with a code base!

  • Configuration as Code: focuses on configuring those resources with Ansible.

To reduce the size of the guide, we’ll opt not to use Active Directory or go over that in any way.


Note: You do not need to install anything for this portion of the guide.

If you choose to follow along, you’ll need:

  • Access to Azure. Preferably in your own subscription.
  • Windows 10.
  • The ability to install the following tools (later in the series):
    • Windows Subsystem for Linux
    • Azure CLI or ‘Az’ module for PowerShell
    • Terraform (for Part 2, Infrastructure as Code)
    • Packer (for part 2, Infrastructure as Code)
  • Familiarity with PowerShell.

Installing Windows Subsystem for Linux (Windows 10)

Follow the guide located here: Install WSL on Windows 10

Installing Ansible (WSL / Ubuntu Distribution)

  1. Open the WSL Bash window.
  2. Run apt-get update and then apt-get upgrade.
  3. Run the following commands:
sudo apt-get update
sudo apt-get install software-properties-common
sudo apt-add-repository --yes --update ppa:ansible/ansible
sudo apt-get install ansible
pip install "pywinrm>=0.3.0"

Installing Packer and Terraform

  1. Open a PowerShell console.
  2. We will install Chocolatey:
Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString(''))`
  1. Run the following commands:
choco install packer
choco install terraform

This will place packer and terraform in our $path.

Alternatively, you can also do this manually by navigating to the project pages.

Let’s Begin! The Traditional GUI Method

The Azure Portal

After you have your Azure account, navigate to the Azure Portal. The Azure Portal is a great way to get familiar with resources and is going to be your gateway for all services on Azure.

Understanding Resources and Resource Groups

It is important to understand that everything in Azure, utilizes the Azure Resource Manager (the default method of deployment). Those resources are grouped inside Resource Groups. It is a logical grouping mechanism and helps segment resources from other resources.

See this link for details: Azure Resource Manager Overview

Application Gateway - HTTP Load Balancer

Before we can create a virtual machine, we need to deploy a load balancing solution. This is important because our virtual machine will ask us if we are placing it behind one during virtual machine creation.

I will create a Application Gateway for the purposes of the guide since we want our web application behind a Layer 7 Load Balancer. We want control over the traffic and this allows us to do so.

The Application Gateway Resource, will auto-create several resources required for the Application Gateway. These resources include the virtual network, or the subnets inside that network necessary for the load balancer to function. Since we are using the Application Gateway as our primary resource to gain access to our website, we will start with this process.

  1. Log into and click “Create a Resource” on the left side of the screen.
  2. Type “Application Gateway” and click “Create”.
  3. A wizard appears! Let’s start filling out the basic info for our Application Gateway
  • Name: tutorialgui-appgw
  • Tier: Standard
  • Instance Count: 2
  • Sku Size: Small
  • Subscription: Select your subscription
  • Resource Group: Create New
    • Name: tutorial-gui
  • Location: East US 2
  1. The next screen involves creating a new network for our Application Gateway, with its own subnet, and public IP address:
  • Subnet Configuration
    • Click Choose a Virtual Network, Create New
    • Name: appgw-vnet
    • Address Space:
    • Subnet: appgw-subnet
    • Subnet Address Range:
  • Frontend IP Configuration
    • IP Address Type: Public
    • Create New: tutorialgui-appgw-ip
    • DNS Label: (this will be where we access our web app)
  • Listener Configuration
    • Protocol: http
    • Port: 80
    • Additional Settings: HTTP2
  1. What the summary screen looks like:


Watch the notifications icon for when it finishes (it may take a while…so grab a coffee).

Once deployed … let’s move on to handling Network Security.

Creating A Network Security Group

We need to create a Network Security Group to secure our virtual machines and allow us the ability to connect to them.

  1. Click “Create a Resource” on the left side of the screen.
  2. Type “Network Security Group” and click “Create”.
  3. Name: iis-nsg
  4. Location: East US 2
  5. Select the resource group, tutorial-gui that we created in the previous step.

In a few moments, we will configure the Network Security Group further.

Our first virtual machine (IIS #1)

  1. Click “Create a Resource” on the left side of the screen.
  2. Type “Windows 2016 Datacenter” and click “Create”.
  3. A wizard appears! Let’s start filling out the info for our first virtual machine
  • Basics
    • Subscription: This should be pre-filled with your subscription id.
      • Resource Group: tutorial-gui
    • Instance Details aka Virtual Machine Details
      • Virtual Machine Name: tutorialvm1
      • Region: East US 2 (or a region closest to your location)
      • Availability Options: Availability Set (See here for the differences between a Zone and a Set)
      • NOTE: We will cover Availability Zone/Set when we reach that portion of the tutorial.
      • Availability Set Name: (new) tutorialset
        • Fault Domains: 2
        • Update Domains: 5
      • Size: Standard_F1s
      • Assign Local Administrator account details.
      • Public Inbound Ports: none
        • We are going to allow inbound ports through a Layer 4 load balancer, later in the guide.
      • If you qualify for Azure Hybrid Benefit, select this Yes; Otherwise, select No.
  • Disks
    • Under Disk Options, OS Disk Type: Standard SSD
  • Networking
    • Virtual Network:
      • Select: appgw-vnet
      • Click “Manage Subnet Configuration”
        • Click + Subnet
        • Name: iis-subnet
        • Configure Network Security Group: iis-nsg
        • Select OK and leave all other options
          • This will immediately create the subnet.
    • Navigate back to the Create Machine wizard
    • Public IP: none
    • NIC Network Security Group: Advanced
      • Select the iis-nsg under Configure Network Security Group dropdown.
    • Accelerated Networking should be off.
    • Load Balancing, select Yes to place the machine behind a load balancer
      • Load Balancing Options: Application Gateway
      • Application Gateway: tutorialgui-appgw
      • Backend Pool: appGatewayBackendPool
  • Management
    • Boot Diagnostics: On
    • OS Guest Diagnostics: On
    • Diagnostic Storage Account: (new) tutorialguidiag
    • All other settings off.
  • Skip Guest Config/Tags
  • Finish the wizard, ensure validation has passed and the deployment will kick off.

A taste of automation - deploying the second vm with Azure Resource Manager Templates

Let’s now deploy a second virtual machine, but this time we will use a Resource Manager template. All resources created using the Portal use Azure Resource Manager templates.

  1. Navigate to the Resource Group.
  2. In the main pane window, select Deployments. It should say something like “4 succeeded”.
  3. Select the deployment for CreateVM...
  4. This will provide a summary of your successful previous deployment.
  5. Click the Redeploy button.
  6. Reselect your resource group, tutorial-gui
  7. Change the values of your vm, such as adding an incrememental number, i.e., tutorialvm1. Generally the only fields you’ll need to change are the name, password, and network interface name.

To learn more about Azure Resource Templates, head here:

A new deployment will occur and will be added to the same availability set cluster automagically! :) If everything went well, we will have deloyed the following resources:


Gaining access to our machines

So the goal, as you might remember, is to have these machines isolated from the Internet. As such there are no public IPs used for the virtual machines themselves. So how do we gain access? Well, we could create a virtual machine and use it as a “JumpBox”. However, in this tutorial we will leverage the Layer 4 Load Balancer for this instead and use NAT.

Working with Network Security Groups

But before we do any of that. let’s take a step back…

All traffic that enters the Azure Virtual Network is screened. That traffic is screened by the Network Security Group. This is traffic filtered BEFORE it touches any VMs (it is important to also utilize the Operating System Firewall).

Let’s enable HTTP and RDP access to our network:

  1. Open the iis-nsg Network Security Group.
  2. Under Inbound Security Rules add the following rules:
  • Name: RDP-Allow
    • ANY Source, ANY Destination.
    • Destination Port Range: 3389
  • Name: HTTP-Allow
    • ANY Source, ANY Destination.
    • Destination Port Range: 80
  • Name: SFTP-Allow
    • ANY Source, ANY Destination.
    • Destination Port Range: 22

Enter Azure Load Balancer. This is a Layer 4 Load Balancer and is capable of handling Layer 4 traffic such as RDP and SFTP traffic. This is what we will use to gain access to our machines.

NOTE: Contrast that with the Azure Application Gateway we created originally, which only handles Layer 7 Traffic (such as HTTP).

Creating the Load Balancer

  1. Launch the new resource wizard.
  2. Type “Load Balancer” and choose the Microsoft branded resource.
  • Resource Group: tutorial-gui
  • Name: tutorial-lb
  • Type: public
  • SKU: Basic
  1. Public IP: Create New
  • Name: tutorial-lb-public
  • Assignment: Static
  1. Place the load balancer in the same resource group, tutorial-gui. Create the resource.
  2. After the deployment is finished, navigate to the load balancer resource in our resource group. And select Backend Pools.
  3. Add a new backend pool:
  • Name: iisvm-backendpool
  • Associated to: availability set
  • Select tutorialset

Configuration for the backend pool looks like the below image:


The backend pool is a collection of virtual machines to load balance.

Assigning NAT Rules, Health Probes and Connecting via RDP

Once the backend pool is finished, we will add a NAT rule. This will allow us to communicate with the machines even though they do not have public IPs.

Under the load balancer pane:

  1. Under Inbound NAT rules, add a new load balancing NAT rule:
  • Name: iisvm-rdp-nat
  • Service: RDP
  • Target VM: tutorialvm1
  • Network IP Configuration: ipconfig1


  1. We need an additional probe for sftp. Under Health Probes, create a new probe:
  • Name: sftp-probe
  • Protocol and Port: TCP 22
  1. Under Load Balancing Rules, create a new load balancing rule:
  • Name: sftp-lb-rule
  • IP Version: 4
  • Protocol and Port (and Backend Port): TCP 22
  • Session Persistence: none

Let’s connect via RDP!

  1. Locate your Public IP address of the load balancer by doing the following:
  • One the overview screen of our load balancer, tutorial-lb
  • Find the Public IP address
  1. Enter the IP address in the RDP box and log in with the credentials you specified during VM creation.
  2. Voila!


We can also verify that the load balancer mapped us to the right vm by running ipconfig in the virtual machine RDP session.

Setting up SFTP

Now that we’ve connected to our virtual machine, let’s install IIS and set up sftp.

We are running the below script as the tutorialadmin user, originally created during the Portal setup. Run the following PowerShell script as Administrator inside our newly created VM.

The below script will configure Bitvise, a SSH client for Windows. In addition, we will install IIS. For advanced users, here’s the Bitvise SSH reference.

Be sure to edit the line: $"secret-password-here") with the proper value.

$url = ""
$output = "C:\Apps\BvSshServer-Inst.exe"
$outputDirectory = "C:\Apps\"
$settingsFilePath = "C:\Apps\bitvise-settings"
$start_time = Get-Date

Install-WindowsFeature -Name "Web-WebServer","Web-Mgmt-Console" -Verbose

# Create BitVise Directory
Write-Output "Creating $outputDirectory"
New-Item -ItemType Directory -Path $outputDirectory -Force

# Download the file
Write-Output "Downloading $url to $output"
(New-Object System.Net.WebClient).DownloadFile($url, $output)

Write-Output "Time taken: $((Get-Date).Subtract($start_time).Seconds) second(s)"

# Run unattended install
Write-Output "Running Bitvise Unattended Install"
& C:\Apps\BvSshServer-Inst.exe -acceptEULA -defaultInstance

# Create BitVise settings file
Write-Output "Creating BitVise Settings File"
New-Item -ItemType File -Path $settingsFilePath -Force

$settingsFile = '$cfg = new-object -com "BssCfg815.BssCfg815"

$cfg.settings.server.windowsFirewall.sshPortsFirewallSetting = 3 # $cfg.enums.WindowsFirewallSetting.globalScope
$cfg.settings.server.windowsFirewall.forwardedPortsFirewallSetting = 3 # $cfg.enums.WindowsFirewallSetting.globalScope
$cfg.settings.server.windowsFirewall.ftpPortsFirewallSetting = 3 # $cfg.enums.WindowsFirewallSetting.globalScope
$ = "tutorialadmin"
$ = "Virtual Users"
$ = 1 # $cfg.enums.DefaultYesNo.yes
$ = 10 # $cfg.enums.ShellAccessWD.bvshell
$ = 1 # $cfg.enums.DefaultYesNo.yes
$ = 1 # $cfg.enums.DefaultYesNo.yes
$ = 1 # $cfg.enums.DefaultYesNo.yes
$ = $false
$ = "C:\inetpub\wwwroot"
$ = $false
$ = 2 # $
$ = 2 # $


Set-Content -Path $settingsFilePath -Value $settingsFile -Verbose -NoNewline

# Apply the settings file configuration
Write-Output "Applying configuration with BssCfg"
& 'C:\Program Files\Bitvise SSH Server\BssCfg.exe' settings importText $settingsFilePath

# Start the service
Get-Service BvSshServer | Start-Service -Verbose

Write-Output "
Script completed. It is advised to restart the machine.

When the installation succeeds, a message appears:

Script completed. It is advised to restart the machine.

Setting up Our Second Machine

While connected via RDP, open a secondary RDP console and log into the other VM.

Launch a PowerShell console and enter the above script and run it.

Running Commands Through the Portal

Alternatively, you can also use the Azure Portal to run the code without even logging into the VM!

  1. Navigate to the VM in the Portal.
  2. On the left side, click Run Command.
  3. Click PowerShell Script and paste the command above.
  4. Click Run, and allow the script to execute.
  5. After a few seconds, the script output will appear:


  1. It is advised to restart the machine from the Portal after this has completed.

Testing Load Balancing

Now that we have our infrastructure in place. Let’s test!

Verify access our website (HTTP)

Recall that our website is managed behind a Layer 7 Load Balancer. We will open a web browser and point to the DNS Name (not the IP) of the Application Gateway.

NOTE: The reason we access the DNS name is because the Application Gateway does not support static ip addresses.

  1. Navigate to the Application Gateway Resource’s public ip, in my case it is tutorialgui-appgw-ip
  2. Make a note of the DNS Name, in my case it is
  3. Open a web browser and paste it in!


Verify access to the SFTP

Recall that our website can be modified with sftp via a Layer 4 Load Balancer.

  1. Navigate to the Load Balancer Resource’s public ip, in my case it is tutorial-lb-public
  2. Make a note of the IP address.
  3. Use a program such as WinSCP, and enter the details of the IP of the load balancer, and any authentication details.
  4. Confirmation of connection:

Using Bash:

tutor@workstation:~$ ssh tutorialadmin@ip-of-lb
tutorialadmin@ip-of-lb's password:
bvshell:/$ ls
iisstart.htm  iisstart.png

Using WinSCP:


-Note:- You may receive a ssh-key warning prompt if you attempt quickly to connect multiple times or for some reason the Load Balancer round-robins you to another VM. This is merely referencing the SSH key of the secondary VM in our availability set and is safe to proceed. Simply add the key to the existing trustedhosts list.

On Linux: ssh-keyscan -H ip-of-load-balancer >> ~/.ssh/known_hosts

Setting up Shared Storage (Azure Files)

We will use Azure Files for Shared Storage.

  1. Create a new Storage Account. I’ve named mine tutorialwebfiles.
  2. Navigate to the account and click Files.
  3. Create a new File Share. I’ve named mine webfiles.
  4. Click the new share, and click the Connect button. This will provide us valuable connection information to be used shortly.
Mapping the Shared Storage

Mapping the storage requires us to map the drive. But we also need to map the drive using IIS later on. So it is crucial we create a local user for this process.

  1. Run the following PowerShell script in the VM or via the Run Command prompt in the Portal:
# Username from the Storage Account
$username = "tutorialwebfiles"
# Storage Account Key used to authenticate
$password = "storage-account-key-ends-with=="

$securepassword = $password | ConvertTo-SecureString -AsPlainText -Force
New-LocalUser -Name $username -Password $securepassword -Description "IIS & Azure Files User" -Verbose
Add-LocalGroupMember -Group "IIS_IUSRS" -Member $username -verbose

RDP into the machine and run the following while logged in:

cmdkey / /user:tutorialwebfiles /pass:storage-account-key-ends-with==
net use * \\\webfiles

This will map the Azure Files drive, and allow the drive to persist after reboots.

Configuring IIS for Azure Files

Adjusting the Application Pool

We need to adjust the Application Pool Identity to our local user, tutorialwebfiles. Run the following PowerShell Script:

$UserName = "tutorialwebfiles"
$Password = "storage-account-key-ends-with=="
Get-IISAppPool -Name DefaultAppPool

# Set the Application Identity
$appPoolConfigSection = Get-IISConfigSection -SectionPath "system.applicationHost/applicationPools"
$appPoolDeefaultsElement = Get-IISConfigElement -ConfigElement $appPoolConfigSection -ChildElementName "applicationPoolDefaults"
$processModelElement     = Get-IISConfigElement -ConfigElement $appPoolDeefaultsElement -ChildElementName "processModel"
Set-IISConfigAttributeValue -ConfigElement $processModelElement -AttributeName "identityType" -AttributeValue "SpecificUser"

# Set password value
$appPoolConfigSection = Get-IISConfigSection -SectionPath "system.applicationHost/applicationPools"
$appPoolDeefaultsElement = Get-IISConfigElement -ConfigElement $appPoolConfigSection -ChildElementName "applicationPoolDefaults"
$processModelElement     = Get-IISConfigElement -ConfigElement $appPoolDeefaultsElement -ChildElementName "processModel"
Set-IISConfigAttributeValue -ConfigElement $processModelElement -AttributeName "password" -AttributeValue $password

# Set user value
$appPoolConfigSection = Get-IISConfigSection -SectionPath "system.applicationHost/applicationPools"
$appPoolDeefaultsElement = Get-IISConfigElement -ConfigElement $appPoolConfigSection -ChildElementName "applicationPoolDefaults"
$processModelElement     = Get-IISConfigElement -ConfigElement $appPoolDeefaultsElement -ChildElementName "processModel"
Set-IISConfigAttributeValue -ConfigElement $processModelElement -AttributeName "userName" -AttributeValue $userName

Start-Sleep -Seconds 5
Write-Output "Sleeping 5 seconds to wait for app pool to initialize changes."
# Start the App Pool
(Get-IISAppPool -Name DefaultAppPool).start()

Now let’s configure Shared Configuration:

Import-Module IISAdministration
Import-Module WebAdministration
# Modify your variables
$AzureFilesPath = "\\\webfiles\sharedconfig"
$WebRootPath = "\\\webfiles\wwwroot"
$Username = "tutorialwebfiles"
$UserPassword = ConvertTo-SecureString "storage-account-key-ends-with==" -AsPlainText -Force
$PlainPassword = "storage-account-key-ends-with=="
$KeyEncryptionPassword = ConvertTo-SecureString "shared-config-encrypted-password" -AsPlainText -Force

if (Test-Path -Path $AzureFilesPath) {
  Write-Output "$AzureFilesPath already exists"
} else {
  New-Item -ItemType Directory -Path $AzureFilesPath

(Get-IISAppPool -Name DefaultAppPool).stop()
Start-Sleep -Seconds 5
Write-Output "DefaultAppPool stopped"
Set-ItemProperty 'IIS:\Sites\Default Web Site\' -Name physicalPath -Value $WebRootPath
Set-ItemProperty 'IIS:\Sites\Default Web Site\' -Name userName -Value $UserName
Set-ItemProperty 'IIS:\Sites\Default Web Site\' -Name password -Value $PlainPassword
(Get-IISAppPool -Name DefaultAppPool).start()
Write-Output "DefaultAppPool started"

Export-IISConfiguration -UserName $Username -Password $UserPassword -PhysicalPath $AzureFilesPath -KeyEncryptionPassword $KeyEncryptionPassword -Force
Enable-IISSharedConfig -UserName $Username -Password $UserPassword -PhysicalPath $AzureFilesPath -DontCopyRemoteKeys -Force

if ((Get-IISAppPool -Name DefaultAppPool).State -eq 'Started') {
    Write-Output "DefaultAppPool already started"
} else {
    Write-Output "Starting DefaultAppPool"
    (Get-IISAppPool -Name DefaultAppPool).start()

This can also be done using the IIS Manager located under the Server Node, and selecting Shared Configuration. Always remember to export Configurations before modifications.

Troubleshooting Issues with IIS Shared Configuration

The vast majority of times, IIS will sometimes complain and APP Pools will start, then stop. Or there will be errors such as Unable to decrypt password. These errors are generally because IIS is having issues with the Application Pool Identity.

Some tricks to resolve this:

  • Remove the website, and re-add it.
  • Reinitialize the identity using the IIS Manager GUI and re-entering the password.
  • Turn off Shared Configuration and turn it back on by re-entering and decrypting the configuration.

Updating SSH to point to Azure Files

Notice we are now pointing to the local user we created previously. Instead of using a virtual account. Be sure to change the drive letter under if your drive letter is different.

Replace the strings storage-account-key-ends-with== with the one of the Azure Files keys.

$settingsFilePath = "C:\Apps\bitvise-settings"
$settingsFile = '$cfg = new-object -com "BssCfg815.BssCfg815"

$cfg.settings.server.windowsFirewall.sshPortsFirewallSetting = 3 # $cfg.enums.WindowsFirewallSetting.globalScope
$cfg.settings.server.windowsFirewall.forwardedPortsFirewallSetting = 3 # $cfg.enums.WindowsFirewallSetting.globalScope
$cfg.settings.server.windowsFirewall.ftpPortsFirewallSetting = 3 # $cfg.enums.WindowsFirewallSetting.globalScope
$ = "tutorialwebfiles"
$ = "Virtual Users"
$ = 1 # $cfg.enums.DefaultYesNo.yes
$ = "\\\webfiles"
$ = $true
$ = "tutorialwebfiles"
$ = "Z:"
$ = 10 # $cfg.enums.ShellAccessWD.bvshell
$ = 1 # $cfg.enums.DefaultYesNo.yes
$ = 1 # $cfg.enums.DefaultYesNo.yes
$ = 1 # $cfg.enums.DefaultYesNo.yes
$ = $false
$ = "Z:\wwwroot"
$ = $false
$ = 2 # $
$ = 2 # $


New-Item -ItemType File -Path $settingsFilePath -Verbose -Force

Set-Content -Path $settingsFilePath -Value $settingsFile -Verbose -NoNewline

# Apply the settings file configuration
Write-Output "Applying configuration with BssCfg"
& 'C:\Program Files\Bitvise SSH Server\BssCfg.exe' settings importText $settingsFilePath

Locking Down Network Traffic

  • Always ensure that critical traffic such as RDP, sftp, etc. is restricted only to networks you will connect from.
    • Always ensure that the Firewall inside the Virtual Machine is also locked down.

Wrapping Up (GUI)

We are now finished with the GUI configuration. In the next guide, we’ll build these resources using PowerShell!