Skip to content
~ 6 min readFR

Quand terraform plan dit « no changes » et que la feature est cassée quand même

Deux automatismes Azure bien intentionnés — le wiring DNS piloté par policy et la suppression de drift via ignore_changes — se combinent en une panne silencieuse qu'aucun plan ne voit. Une war story de private endpoint AVD, et la leçon générale.

terraformazurelanding-zoneavdprivate-endpointiac

Tout était vert. Workspace déployé, host pool up, users assignés, RBAC propre. Puis un utilisateur ouvre Windows App et tombe sur : « No devices or apps found ». Le feed ne charge jamais.

Rien dans le portail Azure n’a l’air anormal. Et terraform plan affiche, avec une confiance totale, no changes. C’est cette dernière partie qui est dangereuse — parce que la feature est cassée, et le plan te dit qu’elle ne l’est pas.

C’est l’histoire de deux mécanismes de sécurité, chacun correct isolément, qui se combinent en une panne qu’aucun plan ne ferait jamais remonter. Si tu fais tourner Azure Virtual Desktop derrière des private endpoints dans une landing zone, tu vas la rencontrer. La leçon, elle, dépasse largement AVD.

Le symptôme : un feed qui refuse de découvrir

La feed discovery initiale d’AVD résout rdweb.wvd.microsoft.com. Dans un setup full-private — DNS forcé via le resolver du hub, une règle catch-all qui envoie tout le reste vers un proxy DNS firewall — ce record public Microsoft peut échouer à résoudre ou se faire bloquer en sortie. Le client n’a pas de feed, donc il n’affiche rien. L’erreur accuse les apps de l’utilisateur ; la cause, c’est la résolution de nom.

Le fix supporté est architectural : déployer un private endpoint pour le subresource AVD global sur un workspace dédié. Microsoft fait alors le CNAME de rdweb.wvd.microsoft.com vers un record privé dans privatelink-global.wvd.microsoft.com, et la discovery reste à l’intérieur du réseau. Propre. Donc tu ajoutes le private endpoint et tu t’attends à du vert.

Ce n’est pas vert. Et c’est ici que les deux automatismes commencent à jouer contre toi.

Automatisme #1 : la policy qui wire le DNS — sauf celui-là

Les landing zones d’entreprise s’appuient sur des policies DeployIfNotExists pour auto-wirer le DNS privé. Tu poses un private endpoint, et les assignments Deploy-Private-DNS-* créent le zone group à ta place — les records apparaissent tout seuls, sans Terraform. Pour les subresources standards (les endpoints feed et connection), ça marche parfaitement. Tu arrêtes d’y penser. C’est tout l’intérêt de la policy.

Mais le subresource global vit dans une zone plus récente, privatelink-global.wvd.microsoft.com, et le set de policies assigné dans l’environnement n’a pas d’assignment équivalent pour elle. Donc le private endpoint est créé, et ensuite… rien. Pas de zone group. Pas de records. La chose que tu as déployée précisément pour régler la résolution DNS n’a pas de DNS.

Le piège, ce n’est pas que la policy manque. Le piège, c’est que chaque autre private endpoint t’a appris à supposer que la plateforme s’en occupe. Le trou est invisible justement parce que l’automatisme autour est fiable.

À chaque fois que tu introduis un nouveau type de private endpoint, vérifie que la policy DINE le couvre réellement. az network private-endpoint dns-zone-group list sur le nouvel endpoint. Si c’est vide après quelques minutes, la plateforme ne va pas le wirer — c’est toi.

Automatisme #2 : le ignore_changes qui masque ton fix

Bon — la policy ne le wire pas, donc tu le wires en code. Tu ajoutes le bloc private_dns_zone_group dans la config Terragrunt de l’endpoint, tu lances un plan, et tu te prépares au diff.

No changes.

Le module private endpoint ignore les changes sur private_dns_zone_group. L’intention derrière est bonne — ça laisse la policy de la plateforme posséder le zone group sans que Terraform se batte avec elle à chaque apply en revertant des records gérés par la plateforme. Dans le cas courant où la policy wire effectivement le DNS, ignore_changes est exactement le bon choix : il évite que l’IaC et la policy ne se tirent dessus.

Mais ignore_changes est inconditionnel. Il ne sait pas distinguer « la plateforme possède ça, n’y touche pas » de « la plateforme a laissé un trou et j’essaie de le combler ». Donc quand tu ajoutes le bloc à la main, Terraform est aveugle dessus. Le plan est honnête vis-à-vis de son propre modèle du monde — sauf qu’on a dit à ce modèle de détourner le regard du seul champ qui compte.

Deux automatismes, tous les deux défendables. Le job de la policy : wirer le DNS pour que tu n’aies pas à le faire. Le job du module : ne pas se battre avec la policy. Sur le seul subresource que la policy ne couvre pas, ces deux jobs laissent un trou et étouffent tout signal que ce trou existe.

Pourquoi cette classe de bug est pire qu’un crash

Un apply qui échoue, c’est une bonne journée : bruyant, une stack trace, le pipeline s’arrête. Tu corriges et tu avances.

Là, c’est l’inverse. L’apply réussit, le plan est propre, le portail est vert. Tous les dashboards auxquels tu fais confiance s’accordent à dire que le système est sain — et la feature est morte. La seule chose qui connaît la vérité, c’est un utilisateur qui clique sur Connect.

La vraie leçon n’a rien à voir avec AVD : les pannes les plus coûteuses sont celles que ton outillage rapporte comme un succès. ignore_changes, DeployIfNotExists, toute abstraction qui étouffe le bruit du chemin courant achète ce silence en dépensant ta capacité à voir le chemin non-courant. Excellent compromis la plupart du temps. Le jour où ça ne l’est pas, tu débugues à l’aveugle — entraîné, toi et tes outils, à ignorer pile l’endroit où vit le problème.

S’en sortir

Puisque ignore_changes aveugle l’apply normal, tu forces le change hors-bande :

terragrunt apply -replace='module.pe.azurerm_private_endpoint.this["this"]'

Le -replace détruit et recrée l’endpoint avec le zone group en place dès la naissance, donc ignore_changes n’a jamais l’occasion de le masquer. Tu perds l’IP de l’endpoint dans l’échange — acceptable ici, mais à savoir avant de le lancer sur quelque chose où l’IP est figée. L’alternative one-shot : créer le zone group directement via az et laisser ignore_changes ne plus y toucher ensuite.

Un module mieux conçu rendrait ignore_changes conditionnel — gérer le zone group quand le bloc est présent en config, l’ignorer quand il est absent. Terraform ne supporte pas le ignore_changes dynamique, donc tu ne peux pas l’exprimer nativement. Ce qui est la fin honnête de l’histoire : ce n’est pas un bug qu’on patche une fois. C’est une arête vive dans la façon dont les plateformes pilotées par policy et les modules à drift étouffé se composent, et elle reviendra la prochaine fois qu’un cloud provider sortira un subresource avant la policy censée le couvrir.

Ce qu’il faut vraiment faire différemment

Quand une feature gavée de private endpoints marche dans le portail mais échoue pour les users, lâche le plan et lâche le vert : va vérifier le data path à la main — le zone group existe-t-il, les records résolvent-ils, le client obtient-il une réponse. Et chaque fois que tu poses un ignore_changes ou une DeployIfNotExists, note quelque part — là où un toi futur ira regarder — ce que ce mécanisme a choisi de ne pas te dire. C’est la différence entre un fix de cinq minutes et deux heures de debug à l’aveugle.