Comment transformer un de ses services avec Docker : gogs

closeCet article a été publié il y a 6 ans 4 mois 15 jours, il est donc possible qu’il ne soit plus à jour. Les informations proposées sont donc peut-être expirées.

Maintenant que je sais utiliser mon cluster swarm, il est temps d’expérimenter l’utilisation et surtout la transformation des services existants. La première montagne à laquelle j’ai décidé de m’attaquer : ma forge Git personnelle.

Gogs est un service Git à héberger soi même. Son installation n’est pas toujours de tout repos, surtout sous Debian, d’autant plus dans la configuration où je me trouve (derrière un bastion sur un réseau privé). J’ai en plus ajouté une couche de complexité en choissant comme base de données le moteur Postgresql.

Que ça ne me retienne pas, la structure du projet est selon moi parfaite pour se faire la main et transformer cette installation en stack sur Docker Swarm. Commençons par le début, l’inventaire des logiciels et points d’entrée, et les points persistants.

Postgresql

La VM qui héberge actuellement Gogs est une Debian 8 (la 9 n’était pas encore sortie). Postgresql est donc en version 9.4, ce qui n’est pas un problème puisqu’il est encore supporté (la 9.2 vient seulement de se mettre en retraite). Je vais pouvoir disposer d’un container dédié, il y a une image officielle, j’ai donc le choix entre simplement copier le dossier data en conservant la version 9.4 (après une première analyse c’est chaud, l’installation entre Docker et Debian semble un peu bizarre), monter un 9.6 ou 10 tout neuf en important un dumpall. Dans les deux cas, le dossier data du container postgresql doit être persistant. Son port par défaut sera exposé de manière classique, on verra comment gérer l’accès en fonction du nœud.

Gogs

Le point central, puisque c’est le service lui-même. Le déploiement via Docker est grandement documenté, le dossier /data/ du container doit contenir tout ce que le service Gogs nécessite : dépôts, configurations… Le point le plus tendu pour moi, avec la configuration en bastion, va être de pouvoir relier SSH sans savoir à l’avance sur quel nœud il sera actif. La partie HTTP est facile à mettre en place (je reviendrai dessus tout à l’heure), mais SSH, c’est autrement plus compliqué. Le réseau overlay de Docker permet de router correctement les connexions à un port vers le nœud qui l’expose actuellement, je pourrais donc me contenter de définir la connexion à un nœud en particulier. Mais le concept de l’architecture est de pouvoir continuer à disposer du service même si le nœud est manquant. On verra donc comment résoudre ce problème.

Avant de parler plus finement du réseau, Je voulais vous présenter le docker-compose, mais pendant la mise en place, j’ai rencontré tellement de problèmes, que j’ai du reprendre pas mal de choses. Et comme les erreurs sont aussi intéressantes que les réussites, autant vous partager tout ça, pas à pas, pour comprendre comment j’en suis arrivé au résultat que j’exploite désormais.

Premier fail : le démarrage du conteneur Postgresql

Quand j’ai regardé la structure des dossiers entre la version actuellement installée et ce qu’allait faire en théorie le conteneur, j’ai préféré lancer le tout comme pour une première installation pour comparer, et je n’avais que cinq dépôts, repartir de zéro n’est pas non plus catastrophique. Je lance la stack, composée de deux services (un Postgresql, l’autre Gogs), et alors que j’expérimente déjà plusieurs lenteurs sur le téléchargement des images depuis le hub (un problème récurrent semble-t-il), j’ai également un comportement étrange : Postgresql ne semble pas démarrer. En fouillant plus finement, notamment la documentation sur le hub, on apprend que si Postgresql n’est pas tatillon avec l’utilisateur sur lequel il est lancé, ce n’est pas le cas d’initdb, et manifestement il n’arrive pas à initialiser le dossier. Mais rien à faire, toutes mes tentatives pour lancer ce conteneur échouent.

Après deux heures de lectures, d’essais ratés (pour accélérer les choses je lance le conteneur indépendamment sur un nœud, en spécifiant le volume de destination sans beaucoup plus de succès), après réflexion je ne touche jamais à Postgresql, même sur l’installation actuelle, du coup autant repartir de zéro sur une base sqlite. Un contournement sale, facile peut-être, mais le propre d’une architecture est aussi de faire des choix adaptés à la configuration, et ma forge personnelle ne va pas faire grand chose de violent, donc une grosse base de données n’est pas forcément le choix le plus éclairé.

Deuxième fail : l’initialisation du conteneur gogs

Ben oui, autant le dire tout de suite, évidemment ça ne s’est pas passé correctement du premier coup pour lui aussi. Premier écueil, il n’arrive pas à écrire dans le dossier. Je contourne salement avec un chmod 777, pour me rendre compte d’une chose : il essaie de changer des propriétaires sur les fichiers mais n’y parvient pas (« Operation not permitted »), idem pour certaines permissions. Pourtant l’installation semble tout de même arriver au bout et Gogs est facilement manipulable via son interface web. Jusqu’à me rendre compte que je me suis trompé sur le point de montage et qu’une partie des données est stockée dans le container, ce qu’il faut à tout prix éviter. On scratch donc tout pour tout refaire, ça va très vite, et je continue de constater ces erreurs de changements de droits et de propriétaires, sans pour autant impacter plus que ça le service, en apparence. Au moins toutes les données essentielles (configuration, base de données, dépôts) sont exportées correctement et partagées sur les nœuds.

Jusqu’au premier push via SSH : le contenu du dépôt ne s’actualise pas sur l’interface web (deuxième écueil). Après une vingtaine de minutes de recherches je découvre alors que c’est le montage NFS qui est en cause, via l’utilisation de l’option noexec dudit montage qui empêche l’exécution du hook qui met à jour les infos du dépôt pour l’interface web. Les données, elles, sont bien présentes dans le dossier gogs-repositories. Correction du montage sur les trois nœuds (merci Ansible), redémarrage du container, suppression du dépôt, recréation, push, et paf, ça fait des Chocapic.

Dernier écueil, qui semble en lien avec les soucis de droits, le miroir sur GitHub ne fonctionne pas, SSH refuse de lire la clé privée, car elle ne lui appartient pas et les droits ne le permettent pas. Tout à fait logique, mais dans l’immédiat je ne considère pas ce point comme prioritaire, je remets donc l’analyse à plus tard. Si vraiment je veux pousser une mise à jour je peux toujours ajouter un deuxième remote « github » au dépôt concerné sur mon poste et pousser dessus, mais c’est moins élégant.

Une gestion du réseau particulière

Commençons par les plus simples. Le reverse-proxy HTTP, Nginx en l’occurrence, tape en mode load-balancer sur les trois nœuds, je n’ai donc pas à me soucier du nœud sur lequel il tourne, je suis tranquille de ce côté-là, puisqu’il gère la disponibilité du service tout seul, et le réseau overlay s’occupe du routage sur le nœud actif dans tous les cas. Super simple donc. Voilà à quoi ressemble par exemple le vhost du visualizer :

On comprend que le port exposé est le 8080, et qu’il ne faudrait que peu de choses pour basculer tout ça en HTTPS si je le voulais (ce que j’ai fait pour le registry, que j’aborderai après).

SSH cependant… c’est une autre affaire. Il est très compliqué de le proxyfier, voire impossible, sans même parler de le load-balancer, c’est juste une aberration protocolaire; d’où le bastion d’ailleurs, vous pouvez vous rafraîchir la mémoire si vous comprenez mal de quoi je parle. Comme on n’est jamais certain que le nœud sur lequel on va se connecter est debout, il faut un moyen d’avoir une entrée DNS à jour. Et un fichier de configuration SSH qui lance la connexion derrière le bastion sur le bon port aussi, sinon on a une surprise comme moi à tenter de pousser un dépot git sur une installation (l’hote qui héberge les conteneurs) qui ne dispose pas de forge git adaptée.

La solution va sembler tordue, mais je la trouve particulièrement intéressante pour m’attaquer à une autre fonctionnalité de Docker : la création de sa propre image, ainsi que l’utilisation de Python pour taper sur une API REST, ce que je n’ai pas encore suffisamment l’occasion de faire.

DockerFile + Python + API OVH : une sacrée expérience !

Cette solution particulière est venue de Flemzord en discutant du sujet sur Telegram : la mise à jour dynamique de l’entrée DNS en fonction du nœud actif. Et c’est tout à fait possible, j’ai la chance d’avoir l’API d’OVH sous la main pour mettre à jour ma zone. J’ai un peu tâtonné pour créer les clés nécessaires, mais au final ça a été rapide.

Pour me simplifier la tâche, je suis passé par la classe Python qu’OVH fournit et maintient sur pip. Un réflexe « de l’ancien monde » consisterait à simplement lancer le script à intervalles réguliers via une tâche cron sur l’un des managers, et mettre à jour la zone DNS si besoin, mais une fois de plus étant dans un cluster on ne peut s’assurer que le manager en question est toujours debout et en bon état. Il est donc préférable d’utiliser là aussi un conteneur, dont la particularité sera d’être lancé obligatoirement sur un manager, et de pouvoir accéder au socket pour identifier le host qui exécute gogs à un instant T.

Étant donné la spécificité du script je préfère construire ma propre image, ne serait-ce que parce que je n’ai pas trouvé grand chose d’intéressant existant sur le hub pour gérer l’API OVH + la connexion au socket docker (ou pas documenté/pas à jour). Et ça fait un exercice de plus, à la fois sur Python, et sur la construction d’une image custom. J’ai voulu partir au départ sur la version 3.5 de python, qui correspond à la version disponible sur Debian 9, puisque j’ai fait mes tests « bare » avec, via une des images basées sur Alpine fournies officiellement (3.5-alpine), mais mon script repose sur l’exécution d’une commande « docker service ps » qui nécessite du coup une version de Docker récente qui intègre le mode Swarm, hors la version de Docker correspondante dans les dépôts Alpine 3.4 utilisée s’avère être une 1.11, et il faut la 1.12. J’ai fini par intégrer une autre image tierce pour ne plus perdre de temps, pas forcément maintenue, mais qui m’a permis de débloquer la situation. Je verrais pour disposer d’un truc un peu plus propre dans le futur.

Le fonctionnement du script est simple :

  • j’extraie l’IP actuelle de l’entrée DNS via l’API
  • j’identifie le host en cours du conteneur gogs, et via un dictionnaire fait correspondre l’IP avec le nom du host
  • je compare les deux IP, si c’est différent je mets à jour la zone et je la rafraîchis

Le tout dans une boucle while true: avec un sleep(60) à la fin pour faire les opérations une fois par minute. Le coup du dictionnaire c’est sale, comme j’ai les entrées DNS correspondantes aux différents hosts dans la zone, je pourrais et je ferai certainement tout via l’API dans un futur proche. L’idée était d’avoir un truc rapide qui fonctionne.

J’ai pas mal tâtonné pour le fonctionnement en brut de ce script, notamment pour l’utilisation de docker via subprocess. En effet, au début naïvement j’ai voulu passer par os.system(), sans avoir lu la documentation, et je peinais à comprendre le comportement et le résultat de la commande, avant de percuter qu’on ne récupère pas la sortie de la commande mais juste son code de retour. L’utilisation de subprocess.check_output() ne fut pas non plus de tout repos avant d’arriver à un résultat exploitable, et aura demandé une bonne demi-heure d’échecs successifs et d’erreurs peu claires pour un novice. Surtout quand, en bon débile qui se respecte, vous commencez par lancer votre script avec la version par défaut de Python sur Debian 9 qui est encore la 2.7 et pas la 3.5.

Alors que c’était mon premier, j’ai moins galéré pour le Dockerfile finalement, mais c’est plus lié à l’image, pour intégrer Docker en sus, finalement ça n’a pas été aussi long. Il ne contient que 7 lignes, qui consistent à mettre à jour les dépôts Alpine, installer Docker, installer le module « ovh » via pip, copier le contenu du dossier (script et configuration d’API OVH) et lancer le script. Bon par contre ça fait une image de presque 160Mo pour un script python de 30 lignes, c’est pas ouf, mais au moins ça fonctionne parfaitement.

Reste un problème : je ne compte pas publier cette image sur le hub Docker en l’état, entre autres parce que c’est dégueulasse au possible, il me faut donc un registry privé. La communication du daemon docker avec un registry se fait via HTTPS, et il existe une image docker simple qui remplit parfaitement le boulot. J’ai donc créé une « mini-stack » pour le registry avec un volume partagé pour les images, et un petit vhost sur la frontière avec pour l’occasion acme.sh qui m’a fait le certificat HTTPS en un tour de main (et un request_body_size à 0 pour s’éviter les erreurs 413 en poussant dessus…).

Dernier exercice nouveau pour moi, j’avais bossé sur un des nœuds tout du long pour terminer ce mini-projet, j’ai terminé en créant l’image « ovhdnsupdate » sur mon poste, en la poussant sur le registry, et en redéployant la stack avec le chemin vers cette « nouvelle » image provenant du registry privé. Et ça a fonctionné du premier coup 😛

Un fichier compose finalement très simple

Je n’ai donc pas besoin de beaucoup d’éléments, après l’abandon de postgresql, les deux que j’ai mentionné sont les seuls nécessaires, puisque le reverse-proxy se trouve pour sa part au niveau du bastion. En lieu et place d’Nginx certains passeront par Traefik qui peut tout gérer automatiquement (à la définition d’un service, si on ajoute les infos pour Traefik il va générer tout seul sa configuration et le certificat Let’s Encrypt kivabien), mais qui demande d’exposer directement son cluster, ce que je n’ai pas trop envie de faire pour l’instant.

Comme indiqué, il n’y a que les deux composants dont j’ai besoin, j’ai sacrifié les configurations existantes de Gogs à cause des structures de dossiers utilisées par le container, qui n’a pas grand chose à voir avec la structure du projet quand on l’installe en bare-metal via clonage du dépôt Git. Aucune redondance nécessaire sur les services, seulement le démarrage automatique, sebnet est le nom du réseau overlay que j’ai créé, les dossiers partagés sont dans /home/seboss666/docker qui est un montage NFS sur le NAS (pour être partagé entre les nœuds, donc pas de souci de ce côté-là).

Voilà le futur déjà présent qui se profile, et qu’on appelle l’infrastructure as code : une fois affranchi de l’installation matérielle, la définition de la configuration des hôtes via Ansible, et des cinq lignes de commandes pour créer un cluster swarm, au-delà des erreurs que j’ai pu rencontrer, il ne faut qu’une trentaine de lignes dans un fichier docker compose, une trentaine de lignes python pour mon script (quarante en comptant la configuration), 7 lignes de Dockerfile, pour définir le déploiement d’une pile d’application nécessaires à la fourniture et l’exploitation de votre service. Sachant que les premières étapes peuvent elles aussi être regroupées au sein d’un fichier dédié pour le déploiement sur une plateforme publique ou privée (je continue à faire à la main pour apprendre), via Terraform par exemple. On lance le déploiement, on attend, on profite.

Reste un dernier point qui me titille malgré tout, cette histoire de droits coincés sur le stockage NFS.

NFS, ce petit vicieux qui cache ses imperfections (et une autre de mes erreurs)

En effet, j’ai encore à travailler sur la solution pour ne plus rencontrer ce problème de changement de propriétaire qui a une source : le montage NFS. Par curiosité, j’ai fait un petit test à la main sur l’un des noeuds du cluster :

La dernière fois que j’ai rencontré cette erreur, c’est lorsque j’ai monté un partage NFS en sélectionnant le protocole NFSv4 alors que la source ne supportait que le NFSv3. Là, c’est le contraire, j’ai sélectionné volontairement la v3 car j’ai déjà expérimenté des différences importantes de performances et qu’il est plus simple à optimiser de ce point de vue, mais manifestement, le NAS Asustor ne l’entend pas de cet oreille. Hors, je n’ai absolument pas la main sur la totalité de la configuration des exports et des protocoles supportés.

Il s’avère que c’est finalement un « mappage » utilisateur dans les paramètres du partage NFS que j’avais positionné sur mon utilisateur, et qu’il fallait positionner à « root ». Comme je n’avais « restauré » qu’un seul dépôt pour les tests, j’ai de nouveau refait l’installation de zéro histoire d’avoir un truc réellement propre. Enfin presque, il reste un détail d’UID par défaut, mais c’est lié au conteneur gogs et sans grosse incidence sur le service en lui-même, au moins quand il essaie de changer quelque chose (propriétaire, permissions), ça fonctionne.

Mon dernier problème que je pensais lié au NFS était le mirroring GitHub, qui finalement était lié au fait que sans interactivité, pas de validation de la connexion SSH la première fois. En recopiant le fichier known_hosts de la VM d’origine, ça fonctionne !

Un projet particulièrement instructif

J’ai appris énormément avec cette conversion, autant des erreurs que des succès, les deux étant liés de près ou de loin non seulement à mon apprentissage de la technologie, mais aussi à l’installation de mon cluster (généralement on débute sur une machine unique pour faire ce genre de choses) et le choix de l’utilisation d’un partage NFS comme point de montage partagé. Mon sentiment à propos de la technologie ne change pas fondamentalement, mais mettre les mains dedans profondément est d’autant plus intéressant, voire plus que toutes les documentations et partages d’expérience, que ça permet d’en saisir les limitations dans certains contextes.

On comprend aussi beaucoup mieux les avantages et inconvénients. J’imagine que l’installation hors container d’un registry privé pour mes images Docker m’aurait pris beaucoup plus de temps, et finalement ce qui m’a ralenti c’est l’installation d’acme.sh et la génération du certificat Let’s Encrypt sur le reverse proxy, et non pas l’installation du service lui-même. Un service peut donc être particulièrement rapide à déployer, d’autant plus quand il repose sur plusieurs logiciels qui sont déployés d’une seule traite. Dans le contexte de clustering, la mise à l’échelle (lancer plusieurs instances d’un service pour absorber la charge) est aussi très grandement facilité. Je ne vais pas revenir sur les aspects négatifs de la conteneurisation, Aeris l’a déjà très bien fait et comme le concept n’a pas fondamentalement évolué, la plupart des critiques restent valides.

J’ai beau ne pas l’accueillir avec un très grand enthousiasme, la montée en puissance de la conteneurisation et la tendance au « serverless » sont une réalité, et si tous les acteurs n’ont pas nécessairement besoin ou envie d’y passer, il est impossible de ne pas au moins s’armer un minimum avant que ça ne vous tombe dessus. C’est d’autant plus vrai quand Amazon annonce que Kubernetes, qui est un orchestrateur « concurrent » de Swarm libéré par Google, qui est exploité sur sa plateforme Google Cloud Engine, sera également proposé en tant que service, en plus de l’offre maison Elastic Container Service.

D’ailleurs, Kubernetes sera certainement ma prochaine montagne. Beaucoup de concepts de clustering sont communs avec Swarm, il n’est donc pas illogique de s’y intéresser également, surtout quand on sait que c’est l’orchestrateur majoritaire sur le « marché » (si tant est qu’on peut parler de marché dans un contexte où ils sont majoritairement gratuits et open-source).

On veut les fichiers !

Il va falloir patienter un peu pour voir tout ou partie de ce que j’ai pu écrire pour terminer ce déploiement. Si les infos de connexion à l’API sont dans un fichier à part qui se trouve dans le gitignore du dépôt, les informations liées à la zone DNS, à l’identifiant de l’entrée, la gestion de la liste d’IP du cluster, tout ça est codé en dur dans le script python, et tout le monde conviendra que c’est très sale. Le boulot n’est donc pas encore complètement terminé, mais ça concerne principalement le fait de faire les choses proprement. Idem pour la définition de la stack, il faudra variabiliser l’adresse du registry pour l’image correspondant au script. A moins que je ne choisisse de faire construire l’image à la volée, mais ça me semble un peu plus compliqué à faire. Rien que lors de la finalisation de l’écriture de ce billet sacrément long, en testant une panne d’un nœud, j’ai découvert un problème avec la façon sommaire que j’utilisais pour récupérer le nœud qui héberge le service gogs, et j’ai déjà du refaire l’image et redéployer; ça m’a permis d’ailleurs d’apprendre à me servir correctement des tags, même quand on pensait avoir suffisamment appris, ça ne s’arrête jamais…

Surtout que dans l’absolu, je n’ai rien révolutionné non plus : un Dockerfile est un Dockerfile, un fichier docker-stack.yml est… enfin bref, vous avez compris. Dans l’esprit et au final, je vais tout partager, mais il faut d’abord que ça ressemble à quelque chose de potable et de réutilisable à peu de frais pour vous. Et là dans l’immédiat, j’ai mes dépôts à recréer et re-remplir 🙂

UPDATE : le dépôt est disponible à l’adresse suivante 😉

1 Commentaire
Le plus ancien
Le plus récent
Commentaires en ligne
Afficher tous les commentaires
Denis
13/09/2019 05:58

Franchement, Docker est une usine à gaz en comparaison à LXC.