auto-hébergement hybride

Dans la série sur Docker, voici un nouvel article sous un titre un peu étrange qui éveillera sans doute votre curiosité et c’est bien le but. Disclamer : cet article est tout frais la peinture n’est pas sèche, avec le temps son contenu sera modifié.

Historique

Pour les geeks sortant d’un caisson cryogénique, voici les 2 premiers lien expliquant ce qu’est l’auto-hébergement : auto-hebergement et sur wikipedia. En résumé cela consiste à héberger ses données sur ses machines personnelles chez soi au lieu de les stocker chez Google, Facebook, Apple, etc.

Cela vient d’une démarche qui consiste à dire que personne n’est mieux placé que soit même pour avoir une garantie sur la protection de ses données personnelles. Cela implique un minimum de compétences techniques et je ne vais malheureusement pas contedire cela ici.

Motivation

Je n’ai jamais été un fervent partisant de ce concept. Cela demandait trop de temps et de confiance dans son propre matériel afin de ne pas perdre mes données. Quid de celles-ci si le PC crame ou les disques dur ? Entre temps sont sorties les révélations de Edward Snowden qui ont contre-balancé à mes yeux les inconvénients de l’AH.
Une solution est de louer des serveurs physiques ou vrituels chez un hébergeur, type OVH, afin de déployer les logiciels libres permettant de gérer ses données. Cette solution qui semble plus rassurante que l’AH, puisque les serveurs sont dans un datacenter, la sécurité physique étant garantie, mais il y a aussi une bande passante beaucoup plus importante que son ADSL ou même sa fibre chez soi.

Les problèmes de cette solution sont le coût du stockage et le coût CPU/RAM. Or un simple PC récent, type core I5, 8Go de RAM et 2 To de disque dur ne coute presque rien en comparaison du coût annuel chez un hébergeur à configuration identique.

J’avais ces données en tête, mais il me restait un dernier frein.
l’AH implique que les services hébergé chez soi, comme un blog, les visiteurs arrivent sur l’IP publique fournie par son FAI… Or il est très facile de faire un DDOS vers un accès Internet personnel ; dans ce cas je doute que l’usage d’Internet chez soi à ce moment là soit très réactif …

Il restait aux hébergeurs de serveurs l’avantage d’offrir de base des protection anti DDOS ce qui est loin d’être superflu de nos jours.

Hybridation

L’idéal serait de mixer ces 2 types d’environnement. Techniquement il suffit de monter un VPN entre une VM chez un hébergeur vers un serveur chez soi. Ensuite un serveur web configuré en reverse proxy afin de relayer les requêtes HTTP vers ses serveurs applicatifs. Le serveur web expose ainsi une IP de l’hébergeur, et non pas l’IP de son FAI et il bénéficie de l’anti DDOS.
Je présume que je n’ai rien inventé et que certains le font depuis longtemps mais jusqu’à présent, cette solution impliquait de modifier à chaque ajout/suppression d’un service la configuration du reverse proxy. Idem si l’on souhaitait répartir la charge entre 2 serveurs physique chez soi ou placer un serveur supplémentaire chez un proche afin de rendre l’infrastructure plus résiliente.

La mise en place du VPN pouvait être également laborieuse avec OpenVPN. Tout cela n’a rien d’impossible mais ma configuration devrait rendre tout cela bien plus simple et automatisé.

Elle est constitué de docker swarm , traefik, vpncloud et syncthing

Description

Le serveur

pour ce projet j’ai recyclé une tour de 2007 (aille la facture élec)

  • Intel® Core™2 Quad CPU Q6600 @ 2.40GHz
  • 8 Go de RAM (max de la carte mère)
  • 2 HDD de 1To

Je ne dispose que d’une seule machine, or un docker swarm en nécessite au moins 3 pour avoir un intérêt (1 leader, 2 workers). J’ai choisi de virtualiser 3 VMs centOS (qui seront des nodes worker docker) avec KVM (libvirtd/qemu) avec un bridge réseau sur le serveur physique afin de les voir comme des machines dans mon réseau local. Une configuration avec des VMs NATé aurait fonctionné également mais moins souple, par exemple pour scripter des tâches avec ansible directement vers l’IP des VMs.

Le serveur est rélié au réseau en CPL, ce qui est pratique lorsqu’il est dans un placard de la cuisine et qu’on ne peut pas tirer un cable jusqu’à la box.

A terme l’idéal serait d’avoir 1 ou 2 barbones récent, pour moins de consommation électrique que cette tour ; ou pourquoi pas un cluster de raspberry pi et supprimer ainsi la couche de virtualisation. Attention dans ce cas avec docker il est nécessaire d’utiliser des conteneurs pour ARM comme ceux fournis par hypriot ce qui limite le choix logiciel. De plus il sera nécessaire d’acheter une carte d’extension SATA afin d’y connecter ses disques durs ; outre le surcoût non négligeable, le bus interne de Pi sera toujours en USB … Quant à utiliser le disque flash si le but est d’y stocker ses photos/vidéos c’est sans doute une fausse bonne idée. Mais je serais ravi d’être contredis à ce sujet.

Docker swarm

Mon objectif avec swarm est de pouvoir instancier rapidement n’importe quel logiciel libre dans mon infra sans me préoccuper de la couche matériel ni du setup complet du service. Une simple ligne de commande doit me permettre d’utiliser un service et de l’exposer sur Internet quelques secondes/minutes après. Il reste malgré tout une part de configuration mais elle reste bien plus souple qu’une installation/configuration à l’ancienne. De plus le serveur hôte n’est pas pollué par les nombreuses blibliothèques nécessaires aux services.
La moins ancienne manière de faire serait d’installer une VM par service. Cependant une VM consomme énormément de ressource ce qui limite leur nombre, de plus on multiplie le problème de la mise en place des services, et de la cohabitation de leurs bibliothèques (difféntes versions de PHP/Ruby/Python/Java par exemple).
Avec swarm j’ai certes 3 VMs mais qui servent uniquement à héberger des conteneurs, et je peux espérer en avoir une dizaine par VMs (2Go de RAM/VM ce qui en laisse 2go pour le serveur hôte). A long terme j’espère migrer sur 3 serveurs barmetal avec uniquement un démon docker.

vpncloud.rs

Pour relier ma VM chez OVH et mes 3 VMs locales, je souhaitais utiliser un vpn simple à mettre en oeuvre, sans serveur centralisé, en réseau maillé. J’avais utilisé il y a quelques années freelan qui correspondait à ce cahier des charges. Cependant le projet ne propose de paquet rpm (j’utilise maintenant centos) et je ne me sentais pas me lancer dans une compilation laborieuse. Il n’est pas dit que je ne le reteste pas à l’avenir.

J’ai testé meshbird qui est très simple à utiliser et à compiler (merci Golang). J’ai eu malheureusement des bugs lors de mon test.

En fouillant github j’ai trouvé vpncloud.rs. Lorsqu’on oublie pas d’installer la libtool (sick) il se compile tout seul, ce qui est pratique car le projet ne propose pas encore de rpm.
Codé en langage Rust, le makefile installe les bibliothèque nécessaires grâce à cargo. Plutôt long à compiler par rapport à Go, on obtient un binaire très performant . Il utilise la libsodium (bisous openssl) et le wiki propose 2 configurations en fonction de l’architecture souhaitée.

Le principe d’un réseau VPN sans serveur central, est que chaque noeud écoute sur un port (UDP autant que possible) exposé sur Internet. On indique le DNS de chaque noeud dans le fichier de config puis ils se mettent en relation une fois le chiffrement/déchiffrement réalisé (désolé pour l’explication vague).
Le problème se pose pour son accès Internet, puisqu’on ne dispose que d’une IP publique (cet article est basé sur un environnement en IPv4 /o\ ) or je souhaite connecter 3 VMs interne vers une VM externe chez un hébergeur.

La solution est de configurer la box/routeur du FAI pour qu’elle forward le port UDP du VPN vers l’IP fixe du serveur physique interne. Seul ce dernier aura le client VPN et pourra se connecter vers la VM “dans le cloud”. Ensuite une simple règle de routage permettra aux VMs de se parler en passant par l’interface réseau du serveur hôte.

L’explication en image avec cette page Dial in Tutorial.

dial_in_scenario.png

Le problème de cette infra est que l’on ne dispose bien souvent pas d’une IP fixe chez soi. Ma box permet de se connecter vers des services de type dyndns, mais un simple script lancé dans un cron pourra offrir le même service. Voir la page Dynamic DNS et les conseils de Korben.
Ceci fait, dans la configuration du client vpncloud chez son hébergeur on indiquera l’adresse dns obtenue, (exemple : chezmoi.ddns.net). Ainsi lorsque le FAI change l’IP, le vpncloud “dans le cloud” pourra se reconnecter sans problème à la box.

syncthing

Le principe de docker est qu’un conteneur est jetable , un simple docker run permet de relancer le service, on peut ainsi en supprimer/instancier très rapidement en fonction des besoins. Cependant tout ce que contient le conteneur disparait lorsqu’il est supprimé. Il faut donc créer des volumes, qui sont de simple répertoire sur le serveur hôte (ou un montage NFS/glusterfs) puis on les attache au conteneur à son lancement. Ainsi le conteneur écrit dans le volume persistant, si le conteneur est détuit et relancé il retrouvera ses données immédiatement.
Dans un swarm le conteneur peut etre relancé sur un autre noeud, qui correspond à une autre machine (VM ou pas). En effet si le noeud où était le conteneur a planté ou à été éteint (VM planté, VM stoppé, raspberry pi éteint, …), le noeud leader du swarm détecte le problème et réinstancie les conteneurs qui étaient sur ce noeud vers un autre noeud en vie. Les volumes qui contiennent les données sont par contre toujours sur le noeud stoppé, en relancant les conteneurs sur un autre, ils ne retrouveront pas leurs données… boom.

Il existe plusieurs solutions pour palier à cela, par exemple utiliser un montage NFS sur chaque noeud vers un serveur NFS. Cela implique d’avoir un serveur NFS dédié à cela, mais qui sera un SPOF de l’infra. De plus si je souhaite avoir un noeud docker situé à des km de chez moi, les temps d’accès NFS via le VPN risquent de tout plomber. ou pire si le lien VPN tombe.
Une autre solution est d’utiliser un système de fichier distribué type glusterFS. Chaque noeud (ici VM) fera un montage gluster grâce à un client glusterfs, mais cela implique que chaque VM soient aussi serveur glusterFS… Compliqué à mettre en oeuvre, sans parler des problèmes à gérer avec ce logiciel (split brain).
Le plus simple pour une infra HA est d’utiliser un outil de synchronisation de fichiers comme l’explique cet article avec bittorrentsync, Distributed volumes with BitTorrent Sync. J’ai choisi la même architecture mais avec syncthing qui a l’avantage d’être libre.

traefik

traefik est un serveur HTTP reverse proxy en Golang. C’est un superbe projet libre développé par un Lyonnais (cocorico), Emile Vauge propose d’ailleurs ses services via sa startup containous. L’intérêt de cet outil est qu’il se connecte directement à l’API de docker, lorsqu’on lance un conteneur traefik est immédiatement informé et expose automatiquement le service. De plus il peut demander de lui même un certificat let’s encrypt. Même s’il ne pourra pas atteindre les performances d’un nginx codé en C, il est très rapide. Pour les curieux un lien vers l’histoire de La naissance de Traefik.io.

Configuration

VPN

prérequis

Je présuppose que vous avez une VM chez un hébergeur. Je déconseille l’usage d’une VM chez scaleway tant que ce bug ne sera pas corrigé, docker swarm ne fonctionne pas du tout avec leur kernel 4.10.8-docker. Pour ma part j’ai migré vers une VM à 3€ chez OVH qui suffit largement. Cette VM sera leader du swarm, c’est elle qui gérera les conteneurs dans la cuisine sur les noeuds workers. On peut améliorer la résilience de l’infra en ajoutant 2 VMs louées qui seront manager. Si la VM qui possède le node leader tombe un des 2 autres managers sera élu leader du swarm (il faut 1 leader ou 3 mais pas 2). La VM leader sert uniquement à instancier l’image traefik.

Pour le VPN si vous n’avez pas d’IP fixe il faut créer un compte sur un service de dns dynamique, voir l’article de Korben , pour ma part j’ai pris un compte gratuit noip. Ensuite si votre box le permet lui indiquer votre compte dyndns afin qu’elle le mettre à jour avec votre nouvelle IP, soit lancer dans un cron un script qui fera ce travail. Enfin il faut configurer votre box pour qu’elle forward le port UDP du VPN (ici 3210) vers l’IP de votre serveur interne. Cela implique que le dhcp de votre box lui fournisse une IP fixe (associer l’adresse MAC de la carte de votre serveur à une IP).

config

Sur le serveur physique on compile vpncloud.rs.

yum install rust libtool cargo git
git clone https://github.com/dswd/vpncloud.rs
cd vpncloud.rs
make build
cp target/release/vpncloud /usr/local/bin

On créé le fichier de configuration, bien remplacer achanger et achanger2 par ce que vous-voulez.

cat /etc/vpncloud/fredix.net

cat /etc/vpncloud/fredix.net 
# This configuration file uses the YAML format.

# This configuration can be enabled/disabled and controlled by adding the
# network to `/etc/default/vpncloud` and starting/stopping it via
# `/etc/init.d/vpncloud start/stop` on non-systemd systems and via
# `systemctl enable/disable vpncloud@NAME` and
# `service vpncloud@NAME start/stop` on systemd systems.


# The port number on which to listen for data.
# Note: Every VPN needs a different port number.
port: 3210

# Address of a peer to connect to. The address should be in the form
# `addr:port`. If the node is not started, the connection will be retried
# periodically. This parameter can be repeated to connect to multiple peers.
# Note: Several entries can be separated by spaces.
#peers:
#  - node2.example.com:3210
#  - node3.example.com:3210

peers: []

# Peer timeout in seconds. The peers will exchange information periodically
# and drop peers that are silent for this period of time.
#peer_timeout: 1800

# Switch table entry timeout in seconds. This parameter is only used in switch
# mode. Addresses that have not been seen for the given period of time  will
# be forgot.
#dst_timeout: 300

# An optional token that identifies the network and helps to distinguish it
# from other networks.
magic: "achanger"

# An optional shared key to encrypt the VPN data. If this option is not set,
# the traffic will be sent unencrypted.
#shared_key: ""
shared_key: "achanger2"

# The encryption method to use ("aes256", or "chacha20"). Most current CPUs
# have special support for AES256 so this should be faster. For older
# computers lacking this support, only CHACHA20 is supported.
crypto: chacha20
#crypto: aes256

# Name of the virtual device. Any `%d` will be filled with a free number.
#device_name: "vpncloud%d"
device_name: "vpncloud%d"

# Set the type of network. There are two options: **tap** devices process
# Ethernet frames **tun** devices process IP packets. [default: `tap`]
#device_type: tap
device_type: tap

# The mode of the VPN. The VPN can like a router, a switch or a hub. A **hub**
# will send all data always to all peers. A **switch** will learn addresses
# from incoming data and only send data to all peers when the address is
# unknown. A **router** will send data according to known subnets of the
# peers and ignore them otherwise. The **normal** mode is switch for tap
# devices and router for tun devices. [default: `normal`]
#mode: normal
mode: normal

# The local subnets to use. This parameter should be in the form
# `address/prefixlen` where address is an IPv4 address, an IPv6 address, or a
# MAC address. The prefix length is the number of significant front bits that
# distinguish the subnet from other subnets. Example: `10.1.1.0/24`.
# Note: Several entries can be separated by spaces.
subnets:
#  - 10.1.1.0/24
  - 192.168.254.0/24

# A command to setup the network interface. The command will be run (as
# parameter to `sh -c`) when the device has been created to configure it.
# The name of the allocated device will be available via the environment
# variable `IFNAME`.
#ifup: ""
ifup: "ifconfig $IFNAME 192.168.254.254/24 mtu 1400; sysctl -w net.ipv4.ip_forward=1"

# A command to bring down the network interface. The command will be run (as
# parameter to `sh -c`) to remove any configuration from the device.
# The name of the allocated device will be available via the environment
# variable `IFNAME`.
#ifdown: ""
ifdown: "ifconfig $IFNAME down"

# Store the process id in this file when running in the background. If set,
# the given file will be created containing the process id of the new
# background process. This option is only used when running in background.
#pid_file: ""

# Change the user and/or group of the process once all the setup has been
# done and before spawning the background process. This option is only used
# when running in background.
#user: ""
#group: ""

si vous avez un vieux CPU comme moi il faut mettre définir la crypto en chacha20, sinon mettre aes256. Puis on créé le fichier init systemd /usr/lib/systemd/system/vpncloud@.service

cat /usr/lib/systemd/system/vpncloud\@.service
[Unit]
Description=VpnCloud network '%I'
Before=systemd-user-sessions.service docker.service

[Service]
Type=forking
ExecStart=/usr/local/bin/vpncloud --config /etc/vpncloud/%i.net --daemon --log-file /var/log/vpncloud-%i.log --pid-file /run/vpncloud-%i.run
WorkingDirectory=/etc/vpncloud
PIDFile=/run/vpncloud-%i.run

[Install]
WantedBy=multi-user.target

l’activer et le lancer

systemctl enable vpncloud@fredix.service
systemctl start vpncloud@fredix.service

On doit voir une interface vpncloud0

vpncloud0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1400
    inet 192.168.254.254  netmask 255.255.255.0  broadcast 192.168.254.255

Il faut copier le binaire vpncloud sur la VM de l’hébergeur, puis créer son fichier de config, remplacer chezmoi.ddns.net par le DNS fourni par le service choisi.

cat /etc/vpncloud/fredix.net 
# This configuration file uses the YAML format.

# This configuration can be enabled/disabled and controlled by adding the
# network to `/etc/default/vpncloud` and starting/stopping it via
# `/etc/init.d/vpncloud start/stop` on non-systemd systems and via
# `systemctl enable/disable vpncloud@NAME` and
# `service vpncloud@NAME start/stop` on systemd systems.


# The port number on which to listen for data.
# Note: Every VPN needs a different port number.
port: 3210

# Address of a peer to connect to. The address should be in the form
# `addr:port`. If the node is not started, the connection will be retried
# periodically. This parameter can be repeated to connect to multiple peers.
# Note: Several entries can be separated by spaces.
peers:
#  - node2.example.com:3210
#  - node3.example.com:3210

  - chezmoi.ddns.net:3210

# Peer timeout in seconds. The peers will exchange information periodically
# and drop peers that are silent for this period of time.
#peer_timeout: 1800

# Switch table entry timeout in seconds. This parameter is only used in switch
# mode. Addresses that have not been seen for the given period of time  will
# be forgot.
#dst_timeout: 300

# An optional token that identifies the network and helps to distinguish it
# from other networks.
magic: "achanger"

# An optional shared key to encrypt the VPN data. If this option is not set,
# the traffic will be sent unencrypted.
#shared_key: ""
shared_key: "achanger2"

# The encryption method to use ("aes256", or "chacha20"). Most current CPUs
# have special support for AES256 so this should be faster. For older
# computers lacking this support, only CHACHA20 is supported.
crypto: chacha20
#crypto: aes256

# Name of the virtual device. Any `%d` will be filled with a free number.
#device_name: "vpncloud%d"
device_name: "vpncloud%d"

# Set the type of network. There are two options: **tap** devices process
# Ethernet frames **tun** devices process IP packets. [default: `tap`]
#device_type: tap
device_type: tap

# The mode of the VPN. The VPN can like a router, a switch or a hub. A **hub**
# will send all data always to all peers. A **switch** will learn addresses
# from incoming data and only send data to all peers when the address is
# unknown. A **router** will send data according to known subnets of the
# peers and ignore them otherwise. The **normal** mode is switch for tap
# devices and router for tun devices. [default: `normal`]
#mode: normal
mode: normal

# The local subnets to use. This parameter should be in the form
# `address/prefixlen` where address is an IPv4 address, an IPv6 address, or a
# MAC address. The prefix length is the number of significant front bits that
# distinguish the subnet from other subnets. Example: `10.1.1.0/24`.
# Note: Several entries can be separated by spaces.
subnets:
#  - 10.1.1.0/24
  - 192.168.254.1/32

# A command to setup the network interface. The command will be run (as
# parameter to `sh -c`) when the device has been created to configure it.
# The name of the allocated device will be available via the environment
# variable `IFNAME`.
#ifup: ""
ifup: "ifconfig $IFNAME 192.168.254.1/16 mtu 1400; route add 192.168.0.0/24 via $IFNAME"

# A command to bring down the network interface. The command will be run (as
# parameter to `sh -c`) to remove any configuration from the device.
# The name of the allocated device will be available via the environment
# variable `IFNAME`.
#ifdown: ""
ifdown: "ifconfig $IFNAME down"

# Store the process id in this file when running in the background. If set,
# the given file will be created containing the process id of the new
# background process. This option is only used when running in background.
#pid_file: ""

# Change the user and/or group of the process once all the setup has been
# done and before spawning the background process. This option is only used
# when running in background.
#user: ""
#group: ""

créer le fichier init systemd, enable et start comme au dessus. On doit voir maintenant une interface réseau vpncloud0

vpncloud0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1400
    inet 192.168.254.1  netmask 255.255.0.0  broadcast 192.168.255.255

Sur la VM louée on ajoute une route vers son réseau local en indiquant l’interface vpncloud0 à utiliser et l’ip de son serveur local (à changer avec la votre)

route add -net 192.168.0.0 netmask 255.255.0.0 gw 192.168.0.36 dev vpncloud0

pour rendre cette configuration persistente sur centOS il faut créer le fichier suivant

cat /etc/sysconfig/network-scripts/route-eth0
192.168.0.0/24 via 192.168.0.36 dev vpncloud0

Si tout va bien on doit pouvoir pinger la passerelle du serveur local. Les VM chez soit ne sont pas encore joignable, sur chacune d’elle il suffit de leur ajouter cette route

route add -net 192.168.254.0 netmask 255.255.255.0 gw 192.168.0.36

on la rend persistente

cat /etc/sysconfig/network-scripts/route-eth0
192.168.254.0/24 via 192.168.0.36 dev eth0

Ainsi seul les VMs dans lesquelles on a ajouté cette règle de routage seront joignable depuis la VM louée, ce qui est mieux pour la sécurité.

Docker

Sur une VM CentOS il faut suivre ce tutoriel pour installer le dépôt docker. Faire de même sur les VM ou serveur physique chez soi.

on initialise le swarm sur la VM louée en précisant l’ip de l’interface vpncloud0

docker swarm init \  
    --listen-addr 192.168.254.1 \
    --advertise-addr 192.168.254.1

docker affiche la commande à lancer sur les nodes workers pour qu’ils rejoignent le swarm, on peut l’obtenir à tout moment avec cette commande

 docker swarm join-token worker
 To add a worker to this swarm, run the following command:

    docker swarm join \
    --token unetoken\
    192.168.254.1:2377

on peut vérifier que le swarm est bien relié

 docker node ls
ID                           HOSTNAME              STATUS  AVAILABILITY  MANAGER STATUS
gxy553ef1eudvfwidxh95dqa9    centos-2.localdomain  Ready   Active        
nd0edqf6txl1zry90bh4c7f6j    centos-1.localdomain  Ready   Active        
vyfozvy9gmu1bjf7txdr0qk2y *  vm.ovh.net     Ready   Active        Leader
zjstkea2kdqql5ujsgyeau3jl    centos-3.localdomain  Ready   Active        

on va taguer les noeuds worker, l’objectif et de pouvoir lancer des conteneurs uniquement dans nos VMs à la maison. En effet si on loue une petite VM pas cher il n’est pas concevable que les conteneurs puissent se retrouver sur elle et la saturer. Par exemple imaginez que vous souhaitiez héberger une instance mastodon et que les 5 conteneurs nécessaire s’y retrouve dessus (rails, redis, postgresql…)

on execute cette commande sur le leader, pour les 3 VMs, le hostname dépend du nom de vos VMs chez vous

docker node update --label-add location=home centos-2.localdomain
docker node update --label-add location=home centos-2.localdomain
docker node update --label-add location=home centos-3.localdomain

le swarm est prêt à accueillir des conteneurs, voici un exemple avec la contrainte, mais il manque encore le reverse proxy http

docker service create --name test_bee --constraint 'node.labels.location == home' --network traefik-net --label traefik.frontend.rule=Host:test.fredix.xyz --label traefik.port=8080 fredix/test_bee

L’intérêt du swarm est qu’en cas de défaillance d’un node, le leader instanciera automatiquement tous ses conteneurs vers un autre node. Cependant on peut souhaiter désactiver un node manuellement pour mettre à jour la VM par exemple, ou remplacer un disque. Rien de plus simple, quelques commandes avant

on liste les nodes :

docker node ls
ID                           HOSTNAME              STATUS  AVAILABILITY  MANAGER STATUS
gxy553ef1eudvfwidxh95dqa9    centos-2.localdomain  Ready   Active        
nd0edqf6txl1zry90bh4c7f6j    centos-1.localdomain  Ready   Active        
vyfozvy9gmu1bjf7txdr0qk2y *  vps410678.ovh.net     Ready   Active        Leader
zjstkea2kdqql5ujsgyeau3jl    centos-3.localdomain  Ready   Active        

on se connecte en ssh sur le noeud choisi pour vérifier

root@centos-2 ~]# docker ps
CONTAINER ID        IMAGE                                                                                         COMMAND                  CREATED             STATUS              PORTS               NAMES
2b666e08291d        gogs/gogs@sha256:1d3b11cd430cee3d526286876b2b4bd5173d623a4af4af7ad03cb9ab11362b68             "/app/gogs/docker/..."   12 hours ago        Up 12 hours         22/tcp, 3000/tcp    gogs.1.ko7abchgwnssgrojy5944jt8a
4c6b98dd728c        fredix/nodecast.net@sha256:5ea18a33a4fc89b510af96777b63208c6cbb9380b3a659605c177a22af4caa00   "/bin/sh -c /app/n..."   12 hours ago        Up 12 hours         8080/tcp            nodecast.1.ihutzgl5bcaeel8zkpbm30aso
2ddbf7039029        fredix/test_bee@sha256:0e12268edae9dfceddcc099506df8707866cf97ca39967708ffbc8ce942afff1       "/bin/sh -c /app/t..."   12 hours ago        Up 12 hours         8080/tcp            test_bee.1.3wsotxc6094la8ww34397i8y8

on le désactive depuis le leader

docker node update --availability drain centos-2.localdomain

le noeud est passé en mode drain

docker node ls
ID                           HOSTNAME              STATUS  AVAILABILITY  MANAGER STATUS
gxy553ef1eudvfwidxh95dqa9    centos-2.localdomain  Ready   Drain         
nd0edqf6txl1zry90bh4c7f6j    centos-1.localdomain  Ready   Active        
vyfozvy9gmu1bjf7txdr0qk2y *  vps410678.ovh.net     Ready   Active        Leader
zjstkea2kdqql5ujsgyeau3jl    centos-3.localdomain  Ready   Active        

docker ps sur le noeud, au bout de quelques secondes il est vide

[root@centos-2 ~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

si on liste les services ils sont toujours là, mais déplacés

[root@vps410678 ~]# docker service ls
ID            NAME                MODE        REPLICAS  IMAGE
67qdbifaytkk  nodecast            replicated  1/1       fredix/nodecast.net:latest
6xymi9ok4p6z  traefik             replicated  1/1       containous/traefik:latest
nrx0e32un5eg  gogs                replicated  1/1       gogs/gogs:latest
ozk0527bxa1k  test_bee            replicated  1/1       fredix/test_bee:latest
vazbqubeofuf  hugo                replicated  1/1       fredix/hugo:latest

une fois l’action de maintenance effectuée sur le noeud on réactive le node

docker node update --availability active centos-2.localdomain

par contre les conteneurs déplacés restent à leur place, le node centos-2 reste vide à moins de lancer des conteneurs. On peut cependant forcer de les déplacer

docker service update --force nodecast

cela devrait forcer docker à déplacer le conteneur nodecast sur le node le moins chargé, donc centos-2

[root@centos-2 ~]# docker ps
CONTAINER ID        IMAGE                                                                                         COMMAND                  CREATED              STATUS              PORTS               NAMES
3e968837fa3c        fredix/nodecast.net@sha256:5ea18a33a4fc89b510af96777b63208c6cbb9380b3a659605c177a22af4caa00   "/bin/sh -c /app/n..."   About a minute ago   Up 54 seconds       8080/tcp            nodecast.1.s16q3biuiuafnrlv1885mun89

Dernière commande est pas des moindres. On a un conteneur lancé, mais entre temps l’image a été mise à jour par l’auteur, ou soit même. Rien de plus simple pour mettre à jour l’instance

docker service update --image fredix/hugo hugo

Ici je met à jour mon conteneur hugo qui utilise une image à moi. Docker relance tout seul mon conteneur mis à jour…

Une autre commande utile permet de passer un worker en leader

 docker node promote nom-du-node

ou l’inverse, passer un leader en worker

  docker node demote nom-du-node

Docker swarm possèdent de nombreuses commandes utiles dont je n’ai fais qu’un petit tour ici. Le grand concurrent de swarm est kubernetes. Il possède une interface web d’administration très complète, cependant je le trouve trop complexe et overkill pour un contexte en Auto-Hébergement comme ici. Mais comme souvent il est possible que je change d’avis.

Traefik

Traefik va nous permettre de rendre nos conteneurs accessible depuis Internet dynamiquement, mais il va de plus générer un certificat let’s encrypt automatiquement. Traefik va uniquement tourner sur le leader, et c’est lui qui exposera l’ip de l’hébergeur.

On doit lui créer un répertoire de travail et son fichier de configuration (j’utilise /traefik comme point de montage)

cat /traefik/etc/traefik.toml

traefikLogsFile = "/log/traefik.log"
logLevel = "WARNING"
defaultEntryPoints = ["http", "https"]
[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
      entryPoint = "https"
  [entryPoints.https]
  address = ":443"
    [entryPoints.https.tls]
entryPoint = "https"
[acme]
email = "fredix@protonmail.com"
#storageFile = "/certs/acme.json"
storageFile = "/etc/traefik/acme/acme.json"
entryPoint = "https"
acmeLogging = true
onDemand = false
OnHostRule = true
#[[acme.domains]]
# main = "swarm.fredix.xyz"
[[acme.domains]]
  main = "fredix.xyz"
  sans = ["www.fredix.xyz", "test.fredix.xyz", "gogs.fredix.xyz", "wallabag.fredix.xyz", "whoami0.fredix.xyz", "swarm.fredix.xyz", "pouet.fredix.xyz"]
[[acme.domains]]
  main = "nodecast.net"
  sans = ["www.nodecast.net"]
[web]
address = ":8080"
[docker]
domain = "traefik"
endpoint = "unix:///var/run/docker.sock"
watch = true
exposedbydefault = true

On voit ici la configuration de mes services, la doc est très complète https://docs.traefik.io/toml/

Il faut lui créer un réseau dédié

docker network create --driver=overlay traefik-net

puis on le lance on indiquant les volumes qu’il doit utiliser afin de ne pas perdre sa configuration et les certificats

docker service create --name traefik --constraint=node.role==manager -p 443:443 -p 80:80 -p 8080:8080 --mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock --mount type=bind,source=/traefik/etc/,target=/etc/traefik/ --mount type=bind,source=/traefik/log/,target=/log/ --mount type=bind,source=/traefik/etc/certs,target=/certs --network traefik-net  containous/traefik --docker --docker.swarmmode --docker.domain=traefik --docker.watch --web

il bind les ports 80,443 mais aussi le 8080. Ce port permet de consulter les différents conteneurs qu’il gère comme on peut le voir chez moi http://fredix.xyz:8080 , c’est interface n’est qu’en lecture.

On peut maintenant lancer un conteneur dans le swarm

docker service create --name test_bee --constraint 'node.labels.location == home' --network traefik-net --label traefik.frontend.rule=Host:test.fredix.xyz --label traefik.port=8080 fredix/test_bee

Ce conteneur est basé sur une image tout simple que j’ai créé et disponible sur https://hub.docker.com/r/fredix/test_bee/. Il suffit de remplacer Host:test.fredix.xyz par votre sousdomaine.domain.tld pour tester. Ce conteneur lance un processus en Go qui écoute sur le port 8080.

Le plus pénible est de devoir créer chez l’opérateur qui gère votre DNS un sous domaine par service, or il suffit de mettre un jocker * en type A vers l’IP de votre hébergeur pour que tous les sous-domaines soient résolu.
Au final il suffira de lancer un service par docker pour qu’il soit installé à domicile puis exposé automatiquement sur votre domaine.

Syncthing

cette partie est sans doute la plus pénible mais nécessaire pour résoudre le problème de réplication de vos données. En effet si les conteneurs peuvent être migré d’un node à l’autre, donc d’une machine (vm ou physique) à une autre, les données dans les volumes ne bougeront pas. Dans l’exemple si dessus test_bee ce n’est pas génant car aucune données n’est généré, mais pour un conteneur qui génère des fichiers ou utilise une base de données c’est fatal.
Il existe des solutions comme NFS, mais cela oblige à avoir un serveur NFS tout le temps disponible, de plus si l’on souhaite lancer un conteneur sur un node hors de chez soi, il devra accéder à ses données par le VPN, ce qui pourrait poser des problèmes de latence ou pire une indisponibilité si le lien VPN est coupé entre les workers.

J’ai choisi d’utiliser syncthing pour synchroniser les répertoires qui servent de volumes docker. Syncthing fonctionne en P2P, il peut se connecter entre chaque noeud via un serveur central qui les mets en relation, il peut aussi découvrir d’autres noeuds locaux afin de pouvoir les connecter facilement. Nul besoin de cela ici puisque l’on connait l’ip de nos VMs, on va les indiquer en dur et désactiver la découverte.

Sur des centOS il existe un rpm sur copr, à rajouter comme dépot

cat /etc/yum.repos.d/_copr_decathorpe-syncthing.repo
[decathorpe-syncthing]
name=Copr repo for syncthing owned by decathorpe
baseurl=https://copr-be.cloud.fedoraproject.org/results/decathorpe/syncthing/epel-7-$basearch/
type=rpm-md
skip_if_unavailable=True
gpgcheck=1
gpgkey=https://copr-be.cloud.fedoraproject.org/results/decathorpe/syncthing/pubkey.gpg
repo_gpgcheck=0
enabled=1
enabled_metadata=1


yum update
yum install syncthing

systemctl enable syncthing@root.service



cat /etc/systemd/system/multi-user.target.wants/syncthing\@root.service 
[Unit]
Description=Syncthing - Open Source Continuous File Synchronization for %I
Documentation=man:syncthing(1)
After=network.target
Wants=syncthing-inotify@.service

[Service]
User=%i
ExecStart=/usr/bin/syncthing -no-browser -no-restart -logflags=0
Restart=on-failure
SuccessExitStatus=3 4
RestartForceExitStatus=3 4

[Install]
WantedBy=multi-user.target

on start le service puis on le stop 2 secondes après

systemctl start syncthing@root.service
sleep 2
systemctl stop syncthing@root.service

cela pour qu’il génère ses certificats et son fichier de configuration dans /root/.config/syncthing/

 ls .config/syncthing/
cert.pem  config.xml  https-cert.pem  https-key.pem  index-v0.14.0.db  key.pem

Cette operation est à effecturer sur toutes les VMs locale. La VM louée ne stockera pas de volume. On édite ensuite le fichier config.xml
Dans ce fichier il faut indiquer les devices id des 3 VMs dans la section folder et aussi pour chaque device. Dans le champ adresse il faut indiquer l’ip de chaque VM et mettre à false tous les champs Announce. J’ai de plus diminué le rescanIntervalS à 5 afin qu’une modification/ajout d’un fichier sur un noeud soit répercuté rapidement sur les autres (syncthing-inotify ne semble pas être dispo sur centOS ce qui aurait évité un scan régulier). Le descendre plus bas risque d’augmenter la charge CPU, mais il y a surement du tuning à faire ici meilleur que le mien. Ici le chemin du répertoire à synchroniser est /sync. Il faudra donc lancer les conteneurs un indiquant que leurs volumes de données se trouvent sur /sync/nonduconteneur/
Remplacer VM1/2/3 par l’id unique généré par syncthing.

<configuration version="20">
    <folder id="sync" label="sync" path="/sync/" type="readwrite" rescanIntervalS="5" ignorePerms="false" autoNormalize="true">
    <device id="VM1" introducedBy=""></device>
    <device id="VM2" introducedBy=""></device>
    <device id="VM3" introducedBy=""></device>
    <minDiskFree unit="%">1</minDiskFree>
    <versioning></versioning>
    <copiers>0</copiers>
    <pullers>0</pullers>
    <hashers>0</hashers>
    <order>random</order>
    <ignoreDelete>false</ignoreDelete>
    <scanProgressIntervalS>0</scanProgressIntervalS>
    <pullerSleepS>0</pullerSleepS>
    <pullerPauseS>0</pullerPauseS>
    <maxConflicts>-1</maxConflicts>
    <disableSparseFiles>false</disableSparseFiles>
    <disableTempIndexes>false</disableTempIndexes>
    <fsync>false</fsync>
    <paused>false</paused>
    <weakHashThresholdPct>25</weakHashThresholdPct>
    <minDiskFreePct>0</minDiskFreePct>
    </folder>
    <device id="VM1" name="centos-1" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
    <address>tcp://192.168.0.50:22000</address>
    <paused>false</paused>
    </device>
    <device id="VM2" name="centos-2" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
    <address>tcp://192.168.0.51:22000</address>
    <paused>false</paused>
    </device>
    <device id="VM3" name="centos-3" compression="metadata" introducer="false" skipIntroductionRemovals="false" introducedBy="">
    <address>tcp://192.168.0.52:22000</address>
    <paused>false</paused>
    </device>
    <gui enabled="false" tls="false" debugging="false">
    <address>127.0.0.1:8384</address>
    <apikey>apikey</apikey>
    <theme>default</theme>
    </gui>
    <options>
    <listenAddress>tcp://192.168.0.50:22000</listenAddress>
    <globalAnnounceServer>default</globalAnnounceServer>
    <globalAnnounceEnabled>false</globalAnnounceEnabled>
    <localAnnounceEnabled>false</localAnnounceEnabled>
    <localAnnouncePort>21027</localAnnouncePort>
    <localAnnounceMCAddr>[ff12::8384]:21027</localAnnounceMCAddr>
    <maxSendKbps>0</maxSendKbps>
    <maxRecvKbps>0</maxRecvKbps>
    <reconnectionIntervalS>60</reconnectionIntervalS>
    <relaysEnabled>true</relaysEnabled>
    <relayReconnectIntervalM>10</relayReconnectIntervalM>
    <startBrowser>false</startBrowser>
    <natEnabled>false</natEnabled>
    <natLeaseMinutes>60</natLeaseMinutes>
    <natRenewalMinutes>30</natRenewalMinutes>
    <natTimeoutSeconds>10</natTimeoutSeconds>
    <urAccepted>0</urAccepted>
    <urUniqueID></urUniqueID>
    <urURL>https://data.syncthing.net/newdata</urURL>
    <urPostInsecurely>false</urPostInsecurely>
    <urInitialDelayS>1800</urInitialDelayS>
    <restartOnWakeup>true</restartOnWakeup>
    <autoUpgradeIntervalH>12</autoUpgradeIntervalH>
    <upgradeToPreReleases>false</upgradeToPreReleases>
    <keepTemporariesH>24</keepTemporariesH>
    <cacheIgnoredFiles>false</cacheIgnoredFiles>
    <progressUpdateIntervalS>5</progressUpdateIntervalS>
    <limitBandwidthInLan>false</limitBandwidthInLan>
    <minHomeDiskFree unit="%">1</minHomeDiskFree>
    <releasesURL>https://upgrades.syncthing.net/meta.json</releasesURL>
    <overwriteRemoteDeviceNamesOnConnect>false</overwriteRemoteDeviceNamesOnConnect>
    <tempIndexMinBlocks>10</tempIndexMinBlocks>
    <trafficClass>0</trafficClass>
    <weakHashSelectionMethod>auto</weakHashSelectionMethod>
    <stunServer>default</stunServer>
    <stunKeepaliveSeconds>24</stunKeepaliveSeconds>
    <defaultKCPEnabled>false</defaultKCPEnabled>
    <kcpNoDelay>false</kcpNoDelay>
    <kcpUpdateIntervalMs>25</kcpUpdateIntervalMs>
    <kcpFastResend>false</kcpFastResend>
    <kcpCongestionControl>true</kcpCongestionControl>
    <kcpSendWindowSize>128</kcpSendWindowSize>
    <kcpReceiveWindowSize>128</kcpReceiveWindowSize>
    <minHomeDiskFreePct>0</minHomeDiskFreePct>
    </options>
</configuration>

on peut lancer ensuite le service et vérifier qu’il se connecte aux autres syncthing

 systemctl start syncthing@root.service 
 systemctl status syncthing@root.service -l

il suffit de vérifier en faisant un touch /sync/toto et qu’il se répercute bien dans les autres VMs.

Pour finir ce roman voici comment je lance un conteneur gogs dans mon swarm

docker service create --name gogs --constraint 'node.labels.location == home' --network traefik-net --label traefik.frontend.rule=Host:gogs.fredix.xyz --label traefik.port=3000 --label traefik.backend=gogs --mount type=bind,source=/sync/gogs/,target=/data/ gogs/gogs

La syntaxe de montage des volumes en swarm est différente du docker classique, on voit bien ici que j’indique comme source /sync/gogs (répertoire que j’ai créé auparavant et répliqué par syncthing). Gogs peut utiliser un fichier sqlite à la place d’un sgbd, il stoke le fichier ici /sync/gogs/gogs/data/gogs.db

Quelque soit le node ou est lancé gogs, syncthing va synchroniser le répertoire de données /sync/gogs/ entre toutes les VMs, l’intérêt est que si swarm déplace le conteneur, ou que vous le relanciez à la main (docker service rm gogs, docker service create …) il y a des chances qu’il soit lancé sur un autre node. Grâce à syncthing il pourra retrouver ses données à jour.

Pour améliorer la résilience de notre infra, qui dépend après tout d’une ligne chez un FAI et d’une machine à domicile, on peut imaginer déposer une autre machine chez un proche de confiance. En la reliant au VPN et au swarm elle pourra recevoir des conteneurs si le leader n’arrive plus à joindre ses workers à domicile. On obtient alors un datacenter auto-hébergé réparti :)

Le prochain article parlera d’un service bien lourd et complexe à mettre en oeuvre, mastodon un twitter opensource et décentralisé. De part son architecture il se prête idéalement à une infrastructure auto-hébergée hybride.

quelques liens :

https://docs.traefik.io/user-guide/swarm-mode/
Traefik et Docker, le couple ultime !
Docker Swarm par l’exemple
http://blog.octo.com/tag/swarm/
http://jmkhael.io/traefik-as-a-dynamic-reverse-proxy-for-docker-swarm/
http://blog.wescale.fr/2017/01/04/tutoriel-infastructure-resiliente-et-scalable-avec-swarm-consul-et-traefik/