Rappel des épisodes précédents

Nous voilà arrivés à une architecture un peu plus claire, bien que l’ensemble soit encore fragile.

Certains éléments sont encore en dehors du cluster swarm, et je ne trouve pas que ce soit une bonne solution.

Objectifs de l’épisode 5

Architecture cible

Je vais essayer de mettre en place une architecture de test, sur des serveurs hébergés sur Online (de petites Dedibox) et faire tourner les services suivants:

  1. Base de données
  2. Traefik pour la gestion du trafic entrant
  3. Un simple serveur NGinX pour héberger un site static.

Au cours de l’installation, vous verrez de nouveaux composants arriver. Docker, et de manière générale les environnements de Saas et assimilés, amènent avec eux tout un écosystème de petits programmes construits avec la philosophie unix de la modularité: ils font une seule chose, et se raccordent aux autres.

Le cluster swarm

Je vais avoir besoin de deux serveurs, au moins, pour tester le mode de fonctionnement du Swarm en réel - et c’est là que les choses intéressantes commencent. Il se trouve que j’ai deux serveurs chez Online, dans deux différents data centers. Comment vont-il donc se parler de manière sécurisée ?

La bonne nouvelle, c’est que le Swarm Docker met en place automatiquement des liens sécurisés (au moins TLS 1.2) entre les managers et les workers.

Voilà qui va nous éviter de configurer un lien OpenVPN; si jamais j’avais décidé de mettre MySQL sur l’un des deux serveurs, cela n’aurait pas pu marcher. Le fait que ce soit dans le swarm va nous permettre d’utiliser un routage interne ET sécurisé, sans charge de “travail” supplémentaire, ce qui n’est pas plus mal, car j’ai une relation très compliquée avec ce truc de “travail” (dit le gars qui écrit des articles à 7h00 le dimanche matin).

(Re-)Commençons du début.

Le premier serveur portera l’IP publique; il sera également le manager du swarm. Nous allons donc installer docker dessus, faire un init du swarm, etc.

Petite précaution qui n’a pas grand chose à voir

Ça fait à peine 10 minutes que le serveur est installé, et voyez par vous-même:

# grep "Failed password for root" auth.log | wc -l
530
# grep "Invalid user" auth.log | wc -l
52

Je pense qu’installer et paramètrer un firewall minimaliste sera tout sauf un luxe. J’avais dans l’idée de restreindre l’accès SSH à mon adresse IP (fixe) mais ce genre de choses peut vite mal se terminer.

Je vais plutôt faire le minimum vital et mettre en place un fail2ban pour éviter le gros des casse-pieds.

# apt-get install fail2ban ufw
...
# ufw allow 22/tcp
Rules updated
Rules updated (v6)
root@sd-91418:/var/log# ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
root@sd-91418:/var/log# ufw status
Status: active

To                         Action      From
--                         ------      ----
22/tcp                     ALLOW       Anywhere                  
22/tcp (v6)                ALLOW       Anywhere (v6)   

Je mettrai plus tard les ouvertures de flux entre les membres du swarm.

fail2ban est configuré correctement. J’ai fait des essais de connexions depuis un serveur tiers, et je suis bloqué au bout de cinq essais - notez que la durée du ban est de 10 minutes par défaut. J’ai passé cette durée à 86400 secondes, soit une journée; de toutes façons, personne à part moi ne devrait venir en SSH sur la plateforme.

Installation de Docker-CE

Je déroule les étapes d’installation de la documentation Docker; voilà les commandes en résumé, histoire de vous montrer à quel point c’est compliqué:

# apt-get update
# apt-get install \
    apt-transport-https \
    ca-certificates curl \
    software-properties-common
# curl -fsSL https://download.docker.com/linux/debian/gpg | apt-key add -
# apt-key fingerprint 0EBFCD88
# add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/debian \
   $(lsb_release -cs) \
   stable"
# apt-get update
# apt-get install docker-ce

Plus quelques étapes pour la post-installation… et je crois qu’on peut commencer.

Initialisation du swarm

Je lance le swarm avec l’adresse IP publique.

docker swarm init --advertise-addr A.B.C.D

Ce qui renvoie un token pour ajouter des serveurs par la suite.

J’installe le docker-ce sur l’autre machine, et je fais les ouvertures de flux, et maintenant je peux ajouter un worker sur la deuxième machine:

docker swarm join --token SWMTKN-1-[very long token] A.B.C.D:2377

Ce qui nous donne :

docker node ls 
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
3p8fiavr2pk1klo8rpze06qhe     sd-127251           Ready               Active              
4cqxlzxre3jt4nglzfo6a1bon *   sd-91418            Ready               Active              Leader

Je crée un réseau chiffré interne :

docker network create --opt encrypted --driver overlay swarm-net

Nous voilà prêts à déployer des services sur le swarm.

Base de données relationnelle

Je pense qu’il faut faire des choix en ce qui concerne le stockage des données. De toutes façons, la question de la haute disponibilité pourra être envisagée par la suite; d’autant plus, et c’est un fait à prendre en considération, que je n’ai jamais vu, en plus de 15 ans, une base MySQL s’arrêter purement et simplement sans que quelqu’un ne lui en ai spécialement donner l’ordre, ou sans qu’il se produise une coupure de courant. J’ai moins de recul avec MemCached mais j’en ai avec Linux et ses daemons, en général: ils sont suffisament robustes pour que la question du secours puisse être remise à plus tard.

Je ne veux pas paraître léger sur cette question: mais avant de lutter pour obtenir un mécanisme de haute disponibilité hors de prix, mieux vaut veiller à faire de bonnes sauvegardes régulièrement. Comme pour les sessions, la sauvegarde doit s’envisager globalement, avec une réflexion fonctionnelle.

J’ai donc décidé de placer MariaDB dans un container docker, en partant des images qui se trouvent sur le Docker Hub:

MariaDB : https://hub.docker.com/_/mariadb/

La commande pour faire tourner un docker MariaDB est habituellement celle-ci:

docker run --name some-mariadb -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mariadb:tag

Convertie en mode swarm, cela donne :

docker service create \
    --name mariadb \
    --publish 3306:3306 \
    --replicas 1 \
    --detach=true \
    --network swarm-net \
    mariadb:10.3.1 -e MYSQL_ROOT_PASSWORD=my-secret-pw

Tiens, ça ne se passe pas tout à fait comme prévu, n’est-ce pas ?

docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
4o0mzu5ost9v        mariadb             replicated          0/1                 mariadb:10.3.1      *:3306->3306/tcp

C’est au niveau de REPLICAS : 0/1 que je suis un peu déçu.

Apparement, il est en train de galérer pour créer le service que je lui ai demandé.

# docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                         PORTS               NAMES
d764e5a6a822        mariadb:10.3.1      "docker-entrypoint..."   13 seconds ago      Exited (1) 4 seconds ago                           mariadb.1.xnsegqjpes3apbfk6qrex9jiq
1e046cd4a62e        mariadb:10.3.1      "docker-entrypoint..."   33 seconds ago      Exited (1) 24 seconds ago                          mariadb.1.l2pzfoulbdbgn5fdbbpaqsyfz
f06cadb7b245        mariadb:10.3.1      "docker-entrypoint..."   53 seconds ago      Exited (1) 43 seconds ago                          mariadb.1.fpj6sg1p1mcaby0mpuer2yxdu

On voit qu’il les lance, qu’ils s’arrêtent, et retour au départ…

Au bout d’un moment, il avoue son échec :

dockerd[5728]: time="2017-09-03T17:34:42.910510537+02:00" level=error msg="fatal task error"
error="task: non-zero exit (1)" module="node/agent/taskmanager" 
node.id=4cqxlzxre3jt4nglzfo6a1bon 
service.id=4o0mzu5ost9vuzhmy3rq3lyrz 
task.id=nyd85l2npn4xq86bg38di85hj

Enfin, il n’a pas avoué publiquement: j’ai du aller chercher le message dans les logs… Peut-être qu’on a vu trop loin trop vite. Essayons d’abord avec un container tout simple.

# docker service create --replicas 1 --name helloworld --network swarm-net alpine ping docker.com 
ruo8azkw5uq1ogjm91ep9bwr7

# docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE               PORTS
ruo8azkw5uq1        helloworld          replicated          1/1                 alpine:latest

Si je change le nombre de réplicas à 5, 3 d’entre eux se mettent à tourner sur le noeud secondaire, c’est la preuve que le réseau fonctionne correctement.

Après lecture de quelques articles, il semble que la version standard de MariaDB ne soit pas prévue pour tourner en mode swarm. Il existe cependant d’autres versions qui permettent de le faire, et la beauté du système, c’est que ces versions ne sont que des surcharges de l’image MariaDB de base.

J’en trouve notamment une qui se prête correctement à l’exercice:

https://hub.docker.com/r/toughiq/mariadb-cluster/

docker service create --name dbcluster \
--network swarm-net \
--replicas=1 \
--env DB_SERVICE_NAME=dbcluster \
--env MYSQL_ROOT_PASSWORD=my-secret-pw 
--detach=true \
toughiq/mariadb-cluster

Donne le résultat suivant:

docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                            PORTS
q8tvec720iio        dbcluster           replicated          1/1                 toughiq/mariadb-cluster:latest   

Une fois lancé correctement, je peux scaler le service :

docker service scale dbcluster=3

Ce qui donne le résultat suivant :

docker service ps dbcluster
ID                  NAME                IMAGE                            NODE                DESIRED STATE       CURRENT STATE                ERROR               PORTS
wftd2utfvot5        dbcluster.1         toughiq/mariadb-cluster:latest   sd-127251           Running             Running about a minute ago                       
z71cwizr3ynu        dbcluster.2         toughiq/mariadb-cluster:latest   sd-91418            Running             Running 9 seconds ago                            
0zpxmam3b9tp        dbcluster.3         toughiq/mariadb-cluster:latest   sd-91418            Running             Running 10 seconds ago

Je lance maintenant un service qui va agir comme un proxy pour l’accès à ce cluster. Il n’est en rien indispensable, mais il apporte des fonctionnalités interressantes.

Plus de détails ici : https://github.com/toughIQ/docker-maxscale

docker service create --name maxscale \
--network swarm-net \
--env DB_SERVICE_NAME=dbcluster \
--env ENABLE_ROOT_USER=1 \
--publish 3306:3306 \
--detache=true
toughiq/maxscale

Les services pour l’instant :

docker service ls
ID                  NAME                MODE                REPLICAS            IMAGE                            PORTS
q8tvec720iio        dbcluster           replicated          3/3                 toughiq/mariadb-cluster:latest   
t5a8wq4bn3hw        maxscale            replicated          1/1                 toughiq/maxscale:latest          *:3306->3306/tcp

Par contre pour se connecter sur la base… pour l’instant, c’est pas ça… En interrogeant le maxscale (j’avais bien senti qu’il serait utile) :

docker exec -it maxscale.1.owmjn2blt8i43rh534hilnq8n maxadmin -pxxxx list servers
Servers.
-------------------+-----------------+-------+-------------+--------------------
Server             | Address         | Port  | Connections | Status              
-------------------+-----------------+-------+-------------+--------------------
10.0.0.5           | 10.0.0.5        |  3306 |           0 | Down
10.0.0.3           | 10.0.0.3        |  3306 |           0 | Running
10.0.0.4           | 10.0.0.4        |  3306 |           0 | Down
-------------------+-----------------+-------+-------------+--------------------

Au bout d’un moment la situation s’améliore…

docker exec -it maxscale.1.owmjn2blt8i43rh534hilnq8n maxadmin -pxxxx list servers
Servers.
-------------------+-----------------+-------+-------------+--------------------
Server             | Address         | Port  | Connections | Status              
-------------------+-----------------+-------+-------------+--------------------
10.0.0.5           | 10.0.0.5        |  3306 |           0 | Master, Synced, Running
10.0.0.3           | 10.0.0.3        |  3306 |           0 | Running
10.0.0.4           | 10.0.0.4        |  3306 |           0 | Down
-------------------+-----------------+-------+-------------+--------------------

Et finalement:

-------------------+-----------------+-------+-------------+--------------------
Server             | Address         | Port  | Connections | Status              
-------------------+-----------------+-------+-------------+--------------------
10.0.0.5           | 10.0.0.5        |  3306 |           0 | Master, Synced, Master Stickiness, Running
10.0.0.3           | 10.0.0.3        |  3306 |           0 | Slave, Synced, Running
10.0.0.4           | 10.0.0.4        |  3306 |           0 | Slave, Synced, Running
-------------------+-----------------+-------+-------------+--------------------

Bon, ce n’est pas super rassurant dans la mesure où je n’ai pas la moindre idée de la façon dont cela fonctionne, mais… on va faire avec pour l’instant.

En me connectant via maxscale et en créant une base de données, j’ai pu vérifier qu’elle était répliquée sur les autres noeuds, ce qui est plutôt une bonne chose.

La capacité de l’ensemble à supporter l’arrêt du master, par exemple, devra être testée par la suite, mais en attendant, j’ai fait le test d’arrêter le service docker sur le worker, et la base a changé de place, sans s’arrêter.

Je vais être honnête avec vous; je n’avais pas vraiment l’intention de faire ce test. Voilà comment on en est arrivé là: l’utilisateur d’un site - j’entends par là, l’utilisateur technique d’un site, servant à la connexion à la base de données, s’en en vu refusé l’accès depuis le moment où j’ai mis en place le dit worker.

Ne sachant d’où venait le problème précisément, j’ai arrêté le service docker sur le worker - ce qui a eu deux effets : l’accès a été de nouveau permis à cet utilisateur, et je me suis aperçu que le service de cluster de MariaDB n’en a pas souffert. Il faut dire aussi qu’il n’y a pas de données: cela a du faciliter la migration.

J’ai eu tôt fait de comprendre que le problème venait des paramêtres d’accès à la base, soit 127.0.0.1, au lieu de localhost. Je l’ai compris, pour ne rien vous cacher, parce que dans le même temps, d’autres sites continuaient de fonctionner parfaitement. En comparant les configurations, j’ai pu corriger, et relancer le service docker sur le worker.

Seule chose qui m’ennuie un peu, c’est que les services sont depuis sur le manager et ne sont pas revenus sur le worker. Que diable, c’est sans doute passager, ou bien l’affaire de quelque commande que je ne connais pas encore.

Je file noter en bas de page deux choses à voir dans le prochain épisode, et je reviens.

Key-Value Storage

Le but de ce stockage, c’est de contenir la configuration des services, notamment de Traefik. Mais aussi les certificats qu’il va générer à la volée.

Je choisis donc d’utiliser Consul pour cela :

https://www.consul.io/

Il offre les fonctionnalités suivantes:

  • Service Discovery: enregistrement de services, découverte de services, via HTTP ou DNS,
  • Multi Data-center: bon là, c’est un peu du luxe… pour l’instant !
  • Failure Detection: c’est pertinent pour éviter de donner l’adresse de services en panne,
  • Key-Value Storage: et là, c’est ce qu’il faut pour notre traefik.
docker service create \
  --name swarm_consul \
  --publish 8400:8400 --publish 8500:8500 --publish 8600:53/udp \
  --replicas 1 \
  --detach=true \
  --constraint=node.role==manager \
  --network swarm-net \
  progrium/consul -server -bootstrap -log-level debug -ui-dir /ui

Pour l’instant on a pas la possibilité de le scaler (du moins je ne sais pas le faire, mais je sais que c’est possible…) mais il tourne.

J’ouvre les flux pour la console (mais uniquement depuis chez moi)

ufw allow from W.X.Y.Z/32 to 163.172.29.216 port 8500

À noter que Consul offre une UI (écrite avec EmberJS) qui permet de parcourir son contenu ET de le modifier: en gros on pourrait modifier la configuration de Traefik directement ! Ça ressemble à ceci :

La console de Consul

Et quand je dis “on pourrait”, c’est un fait qu’un composant - par exemple le saas - pourrait profiter de son accès local au KV pour effectivement paramètrer des services mais aussi récupérer des métriques, vérifier les états de santé, etc.

Le Registry Docker

Le registry docker, c’est l’endroit on l’on va stocker les images fabriquées sur un des noeuds pour les mettre à disposition de l’ensemble du Swarm.

Quand on commence à avoir plusieurs machines dans le swarm, il faut s’assurer que chacune aura accès aux images, pour éventuellement instancier un des services localement. La question ne s’est pas posée pour MariaDB, me direz-vous. Certes. La raison est toute simple: l’image de MariaDB n’est pas locale, elle est sur le Docker Hub, sur internet. Mais les images que nous allons fabriquer (nos applications, l’instance particulière de traefik, etc.), elles, n’y seront pas.

Il est possible de créer un compte sur Docker Hub, et d’après ce que j’ai compris, d’y stocker des images, etc. Mais pour l’instant, je vais me simplifier les choses en créant un service de registry local.

Création du service :

# docker service create --name registry --publish 5000:5000 registry:2

Je le teste (au moins son accès)

# curl 127.0.0.1:5000/v2/_catalog
{"repositories":[]}

Nous allons utiliser ce registry… très bientôt !

Le frontal

À présent, nous allons faire tourner Traefik dans le swarm, écoutant, pour le service public, sur le port 443 - il va donc falloir du SSL; et un nom. Je file m’occuper du DNS pour faire pointer saas.joe-linux.org sur mon IP publique.

Le trafic venant sur le port 80 sera impitoyablement routé vers le port 443. Le HTTP sans SSL, c’est so 2000, on peut plus se permettre ça.

Je mets en place la génération automatique des certificats par Let’s Encrypt… qui devraient se faire au moment où on déclare un backend repondant à une rule spécifique. Pour l’instant je fais pointer vers le server staging de Let’s Encrypt, histoire de pas casser quelque chose.

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
    [entryPoints.https.tls]

[acme]
email = "dvaldenaire@gmail.com"
storage = "traefik/acme/account"
entryPoint = "https"
# Enable certificate generation on frontends Host rules.
OnHostRule = true
# The Staging Server - be careful before goind to prod !
caServer = "https://acme-staging.api.letsencrypt.org/directory"

[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "traefik"
watch = true
swarmmode = true

[consul]
  endpoint = "swarm_consul:8500"
  watch = true
  prefix = "traefik"

[web]
  address = ":8080"
  readonly = true

Notons qu’en théorie, cette configuration sera chargée dans le KV Store.

Ok, je mets ça dans le fichier de configuration traefik.toml en compagnie du Dockerfile, et je construis une image.

# docker build -t swarm_traefik .

Je tagge l’image du swarm :

# docker tag swarm_traefik 127.0.0.1:5000/swarm_traefik

Et je la pousse dans le registry:

# docker push 127.0.0.1:5000/swarm_traefik
The push refers to a repository [127.0.0.1:5000/swarm_traefik]
0bfa2764fbd0: Pushed 
96e939c31d1a: Pushed 
3b10c345cfdd: Pushed 
latest: digest: sha256:e41276b674[...]82ab67d4f0f size: 946

On oublie pas d’ouvrir les flux… (pour la console, uniquement depuis chez moi)

# ufw allow 80/tcp
# ufw allow 443/tcp
# ufw allow from W.X.Y.Z/32 to 163.172.29.216 port 8500

C’est parti:

docker service create \
    --name running_traefik \
    --publish 80:80 --publish 443:443 --publish 8080:8080 \
    --replicas 1 \
    --detach=true \
    --constraint=node.role==manager \
    --network swarm-net \
    --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
    127.0.0.1:5000/swarm_traefik

Je consulte la console de Traefik, elle est vide, il n’y a pas de services pour le moment, mais elle fonctionne.

Il faut pas oublier de “vraiment” stocker la configuration dans le KV. Cela se fait par une commande lancée avec docker exec et qui initialise ce stockage.

docker exec -it running_traefik.1.lqnz2fy73yoc8km954ek8n9q3 /traefik storeconfig

Histoire de valider le fonctionnement, je vais juste lancer un docker NGinX

docker service create --name running_test \
  --replicas 1 \
  --label traefik.backend=test \
  --label traefik.frontend.rule="Host:saas.joe-linux.org" \
  --network swarm-net \
  --detach=true \
  --label traefik.port=80 nginx:1.13.5-alpine

Je le vois dans l’UI de Traefik, mais c’est à présent le moment de vérité ! Je vais accèder sur http://saas.joe-linux.org/ qui doit renvoyer vers https://, lequel va générer le certificat à la volée, et nous montrer la page par défaut de NGinX.

Et…

Ça marche !

Ne me croyez pas sur parole, vous pouvez regarder par vous même:

http://saas.joe-linux.org

Attention !!! Le certificat n’est pas valide parce que je suis sur le serveur de test de LetsEncrypt - je le passerai en production dans l’épisode suivant.

À suivre !

Dans le prochain épisode, je propose que nous regardions de plus près:

  • comment fonctionne le cluster de base de données,
  • comment faire revenir des services sur le worker,

D’autre part, je vais faire tourner ce blog (qui ne contient que des fichiers statiques) et sa partie analytics (une installation de Piwik) sur le cluster.

À bientôt pour la suite des aventures avec Docker !