déploiement continu avec drone sur ARM64

Cet article est une mise à jour du déploiement continu avec drone. Cette fois-ci l’idée est d’utiliser des serveurs en ARM64 disponible chez sacleway. J’utilise toujours une infra docker swarm comme présenté dans cet article auto-hébergement hybride mais à cause de certaines spécifités de l’ARM64 je vais repartir de zéro.

Scaleway

J’utilise des serveurs baremetal ARM64-2GB car pour le même prix qu’un VPS on a ici un quad core physique ce qui est bien plus efficace qu’une VM à mon sens. L’autre intérêt est de pouvoir se faire la main sur cette architecture afin de pouvoir y basculer chez soi sur des raspberry pi 3 ou équivalent en 64bits.

J’ai installé des CentOS 7 avec le kernel docker proposé. En effet il vaut mieux utiliser ce kernel plutôt que celui par défaut dans lequel il risque de manquer des modules nécessaires à Docker.
Mon infra est composée de 2 serveurs ARM64 et d’un X86-64 à la maison. Le premier ARM64 sera le manager docker, les 2 autres des workers. Avoir un noeud en x86-64 me semble indispensable car certaines images ne sont pas disponible sur ARM64 et sont difficilement portable sur une autre architecture, cela peut donc dépanner.

Docker

Sur la CentOS 7 la version de docker est très ancienne, il faut malgré tout l’installer avant de mettre à jour manuellement la dernière version. Cet article , Get started with Docker on 64-bit ARM, m’a servi de base pour mettre à jour Docker dans une version descente (à ce jour 17.05.0-ce).

mkdir -p sources/gits
cd sources/gits
git clone https://github.com/moby/moby
git checkout tags/v17.05.0-ce
make tgz

sortir prendre un café :)

La compilation de docker nécessite docker (sic), d’où l’intérêt d’avoir installé auparavant la version de CentOS. Un tgz a été généré ici

bundles/17.05.0-ce/tgz/linux/arm64/docker-17.05.0-ce.tgz

à décompresser dans un repertoire temporaire pour y trouver les binaires :

completion  docker-containerd      docker-containerd-shim  docker-init   docker-runc
docker      docker-containerd-ctr  dockerd                 docker-proxy

il vont remplacer la version en cours. Avant on purge docker de l’OS :

systemctl stop docker
yum remove docker*
rm -rf /var/lib/docker/

vérifier que tous les binaires ont bien été supprimé puis copier les nouveaux :

ls /usr/bin/docker*
cp bundles/17.05.0-ce/tgz/linux/arm64/docker-17.05.0-ce.tgz /tmp
cd /tmp && tar xvzf docker-17.05.0-ce.tgz 
cd docker
cp docker* /usr/bin/

installer les 2 fichiers service pour systemd :

/usr/lib/systemd/system/docker-storage-setup.service

[Unit]
Description=Docker Storage Setup
After=cloud-init.service
Before=docker.service

[Service]
Type=oneshot
ExecStart=/usr/bin/docker-storage-setup
EnvironmentFile=-/etc/sysconfig/docker-storage-setup

[Install]
WantedBy=multi-user.target

/usr/lib/systemd/system/docker.service

[Unit]
Description=Docker Application Container Engine
Documentation=https://docs.docker.com
After=network.target vpncloud@fredix.service

[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd
ExecReload=/bin/kill -s HUP $MAINPID
# Having non-zero Limit*s causes performance problems due to accounting overhead
# in the kernel. We recommend using cgroups to do container-local accounting.
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
# Uncomment TasksMax if your systemd version supports it.
# Only systemd 226 and above support this version.
#TasksMax=infinity
TimeoutStartSec=0
# set delegate yes so that systemd does not reset the cgroups of docker containers
Delegate=yes
# kill only the docker process, not all processes in the cgroup
KillMode=process

[Install]
WantedBy=multi-user.target

Ce fichier fait référence à vpncloud que j’utilise pour relier le swarm docker. Voir mon article précédent pour la config sauf si vous utilisez une autre solution. A savoir qu’il est nécessaire de désactiver le service firewalld qui n’est pas compatible avec swarm.

systemctl stop firewalld
systemctl disable firewalld

On peut maintenant lancer Docker et vérifier que tout fonctionne :

systemctl daemon-reload 
systemctl start docker
systemctl status docker
docker version
Client:
 Version:      17.05.0-ce
 API version:  1.29
 Go version:   go1.7.5
 Git commit:   89658be
 Built:        Thu Jun 29 19:21:31 2017
 OS/Arch:      linux/arm64

Server:
 Version:      17.05.0-ce
 API version:  1.29 (minimum version 1.12)
 Go version:   go1.7.5
 Git commit:   89658be
 Built:        Thu Jun 29 19:21:31 2017
 OS/Arch:      linux/arm64
 Experimental: false

l’indispensable hello world

docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
efe909565661: Pull complete 
Digest: sha256:07d5f7800dfe37b8c2196c7b1c524c33808ce2e0f74e7aa00e603295ca9a0972
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://cloud.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/engine/userguide/

Swarm

L’installation est à faire également sur chaque noeud. Pour celui en x86-64 il suffira d’installer le dépôt fourni par Docker pour la distribution. Chaque noeud doit pouvoir être relié en VPN. Pour relier les noeuds on utilisera les IPs privés.

192.168.254.1 (manager)
192.168.254.2 (worker 1 ARM64)
192.168.254.10 (worker 2 x86-64)

initialisation du swarm sur le manager

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

on demande la token pour connecter les workers

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

il suffit ensuite de lancer cette commande sur les workers pour connecter les noeuds.

on vérifie que tout est ok

docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
kkzcm1j43i8z2sdl4c6cimnwd     worker2              Ready               Active              
qiens4qo3og9bwmzby7vaqdyj     worker1              Ready               Active              
xy1riie2l28w9jamansynaxqb *   manager              Ready               Active              Leader

Enfin on tag les workers afin de lancer les images au bon endroit. Il serait génant de lancer une image ARM64 sur un noeud en x86-64…

docker node update --label-add location=cloud-x86 worker2
docker node update --label-add location=cloud-arm64 worker1

Drone server

l’infra est prête à recevoir notre outil de déploiement continu. Pour cela on va utiliser docker stack ; il permet de regrouper des services entre eux. On écrit le dockerfile qui va permettre de déployer notre stack drone :

drone-arm64v8.yml

version: '3'
services:
  drone-server:
    image: fredix/arm64v8-alpine-drone-server
#    image: drone/drone
    restart: always
    env_file: .env.production-server
    ports:
      - 8000:8000
      - 9000:9000
      - 80
      - 443
    volumes:
      - /docker_volumes/drone_server:/var/lib/drone/
    networks:
      - drone-infra
      - traefik-net
    deploy:
      placement:
        constraints:
          - node.labels.location == cloud-arm64
      labels:
        - "traefik.port=8000"
        - "traefik.docker.network=traefik-net"
        - "traefik.frontend.rule=Host:drone.fredix.xyz"

  drone-agent:
#    image: fredix/arm64v8-alpine-drone-agent:0.8.2
    image: drone/agent:linux-arm64
    restart: always
    env_file: .env.production-agent
    command: agent
    depends_on:
      - drone-server
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /docker_volumes/drone_agent/drone.key:/drone.key
    networks:
      - drone-infra
    deploy:
      placement:
        constraints:
          - node.labels.location == cloud-arm64

  drone-wall:
    image: drone/drone-wall
    restart: always
    ports:
      - "80"
    networks:
      - drone-infra
      - traefik-net
    deploy:
      placement:
        constraints:
          - node.labels.location == cloud-x86
      labels:
        - "traefik.port=80"
        - "traefik.docker.network=traefik-net"
        - "traefik.frontend.rule=Host:drone-wall.fredix.xyz"

networks:
  traefik-net:
    external: true
  drone-infra:
    external: true

On retrouve 2 informations vues précédement : cloud-x86 et cloud-arm64. Ici j’impose que les conteneurs qui vont lancer les services drone-server et drone-agent soient lancés sur le noeud worker1 (tagué cloud-arm64). Le service drone-wall sera lancé sur un x86-64 car j’ai eu la flemme de faire une image ARM64.
A noter que j’utilise une image personnelle du drone serveur car à ce jour il n’existe pas de version officielle pour ARM64. Vous pouvez cependant utiliser l’image drone/drone sur un serveur amd64, car le serveur ne fait qu’afficher l’interface web et lancer les travaux au noeud agent. Pour ce dernier j’utilise l’image officielle drone/agent:linux-arm64 car mon image plante toutes les 60s pour une raison inconnue (le dockerfile https://github.com/fredix/dockerfile/blob/master/drone/Dockerfile.agent.alpine.arm64v8).

Drone agent

drone-agent va recevoir les tâches du drone-server. Ces tâches sont écrites dans un fichier .drone.yml déposé à la racine du dépôt git du projet à déployer. Le fichier décrit un pipeline que l’agent devra dérouler :

platform: linux/arm64

clone:
  default:
    image: plugins/git:linux-arm64
    depth: 50

pipeline:
  publish:
    image: plugins/docker:linux-arm64
    repo: fredix/arm64v8-blog
    tags: latest
    dockerfile: Dockerfile.arm64
    secrets: [ docker_username, docker_password ]
  ssh:
    image: fredix/arm64v8-alpine-drone-ssh
    host: 192.168.254.1
    port: 22
    username: drone
    volumes:
      - /docker_volumes/drone_agent/drone.key:/root/ssh/drone.key
    key_path: /root/ssh/drone.key
    script:
      - "sudo docker service update --image fredix/arm64v8-blog hugo-arm64"
    when:
      status: success
  telegram:
    image: fredix/arm64v8-alpine-drone-telegram
    token: $PLUGIN_TOKEN
    to: $PLUGIN_TO
    secrets: [ plugin_token, plugin_to ]
    message: >
      {{#success build.status}}
        build {{build.number}} succeeded on {{repo.name}}. Good job {{build.author}}  {{build.link}}
      {{else}}
        build {{build.number}} failed on {{repo.name}}. Fix me please {{build.author}}  {{build.link}}
      {{/success}}
    when:
      status: [ success, failure ]

Après de nombreux tatonnement dus entre autre à la version de drone 0.8.2 et des plugins, ce pipeline fonctionne. La première ligne demande à drone de déployer la tâche à un agent linux/arm64. En l’abscence de cette ligne il mettra par défaut linux/amd64 et la tâche restera indéfiniement en pending.

git

Ensuite on demande à l’agent de faire un clone git du projet. Pour cela il utilise un plugin drone. Il faut bien comprendre que l’agent drone tournant sur un ARM64 il aura besoin de lancer des plugins compilés pour cette architecture. Le dépôt https://hub.docker.com/r/plugins/ propose un ensemble de plugins drone mais ne sont pas forcement tous disponible pour ARM64. Par chance les 2 premiers le sont.

docker

Une fois le clone du dépôt effectué, on demande à l’agent de builder l’image et de la publier. On utilise un plugin docker. L’agent se connecte à la socket du serveur docker local. Grâce à ce plugin il va créer une image docker alors qu’il tourne lui même dans Docker (DockerInDocker). On lui donne le nom du dépôt, le tag, le dockerfile à utiliser et les login/pass du compte hub.docker pour la publication ( à ne pas mettre en clair dans ce fichier, pour cela on utilise les secrets de drone, voir plus bas). Pendant cette étape on voit un conteneur en cours d’execution :

7d0b242a2861        plugins/docker:linux-arm64

avec un docker logs 7d0b242a2861 on peut observer la contruction de l’image et sa publication.

ssh

L’agent passe ensuite à l’étape ssh. J’ai construit ma propre image du plugin https://github.com/appleboy/drone-ssh. Cette étape permet de forcer la mise à jour de l’image en cours d’exécution sur le swarm. Docker télécharge alors depuis le hub.docker.com l’image que l’agent vient de publier, puis met à jour à chaud cette image. Pour cela j’ai créé un utilisateur drone sur le manager Docker, qui a les droits d’executer la commande docker via sudo. J’ai déposé sa clé privée sur le serveur ARM64 ou tourne l’agent, la drone.key. Le plugin peut ainsi se connecter en ssh sur le manager et lancer la commande sudo docker service update –image fredix/arm64v8-blog hugo-arm64.

Dockerfile : https://github.com/fredix/dockerfile/tree/master/drone-ssh

telegram

pour finir j’ai contruis ma propre image du plugin telegram https://github.com/appleboy/drone-telegram. Grâce à ce plugin je peux recevoir en temps réel le statut du déploiement (il faut ajouter dans les secrets de drone la token et l’id du destinaire, voir plus bas).

Dockerfile : https://github.com/fredix/dockerfile/tree/master/drone-telegram

Docker network

on notera que j’utilise 2 réseaux Docker. Le premier est lié à traefik, mon reverse proxy. Il est nécessaire afin que traefik puisse relier le domaine vers le conteneur du drone serveur. Le deuxième qui est drone-infra, est un réseau créé par docker spécifique à drone, ce qui permet à la stack drone d’avoir son propre réseau interne dédié. Pour le créer il suffit de lancer sur le manager :

docker network create --driver=overlay --attachable drone-infra

dockerfile

Pour mettre en place votre propre infra drone vous pouvez soit utiliser mes images, soit construire les votres, dans ce cas voici mes dockerfile : https://github.com/fredix/dockerfile

Variables environnement

Le fichier drone-arm64v8.yml utilise 2 fichiers de variables d’environnement :

env_file: .env.production-server
env_file: .env.production-agent

Il faut créer ces 2 fichiers dans le répertoire où vous allez lancer le docker stack deploy et indiquer différentes informations, tel que le serveur git à utiliser. ces 2 fichiers sont disponible ici en version d’exemple : https://github.com/fredix/swarm/tree/master/drone

Dans le fichier .env.production-server on indique l’url de notre serveur drone, le compte admin, le serveur git à utiliser (github, gogs, gitea, …) avec les tokens, ainsi qu’un secret à partager avec l’agent.

Dans le fichier .env.production-agent le drone secret identique au serveur ainsi que la plateform sur lequel il tourne.

Déploiement setup

A la première connexion sur l’interface web du serveur drone (indiquée dans DRONE_HOST) le serveur fait une connexion oauth2 vers le serveur git choisis. On autorise l’accès à ses dépôts puis on sélectionne les dépôts que l’on souhaite être géré par drone. A ce moment là drone ajoute une webkook dans notre projet sur github.

Dans l’interface web de drone il faut écrire les secrets qui sont utilisés dans le fichier .drone.yml :

    docker_username, docker_password
    plugin_token, plugin_to

La première ligne permet au drone agent de se connecter au hub.docker.com pour y publier l’image quil a construite.

plugin_token correspond à une token fourni par Telegram lorsque vous créez un bot. Le bot @botfather vous indiquera comment faire. plugin_to correspond à l’ID telegram du destinaire qui recevra les messages de build. Pour obtenir le votre il suffit d’interroger le bot @idbot.

On peut maintenant déployer notre stack :

docker stack deploy --compose-file=drone-arm64v8.yml drone-arm64

et vérifier sa bonne exécution :

docker stack ps drone-arm64

la supprimer si nécessaire :

docker stack rm drone-arm64

Déploiement workflow

Le workflow est le suivant. A chaque git push vers github (ou autre) une webhook vers l’api de drone serveur va se déclencher. Ce dernier parse le fichier .drone.yml puis transmet le pipeline à l’agent drone. Celui-ci informe constament le serveur de son avancé ce qui permet de surveiller sur l’interface web les différents steps.

En résumé l’agent drone exécute :

  • git clone du dépôt
  • création d’une image docker (l’agent qui tourne lui même dans Docker, utilise le plugin Docker in Docker)
  • pousse l’image générée vers le registre en utilisant le login / pass stocké dans le serveur drone (secret)
  • l’agent se connecte en ssh via le plugin drone-ssh vers mon docker manager et demande une mise à jour de l’image docker hugo-arm64 qui est lancée
  • Docker manager télécharge la nouvelle image puis relance le conteneur qui est quelque part dans le swarm
  • l’agent utilise le plugin drone-telegram pour m’envoyer une notification de succès ou échec

Drone cli

Drone propose en plus de l’interface web un client en console : http://docs.drone.io/cli-installation/
Ce dernier permet de consulter les builds en cours et de les stopper, ce que ne permet pas l’interface web (sic).

Mise en garde

Malgré mes nombreux tests je constate que des jobs risquent de rester en pending indéfiniment.. Le contournement est pour l’instant de relancer la stack docker, (docker stack rm drone-arm64 / docker stack deploy –compose-file=drone-arm64v8.yml drone-arm64). Je ne sais pas si c’est un problème de l’architecture ARM64, mais cela pourrait être lié à la recente migration du code de drone qui utilise maintenant grpc pour la communication entre le serveur et les agents ( issue GRPC Health Checks ).

A suivre

Pour l’instant j’utilise drone pour déployer automatiquement une mise à jour de mon blog. J’utilise ce dockerfile https://github.com/fredix/fredix.xyz/blob/master/Dockerfile.arm64 qui télécharge tous les fichiers statiques de mon blog. Une autre solution plus élégante serait d’utiliser ansible pour synchroniser mes fichiers markdown de mon blog vers un volume docker de mon serveur. Ainsi la mise à jour du conteneur ne se ferait uniquement que pour une mise à jour de hugo et non pas pour chaque modification/ajout de texte.

Je souhaite utiliser drone sur d’autres projets personnels dans lesquels j’utiliserais sans doute des fonctionnalités plus avancées.

Sources