DEV Community

Cover image for -STORYTIME- Il tente de déployer sans Internet, ça tourne mal

-STORYTIME- Il tente de déployer sans Internet, ça tourne mal

TLDR: on a tendance à oublier la valeur ajoutée gigantesque de tous les outils mis en place pour développer et administrer un service numérique, jusqu’au moment où l’on n’y a plus accès. Faire face aux problèmes que résolvent ces outils permet de pleinement comprendre pourquoi on peut effectivement en avoir besoin, plutôt que de les utiliser partout parce qu’ils sont à la mode.

Le petit nid douillet du dev

Le travail de dev de nos jours est un vrai régal.

J’arrive au travail (ou je sors juste de mon lit), j’allume mon laptop dernier cri acheté reconditionné sur Backmarket, je lance mon IDE et hop, je suis prêt à coder. Tout le code est disponible sur un dépôt stocké en ligne sur une plateforme SaaS. À chaque push mon code est testé, analysé, linté, formaté, et je peux ouvrir des PRs pour intégrer mon travail en continu avec celui de mes collègues.

Il me suffit en général d’un simple clic pour lancer un processus de déploiement en production. Les pipelines de Continuous Delivery se chargeront pour moi de build les artifacts et de les déployer via les ressources en place chez mon cloud provider préféré. Mon infrastructure prendra soin d’assurer un maintien du service pour mes utilisateurs, le tout pendant que je déguste tranquillement un thé au jasmin en attendant la notification de succès sur mon téléphone.

En cas d’incident, je suis averti en temps réel et je peux lancer un roll back en un claquement de doigts. Je dispose de logs, de traces, de rapports d’analyse, de graphiques pour investiguer et produire des post-mortems qui permettront une amélioration continue de mon équipe et de mon architecture.

Il n’y a rien à redire, c’est un bonheur absolu. Jusqu’au moment où je découvre les contraintes techniques imposées par le prochain projet sur lequel je vais devoir travailler.

Petit oiseau si tu n’as pas d’ailes …

Bien calé dans ma bouée voguant sur un océan d’insouciance (avec un thé au jasmin sur le porte gobelet), je découvre la multinationale Cave Inc. (disclaimer : pour des raisons de confidentialité, les noms des entreprises et des personnes présents dans cet article sont fictifs.)

L’entreprise souhaite développer un outil custom pour faciliter des opérations métier. Il sera déployé sur une VM sur son réseau interne et accessible par les employés. Je me redresse sur ma bouée. Le réseau de Cave Inc n’est pas relié à Internet pour des raisons de sécurité et la seule porte d’entrée numérique que l’entreprise nous met à disposition est une boucle de mail dont le poids ne doit pas dépasser 5Mo.

Ma bouée se prend dans un récif et explose, m’envoyant m’échouer sur une île déserte. Je sauve in-extremis mon thé au jasmin.

Il va nous falloir concevoir, coder et livrer un outil à un client qui n’aura pas accès à Internet pour le déployer sur son réseau. Pas question de se décourager, nous sommes des Ingénieurs Logiciel, on est là pour résoudre des problèmes même si on n’a pas forcément accès aux outils habituels pour nous simplifier la vie.

thunderstorm

L’envol

Le projet sera constitué d’un frontend, d’un backend et d’une base de données. Il sera déployé sur une VM Linux par un ingénieur de Cave Inc que nous appellerons René. En un tour de boucle email nous apprenons que la VM de René dispose de Docker. Parfait, nous allons pouvoir livrer le code qui pourra être déployé sous forme de containers qui seront orchestrés par docker-compose. Ce n’est pas idéal comme solution mais c’est mieux que rien.

Aïe premier accroc, René ne disposant pas d’Internet, il ne va pas être en mesure de build les images à partir du code et des Dockerfiles qu’on lui fournit. Nous allons donc build les images nous même et les lui liverons directement prêtes à tourner. On peut utiliser la commande docker save pour sauvegarder dans une archive une liste d’images dans le registry local (préalablement buildée ou pull depuis un registry online). René pourra, lui, utiliser la commande docker load pour charger les images que nous lui auront livrées dans le registry local de la VM et déployer l’outil.

Aïe deuxième accroc. L’archive avec les 3 images pèse 600Mo, impossible de l’envoyer à René par email. Il nous faut nous résoudre à livrer PHYSIQUEMENT le produit à René, par l’intermédiaire d’une clé USB. Nos espoirs s’effondrent, adieu les déploiements en continu. La pluie se met à couler à grosses gouttes sur la plage et dilue le peu de thé au jasmin sauvé du naufrage encore dans ma tasse.

process

On se reprend, il faut composer avec les contraintes du projet. Si on n’a pas le droit à l’erreur, il va falloir bétonner la procédure et la rendre error-proof. En temps normal on définirait des jobs dans la CI qui nous permettraient de produire l’artifact à livrer directement au client en pushant un tag par exemple. Malheureusement le code du projet est hosté sur une instance Gitlab mise à disposition par un provider qui ne permet pas d’utiliser docker-in-docker pour build les images nécessaires dans la CI. Mais après tout une plateforme de CI c’est juste un serveur qui execute des scripts en ayant accès au code. Nos machines de dev joueront le rôle de serveur de CI et on peut définir les mêmes scripts dans un Makefile directement au sein du repo.

Le script qui génèrera un déploiement va :

  • cloner la branche main du repo
  • copier les fichiers .env de production
  • build les images Docker
  • les sauvegarder
  • créer une archive avec le repo et les images
  • cleanup

Plus dure sera la chute

L’heure de la première livraison arrive. Le script marche comme sur des roulettes. On entrevoit un rayon de soleil entre deux nuages. Premier retour de René : “Erreur avec la commande make deploy” suivi d’une stacktrace longue comme le bras. C’est parti pour une session de debugging par email. On est loin d’une solution SaaS d’observabilité avec des journaux d’événements et compagnie.

Visiblement le backend n’arrive pas à joindre la DB. Ah non, en fait il ne parvient pas à démarrer du tout. Et la DB non plus. Et le voilà qui apparait, tel une pépite dans le tamis du chercheur d’or, le log qui allait tout expliquer.

Docker warning: The requested image's platform does not match the detected host platform (amd64)

Comment ça ? Qu’est ce que c’est que cette histoire d’architecture ? Docker n’est pas censé marcher partout quelle que soit la machine ???? Qu’est ce que c’est que cette histoire d’amd64 ??

En réalité le fameux macbook reconditionné acheté sur Backmarket a le bon goût d’être équipé d’un processeur Apple Silicon M2 architecture ARM. En buildant les images sur cette machine, elles avaient donc ARM comme architecture cible. La VM de René quant à elle fonctionnait sur une architecture linux/amd64. Il me confirme avec un docker inspect que les images livrées ont bien comme architecture linux/arm64v8.

Heureusement il existe une solution à ce problème. Docker met à disposition buildx pour build des images avec une architecture différente de la machine sur laquelle il tourne. On peut l’activer et simplement exécuter :

docker buildx --plateform amd64 <IMAGE>

On utilise des images de dev au quotidien et il serait possible de les livrer par inadvertance. Pour les identifier précisément, on ajoute le tag delivery aux images buildées par le script et qui ont pour architecture linux/amd64. Pour éviter toute forme de programmation par coïncidence, le fichier docker-compose de production est configuré pour utiliser les images avec ce tag.

Pour aller plus loin sur le fonctionnement de buildx et des manifests Docker, je vous recommande ces deux articles :

L’heure de la deuxième livraison arrive et c’est un succès ! Tout se passe pour le mieux. René peut déployer sur sa VM et nous pouvons avancer sur le développement de nouvelles features.

Des hauts, puis des bas

On arrive au moment d’implémenter l’internationalisation des contenus dans l’application frontend. Aucun problème, on setup i18n, on remplace les quelques textes existants par des clés de traduction et le tour est joué. Les valeurs associées aux clés sont populées avec des “lorem ipsum” en premier lieu et René pourra modifier après coup comme il le souhaite. Mais se pose un nouveau problème que l’on n’avait pas anticipé : changer les traductions dans le code est possible, mais René va devoir rebuild le frontend et l’image associée pour que ses modifications puissent être déployées et visibles par les utilisateurs. Sans internet ça complique le processus.

Actuellement le Dockerfile qui permet de builder l’image du frontend contient les instructions suivantes :

FROM node:18-alpine as build
WORKDIR /app
COPY . ./
RUN yarn install
RUN yarn build

FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 8000
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

On utilise le multi-stage build proposé par Docker avec une première étape pour build le frontend en se basant sur une image node. On récupère dans une seconde étape le dossier build produit dans la première étape pour pouvoir le servir avec nginx comme reverse-proxy.

La commande yarn install télécharge les dépendances et nécessite Internet. Idéalement, on aurait envie que René dispose déjà des dépendances qui ne vont pas changer entre deux livraisons et que la seule étape qui soit refaite dans le Dockerfile soit le build du frontend (yarn build) avec les nouvelles valeurs des clés de traductions. Et c’est exactement ce qui va se passer grâce au cache Docker.

Une image Docker est constituée d’un ensemble de layer (on les voit se télécharger au fur et à mesure lorsqu’on pull une image depuis un registry).

layers-download

Chaque layer est relié à une instruction du Dockerfile (plus ou moins, selon les instructions). Lorsque l’on build une image, Docker met en cache les différents layers qui pourront être réutilisés pour un prochain build. Chaque layer dispose d’un id qui est calculé à partir de l’id du layer précédent et d’un “diffId” représentant les changements dans le système de fichier par rapport au layer précédent. Si les ids ne changent pas, Docker peut utiliser le layer en cache quand il rebuild une image. Au contraire, si un id change, soit à cause de l’ordre des instructions du Dockerfile, soit à cause d’une mise à jour du système de fichier, pour toutes les étapes suivantes le cache sera invalidé et elles seront refaites.

Pour aller plus loin sur le fonctionnement des layers dans Docker, je vous recommande cet article : How the Docker Image Is Stored on the Host Machine

Ainsi, on peut utiliser le cache Docker pour éviter que René ne doive re-télecharger les dépendances.

On modifie le Dockerfile :

FROM node:18-alpine as build
WORKDIR /app
COPY package.json ./
COPY yarn.lock ./
RUN yarn install
COPY . ./
RUN yarn build

FROM nginx:stable-alpine
COPY --from=build /app/build /usr/share/nginx/html
EXPOSE 8000
CMD ["nginx", "-g", "daemon off;"]
Enter fullscreen mode Exit fullscreen mode

Une fois qu’il aura modifié les clés de traduction, rien n’aura changé du point de vue du système de fichier et dans l’ordre des commandes pour Docker par rapport au moment où l’image a été build par nos soins pour les étapes avant COPY . ./. Ainsi pas besoin de d’exécuter de nouveau yarn install. Et même pour le processus de livraison, pas besoin de télécharger de nouveaux les dépendances si seul le code change. Ça vaut bien une bonne gorgée de thé au jasmin.

Turbulences à l’horizon

Nouvelle semaine, nouvelle livraison, on y croit cette fois-ci. RAS côté application, tout fonctionne comme prévu. Cependant René n’arrive pas à rebuild le frontend avec les clés de traductions mises à jour. Les nuages s’amoncellent de nouveau sur le ciel qui nous avait laissé entrevoir une lueur d’espoir. Le message de René inclut les logs du processus du rebuild qui sont cette fois-ci limpides : “failed to load image node:18-alpine” & “failed to load image nginx:stable-alpine”. Les images en question sont exactement celles nécessaires pour build le frontend comme décrit dans le Dockerfile, et elles n’ont pas été livrées. Nous avions pourtant testé le livrable donné à René et fait devant lui la démonstration de rebuild pour changer les clés de traductions. Nouvel eurêka mêlé à la sensation étrange d’avoir failli : cette démonstration avait été faite sur notre cher macbook reconditionné bien aimé, qui avait été lui même utilisé pour produire l’archive de livraison. Le registry local contenait les images en question, mais elles n’ont pas été mises dans l’archive, et René ne pouvant pas les télécharger depuis un registry online, impossible de rebuild.

Le gros problème dans notre script de livraison qui apparaît clairement avec ce bug est qu’il compte trop sur la machine sur lequel il est exécuté. Dans un monde idéal, c’est la CD, exécutée dans un container ou une VM chez un cloud-provider, qui devrait exécuter le script. Il serait tout à fait indépendant des environnement de dev et on pourrait détecter et prévenir beaucoup plus tôt ce genre d’erreur.

Étant donné qu'on ne peut toujours pas build des images Docker via notre instance Gitlab, on va utiliser une machine dédiée en plus de revoir le script.

On rajoute les étapes suivantes :

  • On pull toutes les images que l’on aura besoin de livrer peu importe si elles sont déjà présentes dans le registry local.
  • Au moment de sauvegarder dans une archive les images buildées, on ajoute aussi celles qui permettront de rebuild le frontend.

Avec ça on garantie la reproductibilité du script et l’indépendance de l'artifact qui sera livré de l’environnement qui sert à le produire.

De plus on va exécuter tout ça sur une machine dédiée Linux avec processeur sous architecture amd64 qui nous servira de “pc serveur”. Le script garde la commande de build avec buildx pour le multi-architecture toujours dans un but de reproductibilité.

C’est justement le moment de mettre à exécution ce nouveau script. Le thé au jasmin mitige à merveille les effets du stress qui monte au fur et à mesure que les logs apparaissent dans le terminal. Aucune erreur. On utilise une nouvelle session indépendante sur la machine pour tester le build. Toute fonctionne, pas de bugs sur les parcours utilisateurs critiques, le soleil brille et la plage sur laquelle on avait échoué quelques semaines auparavant semble plus accueillante que jamais. Quelques jours plus tard, nous remettons la clé USB à René qui dans la foulé nous confirme que tout fonctionne pour lui. Il peut changer les clés de traductions à sa guise et est ravi.

Atterrissage en douceur

Il faut cependant être réaliste, nous avons eu de la chance sur ce projet de pouvoir proposer une solution technique simple qui serait appropriée pour résoudre le problème de Cave Inc. Alors certes, cela peut paraître moins sexy de ne pas utiliser la dernière technologie à la mode (et qui sera dépréciée dans 6 mois), mais le rôle d’un ingénieur est de résoudre des problèmes plutôt que de pousser une complexité pas forcement pertinente qui transformera un projet en usine à gaz. A contrario, il ne faut pas du tout réduire l’impact phénoménal que peuvent avoir les outils IT modernes, mais peut-être se poser la question de leur pertinence face au contexte avec lequel on travaille et le problème à résoudre.

Cette aventure m’aura quand même bien fait ressentir à quel point ces outils modernes comme Docker ont permis d’abstraire une bonne partie du travail et des problèmes rencontrés par les devs pour qu’ils puissent se concentrer sur le métier et la production de fonctionnalités. Avoir expérimenté les difficultés que ces outils permettent d’ignorer reste pour moi le meilleur moyen de juger de leur nécessité et de leur intérêt pour un projet.

Et vous, si vous aviez dû construire un projet de la sorte et le déployer sans Internet, vous auriez fait comment ? N’hésitez pas à poster votre solution en commentaire, ou me contacter pour qu’on en discute autour d’un thé au jasmin.

Top comments (0)