Dans un précédent post, nous avons vu comment mettre en place l'intégration continue sur un projet web Dans ce second post, nous allons voir comment faire en sorte que notre projet soit automatiquement déployé sur une infrastructure de type Swarm.

Contexte

Pour rappel, dans le précédent article sur le sujet nous avions configuré un projet dans l'objectif de mettre en place une intégration continue. A chaque commit, le runner générait une image docker de notre projet et la poussait sur un registry docker (public ou privé).
Pour cela nous avions utilisé le fichier de configuration gitlab-ci.yml qui permet de décrire les différentes étapes de notre intégration continue.

C'est ce même fichier que nous allons éditer et compléter afin de faire en sorte que notre image, fraichement buildée, puisse être déployée sur un environnement de type Docker Swarm

Pour rappel nous utilisions le fichier gitlab-ci.yml suivant :

image: docker:stable

variables:
  DOCKER_HOST: tcp://docker:2376/
  DOCKER_DRIVER: overlay2

services:
  - docker:dind

stages:
  - package

docker-build:
  stage: package
  script:
    - docker login -u gitlab -p ${CI_REGISTRY_TOKEN} ${CI_REGISTRY_HOST}
    - docker build --build-arg VERSION=${CI_COMMIT_TAG:-nightly} -t ${CI_REGISTRY_HOST}/${CI_BUILD_IMAGE}:${CI_COMMIT_TAG:-nightly} .
    - docker push ${CI_REGISTRY_HOST}/${CI_BUILD_IMAGE}:${CI_COMMIT_TAG:-nightly}
  tags:
    - docker

Livraison continue

Principe

Dans notre contexte actuel, la livraison continue n'est pas vraiment différente de l'intégration continue dans le sens où nous souhaitons faire exécuter des commandes docker à un docker-engine. La seule différence est que nous souhaitons faire exécuter ces commandes sur un docker-engine particulier (celui de notre Swarm).

Afin que cela soit possible, plusieurs solutions s'offrent à nous :

  • Faire en sorte que notre runner se connecte à distance au docker-engine de notre Swarm.
  • Mettre en place un runner directement dans notre Swarm afin de pouvoir intéragir directement avec le docker-engine

Le mode de déploiement peut quand à lui se faire service par service ou via une stack, définie par le biais d'un fichier compose. C'est d'ailleurs cette dernière solution que nous choisirons par la suite car elle permet d'utiliser la même commande que ce soit pour un premier déploiement ou pour une mise à jour.


Photo by James Pond / Unsplash

Ajout d'un runner

Nous allons choisir d'ajouter un runner dans notre swarm. Il est important qu'il soit déployé sur un manager car, pour rappel, seuls les managers sont habilités à déployer de nouveaux services sur les nodes qui composent le Swarm.

Pour cela nous allons reprendre la configuration et le référencement d'un runner que nous avons vu dans le précédent article (en version plus concise).
Créons dans un premier temps un script d'enregistrerment register.sh :

#!/bin/sh

docker run --rm -it \
    -v $(pwd)/config:/etc/gitlab-runner:rw \
    -v /var/run/docker.sock:/var/run/docker.sock \
    gitlab/gitlab-runner:latest register \
    --non-interactive \
    --executor "docker" \
    --docker-image alpine:3.8 \
    --docker-privileged \
    --url "https://<GITLAB_INSTANCE>/" \
    --registration-token "<GITLAB_TOKEN>" \
    --description "docker-runner-manager" \
    --tag-list "docker,manager,swarm" \
    --run-untagged \
    --locked="false"

Comme dans le précédent article, nous utlisons l'URL GITLAB_URL de notre gitlab et le token GITLAB_TOKEN de notre projet.
Nous avons ajouté deux nouveaux tag (manager et swarm) qui nous seront utiles par la suite afin de cibler le bon runner, en l'occurence celui qui est un manager dans notre swarm.

Voici un petit rappel sur la méthode utilisée pour lancer le runner après sa configuration :

#!/bin/sh

docker rm -f gitlab-runner
docker run -d --name gitlab-runner \
    -v $(pwd)/config:/etc/gitlab-runner \
    -v /var/run/docker.sock:/var/run/docker.sock \
    --restart always \
    --privileged \
    gitlab/gitlab-runner:latest

Etape de livraison

Maintenant que notre nouveau runner est en place, il faut modifier le fichier gitlab-ci.yml de notre projet afin de pouvoir y ajouter une nouvelle étape de pipeline et la configurer.

Nous avons donc le fichier de base suivant :

image: docker:stable

variables:
  DOCKER_HOST: tcp://docker:2376/
  DOCKER_DRIVER: overlay2

services:
  - docker:dind

stages:
  - package

docker-build:
  stage: package
  script:
    - docker login -u gitlab -p ${CI_REGISTRY_TOKEN} ${CI_REGISTRY_HOST}
    - docker build --build-arg VERSION=${CI_COMMIT_TAG:-nightly} -t ${CI_REGISTRY_HOST}/${CI_BUILD_IMAGE}:${CI_COMMIT_TAG:-nightly} .
    - docker push ${CI_REGISTRY_HOST}/${CI_BUILD_IMAGE}:${CI_COMMIT_TAG:-nightly}
  tags:
    - docker

Nous allons ajouter la nouvelle étape delivery dans les stages afin de pouvoir déclarer un nouveau job de livraison. Celui-ci sera le suivant :

docker-deploy:
  stage: delivery
  before_script:
    - unset DOCKER_HOST
    - unset DOCKER_DRIVER
  script:
    - docker login -u gitlab -p ${CI_REGISTRY_TOKEN} ${CI_REGISTRY_HOST}
    - docker stack deploy -c swarm-compose.yml --with-registry-auth ${CI_STACK_NAME}
  tags:
    - docker
    - manager
    - swarm
  only: 
    - master

A la différence de notre premier job qui avait pour objectif de construire l'image de notre projet en utilisant le service docker, nous avons besoin de nous connecter directement au docker-engine de notre manager.
Pour cela, nous supprimons les variables DOCKER_HOST et DOCKER_DRIVER afin que le runner utilise directement le socket local c'est à dire celui du manager.

Maintenant que notre docker-engine est configuré, nous pouvons exécuter les instructions de déploiement. Dans un premier temps nous allons nous connecter au registry afin d'avoir accès à notre image puis nous allons simplement déployer notre stack avec la commande docker stack deploy.

Ci-dessous une petite explication des paramètres :

  • -c swarm-compose.yml permet d'identifier le fichier compose, disponible à la racine de notre projet, qui contient la définition de nos services (Un site et une base de données par exemple)
  • --with-registry-auth permet de passer l'authentification à notre registry
  • ${CI_STACK_NAME} nous permet enfin de nommer notre stack. Les services qui le composent seront quant à eux nommés selon le pattern <stack-name>_<service-name>.

Comme nous avons également pu le faire dans le premier job, nous allons appliquer une contrainte sur la sélection de notre runner afin d'être sûr de cibler celui présent dans notre swarm. Pour cela nous avons appliqué les tags docker, manager et swarm que nous avions spécifié lors de la configuration du runner.

Finalement, afin de ne pas déployer de version instable, nous appliquons également un filtre sur la branche de notre dépôt git. De cette manière, le service ne sera déployé que s'il s'agit d'une modification de la branche master ce qui permet de limiter grandement le nombre de déploiement tout en s'assurant un minimum de la pérénité de notre code.

Références