6135 links
  • GuiGui's Show
  • Home
  • Login
  • RSS Feed
  • ATOM Feed
  • Tag cloud
  • Picture wall
  • Daily
Links per page: 20 50 100
 
  • Docker et la tendance de la conteneurisation : a-t-elle vraiment été en production ?

    Les limites de Docker exposées par Aeris.

    Docker. Une technologie si jeune (2013) et pourtant dorénavant vendue un peu partout comme la technologie miracle qui va résoudre tous les problèmes de déploiement, vous rendre riche et vous ramener l’être aimé. Et pourtant, en pratique cette technologie me pose pas mal de problèmes et de cas de conscience. Revue de détails.

    Objectif : portabilité

    Le but initial est un peu dans le prolongement de ce qu’a fait Java : construire une fois, déployer partout (« build once, deploy everywhere »). On ne peut nier que l’idée initiale soit bonne tellement le problème du déploiement est un véritable parcours du combattant en pratique.

    Un logiciel est rarement un morceau totalement isolé. Il vient avec son interpréteur, ses bibliothèques, nécessite un moteur de base de données, des fichiers de configuration, un serveur web et tout un tas d’autres choses. Cet écosystème va être à gérer tout au long de la chaîne de fabrication, que ça soit en développement, en phase de validation/qualification, en recette ou encore en production.

    [...]

    Besoins de dev ≠ besoin de la prod

    Le premier rempart à l’usage de Docker est que les besoins entre la production et le développement sont assez différents. Un développeur va vouloir accéder facilement aux journaux, si possible en mode debug, alors que la prod préférera les envoyer à un Logstash et en mode info voire warn ou error. Un développeur préférera utiliser directement le serveur d’application léger de son choix comme Jetty, Thin ou Gunicorn, alors que la production configurera un backend Nginx devant ou utilisera des serveurs d’application plus puissant tel que Tomcat ou Passenger. Un développeur préférera sûrement compiler en mode debug pour avoir des retours utilisables en cas de problème alors que la production insistera pour le faire en mode release. La production voudra mettre en place un pare-feu, ou ses outils de supervision de parc, dont le développement n’a même aucune idée de l’existence puisque ça ne fait pas partie de ses compétences ! [...]

    Pour les environnements différents, il « suffit » que les devs travaillent hors Docker à l’ancienne, puis une fois parvenus à quelque chose de satisfaisant, s’attaquent à la construction d’une image Docker. Ça implique une espèce de mini-chaîne complète (dev/test/validation/qualification/production) faite uniquement par les développeurs, afin de s’assurer que l’image finale est à peu près conforme à ce qui est attendu (quel logiciel fournir, quelles dépendances disponibles…). Parce que du coup ça n’implique plus que si ça tourne sur leur environnement de dev, ça tourne sur l’environnement Docker. Pas forcément très folichon niveau processus sinon avoir ramené toutes les considérations des autres étapes sur celle de développement.

    Pour les besoins propres à la production (pare-feu, monitoring…), Docker permet de créer une image à partir d’une autre. La production repartira donc de l’image fournie par les développeurs pour refaire sa propre image incluant tout le nécessaire. Ça casse aussi l’intérêt de Docker qui garantit que ça tournera en production, étant donné que la production peut elle aussi introduire des bugs. Par exemple l’installation d’un pare-feu va peut-être installer une bibliothèque utilisée par l’application mais dans une version différente. La production devra donc aussi repasser une bonne partie des étapes précédentes (test/validation/qualification) avant la mise en production réelle. Pas folichon non plus.

    Dans les deux cas il aurait été plus intelligent que le dev et la prod travaillent ensemble dès le départ (qui a dit Devops ?) pour fournir une image Docker à la QA qui partira en production telle quelle une fois approuvée. Mais alors qu’on utilise Docker ou n’importe quelle autre technologie (virtualisation classique, automatisation d’installation via un outil comme Chef, Puppet, Ansible ou Salt) on aurait obtenu le même résultat.

    En commentaire, on trouve cela :

    ‒ Dev != prod : Oui effectivement, mais justement c'est à toi de construire ton dockerfile en fonction de tes besoins, et pouvoir donc passer d'un environnement d'intégration à un environnement de production via une simple variable. Tu peux par exemple via ces variables, choisir de passer par syslog pour l'intégration et par logstash (ou autre) pour la prod. Si le dockerfile est bien construit, aucun soucis. Je pense qu'il faut absolument garder le contrôle sur ces images.

    ‒ Dev vs Prod : oui on peut faire, on peut toujours tout faire (même avec du BrainFuck). Avec des terrachiées de variable d’env ou des tonnes de fichiers de config. Mais ça casse tout l’intérêt du « build once run everywhere » tant vanté par Docker. Parce que la version « prod » ne sera pas/peu testée en « dev ». Et donc qu’on repart dans les bons vieux aller-retours dev/prod. Autant virer Docker.


    [...]

    Quid de la sécurité ?

    [...]

    Docker repose sur le principe d’immuabilité des images. Une fois une image livrée, on n’y touche plus et au prochain changement nécessaire, on refait une image depuis zéro.

    Ça pose un problème assez énorme en termes de sécurité. Le jour où vous avez une faille dans le logiciel livré, on comprend bien qu’on n’échappera de toute façon pas à un correctif, une regénération, un passage intégral de toute la chaîne de qualification et une nouvelle mise en production. Mais si c’est une bibliothèque utilisée par le logiciel, par exemple OpenSSL qui connaît au moins une faille critique de sécurité par jour ?

    Dans une infrastructure sans Docker, les gentils administrateurs systèmes se seraient sagement connectés en SSH aux machines impactées, auraient simplement fait un apt update && apt upgrade et basta.
    Un patch de sécurité n’introduisant pas de changement de fonctionnalités et les développeurs de OpenSSL faisant bien leur travail, ils livrent des versions patchées assurant la rétro-compatibilité avec les versions précédentes. Les admins peuvent donc appliquer le patch assez rapidement sans avoir besoin de consulter les développeurs. Et la faille est rapidement corrigée avec un risque de régression négligeable (qu’on a parfaitement su accepter et maîtriser pendant des décennies, et même Docker ne pourra jamais garantir le 0 bug). En prime, le logiciel final, lui, n’a pas changé de version.

    Dans une infrastructure dockerisée, c’est une autre histoire… Les conteneurs devant être immuables, il est interdit aux administrateurs de se connecter aux machines pour les mettre à jour. Un changement de sécurité sur une bibliothèque réclame donc de redérouler l’intégralité de la chaîne : on met à jour la bibliothèque, on construit une nouvelle image Docker (qui change donc de version), on repasse toute la QA, on met en production. La mise en production nécessite un arrêt de l’ancienne image, la migration des données sur la nouvelle image et le redémarrage du système. Bref, vous allez tourner un long moment avec votre image trouée avant d’avoir pu fixer le problème…

    Hum, non, on ne repasse pas la QA. Dans les deux cas (sans Docker ou avec), tu as normalement que la lib qui a changée. Comparons ce qui est comparable. :)


    Quand on évoque ce problème avec la communauté Docker, ils avancent alors leur recette miracle : l’intégration continue. Certes, si vous réussissez à intégrer l’intégralité de la QA (tests unitaires, tests fonctionnels, tests d’intégration, tests de validation, tests de qualification, tests de recette) dans une suite de tests automatiques, c’est peut-être envisageable de faire une modification dans le code, de cliquer sur un bouton et d’avoir automagiquement la nouvelle version en production. En pratique, on a déjà du mal à atteindre 100% de couverture sur les tests unitaires, et plus on monte dans les étages plus c’est compliqué. Les tests fonctionnels doivent simuler des humains presse-boutons, les tests d’intégration sont un enfer à réaliser vu le nombre de composants en jeu, les tests de qualification sont souvent longs (tests de performance, de tenue de charge…), etc.

    Les plus perspicaces d’entre vous auront noté que tout ce qui précède repose sur une hypothèse très forte : on doit être mainteneur de l’image Docker utilisée ! En pratique, beaucoup utilisent des images pré-construites qu’ils assemblent selon leurs besoins. Il existe des dépôts d’images, dont le plus connu est le Hub Docker. Du coup, en cas de faille, il va vous falloir attendre une mise-à-jour. L’unique mainteneur est passé sous un bus ? Vous êtes mal… En termes de sécurité, c’est même encore plus gore dans ce cas, puisque vous n’avez pas beaucoup de moyens de vous assurer que l’image de 600Mo (n’oublions pas que ça intègre un OS complet) que vous allez utiliser n’intègre pas une porte dérobée ou une version obsolète d’une bibliothèque, surtout quand l’image est réalisée avec les pieds et ne permettent aucune vérification a posteriori. Les paquets Debian sont par exemple construits en compilation reproductible et vous pouvez facilement vous assurer que le nginx que vous utilisez est bien le même que celui du paquet Debian officiel alors que sous Docker, je vous souhaite bien du courage ne serait-ce que pour connaître le numéro de version utilisé. Sur chaque image Docker, il faudrait faire une revue assez poussée de ce qui se trouve sur l’image, au moins pour lister les différents composants et bibliothèques intégrés et ainsi connaître les impacts réels sur votre parc d’un correctif de sécurité.

    Haha, excellent l'exemple du nginx compilé depuis les sources dans une image Docker. Oui, identifier le bazar dans chaque couche de l'overlay, c'est déjà une plaie.

    Dans les commentaires :

    Et pour le délai, je prend les paris qu’une image avec 39 overlays comme Sidekiq ne sera pas mise à jour rapidement. Certainement pas en 2h pour les cas très urgents (Heartbleed), ni même en quelques jours. L’image Sidekiq est par exemple toujours bloquée en OpenSSL 1.0.1f, datée de janvier 2014 et toujours faillible à Heartbleed 2 ans après (corrigée dans 1.0.1g…).

    Sur twitter - https://twitter.com/aeris22/status/785399182978875392 :

    59 overlays Docker pris au pif. 38 faillibles à Heartbleed…


    [...]

    Mono-processus : ma vie, mon œuvre

    L’humanité s’est battue pendant des décennies pour mettre en place le multi-processus, puis le multi-thread… Mais ça c’était avant ! Avec Docker, vous ne pouvez lancer qu’un seul et unique processus dans une image. Pas plus. Ça n’a l’air de rien, mais c’est très handicapant en pratique. Vous ne pouvez pas avoir cron à tourner régulièrement par exemple. Donc pas de logrotate. Vous ne pouvez pas avoir de serveur SSH pour vous connecter à distance sur l’image. Pas de serveur de mail non plus, par exemple pour les rapports journaliers de logwatch (de toute façon on n’a pas cron pour les lancer…). Rien. La seule et unique chose que vous allez pouvoir lancer sera donc votre application. Et c’est tout.

    Le problème est que comme on l’a vu précédemment, une application se suffit généralement assez peu à elle-même. Elle nécessite par exemple une base de données, un frontal web, de pouvoir envoyer des courriels… Et donc de lancer plusieurs processus !

    En méthode quick & dirty, vous pouvez contourner le problème en lançant un bête script bash qui lancera tout en arrière-plan, ou plus malin un gestionnaire de processus comme supervisord ou pups qui se chargera lui-même de lancer tout le reste de ce que vous avez besoin. C’est tout de même assez galère à faire puisque votre distribution adorée vous fournira des scripts de démarrage pour le système d’init habituel (sysv, systemd, upstart…) et non pour supervisord ou pups, il vous faudra donc faire un travail de portage pour chaque composant nécessaire.

    La méthode recommandée par Docker pour gérer vos environnements est l’utilisation de Docker Compose. Vous allez créer autant de conteneurs que de composants de votre écosystème (un pour l’application, un pour la base de données, un pour le serveur de courriel…) et les assembler entre-eux pour qu’ils communiquent correctement.
    Pour certains composants comme la base de données, je trouve ça intéressant de séparer du reste, exactement comme on l’aurait fait dans une infrastructure non virtualisée. Pour d’autres, comme un serveur de courriel dédié pour envoyer 3 courriels au mois, c’est du gaspillage de ressources flagrant. Et pour la majorité, c’est d’une prise de tête sans nom… Par exemple dans une application Ruby-on-Rails utilisant Sidekiq comme ordonnanceur, on va se retrouver à avoir 4 conteneurs :

    • 1 pour l’application Ruby-on-Rails
    • 1 pour le backend web nginx
    • 1 pour Sidekiq
    • 1 pour le serveur Redis qui sert à la communication entre RoR et Sidekiq

    Alors que tout mettre sur la même machine se justifie largement tant qu’on n’a pas une volumétrie délirante (je tiens les 200.000 vues quotidiennes sur Cryptcheck sans soucis avec 1 seul conteneur), on se retrouve avec 4 machines à gérer et à devoir mettre à jour (les 4 utilisent OpenSSL par exemple) et une duplication de l’environnement Ruby (RoR & Sidekiq). On risque aussi de rencontrer des dégradations de performances, puisqu’on passe de communications sur la boucle locale voire des sockets UNIX à une communication TCP/IP externe. Et la sécurité devient tout autant un enfer, avec de la gestion de pare-feu à mettre en œuvre.

    On peut très bien avoir plusieurs processus par conteneur Docker mais ce qu'Aeris écrit reste parfaitement valable : si c'est pour déroger à l'un des principes de Docker, le microservice, alors c'est peut-être que ton cas d'utilisation n'entre pas dans la liste de ceux auxquels Docker apporte quelque chose et dans ce cas-là, oui, il vaut peut-être mieux rester à un déployement LXC + ochestration & co. Et la question est : quels microservices tournent de manière parfaitement autonome, sans rien d'autre ?


    [...]

    De la chasse aux méga-octets à la chasse aux bugs non reproductibles

    On a vu juste avant que Docker incite à créer plein de conteneurs pour les assembler entre eux. En interne pour la fabrication de ses propres images, Docker se base aussi sur des images pré-construites qu’il empile les unes sur les autres au travers de OverlayFS. Pour l’image Sidekiq, c’est pas moins de 39 couches empilées.

    Déjà, ça n’aide pas à la sécurité non plus, puisqu’une faille corrigée sur une couche réclame la reconstruction de toutes les images situées sur les couches supérieures. Ou alors vous corrigez violemment sur votre couche terminale, mais vous dupliquez alors vos bibliothèques (installées par une couche N mais masquées via overlayfs par la couche N+X) et vous avez le travail à faire sur toutes vos images utilisant la couche faillible.

    Ensuite, si on utilisait des images standard, on se retrouverait à consommer plusieurs giga-octets pour pas grand-chose. Du coup, la communauté Docker a commencé à faire la chasse aux méga-octets, et s’est prise de passion pour une distribution présentée comme très légère : Alpine Linux.

    Cette distribution vient avec un gros piège… Elle est conçue à la base pour aider à débugger des kernel ! Parce que quand vous développez ce genre de logiciel, vous êtes bien content d’avoir une image de 5Mo qui démarre en 2s, vu le nombre de redémarrages que vous allez faire. Et comme vous ne ferez presque rien côté utilisateur, on peut y mettre des trucs très légers comme musl et busybox au lieu des mastodontes que sont la glibc et les coreutils.

    Sauf que musl n’est pas compatible avec la glibc disponible un peu partout. Ni au niveau binaire, ce qui signifie que vous devez compiler explicitement vos logiciels avec cette libc et donc maintenir à la fois un livrable -glibc pour les gens hors de Alpine et un -musl pour Alpine. Ni au niveau fonctionnalités, ce qui fait que vous pouvez rencontrer des bugs incompréhensibles et non reproductibles sur d’autres plate-formes plus standard. Ça peut aller jusqu’à l’impossibilité totale de compiler votre logiciel, comme c’est le cas actuellement avec OpenJDK 8 ou Phusion Passenger.

    Bref, vous allez vous retrouver à soit utiliser des images du Hub Docker avec une chance non négligeable d’utiliser un conteneur Alpine dans votre chaîne et faire la chasse aux bugs vraiment chiants à comprendre, soit à devoir faire votre propre image personnelle sans Alpine… Le tout en croisant les doigts à chaque construction d’image pour ne pas tomber en plus sur une image contenant une faille de sécurité…

    Au final, Docker passe en plus complètement à côté de la plaque en termes de consommation de ressources. À titre d’exemple, la stack précédente RoR/Redis/Sidekiq/Nginx ramène pour 60 overlays Docker et 3.1 Go d’espace disque, quand je m’en tire pour 1.8 Go pour Cryptcheck avec une stack dev/RoR/Redis/Sidekiq/Nginx/Elasticsearch/CryptCheck/données. Un beau gâchis d’espace…

    [...]

    Un petit conteneur LXC tout simple, construit à partir de debootstrap générant une image de maximum 200Mo toute mouillée et dans laquelle vous allez installer vos logiciels au mieux avec du apt/dpkg standard et au pire quelques scripts automatisant l’installation (du bon vieux bash over chroot ou du ansible/salt), ça fonctionne simplement et c’est facile à gérer côté sécurité. Et vous découpez vos conteneurs non plus par logiciel comme sous Docker, mais par besoin : 1 conteneur pour toute la stack Discourse, 1 pour votre serveur de courriel entrant, 1 pour votre serveur DNS faisant autorité, etc.

    Gros +1

    Mon plus gros reproche, en dehors des problèmes de sécurité présentés par Aeris, c'est qu'il est très facile de mal utiliser Docker pour outrepasser les adminsys qui se contenteraient alors de mettre en prod' des dockers et de ne plus y toucher. Hé bah non, adminsys, c'est un métier, dev' s'en est un autre. Le dev' n'a aucune idée de toutes les emmerdes de la prod' (ce n'est pas ce que l'on attend de lui !) et il faudrait mettre son docker en prod' sans rien faire ? L-O-L, on va droit dans le mur. Docker peut facilement se transformer en boîtes noires mises en prod' sans aucun suivi, bref se transformer en supplétif du code jetable que l'on observe bien trop souvent.

    October 31, 2016 at 3:55:05 PM UTC - permalink - https://blog.imirhil.fr/2016/10/09/docker-container-hell.html
Links per page: 20 50 100
 
Mentions légales identiques à celles de mon blog | CC BY-SA 3.0

Shaarli - The personal, minimalist, super fast, database-free, bookmarking service by the Shaarli community