Azure Virtual Desktop dans une Landing Zone régulée : les pièges qu'aucune doc ne te dit
Retour d'expérience sur un déploiement AVD privé en Landing Zone Azure : split de région control-plane, PE global pour rdweb, Entra Kerberos, auth RDP Entra-joined, RBAC du scaling plan et coût FSLogix. Les learnings non-évidents, pas les RTFM.
TL;DR. Déployer Azure Virtual Desktop 100 % privé dans une Landing Zone (Private Endpoints partout, NVA Palo Alto en sortie, DNS centralisé) marche très bien — une fois qu’on connaît une dizaine de pièges qui ne sont écrits nulle part clairement. Le control plane vit forcément dans une autre région que les session hosts.
rdweb.wvd.microsoft.comne résout pas sans un Private Endpointglobaldédié. Entra Kerberos a une race condition au premier apply. Les session hosts Entra-joined refusent l’auth RDP legacy en silence. Et 70 % du coût d’un POC, c’est le storage Premium FSLogix, pas le compute.
Publié le 20 juin 2026.
Stack : Terraform · Terragrunt · azurerm ~> 4.0 · azapi · Windows 11 25H2 multi-session (M365 Apps) · FSLogix + Azure Files Premium ZRS (Entra Kerberos) · régions germanywestcentral (data plane) + westeurope (control plane).
Le contexte
AVD déployé en spoke d’une Landing Zone Azure, pour des postes virtuels conformes :
- Control plane (host pool, workspaces, application group, scaling plan) en
westeurope. - Data plane (session hosts Win11 multi-session, FSLogix, Key Vault) en
germanywestcentral. - Zéro endpoint public : Private Endpoints sur le workspace (feed + global), le host pool (connection), Azure Files (FSLogix) et le Key Vault.
- Sortie Internet des session hosts via UDR
0.0.0.0/0→ NVA Palo Alto. Zones Private DNS centralisées dans le hub, wirées par les policies ALZ DINE. - Profils utilisateurs sur FSLogix (conteneurs VHDX) avec authentification Entra Kerberos sur Azure Files Premium ZRS.
Ce qui suit n’est pas un tutoriel. Ce sont les learnings non-évidents — ceux qui font perdre des heures et qu’on ne trouve pas en cherchant l’erreur sur le moteur de recherche.
Réseau & control plane
Le control plane vit forcément dans une autre région que les session hosts
Pour un déploiement en germanywestcentral, le control plane AVD (host pool, workspace, app group, scaling plan) doit vivre dans westeurope — la région supportée la plus proche. Les session hosts, FSLogix et les PEs restent en GWC.
Ce n’est pas un compromis de performance : le control plane n’est que de la metadata. Le data plane (sessions RDP, profils) ne traverse jamais la frontière régionale. Mais ça oblige à :
- Splitter la naming (
-gwc-vs-weu-) selon le composant. - Surcharger
location+region_codedans les units Terragrunt des composants control plane. - Assumer qu’un resource group héberge des ressources WEU dans une subscription déclarée « primaire » en GWC.
À retenir : une subscription n’a pas de région primaire — chaque resource group a la sienne. Contre-intuitif, mais parfaitement valide côté Azure.
rdweb.wvd.microsoft.com ne résout pas sans le PE global
C’est le piège n°1 qui casse l’UX final. Tout le reste est OK (workspace, host pool, RBAC users), mais le client Windows App affiche « No devices or apps found » parce que le initial feed discovery échoue.
Le Private DNS Resolver du hub résout les zones privatelink.* et forwarde le reste. Mais avec une NRPT VPN qui catch-all . vers le DNS proxy du Palo, les requêtes rdweb.wvd.microsoft.com (un record public Microsoft) peuvent être bloquées ou non-forwardées selon la config du firewall.
La solution est architecturale : déployer un Private Endpoint subresource global sur un workspace dédié (interdit d’y attacher des application groups, sinon on casse tout le tenant). Microsoft fait alors le CNAME rdweb.wvd.microsoft.com → record privé dans privatelink-global.wvd.microsoft.com. Tout reste privé.
Attention : ce workspace global est un singleton à l’échelle du tenant — un seul PE global pour toute la flotte AVD. S’il existe d’autres déploiements AVD ailleurs dans le tenant, il faut coordonner.
Les policies DINE ne couvrent que les subresources standards
Les policies ALZ Deploy-Private-DNS-* auto-wirent les subresources feed et connection dans la zone privatelink.wvd.microsoft.com (un groupe deployedByPolicy apparaît tout seul sur le PE).
Mais le subresource global (zone privatelink-global.wvd.microsoft.com, plus récente côté Microsoft) n’a pas de policy équivalente assignée. Le PE est bien créé, mais aucun zone group → records absents → résolution impossible.
À chaque nouveau type de PE introduit, vérifier que la DINE existe :
az network private-endpoint dns-zone-group list \
--resource-group <rg> --endpoint-name <pe> -o table
Si la liste est vide après quelques minutes, wirer le zone group manuellement.
Le scaling plan a besoin du scope subscription
Erreur « unable to access host pool » à la création du scaling plan, alors que le rôle Desktop Virtualization Power On Off Contributor était bien assigné sur le resource group des VMs. La logique naïve — « le rôle est sur les VMs, ça suffit pour les start/stop » — est fausse.
Le service principal AVD doit lire le host pool (en WEU, dans un autre RG) pour orchestrer le start/stop des VMs (en GWC). Comme les deux sont dans des resource groups différents, le scope minimal viable est la subscription.
À retenir pour Autoscale : scope subscription, pas resource group. C’est dans la doc Microsoft, mais facile à louper quand on optimise pour le least-privilege.
Identité & authentification
Entra Kerberos : une race condition au premier apply
Symptôme : NotFound: The resource '.../applications/<guid>' does not exist à la création d’un storage account avec directory_type = "AADKERB".
Aucun objet Entra correspondant n’existe (vérifié via az ad app/sp list). C’est le backend Azure qui garde une référence vers une application Graph censée avoir été créée par un apply précédent ayant échoué — une référence fantôme, ni dans Entra ni dans aucune ressource visible. Aucun cleanup possible côté utilisateur.
Workaround : créer le storage account sans AADKERB (en commentant le bloc), puis un 2ᵉ apply qui ajoute le bloc → Azure réinitialise proprement et crée un service principal frais.
À retenir : pour les services qui auto-créent des objets Entra (Storage AADKERB, Managed HSM…), un premier apply qui échoue à mi-course peut laisser des références fantômes. Le pattern 2-phase est la solution générique.
Le token DSC passe par PrivateSettingsRef, pas en clair
Naïvement, on met le registrationInfoToken dans protected_settings.properties. Erreur : « duplicate arguments ». Si on le retire du bloc public, autre erreur.
La vérité : le script DSC de Microsoft attend un pscredential, pas une string. Il faut ce pattern :
settings.properties.registrationInfoTokenCredential = {
UserName = "PLACEHOLDER_DO_NOT_USE"
Password = "PrivateSettingsRef:RegistrationInfoToken"
}
protected_settings.Items.RegistrationInfoToken = <token>
Les exemples ARM de Microsoft l’utilisent partout, mais aucune doc Terraform ne montre ce pattern clairement. La référence PrivateSettingsRef:KEY côté public + Items.KEY côté protected est une convention DSC héritée de WMF — vraiment pas intuitive.
Les session hosts Entra-joined refusent silencieusement l’auth legacy RDP
Le piège qui te fait perdre deux heures après que tout le reste fonctionne. RBAC OK, feed visible, clic « Connect » → boucle « Your credentials did not work » même avec le bon mot de passe.
Le client Windows App envoie une auth legacy NTLM par défaut. Le session host Entra-joined la refuse — et le message d’erreur est trompeur (il suggère un mauvais mot de passe, pas un mismatch d’authentification).
Fix : des custom RDP properties sur le host pool :
targetisaadjoined:i:1;enablerdsaadauth:i:1
targetisaadjoined force le client à passer en flow WAM (Web Account Manager, le popup Microsoft moderne). enablerdsaadauth active le SSO Entra si le device client est lui aussi Entra-joined. Aucun warning, aucune doc évidente.
Terraform & exploitation
Le ignore_changes du module PE masque le wire manuel
Le module PrivateEndpoint ignore les changements sur private_dns_zone_group — intention louable : laisser ALZ DINE gérer sans que Terraform ne revert.
Mais quand la DINE ne wire pas (cas du subresource global) et qu’on rajoute le bloc en config Terragrunt, Terraform ne détecte aucune diff (ignore_changes l’aveugle). Le plan dit « no changes » alors qu’il manque clairement quelque chose.
Workarounds :
terragrunt apply -replace='module.pe.azurerm_private_endpoint.this["xxx"]'→ destroy/recreate avec le nouveau bloc (mais le PE perd son IP au passage).- Fallback
az network private-endpoint dns-zone-group create(one-shot ;ignore_changesne touchera plus rien après).
Un module mieux conçu aurait un ignore_changes conditionnel : ignorer si le bloc est absent en config, gérer s’il est présent. Mais Terraform ne supporte pas ignore_changes dynamique — impossible nativement.
Les droits data-plane d’un Key Vault ne suivent pas le control-plane
On peut être Owner sur la subscription (donc full RBAC control plane sur les Key Vaults) et quand même se prendre Forbidden: getSecret/setSecret sur les opérations data plane. Le premier apply qui tente d’écrire un secret échoue.
Owner sur la sub donne des droits administratifs sur les KV (delete, lock, modifier les policies) mais pas la lecture/écriture du contenu chiffré. C’est le design Zero Trust de Key Vault, mais il surprend. Le module KeyVaultStack expose un toggle assign_rbac_to_current_user qui assigne Key Vault Administrator (data plane) au déployeur — à activer pour le bootstrap.
Key Vault récupéré du soft-delete : ne jamais supprimer une PE connection à la main
Un KV avec purge_protection_enabled = true n’est jamais recréé à neuf : le provider azurerm le récupère depuis le soft-delete. Le KV récupéré ramène ses anciens enregistrements de Private Endpoint connection — d’où plusieurs entrées (psc-…-001, …-001.2, …-001.3) en Rejected/Approved pour un seul PE déployé.
Le piège : on ne peut pas identifier la connexion vivante par son nom ou son suffixe. Toutes les entrées pointent vers le même privateEndpoint.id (le PE a été recréé avec le même nom). Supposer que « la plus récente = la vivante » est faux. Si on supprime la mauvaise, le PE passe Disconnected, perd son IP, et Azure ne sait pas reconnecter un PE déconnecté.
Cleanup fiable = recréer le PE, pas trier les connexions :
terragrunt apply -replace='module.pe.azurerm_private_endpoint.this["this"]' --working-dir kv-avd
Le -replace détruit le PE et sa connexion morte, en recrée un frais auto-approuvé ; les orphelines restantes deviennent inoffensives. À retenir : sur un KV soft-delete-recovered, les PE connections orphelines sont cosmétiques — on les laisse, ou on recrée le PE. Jamais de delete manuel ciblé.
Note Windows.
terragrunt apply -replace='res.this["key"]'voit ses quotes strippées par PowerShell 5.1 → « Invalid force-replace address ». Utiliser le sigil stop-parsing--%(terragrunt --% apply -replace='…') ou passer l’adresse via une variable. Sinon, lancer depuis bash.
Coût
70 % du coût d’un POC AVD, c’est le storage Premium FSLogix
Ordre de grandeur pour 1 session host D4s_v5 avec Autoscale en semaine :
| Poste | Coût mensuel |
|---|---|
| Compute (session host) | ~50 € |
| OS disk | ~17 € |
| Azure Files Premium ZRS 500 GiB | ~104 € |
| Private Endpoints | ~32 € |
Azure Files Premium est provisionné — on paie le quota, pas l’usage. Provisionner 500 GiB pour zéro utilisateur actif, c’est 104 € jetés. La règle pour un POC : démarrer au minimum Premium (100 GiB) et monter plus tard.
À retenir : le coût d’un POC AVD se pilote d’abord par le quota FSLogix, pas par les VMs.
Méta-learning : déployer dans l’ordre de l’oignon
L’ordre des phases n’est pas arbitraire — chaque couche dépend fonctionnellement de la précédente :
- Réseau + identité storage — sans ça, rien ne tourne.
- Storage data avec auth — FSLogix prêt à recevoir des connexions.
- Control plane AVD — les services Azure-managed.
- Private Endpoints — la vraie connectivité privée.
- Compute + RBAC + scaling — les session hosts, qui consomment toutes les couches précédentes.
Sauter une étape donne une session host qui boote mais refuse de s’enregistrer au host pool (PE manquant), ou qui s’enregistre mais ne peut pas monter FSLogix (permission storage manquante). Le debug devient pénible parce que les erreurs sont à plusieurs niveaux à la fois.
À retenir : déployer dans l’ordre de l’oignon, vérifier chaque couche avant de passer à la suivante.
Aucun de ces points n’est un secret — ils sont tous quelque part dans la doc Microsoft, éparpillés. Mais les rassembler coûte un POC entier. Si tu déploies AVD en Landing Zone privée, tu viens d’économiser quelques journées.