Docker swarm
Sources
Présentation
Docker swarm est l'orchestrateur de conteneur de Docker. Il est plutôt simple à mettre en place. Je vais ici le déployer en utilisant 3 machines:
- Mon PC comme manager
- swarm1.sq.lan et swarm2.sq.lan comme workers.
Sur les 3 machines, il faut:
- Installer Docker
- Ouvrir les ports 2377/tcp, 7946/tcp, 7946/tcp, 4789/udp
Mise en place, utilisation
Création du swarm, tokens
Sur mon manager:
docker swarm init --advertise-addr <IP du manager>
J'ai alors un retour qui me donne la commande à lancer sur les workers pour rejoindre le swarm.
To add a worker to this swarm, run the following command: docker swarm join --token SWMTKN-1-2sve1h1z0jlvx8x5nim1hjdcoysejx5gyegd1qyol8fhvu3z0c-dnqhw1fx70q9spxl92cqeajwj 192.168.1.70:2377
Je peux revoir cette commande à tout moment sur le manager avec docker swarm join-token worker (et utilise docker swarm join-token manager pour ajouter un manager). Je peux ajouter le flag --rotate pour faire tourner les tokens.
Je lance la commande de join sur mes workers. Je peux ensuite vérifier depuis le manager:
docker node ls
Nodes
Lister les nodes et leur état
docker node ls
Inspecter un node
docker node inspect --pretty swarm1
Drain un node
Consiste à vider un node de ses conteneurs.
docker node update --availability drain swarm1
Faire revenir un node
docker node update --availability active swarm1
Quitter le swarm
Depuis le node:
docker swarm leave
Supprimer un node
docker node rm monnode
Services (sans docker-compose)
Sans réseau
Je peux désormais lancer des services depuis mon manager. Sans docker-compose, la syntaxe est la suivante :
docker service create --replicas 1 --name helloworld alpine ping docker.com
- docker service : on gère les services
- create pour créer un service
- --replicas : en combien d'exemplaires le conteneur va tourner
- --name: On nomme le service
- alpine ping docker.com : le service en question.
inspecter le service, avoir son ID
docker service inspect --pretty monservice
Voir les erreurs du service
docker service ps --no-trunc <mon_service_id>
Voir où tourne le service
docker service ps monservice
Changer le scale d'un service
Je peux changer le nombre de replicas de mon service:
docker service scale SERVICE_ID=NOMBRE_REPLICAS
Je peux ensuite vérifier avec un docker service ps monservice
Supprimer un service
docker service rm monservice
Rolling update
Rolling signifie que les replicas seront mis à jour les uns après les autres. Il va arrêter un conteneur, l'arrête, l'update, le start, passer au suivant. Si il a un échec, il met en pause.
Je part du service:
docker service create --replicas 3 --name monredis --update-delay 10s redis:3.0.6
Je le mets en version suivante:
docker service update --image redis:3.0.7 monredis
Puis je vérifie avec inspect. Si l'update s'est mise en pause:
docker service update monredis
Il est possible de passer des arguments à docker service update; voir
docker service update --help
Après une update, la sortie de docker service ps monservice affiche les mises à jour.
Déployer avec un réseau (routing mesh)
Si tous les ports sont bien ouverts, on peut normalement disposer d'un "routing mesh", en réseau maillé entre nos noeuds afin de publier nos services.
La syntaxe:
docker service create --name truc --publish published=<port exterieur>,target=<port interne> IMAGE
Par exemple avec:
docker service create --name my-web --publish published=8080,target=80 --replicas 2 nginx
--publish=machintruc est la syntaxe longue, mais on peut raccourcir avec la syntaxe habituelle -p 8080:80
... mon conteneur nginx sera dispo sur le port 8080 de chacun de mes workers. En réalité, sur chaque worker, un load balancer est présent; quelque soit le worker auquel j'accède, il saura où renvoyer le trafic. Si l'IP est routable, le port est dispo de l'extérieur de l'hôte. Sinon, le port est dispo depuis l'intérieur de l'hôte. L'ensemble de ces lb forme un réseau maillé.
Je peux publier un port pour mon service à tout moment:
docker service update --publish-add published=<PUBLISHED-PORT>,target=<CONTAINER-PORT> <SERVICE>
Je peux vérifier mes ports:
docker service inspect --format="Modèle:Json .Endpoint.Spec.Ports" my-web
Firewalld me pose problème, mais ça marche en le désactivant. Je n'ai peut-être pas publié dans la bonne zone.
Si je ne donne pas de port published, Docker en choisit un au pif dans les ports dynamiques.
Choisir tcp / udp
Par défaut, les port sont publiés en tcp.
Je peux publier avec les deux protocoles:
docker service create --replicas 2 --publish published=8080,target=80,protocol=udp --publish published=8080,target=80 --name helloworld nginxdemos/hello
En gros, en version longue j'aurais
--publish published=8080,target=80,protocol=machin
Et en version courte
-p 8080:80/udp
Et je peux cumuler.
docker service create --replicas 2 -p 8080:80/udp -p 8080:80 --name truc image
Bypasser le routing mesh (modes de publish)
On peut bypasser le routing mesh pour toujours accéder au worker demandé plutôt que d'être routé. C'est le mode "host". Il faut garder à l'esprit:
- Sans routing mesh, je ne suis pas sûre de tomber sur un node qui fournit le service demandé.
- Si le node fait tourner plusieurs replicas, je ne peux pas trop choisir à quel port target je m'adresse. On peut alors laisser docker choisir le port publié, ou s'assurer qu'on a une seule instance par node.
Bref, la syntaxe:
docker service create --name dns-cache --publish published=53,target=53,protocol=udp,mode=host --mode global dns-cache
Choisir "ingress" plutôt que "host" correspondrait au mode avec le routing mesh.
Configurer un LB externe avec le routing mesh
On peut configurer un LB externe comme HAproxy pour publier nos services, en passant ou pas par le routing mesh. Avec le routing mesh, cela va ressembler à ça, ce qui n'as rien de bien étonnant:
...et j'aurais simplement plusieurs backends dans ma conf haproxy:
global log /dev/log local0 log /dev/log local1 notice ...snip... # Configure HAProxy to listen on port 80 frontend http_front bind *:80 stats uri /haproxy?stats default_backend http_back # Configure HAProxy to route requests to swarm nodes on port 8080 backend http_back balance roundrobin server node1 192.168.99.100:8080 check server node2 192.168.99.101:8080 check server node3 192.168.99.102:8080 check
Configurer un LB externe sans le routing mesh
Sans, il faudra configurer le service avec "--endpoint-mode dnsrr" plutôt que la valeur par défaut (vip pour virtual ip). Ainsi, docker fera des entrées DNS pour que la query DNS renvoie une liste d'adresses IP. Le client s'y connectera directement. J'ai pas tout compris.
Utiliser docker-compose
Le principe est le même, mais en utilisant la commande docker stack. Par exemple, en prenant le docker-compose suivant:
version: '3' networks: wikiinternal: external: false services: mediawiki: image: mediawiki:latest restart: always ports: - 8065:80 links: - mediawikidb deploy: replicas: 2 networks: wikiinternal: mediawikidb: image: mariadb:latest restart: always environment: MYSQL_DATABASE: mediawiki MYSQL_USER: mediawiki MYSQL_PASSWORD: eez7quaec7Ni MYSQL_RANDOM_ROOT_PASSWORD: 'no' MYSQL_ROOT_PASSWORD: qzkdngin0988 deploy: replicas: 2 networks: wikiinternal:
...sans me soucier des volumes pour l'instant, je peux en tout cas déployer mon wiki en utilisant la commande:
sudo docker stack deploy --compose-file docker-compose.yml wikitest
Il existe quelques configuration dispo dans docker-compose spécifiques au mode stack.
Configuration docker-compose spécifiques
Elles sont toutes dispo ici, et rentrer dans la catégorie "deployé (voir l'exemple ci-dessus).
Je vais les lister rapidement:
- endpoint_mode : choisir "vip" ou "dnsrr" pour le mode d'ingress.
- labels : on peut mettre des labels pour le service, comme on peut le faire pour un conteneur.
- mode: "global" (un conteneur par node) ou "replicated" (un certain nombre de conteneurs, mode par défaut).
- placement: choisir le placement des conteneurs. Complexe, voir la doc.
- max_replicas_per_node: Si repliqué, nombre de conteneur par node max.
- replicas: nombre de repliques demandées.
- resources: Déjà documenté dans ma page Docker-compose. Limiter le cpu / ram.
- restart_policy : choisir comment les conteneurs reviennent lors d'un exit. Se configure avec en sous-options:
- condition: "none", "on-failure", ou "any" (défaut). Condition pour relancer le conteneur.
- delay: En secondes, attente entre 2 restart. exemple : "5s"
- max_attempts (défaut : infini). Nombre de tentatives max par window (ci-dessous)
- window : fenetre de temps en secondes (exemple : "120s")
- rollback_config : Configurer comment le service doit rollback en cas d'échec de son update. Sous options:
- parrallelism : Nombre de conteneurs à rollback en même temps. 0 = tous
- delay : délai en secondes entre le rollback de deux conteneurs (défaut "0s")
- failure_action : si le rollback échoue ("continue" ou "pause" par defaut)
- monitor: Combien de temps, après chaque task, on vérifie la failure. Valeur temporelle ((ns|us|ms|s|m|h)), défaut 5s
- max_failure_ratio : Maximum de failures tolérés (défaut 0)
- order : pas compris
- update_config : Configuration des updates. Les sous-options sont les mêmes que pour rollback_config.
Un certain nombre d'options ne sont pas supportées par stack deploy:
- build (utilliser un registry !)
- cgroup_parent
- container_name
- devices
- tmpfs
- external_links
- links
- network_mode
- restart
- security_opt
- userns_mode
Volumes
Docker offre la possibilité de créer des volumes de plusieurs façons, par l'intermédiaire des drivers. Les drivers permettent de s'abstraire de la logique de FS pour, par exemple, proposer des volumes via NFS, ou un s3, etc.
Utiliser un driver de volume
À la création d'un volume, on peut spécifier un driver. Par exemple, la documentation donne l'exemple de SSHFS avec l'installation du plugin au préalable.
docker plugin install --grant-all-permissions vieux/sshfs docker volume create --driver vieux/sshfs \ -o sshcmd=test@node2:/home/test \ -o password=testpassword \ sshvolume
NFS
Je vais passer à NFS, celui qui m'intéresse le plus. NFSv3:
docker service create -d \ --name nfs-service \ --mount 'type=volume,source=nfsvolume,target=/app,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/var/docker-nfs,volume-opt=o=addr=10.0.0.10' \ nginx:latest
NFSv4
docker service create -d \ --name nfs-service \ --mount 'type=volume,source=nfsvolume,target=/app,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/var/docker-nfs,"volume-opt=o=addr=10.0.0.10,rw,nfsvers=4,async"' \ nginx:latest
Ce qui, dans un docker-compose, donne:
volumes: example: driver_opts: type: "nfs" o: "addr=10.40.0.199,nolock,soft,rw" device: ":/docker/example"
Voici un exemple complet et fonctionnel:
version: '3.2' services: notes: image: squi/sqnotes:1.0 ports: - '8080:8080' volumes: - type: volume source: notes target: /app/data volume: nocopy: true deploy: mode: replicated replicas: 3 restart_policy: condition: any delay: 10s max_attempts: 5 window: 60s volumes: notes: driver: local driver_opts: type: "nfs" o: "addr=192.168.1.200,rw,nfsvers=4" device: ":/k8s-data"
SAMBA
La doc donne aussi un example avec Samba:
docker volume create \ --driver local \ --opt type=cifs \ --opt device=//uxxxxx.your-server.de/backup \ --opt o=addr=uxxxxx.your-server.de,username=uxxxxxxx,password=*****,file_mode=0777,dir_mode=0777 \ --name cif-volume