DevOps

DevOps : mettre en place le déploiement continu avec Docker, Ansible et CircleCi

Temps de lecture : 7 minutes

Le déploiement continu (Continous deployment / CD en anglais), permet de livrer au fil de l’eau, de manière automatique, les nouvelles fonctionnalités développées par les équipes. C’est souvent la dernière étape de la chaîne d’intégration continue, elle intervient que si les phases précédentes sont passées avec succès (tests, build…).

L’automatisation du déploiement permet de gagner un temps précieux sur la chaîne de livraison de l’application. L’autre avantage est que chaque modification est aussitôt disponible, par exemple sur la plateforme de test, ce qui garantit un feedback instantané. Les bugs sont décelés au plus vite, l’utilisateur peut tester au fur et à mesure et proposer ses retours en continu.

Nous verrons dans cet article comment mettre en place facilement le déploiement continu sur un projet hébergé sur Scaleway grâce à CircleCI, Ansible et Docker.

Tous les fichiers décrits sont disponibles sur github, un lien est fourni pour chacun d’eux.
C’est parti !


Prérequis

Nous aurons besoin d’un compte Scaleway avec un registry docker attitré, qui nous permettra de pousser les images docker que nous construirons.

L’arborescence des différents fichiers auxquels nous ferons référence est la suivante :

Nous partirons du principe que le service web tourne déjà sur un serveur de staging, sur lequel docker et docker-compose sont installés. Un fichier docker-compose.yml situé sur le serveur, dans /etc/docker/, paramètre le service. Le voici :

Fichier docker-compose.yaml

version: "3.5"
services:
  backend:
    image: rg.fr-par.scw.cloud/thetribe/backend:latest
    restart: unless-stopped
    container_name: backend
    ports: - "8080:8080"
    environment: - NODE_ENV=staging

Détaillons un peu.

  • image décrit l’image que le service backend utilisera, c’est celle que nous allons construire plus bas.
  • restart la politique de relance du service
  • container_name paramètre le nom du conteneur
  • ports: 8080:8080 bind le port 8080 du conteneur au port 8080 de la machine

Comme nous l’avons vu, le docker-compose se base sur une image construite par un fichier Dockerfile qui fait office de «recette de cuisine».

Pour construire notre image, on effectuera l’opération en deux étapes, la création d’une image de build, puis la création de l’image finale. On utilisera le multi-stage build de docker pour construire une image optimisée.

Le principe est d’effectuer le build de l’application dans la première image, puis de ne garder que l’application buildée dans l’image finale. Cette dernière sera donc libérée de toutes les dépendances nécessaires au build et sera donc plus légère.

  1. Construction de l’image de build

Fichier Dockerfile

# ----- build image -----

FROM node:14.15.0-alpine AS build-env

WORKDIR /app

COPY . /app
RUN yarn install
RUN yarn build

RUN mkdir tmp 
&& cp -r build 
package.json 
tsconfig.json 
yarn.lock 
tmp

On copie les sources locale dans /app, puis on lance les commandes d’installation yarn install & yarn build. On termine en copiant l’application buildée et ses dépendances dans un dossier tmp.

  1. Construction de l’image finale
# ----- finale image -----

FROM node:14.15.0-alpine

WORKDIR /app
LABEL maintainer="Tommy Alexandre <tommy.alexandre@thetribe.io>"
COPY --from=build-env /app/node_modules /app/node_modules
COPY --from=build-env /app/tmp /app
EXPOSE 8080

USER node
ENTRYPOINT [ "yarn" ]
CMD [ "start" ]

On copie le dossier tmp de l’environnement précédent dans /app, puis on lance la commande yarn start pour lancer le service.

C’est terminé pour la construction de l’image ! Nous pouvons maintenant passer au paramétrage de Circle.

Paramétrage de Circle CI

Nous avons besoin d’effectuer quelques paramétrages sur Circle, notamment pour récupérer les clés d’accès que nous utiliserons pour le déploiement.

Ajout de la clé SSH du serveur de staging à Circle

  • Aller dans project settings
  • Onglet SSH keys
  • Additional SSH keys
  • Add SSH key

Nous allons ici ajouter la clé SSH permettant de se connecter au serveur de staging. Récupérons la fingerprint, nous l’utiliserons plus tard.

Ajout de la Secret Key Scaleway

  • Aller dans project settings
  • Environment variables
  • Add Environment Variable

Ajouter la variable SCW_SECRET_KEY correspondante à la clé secrète scaleway permettant de se connecter au registry docker de Scaleway.

Cette clé peut être trouvée sur Scaleway (https://console.scaleway.com/project/credentials, clé d’API)

À partir de maintenant, Circle possède toutes les clés nécessaires au déploiement.

Mise en place du déploiement continu

Le déploiement continu se fera intégralement dans la CI. Voici les étapes :

  • Construction (build) de l’application

On construira l’image grâce au Dockerfile et on la poussera sur le registry docker.

  • Déploiement sur la plateforme de test

On lancera un script ansible qui tirera la nouvelle image sur le serveur de staging, puis relancera les services.

C’est parti !

Construction (build) de l’application

Dans le fichier de configuration Circle Ci (.circleci/config.yml), ajoutons la première étape.

Commençons par décrire les executors.

Fichier config.yml

version: "2.1"
executors:
  node:
    docker:
      - image: circleci/node:14.16.0
  ubuntu:
    machine:
      image: ubuntu-2004:202010-01

Maintenant rajoutons notre job, build-docker-image.

jobs:
  build-docker-image:
    docker:
      - image: circleci/node:14.15.0
    working_directory: ~/project
    steps:
      - setup_remote_docker:
          version: 19.03.13
      - run: docker login rg.fr-par.scw.cloud/thetribe -u nologin -p $SCW_SECRET_KEY # ICI VALUER LE BON REGISTRY DOCKER
      - run:
          command: docker build --tag rg.fr-par.scw.cloud/thetribe/backend:latest latest . && docker push rg.fr-par.scw.cloud/thetribe/backend:latest
          no_output_timeout: 30m

Ce job, comme son nom l’indique, va construire l’image docker de notre service et la pousser sur le registry scaleway.

Détaillons le un peu.

    docker:
      - image: circleci/node:14.15.0

On dit à Circle de se baser sur une image docker de type circleci/node (qui correspond à une image officielle de Circle) pour effectuer le job.

    steps:
      - setup_remote_docker:
          version: 19.03.13

Ce premier step, un peu particulier, permet de faire du docker in docker. Comme on a dit à circle d’effectuer le step dans une image docker, et qu’on veut lancer des commandes docker, il est nécessaire de spécifier l’instruction setup_remote_docker. (Attention à bien remplacer le registry docker !)

- run: docker login rg.fr-par.scw.cloud/thetribe -u nologin -p $SCW_SECRET_KEY

Ce second step n’a rien de compliqué, on lance la commande docker login avec en paramètre la clée scaleway qu’on a défini plus haut. La variable $SCW_SECRET_KEY est connue de Circle car nous l’avons ajouté dans les paramètres du projet.

- run:
          command: docker build --tag rg.fr-par.scw.cloud/thetribe/backend:latest latest . && docker push rg.fr-par.scw.cloud/thetribe/backend:latest

On build l’image docker avec le tag latest puis on la pousse sur le registry docker.

Pour terminer, on décrit l’appel du job dans les workflows de Circle.

workflows:
  version: "2"
  build:
    jobs:
      - build-docker-image:

C’est tout bon !

À chaque commit, le job build-docker-image sera appelé, construira et poussera une image docker sur le registry scaleway.

Déploiement sur la plateforme de test

Maintenant, il nous reste à effectuer la dernière étape : faire en sorte que le serveur de staging tire la nouvelle image pour mettre à jour le service. Mettons en place le script ansible de déploiement.

Nous aurons besoin :

  • D’un fichier d’inventaire
  • D’un playbook
  • D’un rôle « deployment »

Le fichier d’inventaire (ansible/staging) :

Fichier staging

[staging:children]
web_server

[web_server]
163.172.214.134 ansible_user=root ansible_become=yes # ICI VALUER LA BONNE IP

Ce fichier d’inventaire va tout simplement définir l’ip de notre serveur de staging. Les paramètres ansible_user=root et ansible_become=yes spécifie que l’utilisateur utilisé sera root et qu’on autorise au script de passer en mode root quand il en a besoin.

Le playbook (ansible/deployment.yaml) :

Fichier deployment.yaml

---
- hosts: web_server
  roles:
      - role: deploy-docker
  vars:
      docker_compose_path: /etc/docker/
  vars_files:
      - vars/scaleway_credentials.yml

Ce playbook est également assez simple.

  • Il définit l’host web_server que nous avons décrit dans le fichier d’inventaire
  • Il spécifie le role deployer-docker qu’on va écrire plus bas
  • Il définit une variable (le path du docker-compose sur le serveur), ainsi qu’un fichier de variables contenant les informations de login au registry scaleway. Pour le moment, ce fichier est vide. Nous viendrons le remplir dans le job Circle.

Le role (ansible/roles/deploy-docker/tasks/main.yaml) :

Fichier main.yaml

---
- name: Log into scaleway docker registry
  command: docker login {{ scaleway_registry }} -u {{ scaleway_user }} -p {{ scaleway_token }}
  no_log: true

- name: Pull docker images
  command: docker-compose pull
  args:
    chdir: "{{ docker_compose_path }}"

- name: Log out of any docker registry
  command: docker logout
  no_log: true

- name: Deploy services
  command: docker-compose up -d
  args:
    chdir: "{{ docker_compose_path }}"

- name: Remove old image
  command: docker image prune -a -f

Le rôle en lui-même ! C’est là que tout se joue 😉 

Ce rôle contient cinq étapes :

  • La connexion au registry docker (utilise les variables définies plus haut)
docker login {{ scaleway_registry }} -u {{ scaleway_user }} -p {{ scaleway_token }}
  • Le pull de la nouvelle image (avec en variable le path du docker-compose)
docker-compose pull
  • La déconnexion du registry docker
docker logout
  • Le déploiement du nouveau service
docker-compose up -d
  • La suppression de l’ancienne image
docker image prune -a -f

C’est terminé pour la partie Ansible. Notre rôle est prêt à déployer une nouvelle image sur notre serveur de staging.

Une fois la configuration Ansible mise en place, il ne reste plus qu’à ajouter l’étape de déploiement dans la CI (.circleci/config.yml) à la suite de l’étape de construction de l’image.

Créons notre nouveau job :

deploy-staging:
    docker:
      - image: ansible/ansible:ubuntu1604
    working_directory: ~/project
    environment:
        ANSIBLE_HOST_KEY_CHECKING: false

Ajoutons les steps un par un.

Step 1 :

    steps:
      - add_ssh_keys:
          fingerprints:
            - "91:76:19:c1:05:1b:09:71:03:56:31:de:a7:3b:c6:b1" # ICI VALUER LA BONNE FINGERPRINT

On ajoute la fingerprint de la clé ssh de notre serveur de test (celle que nous avons définie plus haut) de manière à permettre à Circle de se connecter au serveur.

Step 2 :

      - run:
          name: Create var folder
          command: "mkdir ansible/vars"

Ce step va créer le dossier ansible/vars, dans lequel nous allons pousser nos variables de configuration Ansible (les variables de connexion au registry docker)

Steps 3 : 

      - run:
          name: Set scaleway registry
          command: 'echo -e "---nscaleway_token: $SCW_SECRET_KEY nscaleway_user: nologin nscaleway_registry: rg.fr-par.scw.cloud/thetribe " > ansible/vars/scaleway_credentials.yml'

On écrit les bonnes valeurs dans nos variables de configuration. La valeur du scaleway_token est directement récupérée des variables d’environnement que nous avons ajoutées dans la configuration du projet Circle.

Step 4 :

      - run:
          name: Install ansible
          command: python -m pip install pip==20.1.1 && python -m pip install ansible && python -m pip install jmespath

On installe ansible

Step 5 :

      - run:
          name: Deploy to staging
          command: cd ansible && ansible-playbook -i staging deployment.yml

On lance le déploiement grâce à la commande ansible-playbook. L’option -i permet de spécifier l’inventaire.

Il ne nous reste plus qu’à ajouter notre job à la liste des workflows, et de spécifier le paramètre requires pour dire qu’il dépend du job build-docker-image.

workflows:
  version: "2"
  build:
    jobs:
      - build-docker-image:
      - deploy-staging:
          requires:
            - build-docker-image

Le tour est joué !

Au prochain commit, l’intégration continue construira la nouvelle image, la poussera sur le registry docker, et déclenchera le pull et la relance des services sur la plateforme de test.

C’est une bonne base pour mettre en place le déploiement continu, et cela est déjà bien suffisant pour gagner un temps précieux en déploiement à chaque fois qu’une modification est apportée au projet.

Bien sûr, il est toujours possible d’optimiser les choses. On pourrait par exemple prévoir un système de gestion de version, mettre en place des règles filters dans la CI pour ne déployer que lors d’un merge sur develop ou sur master, construire une infrastructure de test à la volée avec Terraform… 

Tout est possible grâce au DevOps

Tommy Alexandre
Lead developer @theTribe