← Publications

Note terrain · Platform engineering

Ce qui casse quand on essaie vraiment de provisionner un Mac de zéro#

Le schéma Apple Business + Entra + Nix était propre. Le premier Mac effacé m’a rappelé tout ce qu’un diagramme ne montre pas : format PKG, appstored, claims incomplets, Homebrew absent et mauvaise source Nix.

Série Nix, Entra et Apple Business · 5/5 · ← Précédent

Le schéma était propre.

Apple Business installe Determinate puis mon Bootstrap.pkg. Au premier login, Entra identifie l’utilisateur, l’app résout le profil, Nix construit le poste et tout le monde est content.

Puis j’ai effacé un Mac et j’ai essayé pour de vrai.

Évidemment, c’est là que ça devient intéressant.

Voir le package dans le profil ne veut pas dire qu’il est installé#

Premier piège : côté Mac, je voyais bien les déclarations des packages arrivées depuis Apple Business.

J’aurais pu conclure que le problème était ailleurs.

En regardant les logs, la réalité était plus simple : appstored avait bien téléchargé mon package, puis l’installation s’était arrêtée immédiatement.

Le message était assez parlant :

Could not create PKProduct
The file “Distribution” doesn’t exist.

Le réseau marchait. Apple Business avait fait son travail. Le fichier avait été récupéré.

Mon package était juste mauvais.

pkgbuild tout seul ne suffisait pas#

J’avais construit un component package avec pkgbuild.

Il était parfaitement installable localement, donc au premier regard rien de choquant.

Sauf que dans ce chemin de déploiement, appstored attendait un product archive avec un fichier Distribution au niveau supérieur.

J’ai donc changé le pipeline pour passer aussi par productbuild.

Depuis, le build vérifie lui-même le contenu du package final. Il l’expand et échoue si Distribution n’est pas là.

C’est typiquement le genre de bug que je préfère transformer en test plutôt qu’en note dans un wiki.

Sinon, six mois plus tard, quelqu’un reconstruit le package légèrement différemment et on repart lire les logs appstored.

Le Mac propre m’a aussi montré toutes mes dépendances invisibles#

Sur mon poste, Homebrew existe depuis longtemps.

Sur un Mac fraîchement effacé, non.

Ça paraît évident dit comme ça. Ça l’est beaucoup moins quand une activation nix-darwin fonctionne depuis des mois chez toi et casse uniquement pendant un vrai enrollement ADE.

Une partie de mes profils utilisait encore des casks Homebrew. L’activation arrivait donc sur un système où brew n’existait tout simplement pas.

J’ai d’abord ajouté le bootstrap nécessaire, puis j’ai repris le problème dans l’autre sens : est-ce que ces applications ont réellement besoin de passer par Homebrew ?

Le compromis actuel est plus propre. Le preflight traite maintenant Homebrew comme une vraie dépendance : s’il manque, Setup le détecte et sait l’installer dans le contexte utilisateur. Si les Command Line Tools manquent, il demande d’abord leur installation au lieu de laisser Homebrew échouer plus loin.

En parallèle, j’ai déplacé ce qui avait du sens vers Nix. VS Code est par exemple revenu côté Nix alors qu’Edge et Office restent encore des casks dans le profil concerné.

Je préfère nettement ça à l’empilement d’un script caché dans un postinstall juste parce que ma machine de développement avait masqué le problème.

Entra fonctionnait, mais il me manquait quand même une information#

Autre bug plus subtil : le login Entra passait, l’App Role était correct et le bon profil workstation était choisi.

Pourtant, certaines personnalisations utilisateur n’arrivaient pas.

Le problème venait de l’email.

Le rôle ne dépend pas de l’email, et je veux que ça reste comme ça. En revanche, mon inventaire Nix utilise encore l’adresse pour rattacher certains modules personnels connus.

J’avais supposé que preferred_username serait toujours disponible.

Ce n’était pas suffisamment robuste.

J’ai donc ajouté les fallbacks nécessaires et surtout rendu cette donnée obligatoire lorsque le build en a besoin. Si l’app ne sait pas correctement identifier l’utilisateur pour rattacher ses personnalisations, elle ne doit pas continuer silencieusement avec un poste à moitié bon.

C’est un détail, mais il illustre bien la différence entre « l’auth marche » et « j’ai toutes les données nécessaires pour construire correctement la machine ».

J’ai réussi à cloner la bonne config… puis à ne pas l’utiliser#

Celui-là m’a bien fait rire.

Sur un profil Tech, le Device Flow GitHub fonctionnait. Le repository mdm-setup était cloné dans le home utilisateur.

Et ensuite le moteur Nix continuait à construire depuis le snapshot embarqué dans le Bootstrap.pkg.

Donc tout le parcours GitHub était correct, le repo était bien là, mais les derniers changements n’étaient jamais utilisés.

Le Mac construisait consciencieusement la mauvaise version.

J’ai corrigé la résolution des sources pour préférer le checkout utilisateur lorsqu’il existe, puis retomber sur le snapshot du bootstrap en fallback.

Ça marche pour le pilote, mais c’est justement le genre de situation qui m’a convaincu d’aller vers des releases explicites et prébuildées : je préfère qu’une version soit choisie volontairement plutôt que « la source la plus fraîche qu’on a trouvée quelque part sur le disque ».

Et certaines apps n’étaient simplement pas là où je les cherchais#

Home Manager ajoute encore une petite surprise : certaines applications utilisateur arrivent dans ~/Applications/Home Manager Apps/, pas dans /Applications.

Le provisioning disait success.

Je regardais /Applications.

Je ne voyais pas ce que j’attendais.

Ça ressemble à un échec alors que le système a fait exactement ce qu’on lui a demandé.

Depuis, je considère aussi ce genre d’information comme faisant partie des diagnostics. Si le bootstrap installe quelque chose dans un endroit peu évident, il doit être capable de l’expliquer.

C’est beaucoup moins cher que de redécouvrir le comportement à chaque ticket.

« nix existe » n’est pas non plus synonyme de « Determinate est prêt »#

Autre race du premier login : le binaire peut être présent alors que l’environnement n’est pas encore complètement opérationnel.

Au début, mon preflight était trop optimiste.

Je vérifiais en gros que Nix existait.

Maintenant je veux savoir que le daemon répond réellement et que le store est utilisable avant de lancer la suite.

Même chose pour le réseau : une interface Wi-Fi connectée derrière un captive portal n’est pas un accès Internet utilisable pour MSAL.

Cette partie est maintenant explicite dans le preflight. L’app attend un path utilisable, vérifie le captive portal puis teste réellement l’endpoint Microsoft avant d’ouvrir Entra. Si ça ne passe pas, elle attend et retry avec backoff au lieu d’empiler des fenêtres d’auth.

Je préfère largement ça à transformer un problème de réseau en faux incident d’identité.

macOS m’a aussi rappelé que root n’est pas tout-puissant#

Lors du premier déploiement, l’activation nix-darwin a rencontré un refus d’accès sur des fichiers système alors que le helper disposait pourtant des privilèges attendus.

Je n’avais pas un problème de syntaxe Nix : je découvrais simplement une autre couche de protections macOS qui doit être prise en compte dès le provisioning. Le pilote a permis de corriger l’ordre des prérequis et de limiter les autorisations aux composants qui en ont réellement besoin.

C’est un bon exemple de ce qu’un schéma d’architecture masque facilement : être administrateur ne dispense pas de comprendre le modèle de sécurité du système, et une correction manuelle sur un Mac de test ne constitue pas une stratégie de déploiement.

Même nix-store --export m’a fait perdre un peu de temps#

Quand j’ai commencé à produire les closures pour le registry, je pensais que la partie export serait presque triviale.

Le classique :

nix-store -qR "$TOPLEVEL" | nix-store --export

m’a pourtant donné un résultat vide avec mon installation Determinate.

Le problème venait du comportement du nix-store fourni dans cet environnement : les paths passés sur stdin n’étaient pas consommés comme je l’attendais.

J’ai fini par passer les store paths comme arguments, par lots :

nix-store -qR "$TOPLEVEL" \
  | xargs -n 50 nix-store --export

Ce n’est pas un grand problème d’architecture. C’est exactement le genre de petit détail qui te fait perdre une heure alors que tout le modèle autour est correct.

Le fix est maintenant directement dans le script de release. Et ça m’a encore renforcé dans l’idée que le pipeline doit produire lui-même les closures, les hashes et les manifests. Je ne veux pas demander à chaque Mac de redécouvrir ces subtilités.

Un registry privé ajoute aussi ses propres échecs#

Le premier accès au Blob m’a rappelé une autre chose : « l’utilisateur est connecté à Entra » ne veut pas dire « il a automatiquement un token valable pour Azure Storage ».

Setup doit obtenir un second token MSAL pour la ressource Storage, et le compte doit avoir le RBAC Storage Blob Data Reader sur le container privé.

J’ai donc dû gérer proprement le cas où le scope demande encore un consentement, où le RBAC n’est pas présent ou où le Blob répond 401/403.

Je préfère ce genre d’échec explicite à un container public ou un SAS long-lived caché dans l’app. Et le fallback embarqué permet de garder un chemin de provisioning même si le registry distant n’est pas disponible.

Et même l’observabilité peut devenir trop bavarde#

Le premier branchement OTEL marchait, mais j’ai rapidement vu un autre classique : il est très facile de produire beaucoup trop de télémétrie simplement parce qu’on peut le faire.

Je ne voulais pas un span à chaque tick de l’agent, ni un événement pour chaque pourcentage de téléchargement.

J’ai donc réduit le bruit : heartbeat et health côté métriques, événements structurés côté Loki, traces uniquement pour les opérations qui ont un vrai début et une vraie fin. La progression de téléchargement est échantillonnée, et les états très dynamiques ne sont pas transformés en labels Prometheus.

Le dashboard est devenu beaucoup plus lisible après cette passe.

Logs structurés Loki de Omnivya Setup montrant un échec de provisioning zstd

Quand le parcours casse, le même run reste exploitable dans Loki : code, run_id, version du package et erreur de provisioning sont structurés.

C’est un détail qui m’amuse parce qu’il résume assez bien tout le projet : le problème n’est presque jamais de « réussir à collecter plus ». Le problème est de décider quelle information mérite réellement d’exister.

Le disque reste le prochain piège évident#

Si je distribue demain des closures Nix prébuildées, je ne veux pas transformer les SSD de 512 Go en archive historique du parc.

Je garderai la génération courante et la previous-known-good. Une génération plus ancienne ne mérite pas de rester uniquement « au cas où », surtout si le rollback est déjà assuré par la précédente.

Mais le GC ne doit arriver qu’après validation de la nouvelle génération. Sinon, on peut très facilement supprimer le seul rollback qui nous aurait été utile.

Une UI de recovery que je peux enfin montrer#

Au début, beaucoup de logique de reprise existait surtout dans le code.

Elle est maintenant visible.

Le preflight affiche ses gates, le provisioning montre les étapes Build / Install / Activate / Validate, et en cas d’échec l’app affiche les checkpoints déjà passés ainsi que l’endroit exact où Try Again va reprendre.

Le Done screen est lui aussi devenu un vrai petit dashboard avec l’état Entra, GitHub, helper, MDM, bootstrap, macOS, Nix et Homebrew.

Je trouve ça beaucoup plus intéressant qu’un simple écran vert « success ». Le système commence à pouvoir expliquer son propre état.

Finalement, aucun de ces bugs n’a changé l’architecture#

C’est probablement ce que je trouve le plus intéressant.

Apple Business avait bien livré la déclaration.

appstored avait bien téléchargé le package.

PackageKit refusait son format.

Entra avait bien authentifié l’utilisateur.

L’App Role était bon.

Ma logique de personnalisation attendait une autre donnée.

GitHub avait bien cloné le repository.

Nix fonctionnait.

Mon moteur avait simplement choisi la mauvaise source.

À chaque fois, le problème était assez localisé parce que les responsabilités étaient séparées.

C’est exactement ce que je voulais obtenir.

Un erase complet reste indispensable, mais ce n’est pas le seul vrai test#

Après quelques itérations, j’ai arrêté de considérer un test local comme représentatif.

Supprimer deux fichiers puis relancer l’app n’est pas un test de provisioning.

Pour le chemin ADE, le vrai test reste un Mac effacé, Setup Assistant, un premier login propre, le réseau réel, aucune dépendance héritée de mon environnement de développement et un utilisateur qui ne connaît pas l’implémentation.

Mais l’inverse est vrai aussi.

Je ne veux pas valider uniquement le cas confortable du disque vierge alors que plusieurs machines de l’entreprise existent déjà.

Une machine brownfield est presque un meilleur test de convergence : Homebrew est déjà là, des fichiers utilisateur existent, Nix peut déjà avoir été installé, le hostname a une histoire et je n’ai pas le droit de résoudre un conflit en supprimant simplement ce qui me gêne.

Le système doit donc réussir deux exercices différents.

Sur un Mac neuf, il doit construire proprement le poste depuis zéro.

Sur un Mac existant, il doit comprendre suffisamment ce qu’il trouve pour le faire converger sans casser les données ni prétendre qu’il est passé par ADE alors que ce n’est pas le cas.

Dans les deux cas, je veux finir avec la même chose : un profil identifié, une version connue, un état observable et un chemin de rollback.

C’est là qu’on voit si le système gère réellement des workstations.

Ou s’il ne sait fonctionner que sur la machine de la personne qui l’a écrit.

Pour l’instant, c’est probablement la leçon la plus utile de tout le chantier.

Sources officielles#

Étienne Deneuve

CTO expert

Je dirige des équipes, je reste dans le code et les plateformes, et je vérifie ce qui change vraiment le delivery. J’anime aussi ateliers, interventions et formations.

À propos · Accompagnement

Omnivya est l’entreprise que j’ai cofondée avec Taous, et dont je suis CTO. Les missions structurées y passent.

Pour une question ou un échange sans brief commercial : M’écrire · LinkedIn

Continuer la série

Précédent · 4/5Je versionne mes postes de travail comme du logiciel

← Toutes les publications