Le Blog Utux

HTTP 200 GET /

Comparons Kubernetes et Swarm

Rédigé par uTux 2 commentaires

Attention, cet article va exploser le compteur de buzzwords.

J'utilise Docker en standalone depuis bientôt 3 ans, d'ailleurs mon blog tourne dessus à l'aide de deux images : PluXml et OpenSMTPD - pour le formulaire de contact - que j'ai moi-même réalisé. Sans dire que je maîtrise Docker, j'ai un niveau bien avancé. A côté de cela je travaille régulièrement avec OpenShift (la distribution Kubernetes commerciale de Red Hat) et même si je suis bien moins à l'aise qu'avec Docker, j'ai tout de même pas mal de connaissances.

Docker seul est un peu limité, on ne peut pas faire de clustering, ce qui est bien dommage car les containers et les micro-services s'y prêtent fortement. Il existe heureusement Swarm qui étend Docker au support d'une infrastructure à plusieurs nœuds.

D'un autre côté, le monde entier a les yeux rivés sur Kubernetes, le grand concurrent à Docker conçu dès le départ pour les clusters et la haute disponibilité. Ce dernier est très puissant mais aussi beaucoup plus complexe que Docker Swarm (surtout les RBAC dont je ne parlerai pas). Il faut aussi comprendre qu'on ne télécharge pas Kubernetes, on télécharge une distribution Kubernetes, et il en existe plusieurs. Dans le test de performances plus bas dans cet article je vais utiliser k3s, fourni par Rancher et grandement allégé et simplifié.

Alors, faut-il utiliser Swarm ou Kubernetes ?

Architecture

Docker Swarm

Le cluster se compose de nodes manager et workers. Les premiers sont chargés de piloter le cluster, les second exécutent les containers. Les commandes doivent être exécutées sur un des managers. Un système d'encapsulation UDP est en place entre les nodes, il permet aux réseaux des containers de se propager.

Swarm Diagram
Image provenant de la documentation Swarm.

Dans Swarm on déclare des services, soit en cli soit en yaml (docker-compose). Le scheduler va ensuite provisionner les containers nécessaires au service sur un ou plusieurs workers, selon le nombre de replica demandé. Exemple :

$ docker service ls
ID             NAME        MODE         REPLICAS   IMAGE          PORTS
sb40x4z1zqkb   httpd       replicated   1/1        httpd:latest   
owq6yurtagjg   traefik     replicated   1/1        traefik:2.1    *:80->80/tcp, *:443->443/tcp

On peut augmenter ou diminuer manuellement le nombre de replicas, il y a un load balancer interne qui va répartir le trafic de manière transparente. Un système d'affinités permet de lier les containers à une node en particulier, très pratique. Les services peuvent être mis à jour en rolling update, c'est à dire qu'on ne restart pas tous les containers d'un coup mais les uns après les autres, ce qui permet de ne pas interrompre le service. Un rollback est possible.

Et... c'est à peu près tout. Simple et efficace, mais aussi un peu limité il faut l'avouer. Swarm est un bon choix pour un usage personnel de part sa simplcité. Par contre on va voir que pour les cas d'usage avancés, Kubernetes est plus adapté.

Kubernetes

Accrochez-vous. Commençons avec l'architecture Infra.

Le cluster est composé de nodes Control Plane (les masters ou managers) ainsi que des workers. Les Control Planes pilotent le cluster et l'API Kubernetes, à l'aide d'un système de configuration centralisé, qui est souvent basé sur etcd (selon les distributions) mais pas toujours. Par exemple, k3s utilise sqlite. Les workers ont un agent (kubelet) qui reçoit les instructions du scheduler. Là encore une encapsulation UDP est prévue entre les nodes pour permettre la propagation des réseaux des containers.

Swarm Kubernetes
Image provenant de la documentation Kubernetes.

Attaquons maintenant le fonctionnement des ressources. Dans le cluster Kubernetes, tout est objet, tout est yaml ou json. C'est avec ça que l'on contrôle comment nos containers sont déployés et fonctionnent. Les types (kind) d'objets les plus courants sont :

  • Pod : Un espace logique qui exécute un ou plusieurs containers.
  • Service : Sert à exposer un ou plusieurs ports pour un pod, attaché à une IP privée ou publique.
  • DeploymentConfig : Défini ce qu'on déploie. Typiquement l'image à utiliser, le nombre de replica, les services à exposer, les volumes...
  • ReplicaSet : un contrôleur qui va vérifier que le nombre de pods en place correspond au nombre de replicas attendu. Scaling automatique possible.
  • PV, PVC : système d'attribution (automatique ou pas) de volumes persistants.
  • ConfigMap : objet permettant de stocker de la configuration, qui peut être ensuite lue et utilisée par les containers.
  • Namespace : Séparation logique des ressources, seules les ressources affectées au namespace en cours sont visibles.

Exemple d'utilisation :

$ kubectl -n web get all
NAME                          READY   STATUS    RESTARTS   AGE
pod/svclb-httpd-rw7k5        1/1     Running   0          5s
pod/httpd-8647457dd7-s2j4d   1/1     Running   0          5s

NAME             TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
service/httpd   LoadBalancer   10.43.80.117   10.19.2.73    80:30148/TCP   5s

NAME                          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/svclb-httpd   1         1         1       1            1                     5s

NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/httpd   1/1     1            1           5s

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/httpd-8647457dd7   1         1         1       5s

Par souci de simplification je ne parle pas du système de RBAC qui permet une granularité dans la gestion des droits, au prix d'une complexité (très) importante.

Kubernetes n'est pas aussi simple que Swarm mais il permet de faire beaucoup plus de choses. En fait on peut presque dire qu'on peut tout faire avec. Les namespaces sont une fonctionalité très pratique, tout comme le scaling auto et la possibilité d'éditer les objets avec la commande edit. En environnement pro, où vous allez gérer plusieurs clients, beaucoup d'applications et de fortes charges, Kubernetes est quasiment indispensable.

Fonctionnalités

Swarm Kubernetes
Configuration yaml ou json
Oui Oui
Commandes Docker
Oui Non
CLI distant
Oui Oui
Network inter-containers & DNS
Oui Oui
Replicas Oui Oui
Scaling manuel Oui Oui
Auto-scaling Non Oui
Health probes
Non Oui
Modification d'objets en ligne
Non Oui
RBAC Oui (EE) Oui
Namespaces Non Oui
Volumes self-service
Non Oui

Kubernetes est indéniablement plus complet, mais à quel point ces fonctionnalités sont-elles indispensables, surtout pour un usage en perso ? Et bien il y a à mon sens deux points que je trouve excellents dans Kubernetes et qui me manquent dans Swarm:

  • La modification d'objets en ligne. J'entends par là que la commande kubectl edit type/object permet de faire une modification à la volée, par exemple changer un port ou une version d'image dans un DeploymentConfig. Cela n'est à ma connaissance pas possible dans Docker, sauf avec docker-compose (stack dans le cas de Swarm) à condition d'avoir encore les fichiers yaml et que ceux-ci soient à jour.
  • Les namespaces. Pour éviter de mélanger plusieurs ressources de projets qui n'ont rien à voir, Kubernetes propose un système de namespaces. Par exemple je peux créer un namespace utux dans lequel je vais déployer mes images PluXml et OpenSMTPD, ce qui permet de s'y retrouver mais aussi de tout supprimer plus facilement si besoin. Les namespaces sont aussi très utiles lorsque vous partagez ou louez votre Cluster Kubernetes, chaque utilisateur a ainsi son espace dans lequel il ne voit que ses ressources.

Cependant Docker et Docker Swarm sont beaucoup plus simples et n'utilisent que des composant upstream.

Consommation de ressources

Tests effectués sur des instances DEV-1S de chez Scaleway (2 vcpu, 2GiB RAM, no swap). Le système d'Exploitation est Debian 10 Buster x86_64.

  • Système seul: RAM 70.9M/1.94G
  • Swarm seul: RAM 155M/1.94G
  • Swarm + Traefik + PluXml (apache): 209M/1.94G
  • k3s seul: RAM 619M/1.94G
  • k3s + Traefik + PluXml (apache): 678M/1.94G
RAM à vide

Si vous comptez monter un cluster de Raspberry Pi avec 1GB de mémoire, vous pouvez oublier k3s/Kubernetes qui en consomme déjà presque 75% à vide. Si vous avez les moyens de payer des serveurs de calcul un peu plus costauds, avec 16 ou 32GB de mémoire, la différence sera alors négligeable.

Les pré requis pour certaines distributions comme OpenShift sont beaucoup plus importants: 4 vcpus et 16GiB de ram pour chaque master (x3), 2 vpus et 8GiB de ram pour les workers (à multiplier par 4 si vous montez l'infra de logging ElasticSearch). C'est la raison pour laquelle je ne l'ai pas utilisé dans ce comparatif, il est hors compétition.

Exemple

Docker Swarm

Exemple simple avec la création d'un container apache vide :

version: '3'

services:

  web:
    image:
      httpd:latest
    ports:
      - 8080:80
 

Création :

$ docker stack deploy -c web.yaml test

Vérifications :

$ docker service ls
ID            NAME        MODE        REPLICAS    IMAGE         PORTS
8pxc580r3yh6  test_web    replicated  1/1         httpd:latest  *:8080->80/tcp

$ curl http://127.0.0.1:8080
<html><body><h1>It works!</h1></body></html>

Kubernetes

Reprenons notre container apache vide :

apiVersion: v1
kind: Service
metadata:
  name: web
  labels:
    app: web
spec:
  ports:
    - port: 80
  selector:
    app: web
    tier: frontend
  type: LoadBalancer
---
apiVersion: apps/v1 # 
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  selector:
    matchLabels:
      app: web
      tier: frontend
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: web
        tier: frontend
    spec:
      containers:
      - image: httpd:latest
        name: web
        ports:
        - containerPort: 80
          name: web

Chargons ces objets :

$ kubectl create namespace test
$ kubectl -n test create -f web.yaml

Vérifions :

# kubectl -n test get all
NAME                       READY   STATUS    RESTARTS   AGE
pod/svclb-web-jqs6j        0/1     Pending   0          7m17s
pod/web-774f857549-dstkz   1/1     Running   0          7m17s

NAME          TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
service/web   LoadBalancer   10.43.109.52        80:30452/TCP   7m17s

NAME                       DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
daemonset.apps/svclb-web   1         1         0       1            0                     7m17s

NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web   1/1     1            1           7m17s

NAME                             DESIRED   CURRENT   READY   AGE
replicaset.apps/web-774f857549   1         1         1       7m17s

$ curl http://10.43.109.52
<html><body><h1>It works!</h1></body></html>

Conclusion

Kubernetes est plus complet que Swarm mais aussi plus compliqué. Il n'est pas toujours facile de choisir entre les deux, mais je vous donne les conseils suivants :

  • Pour un usage personnel, Docker standalone ou Docker Swarm sera votre choix par défaut, même si rien ne vous empêche d'essayer Kubernetes :)
  • Si vous êtes pragmatique ou que vous souhaitez travailler sur les containers, alors ce sera Kubernetes. C'est l'outil en vogue plébiscité par tout le monde et c'est ce que vous devez avoir sur votre CV.
  • Pour un usage en entreprise, Kubernetes sera incontournable car il y a de nombreuses offres dans le Cloud et On-Premise. Aks ou OpenShift sur Azure, Eks chez Aws, pour ne citer que les plus connus. Tout l'écosystème des containers est focalisé sur Kubernetes et les namespaces vous serons indispensables dans un contexte multi-client.

Pour finir, un court retour d'expérience sur l'utilisation de Kubernetes en entreprise. Tout d'abord, soyez conscient que votre cluster, vos clients et vos utilisateurs vous poseront de nombreux défis. Ne pensez pas que Kubernetes va résoudre magiquement tous vos problèmes et que vos admins vont se tourner les pouces, bien au contraire. Dans le cas d'une plateforme On-Premise, prévoyez 2 admins à temps plein au minimum. Ensuite, vous devez disposer de compétences dans quasiment tous les domaines : réseau, stockage, système, middlewares et surtout avoir une bonne maitrîse de la Elastic Stack (ELK ou EFK) puisque vous aurez à gérer les logs de vos nodes et containers.

Avec cet article j'espère avoir bien présenté les différences entre Docker Swarm et Kubernetes et fourni quelques pistes à ceux qui hésitent :)

Migration sur Traefik

Rédigé par uTux Aucun commentaire

Mon blog fonctionne sous Pluxml dans un container Docker. Jusqu'à présent l'accès http/https se faisait par une instance Nginx frontale faisant office de reverse-proxy et SSL offloading. Je viens de le remplacer par Traefik.

Qu'est-ce que Traefik ? Il se qualifie de Edge Router, un terme qui vous sera peut-être familier si vous travaillez avec Kubernetes ou Openshift. En fait c'est un reverse-proxy adaptatif :

  • Gère les certificats Let's Encrypt comme un grand.
  • Route le trafic http/https vers vos backends applicatifs dans Docker.
  • S'auto-configure, l'ajout d'un container Docker correctement labelisé va déclencher la création d'un certificat Let's Encrypt et d'une route.
  • Scalable
traefik diagram

Je reviendrais un peu plus tard sur la mise en place de Traefik !

Docker swarm, publish et scaleway

Rédigé par uTux Aucun commentaire

On dirait le titre d'un WTC mais ce n'est pas le cas, c'est plutôt un bug à la con qui m'a bloqué toute une soirée.

Je fais tourner mon blog et d'autres sites chez Scaleway, sur un VC1S avec Docker (debian 9 + installation custom). Je fais au plus simple avec docker-compose qui jusque là était satisfaisant pour mes besoins. Mais depuis quelques temps je songe à passer à la vitesse supérieure avec swarm, qui me permettra d'ajouter d'autres nodes et former un vrai cluster.

Après avoir mené une phase de tests en VM, j'ai décidé de me lancer et migrer sous swarm. Mais je me suis confronté à un bug énervant, impossible de publier des ports, par exemple:

$ docker network create -d overlay net-web
t1q2kzso3a5fnlkd0s8tvywid

$ docker service create --network net-web --publish 80:80 nginx
xl35ov6bkehabkx4547omrodj
overall progress: 1 out of 1 tasks 
1/1: running   [==================================================>] 
verify: Service converged

$ telnet 127.0.0.1 80
Trying 127.0.0.1...
telnet: connect to address 127.0.0.1: Connection refused

Connexion refusée, le port n'est donc pas publié :/

Cela m'a rendu fou car en machine virtuelle VirtualBox ou KVM ça fonctionne du premier coup, et la lecture de documentation ou divers blogs ont confirmé qu'il n'y avait pas plus de manipulations à faire, ça devrait juste marcher !

J'ai commencé à soupçonner Scaleway, et en faisant une recherche avec les bons mots clés je suis tombé sur ce blog et cette issue github.

Ben voilà, c'est bien un problème avec Scaleway, car leur architecture est un peu particulière. Les serveurs ou VM n'ont pas de grub, ce sont des nodes provisionnées à la volée en PXE, avec script de démarrage et kernel maison. Et il s'avère qu'avec ces deux bootscripts, ça ne marche pas:

  • x86_64 4.10.8 std #1
  • x86_64 4.10.8 docker #1

C'est con, car le premier est celui proposé par defaut, le second est celui vers lequel on s'oriente naturellement quand on veut faire fonctionner du Docker.

Voici donc le bon bootscript: x86_64 mainline 4.14.23 rev1.

Proposer un kernel custom est une sale habitude des hébergeurs, mais à 3€/mois le vps, peut-on vraiment se plaindre de Scaleway?

Docker-compose et le COMPOSE_PROJECT_NAME

Rédigé par uTux 2 commentaires

Docker-compose est un outil permettant d'utiliser docker à l'aide d'un simple fichier .yml, facilitant ainsi grandement la vie et les interactions entre les containers d'un même service. Mais il n'est pas exempt de bugs ou de défauts de conception.

Le défaut dont je vais parler concerne le nommage des containers. Prenons par exemple deux projets docker : projet1 et projet2 avec l'arborescence suivante :

.
├── projet1
│   └── src
│       └── docker-compose.yml
└── projet2
    └── src
        └── docker-compose.yml

Nos docker-compose.yml sont identiques et commandent le lancement d'un simple container nginx :

# projet1/src/docker-compose.yml
version: '3'
services:

  nginx:
    image:
        nginx:latest

Et :

# projet2/src/docker-compose.yml
version: '3'
services:

  nginx:
    image:
        nginx:latest

Voyons ce qui arrive lorsque je démarre mon projet1 :

utux@docker:~/projet1/src$ docker-compose up -d
Creating src_nginx_1 ... 
Creating src_nginx_1 ... done

On voit que notre container a été nommé src_nginx_1 suivant la logique suivante : $dossier_$service_$numero. Très bien. Démarrons maintenant le projet2 :

utux@docker:~/projet2/src$ docker-compose up -d
src_nginx_1 is up-to-date

docker-compose dit que le container existe déjà alors que non ! En fait il applique le même raisonnement et veut nommer le container src_nginx_1 aussi alors qu'il existe déjà ! C'est un défaut de conception et c'est problématique car en cas de modification sur l'un des projets, docker-compose va recréer les containers, et donc écraser ceux de l'autre...

Pour palier à ce problème, plusieurs solutions sont possibles :

  • Nommer différemment votre dossier de travail. Ce n'est pas toujours possible car on peut avoir un environnement de dev et un environnement de prod avec les mêmes chemins.
  • Utiliser l'attribut container_name mais c'est un peu fastidieux car il faut le faire sur tous les services du docker-compose.yml
  • Utiliser la variable d'environnement COMPOSE_PROJECT_NAME. Malheureusement on ne peut pas la définir dans le docker-compose.yml malgré les demandes répétées des utilisateurs (ici et ) il faut la mettre dans un fichier .env dans votre projet.

Exemple d'utilisation du COMPOSE_PROJECT_NAME :

.
├── projet1
│   └── src
│       ├── docker-compose.yml
│       └── .env
└── projet2
    └── src
        ├── docker-compose.yml
        └── .env

Avec nos fichiers .env :

# projet1/src/.env
COMPOSE_PROJECT_NAME=projet1

Et :

# projet2/src/.env
COMPOSE_PROJECT_NAME=projet2

Démarrons maintenant notre projet1 puis notre projet2 :

utux@docker:~$ cd projet1/src/
utux@docker:~/projet1/src$ docker-compose up -d
Creating network "projet1_default" with the default driver
Creating projet1_nginx_1 ... 
Creating projet1_nginx_1 ... done
utux@docker:~/projet1/src$ cd ../../projet2/src/
utux@docker:~/projet2/src$ docker-compose up -d
Creating network "projet2_default" with the default driver
Creating projet2_nginx_1 ... 
Creating projet2_nginx_1 ... done

Cette fois les deux projets cohabitent bien et ne se marchent plus sur les pieds. La preuve :

utux@docker:~/projet2/src$ docker ps
CONTAINER ID        IMAGE                            COMMAND                  CREATED             STATUS              PORTS                    NAMES
eacabeb7e961        nginx:latest                     "nginx -g 'daemon ..."   4 seconds ago       Up 2 seconds        80/tcp                   projet2_nginx_1
9477b31d2035        nginx:latest                     "nginx -g 'daemon ..."   16 seconds ago      Up 14 seconds       80/tcp                   projet1_nginx_1

En conclusion il ne faut pas avoir une confiance aveugle en docker-compose, qui est un outil bien pratique mais manifestement sujet à des défauts de conception. Imaginez ce genre de confusion de nommage, voire d'écrasement de containers en production. Utilisez COMPOSE_PROJECT_NAME et testez vos projets en pré-production avant de déployer sur la prod.

Docker, Ansible, NixOS : le savoir (re)faire

Rédigé par uTux 4 commentaires

L'informatique, comme tous les métiers, demande un savoir-faire, celui-ci vient avec l'expérience et la pratique. Mais s'il y a un autre point qui est important et souvent négligé, c'est de savoir refaire. Je vais expliquer.

Imaginez-vous administrateur système dans une entreprise, on vous charge d'installer un serveur web pour afficher une simple page html. Vous allez alors installer Debian + Apache puis placer le fichier html à la racine, rien de difficile jusque là.

Imaginez ensuite qu'au bout de 2 mois ce fichier html soit remplacé par un fichier php un peu plus avancé, vous allez alors installer libapache2-mod-php5 là encore c'est facile. Par la suite le fichier évolue encore et vous oblige à installer des modules, par exemple php5-gd et php5-curl.

Puis le serveur vit sa vie et au bout de 2 ans on vous demande d'en mettre en place un deuxième avec exactement le même rôle. Et là ça se complique car 2 ans c'est long et vous avez probablement oublié tout ce que vous et vos collègues avez fait pour configurer ce serveur au fur et à mesure. S'il est facile d'identifier que apache2 et php5 sont présents, en revanche les modules sont beaucoup moins évidents, surtout si vous avez utilisé des gestionnaires tiers tels que pecl, cpan, pip.

L'une des grandes problématiques est donc de trouver un moyen pour garder une trace et refaire rapidement toutes les étapes qui ont accompagné la vie du serveur. Alors bien sûr le grand classique est d'utiliser un wiki, mais qui vous garanti que ce qu'y s'y trouve représente bien ce qui est en production ? Personne n'est à l'abri d'une modification "urgente" en prod non rapportée sur le wiki.

Une solution que l'on retrouve souvent et qui est appliquée dans Ansible, Docker et NixOS, c'est d'éliminer toutes les manipulations sur la prod. On y touche plus, à la place on travaille sur un fichier de recette, que l'on va ensuite deployer et jouer. Sur Ansible ce sont les playbook, sur docker les Dockerfile, et sur NixOS le fichier configuration.nix.

Cela parait beaucoup plus propre et ça l'est, c'est une rigueur pas forcément naturelle qui paye sur le long terme. Néanmoins ce n'est pas toujours évident, une manipulation rapide à faire sur un serveur peut se transformer en plusieurs minutes voire heures de devops dans Ansible. Let's Encrypt est un exemple, son automatisation n'est pas simple car vous allez devoir gérer deux cas différents selon la présence ou non d'un certificat. En effet certbot peut soit utiliser le mode standalone et donc couper Nginx (car nécessité du port 80) ou alors utiliser le mode webroot et justement passer par Nginx + vhost de votre site, or ce dernier a justement besoin du certificat pour démarrer. Et quid du cas de Docker où vous devez créer un nouveau container et le relier à l'existant juste pour renouveler vos certificats.

En conclusion le boulot de sysadmin, développeur ou devops n'est pas seulement de faire ou réparer les choses mais également de prévoir l'avenir, s'imposer une rigueur même si elle parait contre-productive sur le moment. J'aurai probablement l'occasion de parler de plus en détail de Docker et de NixOS dans de prochains articles, restez branchés.

Fil RSS des articles de ce mot clé