top of page
  • 2 days ago
  • 8 min read






Build a highly available public web tier in 60–75 minutes using Azure PowerShell. You will place two private Ubuntu web servers in separate availability zones behind a zone-redundant Standard Load Balancer, prove that both backends serve traffic, simulate one backend failing, verify automatic failover, recover the service, and remove every lab resource.


WORKSHOP AT A GLANCE


Level: Beginner to intermediate

Time: 60–75 minutes

Region: West Europe

Estimated lab cost: less than €0.25 for 75 minutes in the tested configuration

Maximum approved cost: €2

Cleanup: mandatory and included


LEARNING OBJECTIVES


By the end of the workshop, you will be able to:


• Explain why availability zones reduce the impact of a datacenter-level failure.

• Create a zone-redundant Standard public IP and Standard Load Balancer.

• Register two private zonal VMs in a backend pool.

• Configure an HTTP health probe and a TCP port 80 load-balancing rule.

• Explain Azure Load Balancer’s five-tuple traffic distribution.

• Demonstrate probe-driven failover and recovery.

• Verify that the VMs have no direct public IP addresses.

• Delete the complete workshop environment.


PREREQUISITES


• An Azure subscription with permission to create resource groups, networking, and virtual machines.

• Azure PowerShell with the Az.Accounts, Az.Network, and Az.Compute modules.

• PowerShell 7 or Windows PowerShell 5.1.

• The local ssh-keygen command.

• Quota for two Standard_B1s virtual machines in West Europe.


ARCHITECTURE AND IP PLAN


Internet clients connect to one zone-redundant Standard public IP. The Standard Load Balancer accepts TCP port 80 and selects a healthy backend by a five-tuple hash: source IP, source port, destination IP, destination port, and protocol. A health probe checks each backend independently. If a probe fails twice, that instance is temporarily removed from new traffic.


The virtual machines sit in zones 1 and 2. Their NICs use only private addresses in snet-web. The NSG permits TCP 80 from the Internet service tag and health-probe traffic from the AzureLoadBalancer service tag. All other unsolicited inbound traffic stays denied by the default NSG rules.


Resource group: rg-bcwc-lb-workshop-weu

VNet: vnet-bcwc-lb-weu — 10.30.0.0/16

Subnet: snet-web — 10.30.1.0/24

Load balancer: lb-bcwc-web-weu

Backend VM 1: vm-bcwc-web01-weu — zone 1

Backend VM 2: vm-bcwc-web02-weu — zone 2


STEP 1 — AUTHENTICATE SAFELY


Open PowerShell and authenticate interactively. Select the intended subscription by name, then display only the subscription name and Azure environment. Do not put account email, tenant ID, subscription ID, tokens, passwords, or SSH keys into screenshots or shared output.


Connect-AzAccount
Set-AzContext -Subscription 'Visual Studio Enterprise Subscription'
Get-AzContext | Select-Object @{n='Authentication';e={'Succeeded'}},

@{n='Subscription';e={$_.Subscription.Name}},

@{n='Environment';e={$_.Environment.Name}}


Figure 1 — PowerShell authentication succeeded with a privacy-safe subscription context.


Azure PowerShell authentication output showing a successful privacy-safe subscription context.

STEP 2 — SET VARIABLES AND RUN PREFLIGHT


Use reusable variables instead of repeating names. Before deploying, confirm that the resource providers are registered, Standard_B1s is offered in zones 1 and 2, sufficient quota exists, the Ubuntu image is available, and the resource group is absent. The current retail estimate for this short lab is below the €2 ceiling.


$Location = 'westeurope'
$ResourceGroupName = 'rg-bcwc-lb-workshop-weu'
$VmSize = 'Standard_B1s'
$VmZones = '1','2'

Figure 2 — West Europe preflight passed for provider registration, zones, quota, image availability, name collision, and cost.


Azure PowerShell preflight output confirming West Europe zones, quota, Ubuntu image, and cost checks passed.

STEP 3 — CREATE AN ISOLATED RESOURCE GROUP


Keeping every component in one temporary resource group makes ownership and cleanup unambiguous.


New-AzResourceGroup -Name $ResourceGroupName -Location $Location -Tag @{

Workshop = 'Azure Load Balancer HA'

Lifecycle = 'Temporary'

}


Figure 3 — The isolated workshop resource group was created in West Europe.


Azure PowerShell output confirming creation of the isolated load balancer workshop resource group.

STEP 4 — CREATE THE VNET AND WEB SUBNET


The VNet uses 10.30.0.0/16. The web subnet uses 10.30.1.0/24, leaving room for future application and management subnets without changing this lab.


$SubnetConfig = New-AzVirtualNetworkSubnetConfig `

-Name 'snet-web' -AddressPrefix '10.30.1.0/24'


New-AzVirtualNetwork -Name 'vnet-bcwc-lb-weu' `

-ResourceGroupName $ResourceGroupName -Location $Location `

-AddressPrefix '10.30.0.0/16' -Subnet $SubnetConfig


Figure 4 — The VNet and workload subnet use the planned private address ranges.


Azure PowerShell output showing the 10.30.0.0/16 VNet and 10.30.1.0/24 web subnet.

STEP 5 — CREATE THE NSG RULES


Create one rule for Azure Load Balancer probes and one for client HTTP. Service tags are maintained by Azure, so you do not need to track platform IP ranges manually. Apply the NSG to snet-web.


New-AzNetworkSecurityRuleConfig -Name 'Allow-AzureLoadBalancer-Probe' `

-Access Allow -Protocol Tcp -Direction Inbound -Priority 100 `

-SourceAddressPrefix AzureLoadBalancer -SourcePortRange '*' `

-DestinationAddressPrefix '*' -DestinationPortRange 80


New-AzNetworkSecurityRuleConfig -Name 'Allow-Internet-HTTP' `

-Access Allow -Protocol Tcp -Direction Inbound -Priority 110 `

-SourceAddressPrefix Internet -SourcePortRange '*' `

-DestinationAddressPrefix '*' -DestinationPortRange 80


Figure 5 — The NSG allows only HTTP clients and platform health probes for this lab.


Azure PowerShell output listing NSG rules for HTTP clients and Azure Load Balancer health probes.

STEP 6 — CREATE A ZONE-REDUNDANT PUBLIC IP


Standard Load Balancer requires a Standard public IP. Specifying zones 1, 2, and 3 makes the frontend IP zone-redundant in West Europe. The two backend VMs remain private.


$PublicIp = New-AzPublicIpAddress -Name 'pip-bcwc-lb-weu' `

-ResourceGroupName $ResourceGroupName -Location $Location `

-Sku Standard -AllocationMethod Static -Zone '1','2','3'


Figure 6 — The Standard static public IP is configured across all three regional zones.


Azure PowerShell output showing the Standard static zone-redundant public IP across zones 1, 2, and 3.

STEP 7 — CREATE THE LOAD BALANCER FRONTEND AND BACKEND POOL


The frontend configuration binds the zone-redundant public IP. The backend pool is the private destination set that will contain both VM NICs.


$Frontend = New-AzLoadBalancerFrontendIpConfig `

-Name 'fe-public' -PublicIpAddress $PublicIp

$Backend = New-AzLoadBalancerBackendAddressPoolConfig -Name 'be-web'

Figure 7 — The Standard regional load balancer has one public frontend and one backend pool.


Azure PowerShell output showing the Standard Load Balancer frontend and two-member backend pool.

STEP 8 — CONFIGURE THE HTTP HEALTH PROBE


The probe requests the root path on TCP port 80 every five seconds. Two consecutive failures make that backend unhealthy. Health probes decide whether a backend can receive new flows; they do not repair the VM.


$Probe = New-AzLoadBalancerProbeConfig -Name 'hp-http' `

-Protocol Http -Port 80 -RequestPath '/' `

-IntervalInSeconds 5 -ProbeCount 2


Figure 8 — The HTTP root-path health probe uses a five-second interval and two-failure threshold.


Azure PowerShell output showing the HTTP root-path health probe configuration.

STEP 9 — CONFIGURE THE PORT 80 RULE


The rule maps frontend TCP 80 to backend TCP 80 and uses hp-http. TCP reset helps clients close idle connections cleanly. DisableOutboundSNAT prevents the inbound rule from becoming an implicit outbound design; this lab does not require guest package downloads.


$Rule = New-AzLoadBalancerRuleConfig -Name 'rule-http' `

-FrontendIpConfiguration $Frontend -BackendAddressPool $Backend `

-Probe $Probe -Protocol Tcp -FrontendPort 80 -BackendPort 80 `

-EnableTcpReset -DisableOutboundSNAT


Figure 9 — The TCP 80 rule is connected to the public frontend, backend pool, and HTTP probe.


Azure PowerShell output showing the TCP port 80 load-balancing rule and disabled outbound SNAT.

STEP 10 — DEPLOY VM 1 IN ZONE 1


Create nic-bcwc-web01-weu in snet-web, add it to be-web, and do not assign a public IP. Deploy Ubuntu 22.04 LTS Gen2 on Standard_B1s with a Standard SSD and zone 1.


Cloud-init writes a small HTML page and starts a systemd unit that runs the preinstalled Python HTTP server on port 80. The page identifies VM 1 and zone 1, making load-balancer behavior visible without installing packages.


Figure 10 — VM 1 is provisioned in availability zone 1 with a private NIC and Standard SSD.


Azure PowerShell output confirming VM 1 deployed in availability zone 1.

STEP 11 — DEPLOY VM 2 IN ZONE 2


Repeat the same configuration for nic-bcwc-web02-weu and vm-bcwc-web02-weu, changing the zone to 2 and the page identity to VM 2. Apart from identity and zone, the backends should be equivalent.


Figure 11 — VM 2 is provisioned in availability zone 2 with a private NIC and Standard SSD.


Azure PowerShell output confirming VM 2 deployed in availability zone 2.

STEP 12 — VERIFY ZONE PLACEMENT


Never assume zone placement from a name. Query the VM model and confirm that VM 1 reports zone 1 and VM 2 reports zone 2.


Get-AzVM -ResourceGroupName $ResourceGroupName |

Select-Object Name,@{n='Zone';e={$_.Zones -join ','}},

@{n='VmSize';e={$_.HardwareProfile.VmSize}},ProvisioningState


Figure 12 — Get-AzVM confirms successful provisioning in zones 1 and 2.


Azure PowerShell output confirming both virtual machines are provisioned in separate availability zones.

STEP 13 — VERIFY BACKEND-POOL MEMBERSHIP


Each NIC IP configuration must reference be-web. If a VM is healthy but missing from the pool, the load balancer cannot send it traffic.


Get-AzNetworkInterface -ResourceGroupName $ResourceGroupName |

Select-Object Name,

@{n='PrivateIp';e={$_.IpConfigurations.PrivateIpAddress}},

@{n='BackendPool';e={$_.IpConfigurations.LoadBalancerBackendAddressPools.Id | Split-Path -Leaf}}


Figure 13 — Both private NICs belong to the be-web backend pool.


Azure PowerShell output showing both private NICs registered in the load balancer backend pool.

STEP 14 — VERIFY THAT BOTH VMS ARE PRIVATE


The only public ingress point should be the load balancer. Check that neither NIC IP configuration has a public IP reference.


Get-AzNetworkInterface -ResourceGroupName $ResourceGroupName |

Select-Object Name,@{n='PublicIpAttached';e={[bool]$_.IpConfigurations.PublicIpAddress}}


Figure 14 — Both VM NICs pass the private-only check with no direct public IP attachment.


Azure PowerShell output confirming neither virtual machine NIC has a public IP attachment.

STEP 15 — VERIFY THE FRONTEND IN THE AZURE PORTAL


Open the load balancer’s Frontend IP configuration page. Confirm the frontend name fe-public, the associated Standard public IP, and one load-balancing rule. The portal screenshot below is cropped to the resource content and excludes account, tenant, subscription, and notification details.


Figure 15 — Genuine Azure Portal view of the load balancer frontend configuration.


Cropped Azure Portal view of the load balancer frontend IP configuration with private identifiers excluded.

STEP 16 — TEST NORMAL TRAFFIC DISTRIBUTION


Build a request target from the frontend IP and port 80, then create new client connections by disabling keep-alive. The load balancer hashes each new flow, so a short sequence should eventually show both VM names. This is distribution, not strict alternating round-robin.


$PublicIp = (Get-AzPublicIpAddress -Name 'pip-bcwc-lb-weu' `

-ResourceGroupName $ResourceGroupName).IpAddress

$Target = [uri]::new('http', $PublicIp, 80, '/')
1..12 | ForEach-Object {

Invoke-WebRequest -Uri $Target -UseBasicParsing -DisableKeepAlive

}


Figure 16 — Repeated HTTP 200 responses arrive from both zonal backends.


Azure PowerShell output showing successful HTTP responses distributed across both web VMs.

STEP 17 — SIMULATE A BACKEND-INSTANCE FAILURE


Use Azure Run Command to stop only the web service on VM 1. The virtual machine and availability zone stay running. This is a controlled backend-instance failure simulation, not a real Azure availability-zone outage.


Invoke-AzVMRunCommand -ResourceGroupName $ResourceGroupName `

-VMName 'vm-bcwc-web01-weu' -CommandId 'RunShellScript' `

-ScriptString 'sudo systemctl stop bcwc-web.service'


Figure 17 — VM 1’s web service is inactive after the controlled failure command.


Azure Run Command output showing VM 1 web service stopped for the controlled failure simulation.

STEP 18 — VERIFY PROBE-DRIVEN FAILOVER


Wait about 25 seconds for probe convergence, then repeat the requests. Every successful response should come from VM 2 in zone 2. Existing TCP flows are not moved; the load balancer directs new flows only to healthy backends.


Start-Sleep -Seconds 25
1..12 | ForEach-Object {

Invoke-WebRequest -Uri $Target -UseBasicParsing -DisableKeepAlive

}


Figure 18 — Twelve successful requests are served exclusively by VM 2 after VM 1 becomes unhealthy.


Azure PowerShell failover test showing all successful requests served by VM 2 in zone 2.

STEP 19 — RECOVER VM 1


Start the service again, wait for successful probes, and repeat the test. Seeing both identities confirms that the recovered backend automatically rejoined rotation.


Invoke-AzVMRunCommand -ResourceGroupName $ResourceGroupName `

-VMName 'vm-bcwc-web01-weu' -CommandId 'RunShellScript' `

-ScriptString 'sudo systemctl start bcwc-web.service'

Start-Sleep -Seconds 25

Figure 19 — VM 1 is active again and both availability zones resume serving HTTP 200 responses.


Azure PowerShell recovery test showing VM 1 active and both zonal backends serving traffic again.

TROUBLESHOOTING


Only one backend appears before failover


Use more new connections and keep DisableKeepAlive enabled. Azure Load Balancer uses a five-tuple hash, so a small sample is not guaranteed to alternate evenly.


The health probe shows unhealthy


Run systemctl status bcwc-web on the affected VM through Azure Run Command. Confirm that Python listens on port 80, the NSG allows AzureLoadBalancer to port 80, and the probe path is exactly slash.


Requests time out


Confirm that the Standard public IP is attached to fe-public, rule-http maps port 80 to port 80, both NICs are in be-web, and the Internet service tag is allowed to port 80.


Cloud-init did not create the service


Pass the cloud-init document directly to Set-AzVMOperatingSystem -CustomData. That cmdlet encodes the value for the Azure Compute API. Do not pre-encode it again with older Az.Compute versions that already perform the encoding.


Run Command cannot execute


Wait until VM provisioning is Succeeded and the Azure Linux Agent is ready. The VMs do not need public IP addresses for Azure Run Command.


STEP 20 — CLEAN UP EVERYTHING


Delete the complete isolated resource group. Poll until Get-AzResourceGroup returns nothing, then remove the temporary private and public SSH key files from the known workshop work folder. Never use a broad or unresolved path for key deletion.


Remove-AzResourceGroup -Name $ResourceGroupName -Force
do {
    $Remaining = Get-AzResourceGroup -Name $ResourceGroupName -ErrorAction SilentlyContinue
    if ($Remaining) { Start-Sleep -Seconds 10 }
} while ($Remaining)

Figure 20 — Azure reports the resource group as not found, both SSH key files are deleted, and no chargeable lab resources remain.


Azure PowerShell cleanup output confirming the resource group and temporary SSH keys were deleted.

KNOWLEDGE CHECK


1. Why do the VMs use different availability zones?

2. What does the health probe control?

3. Why can repeated requests appear uneven even when both backends are healthy?

4. Which service tag permits Azure’s platform probes?

5. Why should the VMs have no direct public IPs in this design?

6. What is the difference between this failure simulation and a real zone outage?


ANSWERS


1. Separate zones reduce dependence on a single datacenter location.

2. The probe determines which backends can receive new load-balanced flows.

3. Distribution is based on a five-tuple hash, not strict alternating round-robin.

4. AzureLoadBalancer.

5. The load balancer is the controlled public entry point and reduces exposed attack surface.

6. This lab stops one guest service; it does not disrupt an Azure datacenter or availability zone.


NEXT STEPS


Extend the pattern with TLS termination, a reusable image or VM scale set, autoscaling, centralized monitoring, and an explicit outbound design such as NAT Gateway. For production, combine zone distribution with application-level resilience, patching, backup, security hardening, and tested operational runbooks.


WORKSHOP RESULT


You built a zone-aware Azure web tier entirely with PowerShell, verified private backends, demonstrated normal five-tuple distribution, proved health-probe failover and recovery, and removed every workshop resource.

Comments


bottom of page