top of page
  • Jul 26
  • 29 min read

Azure Site Recovery Deep Dive

Replicate Azure VMs and Perform a Regional Failover

Azure Site Recovery provides managed replication, recovery-point management, test failover, regional failover, commit, reverse replication, and failback orchestration for protected workloads.

In this workshop, you will build an Azure-to-Azure disaster recovery environment using PowerShell 7 and the Az PowerShell modules.

You will:

  • create source and recovery networks in two Azure regions;

  • deploy a Linux workload VM with an operating-system disk and a data disk;

  • create a Recovery Services vault in the recovery region;

  • configure Azure-to-Azure replication;

  • create replication policies and mappings;

  • monitor initial synchronization;

  • run an isolated test failover;

  • validate the recovered application and replicated data;

  • clean up the test failover;

  • perform a real regional failover;

  • commit the failover;

  • prepare reverse replication and failback;

  • remove all workshop resources.

For Azure-to-Azure replication, the Recovery Services vault should be separate from the source VM region. Placing the vault in the intended recovery region keeps Site Recovery management operations available during a source-region outage.

Cost warningThis workshop creates virtual machines, managed disks, storage accounts, public IP addresses, replicated disks, recovery points, and a Recovery Services vault.Initial replication, reprotection, failover, and cleanup can take a significant amount of time. Delete the complete environment when the workshop is finished.

Workshop overview

Item

Configuration

Level

Intermediate to advanced

Estimated duration

2–4 hours

Deployment interface

PowerShell 7

Source region

West Europe

Recovery region

North Europe

Source workload

Ubuntu Linux VM

Protected disks

OS disk and data disk

Replication

Azure-to-Azure

Recovery-point retention

24 hours

App-consistent snapshots

Every 4 hours

Test failover

Isolated recovery network

Regional failover

West Europe to North Europe

Failback

Reprotection and reverse-failover planning

What you will learn

By completing this workshop, you will learn how to:

  1. Design a two-region Site Recovery topology.

  2. Place the Recovery Services vault correctly.

  3. Create source, recovery, and isolated test networks.

  4. Deploy a stateful test workload.

  5. Create Site Recovery fabrics.

  6. Create protection containers.

  7. Create a replication policy.

  8. Create forward and reverse protection mappings.

  9. Create forward and reverse network mappings.

  10. Configure replication for managed disks.

  11. Monitor initial replication.

  12. Validate replication health.

  13. Run a test failover without interrupting production.

  14. Detect and validate the test VM.

  15. Verify replicated application and data-disk content.

  16. Clean up a test failover.

  17. Select a recovery point.

  18. Perform a regional failover.

  19. Validate the recovered workload.

  20. Commit the failover.

  21. Plan reverse replication and failback.

  22. Troubleshoot replication, networking, and failover.

  23. Delete the complete environment.

Architecture

                         Azure Site Recovery
                    Recovery Services Vault
                          North Europe
                               │
               Replication policy and mappings
                               │
          ┌────────────────────┴────────────────────┐
          │                                         │
          │                                         │
┌─────────▼────────────────┐            ┌───────────▼────────────────┐
│ Source region           │            │ Recovery region           │
│ West Europe             │            │ North Europe              │
│                         │            │                           │
│ Source resource group   │            │ Recovery resource group   │
│                         │            │                           │
│ VNet                    │            │ Recovery VNet             │
│ 10.10.0.0/16            │            │ 10.20.0.0/16              │
│                         │            │                           │
│ snet-workload           │            │ snet-workload             │
│ 10.10.1.0/24            │            │ 10.20.1.0/24              │
│                         │            │                           │
│ ┌─────────────────────┐ │            │ Replicated VM is created  │
│ │ Source VM           │ │            │ here during real failover │
│ │ vm-asr-workload     │ │            │                           │
│ │                     │ │            │ ┌─────────────────────┐   │
│ │ OS disk             │─┼───────────►│ │ Recovered VM        │   │
│ │ Data disk           │─┼───────────►│ │ OS disk replica     │   │
│ │ Nginx :8080         │ │            │ │ Data disk replica   │   │
│ └─────────────────────┘ │            │ └─────────────────────┘   │
│                         │            │                           │
│ Source cache storage    │            │ Recovery cache storage    │
└─────────────────────────┘            └───────────────────────────┘
                                                    │
                                                    │ Test failover
                                                    ▼
                                      ┌────────────────────────────┐
                                      │ Isolated test VNet         │
                                      │ North Europe               │
                                      │                            │
                                      │ 10.20.0.0/16               │
                                      │ snet-workload              │
                                      │ 10.20.1.0/24               │
                                      │                            │
                                      │ Temporary test VM          │
                                      │ Deleted during cleanup     │
                                      └────────────────────────────┘

Address plan

Component

Address range

Source VNet

10.10.0.0/16

Source workload subnet

10.10.1.0/24

Source VM private IP

10.10.1.4

Recovery VNet

10.20.0.0/16

Recovery workload subnet

10.20.1.0/24

Isolated test VNet

10.20.0.0/16

Isolated test subnet

10.20.1.0/24

The recovery and test VNets deliberately use the same address space.

This is valid because the test VNet remains isolated and is never peered with the recovery VNet.

A test-failover network should be isolated from production and should use matching subnet names. Site Recovery attempts to place the test VM into a subnet with the same name and preserve its configured IP address when that address is available. Test failover does not interrupt ongoing replication or the source production VM.

Recovery workflow

Deploy source VM
       │
       ▼
Enable replication
       │
       ▼
Initial replication
       │
       ▼
ProtectionState = Protected
       │
       ▼
Run isolated test failover
       │
       ▼
Validate application and data
       │
       ▼
Clean up test failover
       │
       ▼
Stop source VM
       │
       ▼
Run regional failover
       │
       ▼
Validate recovery VM
       │
       ▼
Commit failover
       │
       ▼
Reprotect toward source region
       │
       ▼
Plan or execute failback

Important operational distinctions

Test failover

A test failover:

  • creates a temporary VM;

  • uses an isolated test network;

  • does not stop production replication;

  • does not change the active replication direction;

  • is removed through test-failover cleanup.

Regional failover

A real failover:

  • stops normal source-to-target replication;

  • creates or activates the recovery VM;

  • moves workload responsibility to the recovery region;

  • must be validated before it is committed.

Commit

A commit:

  • accepts the failover result;

  • removes the previous recovery points;

  • prevents selecting another recovery point for that failover;

  • is required before reverse replication can begin.

Reprotect

Reprotection:

  • reverses the replication direction;

  • makes the recovery-region VM the new replication source;

  • prepares the workload for failback to the original region.

Site Recovery requires a cache storage account in the current source region. Each managed disk that will be replicated must be represented in the PowerShell disk-replication configuration.

1. Verify PowerShell

The workshop requires PowerShell 7.

$PSVersionTable

Confirm the major version:

if ($PSVersionTable.PSVersion.Major -lt 7) {
    throw "PowerShell 7 or later is required."
}

2. Install the Az PowerShell modules

Check whether the required Site Recovery module is installed:

Get-Module `
    -ListAvailable `
    -Name Az.RecoveryServices |
    Sort-Object Version -Descending |
    Select-Object -First 1 `
        Name,
        Version,
        Path

Install the Az module when required:

if (
    -not (
        Get-Module `
            -ListAvailable `
            -Name Az.RecoveryServices
    )
) {
    Install-Module `
        -Name Az `
        -Scope CurrentUser `
        -Repository PSGallery `
        -Force `
        -AllowClobber
}

Import the required modules:

$RequiredModules = @(
    "Az.Accounts"
    "Az.Resources"
    "Az.Network"
    "Az.Compute"
    "Az.Storage"
    "Az.RecoveryServices"
)

foreach ($ModuleName in $RequiredModules) {
    Import-Module `
        -Name $ModuleName `
        -Force `
        -ErrorAction Stop
}

Display the loaded versions:

Get-Module $RequiredModules |
    Select-Object `
        Name,
        Version |
    Sort-Object Name

3. Sign in to Azure

Connect-AzAccount

List the available subscriptions:

Get-AzSubscription |
    Select-Object `
        Name,
        Id,
        TenantId,
        State |
    Format-Table

Select the subscription:

$SubscriptionId = "<your-subscription-id>"

Set-AzContext `
    -SubscriptionId $SubscriptionId

Verify the active context:

Get-AzContext |
    Select-Object `
        Name,
        Subscription,
        Tenant,
        Account

4. Define the workshop variables

Keep the same PowerShell session open throughout the workshop.

Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"

$SubscriptionId = (
    Get-AzContext
).Subscription.Id

if ([string]::IsNullOrWhiteSpace($SubscriptionId)) {
    throw "No active Azure subscription was found."
}

$SourceLocation   = "westeurope"
$RecoveryLocation = "northeurope"

# Resource groups

$SourceResourceGroup   = "rg-asr-source-weu"
$RecoveryResourceGroup = "rg-asr-recovery-neu"
$VaultResourceGroup    = "rg-asr-vault-neu"
$TestResourceGroup     = "rg-asr-test-neu"

# Source network

$SourceVnetName      = "vnet-asr-source-weu"
$SourceVnetPrefix    = "10.10.0.0/16"
$WorkloadSubnetName  = "snet-workload"
$SourceSubnetPrefix  = "10.10.1.0/24"
$SourceNsgName       = "nsg-asr-source-weu"

# Recovery network

$RecoveryVnetName     = "vnet-asr-recovery-neu"
$RecoveryVnetPrefix   = "10.20.0.0/16"
$RecoverySubnetPrefix = "10.20.1.0/24"
$RecoveryNsgName      = "nsg-asr-recovery-neu"

# Test-failover network

$TestVnetName     = "vnet-asr-test-neu"
$TestVnetPrefix   = "10.20.0.0/16"
$TestSubnetPrefix = "10.20.1.0/24"
$TestNsgName      = "nsg-asr-test-neu"

# Source workload VM

$SourceVmName       = "vm-asr-workload"
$SourceVmNicName    = "nic-asr-workload"
$SourceVmPublicIp   = "pip-asr-workload-weu"
$SourceVmPrivateIp  = "10.10.1.4"
$SourceOsDiskName   = "osdisk-asr-workload"
$SourceDataDiskName = "datadisk-asr-workload-01"
$VmAdminUsername    = "azureuser"
$VmSize             = "Standard_B2s"

# Temporary and recovery public IPs

$TestFailoverPublicIp = "pip-asr-testfailover-neu"
$FailoverPublicIp     = "pip-asr-recovered-neu"

# Site Recovery vault

$VaultName = "rsv-asr-deepdive-neu"

# ASR fabrics

$SourceFabricName   = "asr-fabric-westeurope"
$RecoveryFabricName = "asr-fabric-northeurope"

# ASR protection containers

$SourceContainerName   = "asr-container-westeurope"
$RecoveryContainerName = "asr-container-northeurope"

# Replication policy

$ReplicationPolicyName = "asr-policy-24hour"

# Protection-container mappings

$SourceToRecoveryMappingName = "asr-map-weu-to-neu"
$RecoveryToSourceMappingName = "asr-map-neu-to-weu"

# Network mappings

$SourceToRecoveryNetworkMappingName = "asr-netmap-weu-to-neu"
$RecoveryToSourceNetworkMappingName = "asr-netmap-neu-to-weu"

# Globally unique storage-account names

$UniqueSuffix = Get-Random `
    -Minimum 100000 `
    -Maximum 999999

$SourceCacheStorageName = (
    "stasrweu$UniqueSuffix"
).ToLowerInvariant()

$RecoveryCacheStorageName = (
    "stasrneu$UniqueSuffix"
).ToLowerInvariant()

Set-AzContext `
    -SubscriptionId $SubscriptionId |
    Out-Null

Review the configuration:

[pscustomobject]@{
    SubscriptionId             = $SubscriptionId
    SourceLocation             = $SourceLocation
    RecoveryLocation           = $RecoveryLocation
    SourceResourceGroup        = $SourceResourceGroup
    RecoveryResourceGroup      = $RecoveryResourceGroup
    VaultResourceGroup         = $VaultResourceGroup
    TestResourceGroup          = $TestResourceGroup
    SourceVm                   = $SourceVmName
    SourceCacheStorage         = $SourceCacheStorageName
    RecoveryCacheStorage       = $RecoveryCacheStorageName
}

5. Check for existing workshop resources

$WorkshopResourceGroups = @(
    $SourceResourceGroup
    $RecoveryResourceGroup
    $VaultResourceGroup
    $TestResourceGroup
)

$ExistingResourceGroups = @(
    foreach ($ResourceGroupName in $WorkshopResourceGroups) {
        Get-AzResourceGroup `
            -Name $ResourceGroupName `
            -ErrorAction SilentlyContinue
    }
)

if ($ExistingResourceGroups) {
    $ExistingResourceGroups |
        Select-Object `
            ResourceGroupName,
            Location |
        Format-Table

    throw @"
One or more workshop resource groups already exist.

Delete the previous deployment or change the resource-group names.
"@
}

6. Create the regional resource groups

Create the source resource group:

$SourceResourceGroupObject = New-AzResourceGroup `
    -Name $SourceResourceGroup `
    -Location $SourceLocation `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "source"
        region   = $SourceLocation
    }

Create the recovery resource group:

$RecoveryResourceGroupObject = New-AzResourceGroup `
    -Name $RecoveryResourceGroup `
    -Location $RecoveryLocation `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "recovery"
        region   = $RecoveryLocation
    }

Create the vault resource group:

$VaultResourceGroupObject = New-AzResourceGroup `
    -Name $VaultResourceGroup `
    -Location $RecoveryLocation `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "vault"
        region   = $RecoveryLocation
    }

Create the test resource group:

$TestResourceGroupObject = New-AzResourceGroup `
    -Name $TestResourceGroup `
    -Location $RecoveryLocation `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "test-failover"
        region   = $RecoveryLocation
    }

Display them:

Get-AzResourceGroup |
    Where-Object {
        $_.ResourceGroupName -in $WorkshopResourceGroups
    } |
    Select-Object `
        ResourceGroupName,
        Location,
        ProvisioningState |
    Sort-Object ResourceGroupName |
    Format-Table

7. Create the regional NSGs

The lab permits inbound HTTP on port 8080 so the source, test, and recovered application can be validated from the local workstation.

In production, restrict the source prefix or use a private validation path.

Define an NSG helper function:

function New-AsrWorkshopNsg {
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        [string]$ResourceGroupName,

        [Parameter(Mandatory)]
        [string]$Location
    )

    $HttpRule = New-AzNetworkSecurityRuleConfig `
        -Name "Allow-HTTP-8080" `
        -Description "Allow HTTP validation for the Site Recovery workshop." `
        -Access Allow `
        -Protocol Tcp `
        -Direction Inbound `
        -Priority 100 `
        -SourceAddressPrefix Internet `
        -SourcePortRange "*" `
        -DestinationAddressPrefix "*" `
        -DestinationPortRange 8080

    New-AzNetworkSecurityGroup `
        -Name $Name `
        -ResourceGroupName $ResourceGroupName `
        -Location $Location `
        -SecurityRules $HttpRule `
        -Tag @{
            workshop = "site-recovery-deep-dive"
        }
}

Create the source NSG:

$SourceNsg = New-AsrWorkshopNsg `
    -Name $SourceNsgName `
    -ResourceGroupName $SourceResourceGroup `
    -Location $SourceLocation

Create the recovery NSG:

$RecoveryNsg = New-AsrWorkshopNsg `
    -Name $RecoveryNsgName `
    -ResourceGroupName $RecoveryResourceGroup `
    -Location $RecoveryLocation

Create the isolated test NSG:

$TestNsg = New-AsrWorkshopNsg `
    -Name $TestNsgName `
    -ResourceGroupName $TestResourceGroup `
    -Location $RecoveryLocation

8. Create the source VNet

Create the source subnet configuration:

$SourceSubnetConfiguration = New-AzVirtualNetworkSubnetConfig `
    -Name $WorkloadSubnetName `
    -AddressPrefix $SourceSubnetPrefix `
    -NetworkSecurityGroup $SourceNsg

Create the source VNet:

$SourceVnet = New-AzVirtualNetwork `
    -Name $SourceVnetName `
    -ResourceGroupName $SourceResourceGroup `
    -Location $SourceLocation `
    -AddressPrefix $SourceVnetPrefix `
    -Subnet $SourceSubnetConfiguration `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "source-network"
    }

9. Create the recovery VNet

$RecoverySubnetConfiguration = New-AzVirtualNetworkSubnetConfig `
    -Name $WorkloadSubnetName `
    -AddressPrefix $RecoverySubnetPrefix `
    -NetworkSecurityGroup $RecoveryNsg
$RecoveryVnet = New-AzVirtualNetwork `
    -Name $RecoveryVnetName `
    -ResourceGroupName $RecoveryResourceGroup `
    -Location $RecoveryLocation `
    -AddressPrefix $RecoveryVnetPrefix `
    -Subnet $RecoverySubnetConfiguration `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "recovery-network"
    }

10. Create the isolated test-failover VNet

$TestSubnetConfiguration = New-AzVirtualNetworkSubnetConfig `
    -Name $WorkloadSubnetName `
    -AddressPrefix $TestSubnetPrefix `
    -NetworkSecurityGroup $TestNsg
$TestVnet = New-AzVirtualNetwork `
    -Name $TestVnetName `
    -ResourceGroupName $TestResourceGroup `
    -Location $RecoveryLocation `
    -AddressPrefix $TestVnetPrefix `
    -Subnet $TestSubnetConfiguration `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "isolated-test-network"
    }

Display all three VNets:

@(
    $SourceVnet
    $RecoveryVnet
    $TestVnet
) |
    Select-Object `
        Name,
        Location,
        ResourceGroupName,
        AddressSpace |
    Format-Table

Do not peer the test VNet with any other network.

11. Create the source VM public IP

$SourcePublicIp = New-AzPublicIpAddress `
    -Name $SourceVmPublicIp `
    -ResourceGroupName $SourceResourceGroup `
    -Location $SourceLocation `
    -Sku Standard `
    -AllocationMethod Static `
    -IpAddressVersion IPv4 `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "source-validation"
    }

12. Create the source VM NIC

Resolve the source subnet:

$SourceSubnet = Get-AzVirtualNetworkSubnetConfig `
    -Name $WorkloadSubnetName `
    -VirtualNetwork $SourceVnet

Create the NIC:

$SourceVmNic = New-AzNetworkInterface `
    -Name $SourceVmNicName `
    -ResourceGroupName $SourceResourceGroup `
    -Location $SourceLocation `
    -SubnetId $SourceSubnet.Id `
    -PrivateIpAddress $SourceVmPrivateIp `
    -PublicIpAddressId $SourcePublicIp.Id `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "source-workload"
    }

13. Create the source workload VM

Enter a password for the Linux administrator account:

$VmCredential = Get-Credential `
    -UserName $VmAdminUsername `
    -Message "Enter the password for the Site Recovery workshop VM."

Create the VM configuration:

$SourceVmConfig = New-AzVMConfig `
    -VMName $SourceVmName `
    -VMSize $VmSize

Configure Linux:

$SourceVmConfig = Set-AzVMOperatingSystem `
    -VM $SourceVmConfig `
    -Linux `
    -ComputerName $SourceVmName `
    -Credential $VmCredential

Configure the image:

$SourceVmConfig = Set-AzVMSourceImage `
    -VM $SourceVmConfig `
    -PublisherName "Canonical" `
    -Offer "0001-com-ubuntu-server-jammy" `
    -Skus "22_04-lts-gen2" `
    -Version "latest"

Configure the OS disk:

$SourceVmConfig = Set-AzVMOSDisk `
    -VM $SourceVmConfig `
    -Name $SourceOsDiskName `
    -CreateOption FromImage `
    -StorageAccountType Standard_LRS

Attach the NIC:

$SourceVmConfig = Add-AzVMNetworkInterface `
    -VM $SourceVmConfig `
    -Id $SourceVmNic.Id `
    -Primary

Add a managed data disk:

$SourceVmConfig = Add-AzVMDataDisk `
    -VM $SourceVmConfig `
    -Name $SourceDataDiskName `
    -DiskSizeInGB 8 `
    -Lun 0 `
    -CreateOption Empty `
    -StorageAccountType Standard_LRS

Deploy the VM:

New-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Location $SourceLocation `
    -VM $SourceVmConfig `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "protected-workload"
    }

Verify its state:

Get-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName `
    -Status |
    Select-Object `
        Name,
        Location,
        PowerState,
        ProvisioningState

14. Configure the application and data disk

The configuration creates:

  • an ext4 filesystem on the data disk;

  • a persistent mount at /srv/asrdata;

  • a replicated marker file;

  • an Nginx service on port 8080;

  • a page that displays the marker stored on the data disk.

$WorkloadConfigurationScript = @'
#!/usr/bin/env bash
set -euo pipefail

export DEBIAN_FRONTEND=noninteractive

apt-get update
apt-get install -y nginx curl

DATA_DISK="/dev/disk/azure/scsi1/lun0"
MOUNT_PATH="/srv/asrdata"

for attempt in $(seq 1 60); do
    if [ -e "$DATA_DISK" ]; then
        break
    fi

    sleep 5
done

if [ ! -e "$DATA_DISK" ]; then
    echo "The Azure data-disk path was not found."
    exit 1
fi

mkdir -p "$MOUNT_PATH"

if ! blkid "$DATA_DISK" >/dev/null 2>&1; then
    mkfs.ext4 -F "$DATA_DISK"
fi

DISK_UUID="$(blkid -s UUID -o value "$DATA_DISK")"

if ! grep -q "$DISK_UUID" /etc/fstab; then
    echo "UUID=$DISK_UUID $MOUNT_PATH ext4 defaults,nofail 0 2" >> /etc/fstab
fi

mount -a

MARKER_FILE="$MOUNT_PATH/site-recovery-marker.txt"

cat >"$MARKER_FILE" <<MARKER
Azure Site Recovery Deep Dive
Source region: West Europe
Protected VM: vm-asr-workload
Created: $(date --utc --iso-8601=seconds)
Data disk: /srv/asrdata
MARKER

cat >/etc/nginx/sites-available/default <<'NGINX'
server {
    listen 8080 default_server;
    listen [::]:8080 default_server;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}
NGINX

MARKER_CONTENT="$(cat "$MARKER_FILE")"

cat >/var/www/html/index.html <<HTML
<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Azure Site Recovery Deep Dive</title>
    <style>
        body {
            margin: 4rem;
            background: #07111f;
            color: #80eaff;
            font-family: Arial, sans-serif;
        }

        .card {
            max-width: 760px;
            padding: 2rem;
            border: 2px solid #ff4bd8;
            border-radius: 14px;
            box-shadow: 0 0 28px #ff4bd855;
        }

        h1 {
            color: #ff59dc;
        }

        pre {
            padding: 1rem;
            background: #020811;
            color: #ffffff;
            border-radius: 8px;
        }
    </style>
</head>
<body>
    <div class="card">
        <h1>Azure Site Recovery Deep Dive</h1>
        <p>The workload is online.</p>
        <p>The following data was loaded from the managed data disk:</p>
        <pre>$MARKER_CONTENT</pre>
    </div>
</body>
</html>
HTML

nginx -t
systemctl enable nginx
systemctl restart nginx

echo
echo "=== Data-disk mount ==="
findmnt "$MOUNT_PATH"

echo
echo "=== Replication marker ==="
cat "$MARKER_FILE"

echo
echo "=== Nginx listener ==="
ss -lntp | grep ':8080'

echo
echo "=== Local application test ==="
curl -fsS http://127.0.0.1:8080
'@

Run the configuration:

$SourceConfigurationResult = Invoke-AzVMRunCommand `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName `
    -CommandId "RunShellScript" `
    -ScriptString $WorkloadConfigurationScript

Display the result:

$SourceConfigurationResult.Value.Message

15. Validate the source application

Get the source public IP:

$SourcePublicIp = Get-AzPublicIpAddress `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmPublicIp

Display the URL:

$SourceApplicationUrl = "http://$($SourcePublicIp.IpAddress):8080"

$SourceApplicationUrl

Test the application:

$SourceApplicationResponse = Invoke-WebRequest `
    -Uri $SourceApplicationUrl `
    -TimeoutSec 30

Validate the response:

if (
    $SourceApplicationResponse.Content -notmatch
    "Azure Site Recovery Deep Dive"
) {
    throw "The source application did not return the expected content."
}

if (
    $SourceApplicationResponse.Content -notmatch
    "Data disk"
) {
    throw "The source application did not return the data-disk marker."
}

Write-Host `
    "PASS: The source application and data disk are available." `
    -ForegroundColor Green

16. Create the cache storage accounts

Create the source-region cache storage account:

$SourceCacheStorageAccount = New-AzStorageAccount `
    -Name $SourceCacheStorageName `
    -ResourceGroupName $SourceResourceGroup `
    -Location $SourceLocation `
    -SkuName Standard_LRS `
    -Kind StorageV2 `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "source-replication-cache"
    }

Create the recovery-region cache storage account for later reprotection:

$RecoveryCacheStorageAccount = New-AzStorageAccount `
    -Name $RecoveryCacheStorageName `
    -ResourceGroupName $RecoveryResourceGroup `
    -Location $RecoveryLocation `
    -SkuName Standard_LRS `
    -Kind StorageV2 `
    -Tag @{
        workshop = "site-recovery-deep-dive"
        role     = "reverse-replication-cache"
    }

Display both accounts:

@(
    $SourceCacheStorageAccount
    $RecoveryCacheStorageAccount
) |
    Select-Object `
        StorageAccountName,
        Location,
        Kind,
        Sku |
    Format-Table

17. Create the Recovery Services vault

$Vault = New-AzRecoveryServicesVault `
    -Name $VaultName `
    -ResourceGroupName $VaultResourceGroup `
    -Location $RecoveryLocation

Display it:

$Vault |
    Select-Object `
        Name,
        ResourceGroupName,
        Location,
        ID

Set the Site Recovery vault context:

Set-AzRecoveryServicesAsrVaultContext `
    -Vault $Vault

All subsequent Site Recovery cmdlets in this session operate against this vault.

18. Create an ASR job-waiting function

Most Site Recovery configuration operations are asynchronous.

function Wait-AsrJob {
    param(
        [Parameter(Mandatory)]
        $Job,

        [Parameter(Mandatory)]
        [string]$Operation,

        [int]$TimeoutMinutes = 120,

        [int]$PollSeconds = 15
    )

    $Deadline = (
        Get-Date
    ).AddMinutes($TimeoutMinutes)

    do {
        $CurrentJob = Get-AzRecoveryServicesAsrJob `
            -Job $Job

        Write-Host (
            "{0:u} {1}: {2} - {3}" -f `
                (Get-Date),
                $Operation,
                $CurrentJob.State,
                $CurrentJob.StateDescription
        )

        if ($CurrentJob.State -eq "Succeeded") {
            return $CurrentJob
        }

        if (
            $CurrentJob.State -in @(
                "Failed"
                "Cancelled"
                "Suspended"
            )
        ) {
            $ErrorText = (
                $CurrentJob.Errors |
                Format-List * |
                Out-String
            )

            throw @"
Site Recovery operation failed.

Operation:
$Operation

State:
$($CurrentJob.State)

Errors:
$ErrorText
"@
        }

        Start-Sleep `
            -Seconds $PollSeconds
    }
    while ((Get-Date) -lt $Deadline)

    throw @"
Site Recovery operation timed out.

Operation:
$Operation

Timeout:
$TimeoutMinutes minutes
"@
}

19. Resolve the Azure region display names

Site Recovery fabric creation uses Azure location display names.

$SourceLocationObject = Get-AzLocation |
    Where-Object {
        $_.Location -eq $SourceLocation
    } |
    Select-Object -First 1

$RecoveryLocationObject = Get-AzLocation |
    Where-Object {
        $_.Location -eq $RecoveryLocation
    } |
    Select-Object -First 1

if (-not $SourceLocationObject) {
    throw "The source Azure location was not found."
}

if (-not $RecoveryLocationObject) {
    throw "The recovery Azure location was not found."
}

$SourceLocationDisplayName = $SourceLocationObject.DisplayName
$RecoveryLocationDisplayName = $RecoveryLocationObject.DisplayName

Display them:

[pscustomobject]@{
    SourceCode          = $SourceLocation
    SourceDisplayName   = $SourceLocationDisplayName
    RecoveryCode        = $RecoveryLocation
    RecoveryDisplayName = $RecoveryLocationDisplayName
}

20. Create the source Site Recovery fabric

$SourceFabricJob = New-AzRecoveryServicesAsrFabric `
    -Azure `
    -Location $SourceLocationDisplayName `
    -Name $SourceFabricName

Wait for completion:

Wait-AsrJob `
    -Job $SourceFabricJob `
    -Operation "Create source Site Recovery fabric" `
    -TimeoutMinutes 60

Retrieve the source fabric:

$SourceFabric = Get-AzRecoveryServicesAsrFabric `
    -Name $SourceFabricName

21. Create the recovery Site Recovery fabric

$RecoveryFabricJob = New-AzRecoveryServicesAsrFabric `
    -Azure `
    -Location $RecoveryLocationDisplayName `
    -Name $RecoveryFabricName

Wait for completion:

Wait-AsrJob `
    -Job $RecoveryFabricJob `
    -Operation "Create recovery Site Recovery fabric" `
    -TimeoutMinutes 60

Retrieve the recovery fabric:

$RecoveryFabric = Get-AzRecoveryServicesAsrFabric `
    -Name $RecoveryFabricName

Display both fabrics:

@(
    $SourceFabric
    $RecoveryFabric
) |
    Select-Object `
        FriendlyName,
        FabricType,
        Location,
        FabricSpecificDetails |
    Format-List

22. Create the protection containers

Create the source protection container:

$SourceContainerJob = New-AzRecoveryServicesAsrProtectionContainer `
    -InputObject $SourceFabric `
    -Name $SourceContainerName

Wait for completion:

Wait-AsrJob `
    -Job $SourceContainerJob `
    -Operation "Create source protection container" `
    -TimeoutMinutes 60

Retrieve the source container:

$SourceProtectionContainer = Get-AzRecoveryServicesAsrProtectionContainer `
    -Fabric $SourceFabric `
    -Name $SourceContainerName

Create the recovery protection container:

$RecoveryContainerJob = New-AzRecoveryServicesAsrProtectionContainer `
    -InputObject $RecoveryFabric `
    -Name $RecoveryContainerName

Wait for completion:

Wait-AsrJob `
    -Job $RecoveryContainerJob `
    -Operation "Create recovery protection container" `
    -TimeoutMinutes 60

Retrieve the recovery container:

$RecoveryProtectionContainer = Get-AzRecoveryServicesAsrProtectionContainer `
    -Fabric $RecoveryFabric `
    -Name $RecoveryContainerName

23. Create the replication policy

$ReplicationPolicyJob = New-AzRecoveryServicesAsrPolicy `
    -AzureToAzure `
    -Name $ReplicationPolicyName `
    -RecoveryPointRetentionInHours 24 `
    -ApplicationConsistentSnapshotFrequencyInHours 4

Wait for completion:

Wait-AsrJob `
    -Job $ReplicationPolicyJob `
    -Operation "Create replication policy" `
    -TimeoutMinutes 60

Retrieve the policy:

$ReplicationPolicy = Get-AzRecoveryServicesAsrPolicy `
    -Name $ReplicationPolicyName

Display it:

$ReplicationPolicy |
    Select-Object `
        Name,
        ProviderSpecificDetails |
    Format-List

24. Create the forward protection-container mapping

$SourceToRecoveryMappingJob = New-AzRecoveryServicesAsrProtectionContainerMapping `
    -Name $SourceToRecoveryMappingName `
    -Policy $ReplicationPolicy `
    -PrimaryProtectionContainer $SourceProtectionContainer `
    -RecoveryProtectionContainer $RecoveryProtectionContainer

Wait for completion:

Wait-AsrJob `
    -Job $SourceToRecoveryMappingJob `
    -Operation "Create West Europe to North Europe protection mapping" `
    -TimeoutMinutes 60

Retrieve the mapping:

$SourceToRecoveryMapping = Get-AzRecoveryServicesAsrProtectionContainerMapping `
    -ProtectionContainer $SourceProtectionContainer `
    -Name $SourceToRecoveryMappingName

25. Create the reverse protection-container mapping

The reverse mapping is required for reprotection and failback.

$RecoveryToSourceMappingJob = New-AzRecoveryServicesAsrProtectionContainerMapping `
    -Name $RecoveryToSourceMappingName `
    -Policy $ReplicationPolicy `
    -PrimaryProtectionContainer $RecoveryProtectionContainer `
    -RecoveryProtectionContainer $SourceProtectionContainer

Wait for completion:

Wait-AsrJob `
    -Job $RecoveryToSourceMappingJob `
    -Operation "Create North Europe to West Europe protection mapping" `
    -TimeoutMinutes 60

Retrieve the reverse mapping:

$RecoveryToSourceMapping = Get-AzRecoveryServicesAsrProtectionContainerMapping `
    -ProtectionContainer $RecoveryProtectionContainer `
    -Name $RecoveryToSourceMappingName

26. Create the forward network mapping

$SourceToRecoveryNetworkJob = New-AzRecoveryServicesAsrNetworkMapping `
    -AzureToAzure `
    -Name $SourceToRecoveryNetworkMappingName `
    -PrimaryFabric $SourceFabric `
    -PrimaryAzureNetworkId $SourceVnet.Id `
    -RecoveryFabric $RecoveryFabric `
    -RecoveryAzureNetworkId $RecoveryVnet.Id

Wait for completion:

Wait-AsrJob `
    -Job $SourceToRecoveryNetworkJob `
    -Operation "Create source-to-recovery network mapping" `
    -TimeoutMinutes 60

27. Create the reverse network mapping

$RecoveryToSourceNetworkJob = New-AzRecoveryServicesAsrNetworkMapping `
    -AzureToAzure `
    -Name $RecoveryToSourceNetworkMappingName `
    -PrimaryFabric $RecoveryFabric `
    -PrimaryAzureNetworkId $RecoveryVnet.Id `
    -RecoveryFabric $SourceFabric `
    -RecoveryAzureNetworkId $SourceVnet.Id

Wait for completion:

Wait-AsrJob `
    -Job $RecoveryToSourceNetworkJob `
    -Operation "Create recovery-to-source network mapping" `
    -TimeoutMinutes 60

Display the network mappings:

Get-AzRecoveryServicesAsrNetworkMapping `
    -Fabric $SourceFabric |
    Select-Object `
        Name,
        PrimaryNetworkFriendlyName,
        RecoveryNetworkFriendlyName |
    Format-Table

Get-AzRecoveryServicesAsrNetworkMapping `
    -Fabric $RecoveryFabric |
    Select-Object `
        Name,
        PrimaryNetworkFriendlyName,
        RecoveryNetworkFriendlyName |
    Format-Table

28. Build the disk-replication configuration

Retrieve the source VM:

$SourceVm = Get-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName

Refresh the recovery resource-group object:

$RecoveryResourceGroupObject = Get-AzResourceGroup `
    -Name $RecoveryResourceGroup

Create the OS-disk replication configuration:

$OsDiskStorageType = (
    $SourceVm.StorageProfile.OsDisk.ManagedDisk.StorageAccountType
)

$OsDiskReplicationConfiguration = New-AzRecoveryServicesAsrAzureToAzureDiskReplicationConfig `
    -ManagedDisk `
    -LogStorageAccountId $SourceCacheStorageAccount.Id `
    -DiskId $SourceVm.StorageProfile.OsDisk.ManagedDisk.Id `
    -RecoveryResourceGroupId $RecoveryResourceGroupObject.ResourceId `
    -RecoveryReplicaDiskAccountType $OsDiskStorageType `
    -RecoveryTargetDiskAccountType $OsDiskStorageType

Create replication configurations for all data disks:

$DataDiskReplicationConfigurations = @(
    foreach ($DataDisk in $SourceVm.StorageProfile.DataDisks) {
        $ReplicaDiskType = (
            $DataDisk.ManagedDisk.StorageAccountType
        )

        if (
            $ReplicaDiskType -in @(
                "PremiumV2_LRS"
                "Ultra_LRS"
            )
        ) {
            $ReplicaDiskType = "Premium_LRS"
        }

        New-AzRecoveryServicesAsrAzureToAzureDiskReplicationConfig `
            -ManagedDisk `
            -LogStorageAccountId $SourceCacheStorageAccount.Id `
            -DiskId $DataDisk.ManagedDisk.Id `
            -RecoveryResourceGroupId $RecoveryResourceGroupObject.ResourceId `
            -RecoveryReplicaDiskAccountType $ReplicaDiskType `
            -RecoveryTargetDiskAccountType $DataDisk.ManagedDisk.StorageAccountType
    }
)

Combine the configurations:

$DiskReplicationConfigurations = @(
    $OsDiskReplicationConfiguration
    $DataDiskReplicationConfigurations
)

Display the protected disk IDs:

$DiskReplicationConfigurations |
    Select-Object `
        DiskId,
        LogStorageAccountId,
        RecoveryReplicaDiskAccountType,
        RecoveryTargetDiskAccountType |
    Format-List

29. Enable Site Recovery replication

Create the replication-protected item:

$EnableReplicationJob = New-AzRecoveryServicesAsrReplicationProtectedItem `
    -AzureToAzure `
    -AzureVmId $SourceVm.Id `
    -Name (
        New-Guid
    ).Guid `
    -ProtectionContainerMapping $SourceToRecoveryMapping `
    -AzureToAzureDiskReplicationConfiguration $DiskReplicationConfigurations `
    -RecoveryResourceGroupId $RecoveryResourceGroupObject.ResourceId

Wait for the enable-protection job:

Wait-AsrJob `
    -Job $EnableReplicationJob `
    -Operation "Enable Azure-to-Azure replication" `
    -TimeoutMinutes 180 `
    -PollSeconds 30

The enable-protection job can complete before the initial disk synchronization is fully finished.

30. Wait for initial replication

Initial replication creates replicas of the protected disks in the recovery region. After that initial synchronization completes, the workload enters differential replication and its protection state becomes Protected.

$ProtectionDeadline = (
    Get-Date
).AddHours(4)

do {
    $ReplicationProtectedItem = Get-AzRecoveryServicesAsrReplicationProtectedItem `
        -ProtectionContainer $SourceProtectionContainer |
        Where-Object {
            $_.FriendlyName -eq $SourceVmName
        } |
        Select-Object -First 1

    if ($ReplicationProtectedItem) {
        Write-Host (
            "{0:u} ProtectionState={1}, Health={2}, ActiveLocation={3}" -f `
                (Get-Date),
                $ReplicationProtectedItem.ProtectionState,
                $ReplicationProtectedItem.ReplicationHealth,
                $ReplicationProtectedItem.ActiveLocation
        )
    }
    else {
        Write-Host `
            "Waiting for the replication-protected item to appear..."
    }

    $ReplicationReady = (
        $ReplicationProtectedItem -and
        $ReplicationProtectedItem.ProtectionState -eq "Protected" -and
        $ReplicationProtectedItem.ReplicationHealth -eq "Normal"
    )

    if (-not $ReplicationReady) {
        Start-Sleep -Seconds 60
    }
}
until (
    $ReplicationReady -or
    (Get-Date) -ge $ProtectionDeadline
)

if (-not $ReplicationReady) {
    throw @"
Initial replication did not reach the expected protected state.

Review the Site Recovery jobs and replication health.
"@
}

Display the protected item:

$ReplicationProtectedItem |
    Select-Object `
        FriendlyName,
        ProtectionState,
        ReplicationHealth,
        ActiveLocation,
        TestFailoverState,
        AllowedOperations |
    Format-List

31. Inspect the available recovery points

$RecoveryPoints = @(
    Get-AzRecoveryServicesAsrRecoveryPoint `
        -ReplicationProtectedItem $ReplicationProtectedItem |
    Sort-Object RecoveryPointTime
)

if ($RecoveryPoints.Count -eq 0) {
    throw "No recovery points are available."
}

Display them:

$RecoveryPoints |
    Select-Object `
        RecoveryPointType,
        RecoveryPointTime,
        Id |
    Format-Table

Display the latest point:

$LatestRecoveryPoint = $RecoveryPoints[-1]

$LatestRecoveryPoint |
    Select-Object `
        RecoveryPointType,
        RecoveryPointTime

32. Create a public-IP attachment helper

Site Recovery does not automatically provide a public IP for the test or recovered VM in this workshop.

The following helper creates a temporary Standard public IP and attaches it to the VM’s primary NIC.

function Add-AsrWorkshopPublicIp {
    param(
        [Parameter(Mandatory)]
        [string]$VmResourceGroupName,

        [Parameter(Mandatory)]
        [string]$VmName,

        [Parameter(Mandatory)]
        [string]$PublicIpResourceGroupName,

        [Parameter(Mandatory)]
        [string]$PublicIpName,

        [Parameter(Mandatory)]
        [string]$Location
    )

    $Vm = Get-AzVM `
        -ResourceGroupName $VmResourceGroupName `
        -Name $VmName

    $NicId = (
        $Vm.NetworkProfile.NetworkInterfaces |
        Where-Object {
            $_.Primary -eq $true
        } |
        Select-Object -First 1
    ).Id

    if (-not $NicId) {
        $NicId = $Vm.NetworkProfile.NetworkInterfaces[0].Id
    }

    $NicIdParts = $NicId -split "/"

    $NicResourceGroupName = $NicIdParts[4]
    $NicName              = $NicIdParts[-1]

    $PublicIp = New-AzPublicIpAddress `
        -Name $PublicIpName `
        -ResourceGroupName $PublicIpResourceGroupName `
        -Location $Location `
        -Sku Standard `
        -AllocationMethod Static `
        -IpAddressVersion IPv4 `
        -Tag @{
            workshop = "site-recovery-deep-dive"
            role     = "temporary-validation"
        }

    $Nic = Get-AzNetworkInterface `
        -ResourceGroupName $NicResourceGroupName `
        -Name $NicName

    $Nic.IpConfigurations[0].PublicIpAddress = $PublicIp

    Set-AzNetworkInterface `
        -NetworkInterface $Nic |
        Out-Null

    $Deadline = (
        Get-Date
    ).AddMinutes(10)

    do {
        $PublicIp = Get-AzPublicIpAddress `
            -ResourceGroupName $PublicIpResourceGroupName `
            -Name $PublicIpName

        if (
            [string]::IsNullOrWhiteSpace(
                $PublicIp.IpAddress
            )
        ) {
            Start-Sleep -Seconds 10
        }
    }
    until (
        -not [string]::IsNullOrWhiteSpace(
            $PublicIp.IpAddress
        ) -or
        (Get-Date) -ge $Deadline
    )

    if (
        [string]::IsNullOrWhiteSpace(
            $PublicIp.IpAddress
        )
    ) {
        throw "The public IP did not receive an address."
    }

    return $PublicIp
}

33. Capture the recovery-region VM inventory

This allows the test VM to be detected without relying solely on its generated name.

$RecoveryVmIdsBeforeTest = @(
    Get-AzVM `
        -ResourceGroupName $RecoveryResourceGroup `
        -ErrorAction SilentlyContinue |
    Select-Object -ExpandProperty Id
)

34. Run the isolated test failover

Start the test failover:

$TestFailoverJob = Start-AzRecoveryServicesAsrTestFailoverJob `
    -ReplicationProtectedItem $ReplicationProtectedItem `
    -AzureVMNetworkId $TestVnet.Id `
    -Direction PrimaryToRecovery

Wait for completion:

Wait-AsrJob `
    -Job $TestFailoverJob `
    -Operation "Run isolated test failover" `
    -TimeoutMinutes 180 `
    -PollSeconds 30

A successful test failover creates a temporary VM while the source VM and normal replication remain active.

35. Detect the test-failover VM

$RecoveryVmsAfterTest = @(
    Get-AzVM `
        -ResourceGroupName $RecoveryResourceGroup `
        -Status
)

$TestVm = $RecoveryVmsAfterTest |
    Where-Object {
        $_.Id -notin $RecoveryVmIdsBeforeTest
    } |
    Select-Object -First 1

Use the conventional test name as a fallback:

if (-not $TestVm) {
    $TestVm = Get-AzVM `
        -ResourceGroupName $RecoveryResourceGroup `
        -Name "$SourceVmName-Test" `
        -Status `
        -ErrorAction SilentlyContinue
}

Validate detection:

if (-not $TestVm) {
    Get-AzVM `
        -ResourceGroupName $RecoveryResourceGroup `
        -Status |
        Select-Object `
            Name,
            Location,
            PowerState |
        Format-Table

    throw "The test-failover VM could not be identified."
}

Store its name:

$TestVmName = $TestVm.Name

$TestVm |
    Select-Object `
        Name,
        Location,
        PowerState,
        ProvisioningState |
    Format-List

36. Validate the test VM network

Resolve the test VM NIC:

$TestVmObject = Get-AzVM `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $TestVmName

$TestVmNicId = $TestVmObject.NetworkProfile.NetworkInterfaces[0].Id

$TestVmNicParts = $TestVmNicId -split "/"

$TestVmNicResourceGroup = $TestVmNicParts[4]
$TestVmNicName          = $TestVmNicParts[-1]

$TestVmNic = Get-AzNetworkInterface `
    -ResourceGroupName $TestVmNicResourceGroup `
    -Name $TestVmNicName

Display its network placement:

$TestVmNic |
    Select-Object `
        Name,
        Location,
        @{
            Name = "PrivateIp"
            Expression = {
                $_.IpConfigurations[0].PrivateIpAddress
            }
        },
        @{
            Name = "SubnetId"
            Expression = {
                $_.IpConfigurations[0].Subnet.Id
            }
        } |
    Format-List

The subnet ID should reference:

vnet-asr-test-neu
snet-workload

37. Attach a temporary public IP to the test VM

$TestPublicIp = Add-AsrWorkshopPublicIp `
    -VmResourceGroupName $RecoveryResourceGroup `
    -VmName $TestVmName `
    -PublicIpResourceGroupName $RecoveryResourceGroup `
    -PublicIpName $TestFailoverPublicIp `
    -Location $RecoveryLocation

Display the validation URL:

$TestApplicationUrl = "http://$($TestPublicIp.IpAddress):8080"

$TestApplicationUrl

38. Validate the test-failover application

Wait for the application to become reachable:

$TestApplicationDeadline = (
    Get-Date
).AddMinutes(10)

$TestApplicationResponse = $null

do {
    try {
        $TestApplicationResponse = Invoke-WebRequest `
            -Uri $TestApplicationUrl `
            -TimeoutSec 20
    }
    catch {
        Write-Host `
            "Waiting for the test-failover application..."

        Start-Sleep -Seconds 15
    }
}
until (
    $TestApplicationResponse -or
    (Get-Date) -ge $TestApplicationDeadline
)

if (-not $TestApplicationResponse) {
    throw "The test-failover application did not become reachable."
}

Validate the replicated content:

if (
    $TestApplicationResponse.Content -notmatch
    "Azure Site Recovery Deep Dive"
) {
    throw "The test-failover application content is incorrect."
}

if (
    $TestApplicationResponse.Content -notmatch
    "Data disk"
) {
    throw "The replicated data-disk marker is missing."
}

Write-Host `
    "PASS: Test failover restored the application and data-disk content." `
    -ForegroundColor Green

39. Validate the test VM from inside the guest

$TestGuestValidation = Invoke-AzVMRunCommand `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $TestVmName `
    -CommandId "RunShellScript" `
    -ScriptString @'
set -e

echo "=== Hostname ==="
hostname

echo
echo "=== Region-independent application test ==="
curl -fsS http://127.0.0.1:8080

echo
echo "=== Replicated data-disk mount ==="
findmnt /srv/asrdata

echo
echo "=== Replicated data marker ==="
cat /srv/asrdata/site-recovery-marker.txt
'@

Display the result:

$TestGuestValidation.Value.Message

40. Confirm production remained available

The source VM should still be running during the isolated test failover.

$SourceVmStatus = Get-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName `
    -Status

$SourceVmStatus |
    Select-Object `
        Name,
        PowerState,
        ProvisioningState

Retest the source application:

$SourceRetest = Invoke-WebRequest `
    -Uri $SourceApplicationUrl `
    -TimeoutSec 30

if (
    $SourceRetest.Content -notmatch
    "Azure Site Recovery Deep Dive"
) {
    throw "The source application is not available."
}

Write-Host `
    "PASS: The source workload remained online during the drill." `
    -ForegroundColor Green

41. Clean up the test failover

Start cleanup:

$TestCleanupJob = Start-AzRecoveryServicesAsrTestFailoverCleanupJob `
    -ReplicationProtectedItem $ReplicationProtectedItem `
    -Comment "Application and replicated data validated successfully."

Wait for cleanup:

Wait-AsrJob `
    -Job $TestCleanupJob `
    -Operation "Clean up test failover" `
    -TimeoutMinutes 120 `
    -PollSeconds 30

Delete the temporary public IP:

Remove-AzPublicIpAddress `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $TestFailoverPublicIp `
    -Force `
    -ErrorAction SilentlyContinue

Confirm that the test VM is gone:

Get-AzVM `
    -ResourceGroupName $RecoveryResourceGroup `
    -Status `
    -ErrorAction SilentlyContinue |
    Select-Object `
        Name,
        PowerState |
    Format-Table

The source replication remains protected.

42. Recheck replication health

Refresh the protected item:

$ReplicationProtectedItem = Get-AzRecoveryServicesAsrReplicationProtectedItem `
    -ProtectionContainer $SourceProtectionContainer |
    Where-Object {
        $_.FriendlyName -eq $SourceVmName
    } |
    Select-Object -First 1

Display its state:

$ReplicationProtectedItem |
    Select-Object `
        FriendlyName,
        ProtectionState,
        ReplicationHealth,
        ActiveLocation,
        TestFailoverState |
    Format-List

Required values:

ProtectionState      Protected
ReplicationHealth    Normal

43. Prepare for the regional failover

A real regional failover changes the active workload location.

Before continuing:

  • confirm that the test failover succeeded;

  • confirm that the application was validated;

  • confirm that the replicated data disk was validated;

  • confirm that the source and recovery networks are correct;

  • confirm that the latest recovery point is acceptable.

Refresh the recovery points:

$RecoveryPoints = @(
    Get-AzRecoveryServicesAsrRecoveryPoint `
        -ReplicationProtectedItem $ReplicationProtectedItem |
    Sort-Object RecoveryPointTime
)

$LatestRecoveryPoint = $RecoveryPoints[-1]

Display the selected point:

$LatestRecoveryPoint |
    Select-Object `
        RecoveryPointType,
        RecoveryPointTime,
        Id |
    Format-List

44. Stop the source VM

Stopping the source VM before failover reduces the chance of additional writes occurring after the selected recovery point.

Stop-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName `
    -Force

Confirm the state:

Get-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName `
    -Status |
    Select-Object `
        Name,
        PowerState

Expected state:

VM deallocated

45. Perform the regional failover

Start the failover to North Europe:

$RegionalFailoverJob = Start-AzRecoveryServicesAsrUnplannedFailoverJob `
    -ReplicationProtectedItem $ReplicationProtectedItem `
    -Direction PrimaryToRecovery `
    -RecoveryPoint $LatestRecoveryPoint

Wait for completion:

Wait-AsrJob `
    -Job $RegionalFailoverJob `
    -Operation "Fail over workload to North Europe" `
    -TimeoutMinutes 180 `
    -PollSeconds 30

Do not cancel an active failover job. Failover stops normal replication while the operation runs, and cancelling it can leave the item without active replication.

46. Detect the recovered VM

$RecoveredVm = Get-AzVM `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $SourceVmName `
    -Status `
    -ErrorAction SilentlyContinue

Use a fallback search when required:

if (-not $RecoveredVm) {
    $RecoveredVm = Get-AzVM `
        -ResourceGroupName $RecoveryResourceGroup `
        -Status |
        Where-Object {
            $_.Name -notmatch "-Test$"
        } |
        Select-Object -First 1
}

Validate detection:

if (-not $RecoveredVm) {
    throw "The recovered VM could not be identified."
}

Store the recovered name:

$RecoveredVmName = $RecoveredVm.Name

Display it:

$RecoveredVm |
    Select-Object `
        Name,
        Location,
        PowerState,
        ProvisioningState |
    Format-List

47. Validate the recovered VM network

$RecoveredVmObject = Get-AzVM `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveredVmName

$RecoveredNicId = $RecoveredVmObject.NetworkProfile.NetworkInterfaces[0].Id

$RecoveredNicParts = $RecoveredNicId -split "/"

$RecoveredNicResourceGroup = $RecoveredNicParts[4]
$RecoveredNicName          = $RecoveredNicParts[-1]

$RecoveredNic = Get-AzNetworkInterface `
    -ResourceGroupName $RecoveredNicResourceGroup `
    -Name $RecoveredNicName

Display its configuration:

$RecoveredNic |
    Select-Object `
        Name,
        Location,
        @{
            Name = "PrivateIp"
            Expression = {
                $_.IpConfigurations[0].PrivateIpAddress
            }
        },
        @{
            Name = "SubnetId"
            Expression = {
                $_.IpConfigurations[0].Subnet.Id
            }
        } |
    Format-List

The NIC should reference:

vnet-asr-recovery-neu
snet-workload

48. Attach a public IP to the recovered VM

$RecoveredPublicIp = Add-AsrWorkshopPublicIp `
    -VmResourceGroupName $RecoveryResourceGroup `
    -VmName $RecoveredVmName `
    -PublicIpResourceGroupName $RecoveryResourceGroup `
    -PublicIpName $FailoverPublicIp `
    -Location $RecoveryLocation

Create the URL:

$RecoveredApplicationUrl = "http://$($RecoveredPublicIp.IpAddress):8080"

$RecoveredApplicationUrl

49. Validate the recovered application

$RecoveredApplicationDeadline = (
    Get-Date
).AddMinutes(10)

$RecoveredApplicationResponse = $null

do {
    try {
        $RecoveredApplicationResponse = Invoke-WebRequest `
            -Uri $RecoveredApplicationUrl `
            -TimeoutSec 20
    }
    catch {
        Write-Host `
            "Waiting for the recovered application..."

        Start-Sleep -Seconds 15
    }
}
until (
    $RecoveredApplicationResponse -or
    (Get-Date) -ge $RecoveredApplicationDeadline
)

if (-not $RecoveredApplicationResponse) {
    throw "The recovered application did not become reachable."
}

Validate the page:

if (
    $RecoveredApplicationResponse.Content -notmatch
    "Azure Site Recovery Deep Dive"
) {
    throw "The recovered application content is incorrect."
}

if (
    $RecoveredApplicationResponse.Content -notmatch
    "Data disk"
) {
    throw "The recovered data-disk marker is missing."
}

Write-Host `
    "PASS: The workload is running in North Europe." `
    -ForegroundColor Green

50. Validate the recovered data disk

$RecoveredGuestValidation = Invoke-AzVMRunCommand `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveredVmName `
    -CommandId "RunShellScript" `
    -ScriptString @'
set -e

echo "=== VM identity ==="
hostname

echo
echo "=== Azure Site Recovery application ==="
curl -fsS http://127.0.0.1:8080

echo
echo "=== Data-disk mount ==="
findmnt /srv/asrdata

echo
echo "=== Data-disk marker ==="
cat /srv/asrdata/site-recovery-marker.txt

echo
echo "=== Managed disks ==="
lsblk -f
'@

Display the result:

$RecoveredGuestValidation.Value.Message

51. Commit the failover

Only commit after the recovered workload has been validated.

$CommitFailoverJob = Start-AzRecoveryServicesAsrCommitFailoverJob `
    -ReplicationProtectedItem $ReplicationProtectedItem

Wait for completion:

Wait-AsrJob `
    -Job $CommitFailoverJob `
    -Operation "Commit regional failover" `
    -TimeoutMinutes 120 `
    -PollSeconds 30

A commit accepts the selected recovery point and removes the previous failover recovery-point choices. Reprotection can begin after the failover has been committed.

52. Refresh the post-failover state

$ReplicationProtectedItem = Get-AzRecoveryServicesAsrReplicationProtectedItem `
    -ProtectionContainer $SourceProtectionContainer |
    Where-Object {
        $_.FriendlyName -eq $SourceVmName
    } |
    Select-Object -First 1

Display the state:

$ReplicationProtectedItem |
    Select-Object `
        FriendlyName,
        ProtectionState,
        ReplicationHealth,
        ActiveLocation,
        AllowedOperations |
    Format-List

The active workload is now in North Europe.

53. Failback planning

Failback is not a single operation.

It consists of:

Commit the regional failover
          ↓
Confirm the original region is healthy
          ↓
Confirm the original VM is stopped
          ↓
Use a cache account in the current source region
          ↓
Reprotect from North Europe to West Europe
          ↓
Wait for reverse replication to become healthy
          ↓
Select a reverse recovery point
          ↓
Fail over from North Europe to West Europe
          ↓
Validate the original-region workload
          ↓
Commit the failback
          ↓
Reprotect again toward North Europe

The original source VM must remain stopped before a failback because Site Recovery synchronizes changes from the recovery-region VM back to the source-region disks.

54. Verify the failback prerequisites

Confirm the original VM remains stopped:

Get-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName `
    -Status |
    Select-Object `
        Name,
        Location,
        PowerState

Confirm the recovery-region cache account exists:

$RecoveryCacheStorageAccount = Get-AzStorageAccount `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveryCacheStorageName

$RecoveryCacheStorageAccount |
    Select-Object `
        StorageAccountName,
        Location,
        Kind,
        Sku |
    Format-List

Confirm the reverse container mapping:

$RecoveryToSourceMapping = Get-AzRecoveryServicesAsrProtectionContainerMapping `
    -ProtectionContainer $RecoveryProtectionContainer `
    -Name $RecoveryToSourceMappingName

$RecoveryToSourceMapping |
    Select-Object `
        Name,
        PolicyFriendlyName,
        State |
    Format-List

Confirm the reverse network mapping:

Get-AzRecoveryServicesAsrNetworkMapping `
    -Fabric $RecoveryFabric |
    Where-Object {
        $_.Name -eq $RecoveryToSourceNetworkMappingName
    } |
    Select-Object `
        Name,
        PrimaryNetworkFriendlyName,
        RecoveryNetworkFriendlyName |
    Format-List

55. Optional: start reverse replication

This operation can take as long as the initial replication.

$ReprotectJob = Update-AzRecoveryServicesAsrProtectionDirection `
    -AzureToAzure `
    -ReplicationProtectedItem $ReplicationProtectedItem `
    -ProtectionContainerMapping $RecoveryToSourceMapping `
    -LogStorageAccountId $RecoveryCacheStorageAccount.Id `
    -RecoveryResourceGroupId $SourceResourceGroupObject.ResourceId

Wait for the reprotection job:

Wait-AsrJob `
    -Job $ReprotectJob `
    -Operation "Reprotect North Europe VM toward West Europe" `
    -TimeoutMinutes 240 `
    -PollSeconds 60

56. Optional: wait for reverse replication

After reprotection, refresh the protected item.

$ReverseReplicationDeadline = (
    Get-Date
).AddHours(4)

do {
    $ReplicationProtectedItem = Get-AzRecoveryServicesAsrReplicationProtectedItem `
        -ProtectionContainer $RecoveryProtectionContainer |
        Where-Object {
            $_.FriendlyName -eq $SourceVmName
        } |
        Select-Object -First 1

    if ($ReplicationProtectedItem) {
        Write-Host (
            "{0:u} ProtectionState={1}, Health={2}, ActiveLocation={3}" -f `
                (Get-Date),
                $ReplicationProtectedItem.ProtectionState,
                $ReplicationProtectedItem.ReplicationHealth,
                $ReplicationProtectedItem.ActiveLocation
        )
    }

    $ReverseReplicationReady = (
        $ReplicationProtectedItem -and
        $ReplicationProtectedItem.ProtectionState -eq "Protected" -and
        $ReplicationProtectedItem.ReplicationHealth -eq "Normal"
    )

    if (-not $ReverseReplicationReady) {
        Start-Sleep -Seconds 60
    }
}
until (
    $ReverseReplicationReady -or
    (Get-Date) -ge $ReverseReplicationDeadline
)

if (-not $ReverseReplicationReady) {
    throw "Reverse replication did not reach the protected state."
}

57. Optional: inspect reverse recovery points

$ReverseRecoveryPoints = @(
    Get-AzRecoveryServicesAsrRecoveryPoint `
        -ReplicationProtectedItem $ReplicationProtectedItem |
    Sort-Object RecoveryPointTime
)

$ReverseRecoveryPoints |
    Select-Object `
        RecoveryPointType,
        RecoveryPointTime |
    Format-Table

58. Optional: execute the failback

Stop the recovery-region VM:

Stop-AzVM `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveredVmName `
    -Force

Select the latest reverse recovery point:

$LatestReverseRecoveryPoint = $ReverseRecoveryPoints[-1]

Start the reverse failover:

$FailbackJob = Start-AzRecoveryServicesAsrUnplannedFailoverJob `
    -ReplicationProtectedItem $ReplicationProtectedItem `
    -Direction RecoveryToPrimary `
    -RecoveryPoint $LatestReverseRecoveryPoint

Wait for failback:

Wait-AsrJob `
    -Job $FailbackJob `
    -Operation "Fail back workload to West Europe" `
    -TimeoutMinutes 180 `
    -PollSeconds 30

Validate the source-region VM before committing:

Get-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName `
    -Status |
    Select-Object `
        Name,
        Location,
        PowerState,
        ProvisioningState

Validate the application:

$FailbackResponse = Invoke-WebRequest `
    -Uri $SourceApplicationUrl `
    -TimeoutSec 30

if (
    $FailbackResponse.Content -notmatch
    "Azure Site Recovery Deep Dive"
) {
    throw "The failed-back application did not return the expected content."
}

Write-Host `
    "PASS: The workload is running in West Europe again." `
    -ForegroundColor Green

Commit the failback:

$CommitFailbackJob = Start-AzRecoveryServicesAsrCommitFailoverJob `
    -ReplicationProtectedItem $ReplicationProtectedItem

Wait for the commit:

Wait-AsrJob `
    -Job $CommitFailbackJob `
    -Operation "Commit failback to West Europe" `
    -TimeoutMinutes 120

After the failback commit, reprotect the workload again from West Europe toward North Europe to restore the original disaster recovery posture.

59. Monitor Site Recovery jobs

Display recent Site Recovery jobs:

Get-AzRecoveryServicesAsrJob `
    -StartTime (
        Get-Date
    ).AddDays(-1) |
    Select-Object `
        DisplayName,
        JobType,
        State,
        StateDescription,
        StartTime,
        EndTime,
        TargetObjectName |
    Sort-Object StartTime -Descending |
    Format-Table -AutoSize

Look for operations such as:

Enable protection
Initial replication
Test failover
Test failover cleanup
Failover
Commit
Reprotect

60. Troubleshooting

The Recovery Services vault cannot protect the source VM

Check the regions:

$SourceVm = Get-AzVM `
    -ResourceGroupName $SourceResourceGroup `
    -Name $SourceVmName

$Vault = Get-AzRecoveryServicesVault `
    -ResourceGroupName $VaultResourceGroup `
    -Name $VaultName

[pscustomobject]@{
    SourceVmRegion = $SourceVm.Location
    VaultRegion    = $Vault.Location
}

The vault must not be in the same region as the protected source VM for this regional replication scenario.

Fabric creation fails

Display the Azure location names:

Get-AzLocation |
    Where-Object {
        $_.Location -in @(
            $SourceLocation
            $RecoveryLocation
        )
    } |
    Select-Object `
        Location,
        DisplayName |
    Format-Table

List existing fabrics:

Get-AzRecoveryServicesAsrFabric |
    Select-Object `
        FriendlyName,
        Location,
        FabricType |
    Format-Table

Only one Azure fabric is required for each region inside the vault.

The replication-protected item does not appear

Check the enable-replication job:

Get-AzRecoveryServicesAsrJob `
    -StartTime (
        Get-Date
    ).AddHours(-6) |
    Where-Object {
        $_.JobType -match "Protection|Replication"
    } |
    Select-Object `
        DisplayName,
        JobType,
        State,
        StateDescription,
        Errors |
    Format-List

Check the source container:

Get-AzRecoveryServicesAsrReplicationProtectedItem `
    -ProtectionContainer $SourceProtectionContainer |
    Select-Object `
        FriendlyName,
        ProtectionState,
        ReplicationHealth |
    Format-Table

Initial replication remains in progress

Check replication health:

$ReplicationProtectedItem |
    Select-Object `
        FriendlyName,
        ProtectionState,
        ProtectionStateDescription,
        ReplicationHealth,
        ActiveLocation |
    Format-List

Check the cache account region:

$SourceCacheStorageAccount |
    Select-Object `
        StorageAccountName,
        Location,
        Kind,
        Sku |
    Format-List

The source cache account must be located in the source VM region.

Check the protected disks:

$SourceVm.StorageProfile |
    Format-List

Common causes include:

  • an unsupported disk type;

  • the cache account being in the wrong region;

  • storage-account firewall restrictions;

  • insufficient subscription quota;

  • insufficient target-region compute capacity;

  • an unsupported VM configuration;

  • an Azure Policy blocking replica resources;

  • missing permissions.

No recovery points are available

Refresh the protected item:

$ReplicationProtectedItem = Get-AzRecoveryServicesAsrReplicationProtectedItem `
    -ProtectionContainer $SourceProtectionContainer |
    Where-Object {
        $_.FriendlyName -eq $SourceVmName
    } |
    Select-Object -First 1

Request recovery points:

Get-AzRecoveryServicesAsrRecoveryPoint `
    -ReplicationProtectedItem $ReplicationProtectedItem |
    Select-Object `
        RecoveryPointType,
        RecoveryPointTime |
    Format-Table

Wait until initial replication and recovery-point generation are complete.

The test VM is created in the wrong subnet

Check the test VNet:

Get-AzVirtualNetwork `
    -ResourceGroupName $TestResourceGroup `
    -Name $TestVnetName |
    Select-Object `
        Name,
        AddressSpace,
        Subnets |
    Format-List

The test VNet should contain a subnet named:

snet-workload

Site Recovery attempts to use a subnet with the same name. When no matching subnet exists, it can select the first subnet alphabetically.

The test or recovered VM has no public IP

This is expected.

Run the helper again:

Add-AsrWorkshopPublicIp `
    -VmResourceGroupName $RecoveryResourceGroup `
    -VmName $RecoveredVmName `
    -PublicIpResourceGroupName $RecoveryResourceGroup `
    -PublicIpName $FailoverPublicIp `
    -Location $RecoveryLocation

Check the NIC:

$RecoveredNic |
    Select-Object `
        Name,
        IpConfigurations |
    Format-List

The application is unavailable after failover

Check the VM power state:

Get-AzVM `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveredVmName `
    -Status |
    Select-Object `
        Name,
        PowerState,
        ProvisioningState

Run an internal validation:

Invoke-AzVMRunCommand `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveredVmName `
    -CommandId "RunShellScript" `
    -ScriptString @'
systemctl is-active nginx
ss -lntp | grep ':8080' || true
curl -v http://127.0.0.1:8080
findmnt /srv/asrdata
cat /srv/asrdata/site-recovery-marker.txt
'@ |
    Select-Object -ExpandProperty Value

Check the recovery NSG:

Get-AzNetworkSecurityGroup `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveryNsgName |
    Select-Object `
        Name,
        SecurityRules |
    Format-List

Check:

  • VM power state;

  • subnet placement;

  • NSG rules;

  • Nginx state;

  • data-disk mount;

  • public-IP association;

  • guest operating-system firewall.

The data disk is missing after failover

Inspect the recovered VM:

Get-AzVM `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveredVmName |
    Select-Object -ExpandProperty StorageProfile |
    Format-List

Inspect the guest disks:

Invoke-AzVMRunCommand `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveredVmName `
    -CommandId "RunShellScript" `
    -ScriptString @'
lsblk -f
cat /etc/fstab
findmnt /srv/asrdata || true
'@ |
    Select-Object -ExpandProperty Value

Confirm that the data disk was included in:

$DiskReplicationConfigurations

PowerShell does not automatically include every VM disk unless each disk is represented in the replication configuration.

Test-failover cleanup fails

Check the active test-failover state:

$ReplicationProtectedItem |
    Select-Object `
        FriendlyName,
        TestFailoverState,
        AllowedOperations |
    Format-List

List recent jobs:

Get-AzRecoveryServicesAsrJob `
    -StartTime (
        Get-Date
    ).AddHours(-6) |
    Where-Object {
        $_.JobType -match "Test"
    } |
    Select-Object `
        DisplayName,
        JobType,
        State,
        StateDescription,
        Errors |
    Format-List

Do not manually delete the test VM before attempting Site Recovery cleanup unless the cleanup workflow is already broken.

Regional failover cannot start

Check the protected-item operations:

$ReplicationProtectedItem |
    Select-Object `
        FriendlyName,
        ProtectionState,
        ReplicationHealth,
        AllowedOperations |
    Format-List

Check recovery points:

Get-AzRecoveryServicesAsrRecoveryPoint `
    -ReplicationProtectedItem $ReplicationProtectedItem |
    Select-Object `
        RecoveryPointType,
        RecoveryPointTime |
    Format-Table

Check whether a Site Recovery job is already running:

Get-AzRecoveryServicesAsrJob `
    -StartTime (
        Get-Date
    ).AddHours(-2) |
    Where-Object {
        $_.State -in @(
            "InProgress"
            "NotStarted"
        )
    } |
    Select-Object `
        DisplayName,
        JobType,
        State,
        TargetObjectName |
    Format-Table

Reprotect fails

Confirm that the failover has been committed:

$ReplicationProtectedItem |
    Select-Object `
        FriendlyName,
        ProtectionState,
        AllowedOperations,
        ActiveLocation |
    Format-List

Confirm that the recovery-region cache account exists:

Get-AzStorageAccount `
    -ResourceGroupName $RecoveryResourceGroup `
    -Name $RecoveryCacheStorageName

Confirm the reverse mapping:

Get-AzRecoveryServicesAsrProtectionContainerMapping `
    -ProtectionContainer $RecoveryProtectionContainer `
    -Name $RecoveryToSourceMappingName

Confirm that the original source-region VM is stopped.

61. Production considerations

The workshop protects one Linux VM. A production recovery design requires additional planning.

Application dependencies

Document:

  • databases;

  • identity services;

  • DNS;

  • load balancers;

  • private endpoints;

  • storage dependencies;

  • certificates;

  • secrets;

  • monitoring agents;

  • external integrations.

A VM can start successfully while the application remains unavailable.

Recovery plans

Use recovery plans to:

  • group multiple VMs;

  • define startup order;

  • add manual actions;

  • call automation runbooks;

  • coordinate database and application tiers;

  • document validation gates.

Network recovery

Site Recovery does not automatically reproduce every networking component.

Pre-create and validate:

  • VNets;

  • subnets;

  • NSGs;

  • route tables;

  • firewalls;

  • load balancers;

  • application gateways;

  • private DNS zones;

  • private endpoints;

  • public IP addresses.

IP-address planning

Decide whether recovered workloads require:

  • preserved private IP addresses;

  • new regional private IP addresses;

  • DNS updates;

  • Traffic Manager changes;

  • Front Door origin changes;

  • load-balancer backend updates.

Vault placement

A vault in the recovery region remains manageable during a source-region outage.

A vault in a third region can continue replication during a vault-region outage, but failover and failback management operations are unavailable until the vault region recovers.

Recovery-point objectives

Recovery-point selection affects:

  • possible data loss;

  • recovery time;

  • application consistency;

  • recovery-point availability;

  • operational validation.

Use application-consistent recovery points for workloads that require coordinated in-guest consistency.

DR testing

Run test failovers regularly.

A DR test should validate:

VM startup
Operating-system health
Application startup
Data consistency
Network placement
DNS resolution
Authentication
Monitoring
Security controls
User access
Operational runbooks
Cleanup

Commit discipline

Do not commit a failover until:

  • the VM is running;

  • data has been validated;

  • the application is reachable;

  • dependent services are healthy;

  • security controls are active;

  • the business owner accepts the recovery result.

Commit removes the ability to choose another recovery point from that failover.

Failback planning

Before failback:

  • confirm the source region is stable;

  • confirm quotas and capacity;

  • stop the original source VM;

  • create a cache account in the current source region;

  • verify reverse mappings;

  • complete reprotection;

  • confirm reverse replication health;

  • schedule application downtime;

  • validate the original-region network;

  • prepare a rollback decision.

62. Cleanup

The cleanup section permanently removes the workshop.

First, ensure that no test failover is active.

Set-AzRecoveryServicesAsrVaultContext `
    -Vault $Vault

Refresh the protected item from either container:

$ReplicationProtectedItem = @(
    Get-AzRecoveryServicesAsrReplicationProtectedItem `
        -ProtectionContainer $SourceProtectionContainer `
        -ErrorAction SilentlyContinue

    Get-AzRecoveryServicesAsrReplicationProtectedItem `
        -ProtectionContainer $RecoveryProtectionContainer `
        -ErrorAction SilentlyContinue
) |
    Where-Object {
        $_.FriendlyName -eq $SourceVmName
    } |
    Select-Object -First 1

Disable replication when the protected item still exists:

if ($ReplicationProtectedItem) {
    $DisableReplicationJob = Remove-AzRecoveryServicesAsrReplicationProtectedItem `
        -ReplicationProtectedItem $ReplicationProtectedItem `
        -Force

    Wait-AsrJob `
        -Job $DisableReplicationJob `
        -Operation "Disable Site Recovery replication" `
        -TimeoutMinutes 180 `
        -PollSeconds 30
}

Refresh the vault:

$Vault = Get-AzRecoveryServicesVault `
    -ResourceGroupName $VaultResourceGroup `
    -Name $VaultName `
    -ErrorAction SilentlyContinue

Remove the Site Recovery vault and its lab dependencies:

if ($Vault) {
    Remove-AzRecoveryServicesVault `
        -Vault $Vault `
        -Confirm:$false
}

Require explicit confirmation before deleting the resource groups:

$DeleteConfirmation = Read-Host @"
Type DELETE-ASR-LAB to remove all workshop resource groups.
"@

if ($DeleteConfirmation -ne "DELETE-ASR-LAB") {
    throw "Cleanup cancelled."
}

Delete the source resource group:

Remove-AzResourceGroup `
    -Name $SourceResourceGroup `
    -Force `
    -AsJob

Delete the recovery resource group:

Remove-AzResourceGroup `
    -Name $RecoveryResourceGroup `
    -Force `
    -AsJob

Delete the test resource group:

Remove-AzResourceGroup `
    -Name $TestResourceGroup `
    -Force `
    -AsJob

Delete the vault resource group:

Remove-AzResourceGroup `
    -Name $VaultResourceGroup `
    -Force `
    -AsJob

Monitor the deletions:

do {
    $RemainingGroups = @(
        foreach ($ResourceGroupName in $WorkshopResourceGroups) {
            Get-AzResourceGroup `
                -Name $ResourceGroupName `
                -ErrorAction SilentlyContinue
        }
    )

    Write-Host (
        "{0:u} Remaining resource groups: {1}" -f `
            (Get-Date),
            $RemainingGroups.Count
    )

    if ($RemainingGroups.Count -gt 0) {
        $RemainingGroups |
            Select-Object `
                ResourceGroupName,
                ProvisioningState |
            Format-Table

        Start-Sleep -Seconds 60
    }
}
until ($RemainingGroups.Count -eq 0)

Display completion:

Write-Host `
    "The Azure Site Recovery workshop was deleted." `
    -ForegroundColor Green

Summary

This workshop created a complete Azure-to-Azure Site Recovery configuration:

Source VM in West Europe
          │
          ├── Operating-system disk
          ├── Managed data disk
          ├── Nginx application
          └── Replicated data marker
                    │
                    ▼
          Azure Site Recovery
                    │
          ┌─────────┴─────────┐
          │                   │
          ▼                   ▼
Isolated test failover   Regional failover
North Europe            North Europe
          │                   │
          ▼                   ▼
Temporary test VM       Recovered production VM
          │                   │
          ▼                   ▼
Validation              Validation and commit
          │                   │
          ▼                   ▼
Cleanup                 Reprotect and failback plan

The complete workflow was:

Create two regional networks
          ↓
Create an isolated test network
          ↓
Deploy the source VM
          ↓
Configure the application and data disk
          ↓
Create cache storage
          ↓
Create the Recovery Services vault
          ↓
Create fabrics and protection containers
          ↓
Create forward and reverse mappings
          ↓
Configure OS and data-disk replication
          ↓
Wait for initial replication
          ↓
Run an isolated test failover
          ↓
Validate the application and replicated data
          ↓
Clean up the test failover
          ↓
Stop the source VM
          ↓
Perform regional failover
          ↓
Validate the recovered workload
          ↓
Commit the failover
          ↓
Prepare reprotection and failback
          ↓
Delete the workshop

The central design principles are:

  1. Place the Recovery Services vault outside the source VM region.

  2. Prefer the intended recovery region for the vault.

  3. Use a cache storage account in the current replication-source region.

  4. Configure every managed disk that must be protected.

  5. Pre-create recovery networking and security controls.

  6. Use matching subnet names for predictable test failover.

  7. Keep the test-failover network isolated.

  8. Validate application state, not only VM power state.

  9. Validate data-disk content after recovery.

  10. Run a test failover before a real regional failover.

  11. Stop the source workload before a controlled failover.

  12. Validate the recovered workload before committing.

  13. Treat commit as an irreversible recovery-point decision.

  14. Reprotect before attempting failback.

  15. Keep the original source VM stopped during reverse synchronization.

  16. Test the complete DR procedure regularly.

  17. Delete the billable workshop resources after testing.

 
 
 

Comments


bottom of page