Docker swarm

De Justine's wiki
Aller à la navigation Aller à la recherche

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

source

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