Archiving the Migration: A Production-Grade Blueprint for Moving 10,000 Global Users from On-Premises Citrix to Azure Virtual Desktop

Transitioning an enterprise desktop footprint from on-premises Citrix Virtual Apps and Desktops (CVAD) to Azure Virtual Desktop (AVD) is rarely just a “lift-and-shift.” When operating at a scale of 10,000 global concurrent users spread across APAC, North America, and Europe—with a heavy mix of general knowledge workers and demanding software engineers—success hinges on strict architectural rigor.

This comprehensive guide serves as an end-to-end implementation blueprint for cloud architects, workspace engineers, and infrastructure leads. It covers everything from global topology design and Infrastructure-as-Code (IaC) deployment to profile optimizations, application delivery, and testing frameworks.

1. Persona Mapping: Persistent vs. Non-Persistent VDI

One of the largest cost drivers in cloud VDI is over-provisioning persistent (1:1) virtual machines. To optimize Azure spend without sacrificing user experience, segment your user base into clear operational personas.

+---------------------------------------------------------------------------------------+
|                                10,000 USER DISTRIBUTIONS                              |
+------------------------------------+--------------------------------------------------+
| STANDARD KNOWLEDGE WORKERS (70%)   | DEVELOPERS & POWER USERS (30%)                   |
| 7,000 Users (APAC / US / EU)       | 3,000 Users (APAC / US / EU)                     |
+------------------------------------+--------------------------------------------------+
| Architecture: Non-Persistent       | Architecture: Hybrid Model                       |
| Pool Type: Pooled Multi-Session    | Pool Type: 2,500 Pooled / 500 Personal           |
| Delivery: MSI-X / App Attach / FSLogix| Delivery: Local Dev Tools / Dev Box Integration   |
+------------------------------------+--------------------------------------------------+

Architectural Breakdown by Persona

PersonaUser CountVDI ModelRecommended Azure VM SizeStorage & Disk Architecture
Standard Knowledge Workers7,000Non-Persistent (Pooled)Standard_D8ds_v5 (Windows 11 Multi-Session, 4–6 users/VM)Ephemeral OS Disk + FSLogix Profile Containers
Standard Software Engineers2,500Non-Persistent (Pooled)Standard_E8ds_v5 or E16ds_v5 (High RAM/CPU ratio, 2–3 users/VM)Ephemeral OS Disk + FSLogix + Dedicated Ephemeral Scratch Disk (D:\Scratch)
Heavy / Kernel Developers500Persistent (Personal 1:1)Standard_NV16as_v5 (GPU) or Standard_D16s_v5Premium SSD v2 (P15/P20) for local persistent development setups

Decision Drivers for Software Engineers

  • Pooled Non-Persistent (2,500 Developers): Developers utilizing cloud-based IDEs, web runtimes, containerized workflows in remote environments, or standard VS Code setups that fit into a standardized image with local profile redirections.
  • Persistent Personal (500 Developers): Engineers requiring custom IIS bindings, local hypervisors (nested virtualization), specific kernel-level drivers, or administrative rights to install local SDKs.

2. Global Architecture & Regional Topology

To maintain sub-100ms round-trip time (RTT) for interactive developer tooling and real-time audio/video traffic, host pools and user data planes must be co-located near end-users.

                          +---------------------------------------+
                          |   CENTRAL HUB (Management / CI/CD)    |
                          |  Azure DevOps / Image Gallery / Entra |
                          +-------------------+-------------------+
                                              |
      +---------------------------------------+---------------------------------------+
      |                                       |                                       |
+-----v-----------------+               +-----v-----------------+               +-----v-----------------+
|   AMERICAS REGION     |               |    EUROPE REGION      |               |     APAC REGION       |
| Primary: East US 2    |               | Primary: West Europe  |               | Primary: Southeast Asia|
| Secondary: Central US |               | Secondary: North EU   |               | Secondary: East Asia  |
+-----------------------+               +-----------------------+               +-----------------------+
| • Host Pools (Pooled) |               | • Host Pools (Pooled) |               | • Host Pools (Pooled) |
| • ANF SMB Shares      |               | • ANF SMB Shares      |               | • ANF SMB Shares      |
| • ExpressRoute / UDP  |               | • ExpressRoute / UDP  |               | • ExpressRoute / UDP  |
+-----------------------+               +-----------------------+               +-----------------------+

Core Infrastructure Foundations

  • Identity Infrastructure: Microsoft Entra ID-Joined session hosts (Cloud Native) leveraging Entra Connect Cloud Kerberos Trust for frictionless authentication to local SMB storage shares.
  • Network Connectivity: Dedicated Azure ExpressRoute circuits with direct peering across primary landing zones.
  • Protocol Optimization: RDP Shortpath enabled (UDP-based direct transport) across both private ExpressRoute and public network paths to bypass gateway relaying and reduce input latency.

3. Infrastructure-as-Code Deployment

Automating the landing zone deployment using Infrastructure-as-Code (IaC) ensures operational consistency across global deployment regions.

Option A: Bicep Deployment (Azure Verified Modules)

Code snippet

// Production AVD Landing Zone with Azure NetApp Files using AVM Modules
targetScope = 'subscription'

param location string = 'eastus2'
param environment string = 'prod'
param rgName string = 'rg-avd-${environment}-${location}-001'

resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: rgName
  location: location
}

// 1. Virtual Network & Subnets (Host Pool + NetApp Delegated Subnet)
module vnet 'br/public:avm/res/network/virtual-network:0.1.8' = {
  scope: rg
  name: 'vnet-deployment'
  params: {
    name: 'vnet-avd-${location}-001'
    addressPrefixes: ['10.100.0.0/16']
    subnets: [
      {
        name: 'snet-avd-hosts'
        addressPrefix: '10.100.1.0/24'
      }
      {
        name: 'snet-anf-delegated'
        addressPrefix: '10.100.2.0/28'
        delegation: 'Microsoft.NetApp/volumes'
      }
    ]
  }
}

// 2. Azure NetApp Files Account & Capacity Pool
module anfAccount 'br/public:avm/res/net-app/net-app-account:0.1.3' = {
  scope: rg
  name: 'anf-deployment'
  params: {
    name: 'anf-avd-${location}-001'
    location: location
    capacityPools: [
      {
        name: 'pool-avd-profiles'
        serviceLevel: 'Premium'
        size: 4398046511104 // 4 TB Capacity
        volumes: [
          {
            name: 'vol-fslogix-profiles'
            creationToken: 'vol-fslogix-profiles'
            serviceLevel: 'Premium'
            usageThreshold: 4398046511104
            subnetResourceId: vnet.outputs.subnetResourceIds[1]
            protocolTypes: ['CIFS']
            securityStyle: 'Ntfs'
          }
        ]
      }
    ]
  }
}

// 3. AVD Host Pool (Pooled / Breadth-First Load Balancing)
module hostPool 'br/public:avm/res/desktop-virtualization/host-pool:0.1.2' = {
  scope: rg
  name: 'hostpool-deployment'
  params: {
    name: 'hp-avd-standard-${location}-001'
    location: location
    hostPoolType: 'Pooled'
    loadBalancerType: 'BreadthFirst'
    preferredAppGroupType: 'Desktop'
    validationEnvironment: false
    customRdpProperty: 'drivestoredirect:s:0;audiomode:i:0;videoplaybackmode:i:1;redirectclipboard:i:1;enablecredsspsupport:i:1;enablerdpshortpath:i:1;'
  }
}

// 4. AVD Workspace & App Group Link
module workspace 'br/public:avm/res/desktop-virtualization/workspace:0.1.2' = {
  scope: rg
  name: 'workspace-deployment'
  params: {
    name: 'ws-avd-global-001'
    location: location
    applicationGroupReferences: [
      hostPool.outputs.appGroupResourceId
    ]
  }
}

Option B: Terraform Deployment

Terraform

terraform {
  required_version = ">= 1.7.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.100"
    }
  }
}

provider "azurerm" {
  features {}
}

variable "location" {
  default = "eastus2"
}

resource "azurerm_resource_group" "rg" {
  name     = "rg-avd-prod-${var.location}-001"
  location = var.location
}

# Network Infrastructure
resource "azurerm_virtual_network" "vnet" {
  name                = "vnet-avd-${var.location}-001"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = ["10.100.0.0/16"]
}

resource "azurerm_subnet" "host_subnet" {
  name                 = "snet-avd-hosts"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.100.1.0/24"]
}

resource "azurerm_subnet" "anf_subnet" {
  name                 = "snet-anf-delegated"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.100.2.0/28"]

  delegation {
    name = "netapp_delegation"
    service_delegation {
      name    = "Microsoft.NetApp/volumes"
      actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
    }
  }
}

# Storage Account Engine (Azure NetApp Files)
resource "azurerm_netapp_account" "anf" {
  name                = "anf-avd-${var.location}-001"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

resource "azurerm_netapp_pool" "anf_pool" {
  name                = "pool-avd-profiles"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  account_name        = azurerm_netapp_account.anf.name
  service_level       = "Premium"
  size_in_tb          = 4
}

resource "azurerm_netapp_volume" "anf_vol" {
  name                = "vol-fslogix-profiles"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  account_name        = azurerm_netapp_account.anf.name
  pool_name           = azurerm_netapp_pool.anf_pool.name
  volume_path         = "vol-fslogix-profiles"
  service_level       = "Premium"
  subnet_id           = azurerm_subnet.anf_subnet.id
  storage_quota_in_gb = 4096
  protocols           = ["CIFS"]
  security_style      = "Ntfs"
}

# Azure Virtual Desktop Host Pool
resource "azurerm_virtual_desktop_host_pool" "hp" {
  name                     = "hp-avd-standard-${var.location}-001"
  location                 = azurerm_resource_group.rg.location
  resource_group_name      = azurerm_resource_group.rg.name
  type                     = "Pooled"
  load_balancer_type       = "BreadthFirst"
  preferred_app_group_type = "Desktop"
  custom_rdp_properties   = "drivestoredirect:s:0;audiomode:i:0;videoplaybackmode:i:1;redirectclipboard:i:1;enablerdpshortpath:i:1;"
}

4. Profile Management & FSLogix Optimization

Sustaining fast logon times across 9,500 non-persistent users requires high-throughput storage combined with profile redirection rules.

Production PowerShell FSLogix Configuration Script

Execute this script across all session host templates via Intune or GPO:

PowerShell

# Automated FSLogix Registry Configuration
$RegistryPath = "HKLM:\SOFTWARE\FSLogix\Profiles"

If (-not (Test-Path $RegistryPath)) {
    New-Item -Path $RegistryPath -Force | Out-Null
}

# Core Settings
Set-ItemProperty -Path $RegistryPath -Name "Enabled" -Value 1 -Type DWord
Set-ItemProperty -Path $RegistryPath -Name "VHDLocations" -Value "\\anf-avd-eastus2-001.contoso.internal\vol-fslogix-profiles" -Type MultiString
Set-ItemProperty -Path $RegistryPath -Name "VolumeType" -Value "vhdx" -Type String
Set-ItemProperty -Path $RegistryPath -Name "SizeInMB" -Value 30720 -Type DWord # 30 GB
Set-ItemProperty -Path $RegistryPath -Name "DynamicSizeInMB" -Value 1 -Type DWord
Set-ItemProperty -Path $RegistryPath -Name "DeleteLocalProfileWhenFSLogixProfileShouldApply" -Value 1 -Type DWord
Set-ItemProperty -Path $RegistryPath -Name "FlipDirectoryNameUrl" -Value 1 -Type DWord
Set-ItemProperty -Path $RegistryPath -Name "RoamSearch" -Value 1 -Type DWord
Set-ItemProperty -Path $RegistryPath -Name "LockedRetryCount" -Value 12 -Type DWord
Set-ItemProperty -Path $RegistryPath -Name "LockedRetryInterval" -Value 15 -Type DWord

# Optimization & AppX Settings
Set-ItemProperty -Path $RegistryPath -Name "InstallAppXPackages" -Value 1 -Type DWord

Production Profile Exclusions (redirections.xml)

Place this file on the ANF root share (\\anf-avd-eastus2-001.contoso.internal\vol-fslogix-profiles\redirections.xml) to strip ephemeral build caches (node_modules, .nuget, .m2) out of roaming profile syncs:

XML

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
  <CleanUp>
    <!-- System Caches -->
    <Folder>AppData\Local\Temp</Folder>
    <Folder>AppData\Local\Microsoft\Windows\INetCache</Folder>
    <Folder>AppData\Local\CrashDumps</Folder>
    <Folder>AppData\Local\Package Cache</Folder>
    <Folder>AppData\Local\Microsoft\Terminal Server Client\Cache</Folder>
    
    <!-- Developer Caches & Build Artifacts -->
    <Folder>AppData\Local\npm-cache</Folder>
    <Folder>AppData\Local\NuGet\v3-cache</Folder>
    <Folder>AppData\Local\pip\cache</Folder>
    <Folder>AppData\Local\Yarn\Cache</Folder>
    <Folder>AppData\Local\JetBrains\Transient</Folder>
    <Folder>.m2\repository</Folder>
    <Folder>.gradle\caches</Folder>
  </CleanUp>
  <Includes>
    <Folder>AppData\Roaming\Git</Folder>
    <Folder>AppData\Roaming\Code\User</Folder>
  </Includes>
</Configuration>

5. Application Delivery Framework

Avoid image fragmentation by decoupling the application layer from the base OS image using a tiered delivery matrix.

+------------------------------------------------------------------------------------+
|                         APPLICATION DELIVERY TAXONOMY                              |
+------------------------------------+-----------------------------------------------+
| APPLICATION CATEGORY               | DELIVERY MECHANISM                            |
+------------------------------------+-----------------------------------------------+
| Core Enterprise (Office 365, Edge) | Baked into Master Golden Image (packer/CI-CD) |
| Core Heavy Apps (Visual Studio)    | Baked into Master Image / Azure Compute Gal.  |
| Departmental / User-Specific Apps  | MSI-X App Attach / Azure Virtual Workspace    |
| On-Demand / Legacy Apps            | Microsoft App-V or Cloudpaging / Citrix App   |
+------------------------------------+-----------------------------------------------+

Dynamic Entitlement & Ephemeral Scratch Strategy

  1. MSI-X App Attach: Package targeted applications (e.g., Postman, DBeaver, Power BI, SAP GUI) into .cim packages mounted dynamically during login based on Entra ID Group memberships.
  2. AppLocker Enforcement: Block non-authorized users from launching specialized software inside shared multi-session environments.
  3. Secondary Ephemeral Scratch Disk: For non-persistent developer hosts, map compilation paths (TEMP, TMP, .m2) to an attached secondary temporary scratch disk (D:\Scratch) on the underlying Azure host VM to achieve maximum IOPS without inflating FSLogix profile sizes.

6. End-to-End Validation & Migration Execution

Three-Gate Validation Framework

+---------------------------------------------------------------------------------------+
|                                  THREE-GATE TEST PLAN                                 |
+---------------------------------------------------------------------------------------+
| GATE 1: FUNCTIONAL & REGRESSION TESTING                                               |
| • Verify image stability, FSLogix profile attach/detach without locks.                |
| • App verification: Visual Studio builds, DB connections, peripheral passthrough.     |
+---------------------------------------------------------------------------------------+
                                           |
+---------------------------------------------------------------------------------------+
| GATE 2: PERFORMANCE & LOAD TESTING (Login VSI / AvdBench)                             |
| • Simulate 1,500 concurrent logons in 15 mins ("Logon Storm") per region.              |
| • Target Metrics: Average Logon Time < 25 seconds; ANF Latency < 2ms; CPU < 85%.       |
+---------------------------------------------------------------------------------------+
                                           |
+---------------------------------------------------------------------------------------+
| GATE 3: USER ACCEPTANCE TESTING (UAT)                                                 |
| • Pilot group of 500 users (150 Developers + 350 Standard Users) across US, EU, APAC.  |
| • Latency verification: Ensure RDP Shortpath provides < 80ms RTT for global users.    |
+---------------------------------------------------------------------------------------+

Production SLA Benchmarks

Metric / KPITarget SLAMonitoring Tool
Logon Duration< 25 Seconds (Cold Login)Azure Log Analytics / AVD Insights
FSLogix Storage Latency< 2ms Read/WriteAzure NetApp Files Metrics
Network Latency (RTT)< 80ms (Intra-region)RDP Shortpath Statistics (qwinsta)
CPU / Memory HeadroomPeak utilization < 85%Azure Autoscale / Scaling Plans

Phased Migration Timeline

Execute user cutover across 4 bi-weekly waves:

+---------------------------------------------------------------------------------------+
|                                   ROLLOUT SCHEDULE                                    |
+---------------------------------------------------------------------------------------+
| WAVE 1 (Week 13): 2,500 Standard Users — APAC Region                                  |
| WAVE 2 (Week 15): 2,500 Standard Users — Europe & Americas Regions                    |
| WAVE 3 (Week 17): 2,500 Developers — Global (Pooled Non-Persistent)                   |
| WAVE 4 (Week 19): 2,500 Heavy Developers & Specialized Users (Persistent / GPU)      |
+---------------------------------------------------------------------------------------+

Coexistence & Fallback Strategy

During the migration window, publish AVD workspace resources directly into the existing Citrix Workspace Portal via the Citrix Virtual Apps and Desktops Service Azure Connector. Convert existing Citrix UPM profiles into VHDX containers using the FSLogix Migration Engine (frx.exe) prior to each scheduled wave cutover.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top