Mise en place CI/CD Docker et Dockerhub

Aujourd’hui, nous allons aborder le sujet de la mise en place du CI/CD avec Docker. Mais avant toute chose, de quoi s’agit-il ?

CI/CD est l’acronyme pour Intégration Continue / Livraison et Déploiement Continu. CI/CD est une approche qui permet aux entreprises d’améliorer leurs produits rapidement en mettant l’accent sur la mise en production rapide de petits changements incrémentiels. Conformément à la pratique de CI, les équipes s’efforcent de mettre en place une approche d’intégration en continu.

Un peu de contexte

Dans un monde où la compétition est de plus en plus rude, une véritable course contre la montre se met en place. C’est à quelle sera l’entreprise qui réduira son “time to market” pour sortir son application avant ses concurrents et avec de meilleure performance ?

Tout cela bien sûr en cherchant toujours à offrir la meilleure expérience utilisateur possible.

Autant de contraintes qui impliquent pour les entreprises des projets informatiques de plus en plus importants et de plus en plus coûteux.

Les process CI/CD

C’est dans ce contexte que s’inscrit l’approche CI/CD, qui est un réel avantage concurrentiel pour les entreprises qui la mettent en place. Les bénéfices sont multiples :

  • Livrer des applications avec moins de risques en réduisant l’introduction de bogues. Les pipelines CI / CD standardisent les processus de publication de tous leurs projets en testant automatiquement chaque changement dans le code source.
  • Livrer de nouvelles fonctionnalités plus fréquemment en optimisant vos processus et en supprimant les obstacles à la productivité.
  • Améliorez la productivité des développeurs. Les équipes d’ingénieurs qui ne pratiquent pas le CI / CD travaillent souvent sous stress. Il y a des incendies constants pour les mauvais déploiements et les pannes difficiles à réparer. Les développeurs écrivent beaucoup de code qui n’est jamais utilisé. Les branches de fonctionnalités à longue durée de vie sont trop grandes pour obtenir un examen correct par les pairs, de sorte que le code se dégrade en qualité. D’un autre côté, CI / CD guide la gestion des produits pour optimiser l’impact utilisateur. Les développeurs déploient du code alors qu’il est frais dans leur esprit. Le résultat est une équipe d’ingénieurs heureuse. 😉

En résumé l’approche CI / CD ce sont des cycles de déploiement rapide qui conduisent à des mises à jour moins risquées et plus fréquentes, ce qui conduit enfin à un apprentissage plus rapide et à un feedback plus fréquent et rapide des utilisateurs.

Et Docker dans tout ça ?

Docker est un outil open source qui a été créé pour faciliter la création, le déploiement et l’exécution d’applications à l’aide de conteneurs. Les conteneurs permettent à un développeur de regrouper une application avec toutes les parties dont il a besoin, telles que des bibliothèques et autres dépendances, et de la déployer en un seul package.

Ce faisant, grâce au conteneur, le développeur peut être assuré que l’application s’exécutera sur toute autre machine Linux, quels que soient les paramètres personnalisés de cette machine qui pourraient différer de la machine utilisée pour écrire et tester le code.

L’utilisation de Docker avec une approche CI/CD

Nous allons maintenant passer à la pratique et voir :

  • Un test unitaire simple pour une application en Python avec le framework web Flask
  • Comment implémenter un pipeline CI / CD dans la base de code à l’aide d’un fichier de configuration CircleCI dans le projet
  • La création d’une image Docker
  • Comment pousser l’image Docker vers Docker Hub
  • Le lancement d’un script de déploiement qui exécutera l’application dans le conteneur Docker sur un serveur Digital Ocean

L’approche CI/CD avec Docker : Les Prérequis

Pour commencer il vous faudra :

  • Un compte Docker Hub
  • Définir les variables d’environnement du projet qui spécifient votre nom d’utilisateur et votre mot de passe Docker Hub dans le tableau de bord CircleCI.
  • Un accès SSH à un serveur cloud. Vous pouvez ajouter une clé SSH à votre compte via le portail CircleCI. Dans cet exemple on utilisera un serveur Digital Ocean, mais vous pouvez utiliser le serveur / fournisseur cloud que vous souhaitez.
  • Créer un script de déploiement sur le serveur hôte qui sera utilisé pour déployer cette application. Voici un exemple de script de déploiement deploy_app.sh.

Une fois que vous avez tous les prérequis, nous pouvons passer à la suite !

L’Application Flask Python

Dans cet exemple, nous utiliserons un simple Python Flask, vous pouvez trouver le code source complet de ce projet ici et git clone en local. L’application est un simple serveur Web qui affiche du code HTML lorsqu’une demande lui est adressée. L’application Flask réside dans le fichier hello_world.py :

Code :

from flask import Flask app = Flask(__name__) def wrap_html(message): html = """ <html> <body> <div style='font-size:120px;'> <center> <image height="200" width="800" src="https://infosiftr.com/wp-content/uploads/2018/01/unnamed-2.png"> <br> {0}<br> </center> </div> </body> </html>""".format(message) return html @app.route('/') def hello_world(): message = 'Hello DockerCon 2018!' html = wrap_html(message) return html if __name__ == '__main__': app.run(host='0.0.0.0', port=5000)

La clé à retenir ici est la variable de message dans la fonction « hello_world() ». Cette variable spécifie une valeur de chaîne et la valeur de cette variable sera testée pour une correspondance dans un test unitaire.

Testons le Code

Comme chacun le sait, tout code doit être testé pour s’assurer de sa haute qualité et stabilité avant d’être rendu public. Python est livré avec un framework de test nommé unittest et nous allons l’utiliser ici.

Nous avons maintenant une application Flask complète et elle a besoin d’un test unitaire complémentaire qui testera l’application et s’assurera qu’elle fonctionne comme prévu. Le fichier de test unitaire test_hello_world.py est le test unitaire de notre application hello_world.py. Voyons maintenant le code.

Code :

import hello_world import unittest class TestHelloWorld(unittest.TestCase): 
def setUp(self): self.app = hello_world.app.test_client() self.app.testing = True 
def test_status_code(self): response = self.app.get('/') self.assertEqual(response.status_code, 200) 
def test_message(self): response = self.app.get('/') message = hello_world.wrap_html('Hello DockerCon 2018!') self.assertEqual(response.data, message) if __name__ == '__main__': unittest.main()

Code :

import hello_world
import unittest

Importez l’application hello_world à l’aide de l’instruction import qui donne au test l’accès au code dans hello_world.py. Ensuite, importez les modules unittest et commencez à définir la couverture de test pour l’application.

« class TestHelloWorld(unittest.TestCase) » : le TestHelloWorld est instancié à partir de la classe de base « unittest.Test » qui est la plus petite unité de test. Il vérifie une réponse spécifique à un ensemble particulier d’entrées. « unittest » fournit une classe de base, TestCase, qui peut être utilisée pour créer de nouveaux cas de test.

Code :

def setUp(self): self.app = hello_world.app.test_client() self.app.testing = True

« setUp() » est une méthode de niveau classe appelée pour préparer le montage de test. Ceci est appelé immédiatement avant d’appeler la méthode de test. Dans cet exemple, nous créons et définissons une variable nommée « app » et l’instancions en tant qu’objet « app.test_client() » à partir du code hello_world.py.

Code :

def test_status_code(self): response = self.app.get('/') self.assertEqual(response.status_code, 200)

« test_status_code() » est une méthode qui spécifie un cas de test réel dans le code. Ce scénario de test envoie une demande « get » à l’application flask et capture la réponse de l’application dans la variable « response ».

« self.assertEqual(response.status_code, 200) » compare la valeur du résultat « response.status_code » à la valeur attendue de 200 , ce qui signifie que la demande « get » a réussi. Si le serveur répond avec un « code_état » autre que 200, le test échoue.

Code :

def test_message(self): response = self.app.get('/') message = hello_world.wrap_html('Hello DockerCon 2018!') self.assertEqual(response.data, message)

« test_message() test » une autre méthode qui spécifie un cas de test différent. Ce scénario de test est conçu pour vérifier la valeur de la variable message définie dans la méthode « hello_world() » à partir du code « hello_world.py ». Comme pour le test précédent, un appel « get » est effectué vers l’application et les résultats sont capturés dans une variable de « response ».

On obtient le message suivant :

Code :

message = hello_world.wrap_html('Hello DockerCon 2018!')

La variable « message » se voit attribuer le code HTML résultant de la méthode d’assistance « hello_world.wrap_html() » qui est définie dans hello_world.app. La chaîne Hello DockerCon 2018 est fournie dans la méthode « wrap_html() » qui est ensuite injectée et renvoyée en html. Le « test_message() » vérifiera que la variable de message dans l’application correspondra à la chaîne attendue dans ce cas de test. Si les chaînes ne correspondent pas, le test échouera.

Les Pipelines CI/CD

Maintenant que nous sommes bons au niveau de l’application et de ses tests unitaires, il est temps d’implémenter un pipeline CI / CD dans la base de code. L’implémentation d’un pipeline CI / CD à l’aide de CircleCI est très simple. Avant de continuer, assurez-vous de procéder comme suit :

  1. Créer un compte CircleCI
  2. Configurer votre build dans CircleCI

Mettre en œuvre un pipeline CI/CD avec Docker

Une fois votre projet configuré sur la plateforme CircleCI, tout commit poussé en amont sera détecté et CircleCI exécutera le travail défini dans votre fichier config.yml.

Vous devrez créer un nouveau répertoire à la racine du référentiel et un fichier yaml dans ce nouveau répertoire.

Les nouveaux actifs doivent suivre le schéma de nommage suivant : répertoire: « .circleci/ fichier: config.yml » dans le référentiel git de votre projet. Ce répertoire et ce fichier définissent essentiellement la configuration adn de votre pipeline CI / CD pour la plate-forme CircleCI.

Le fichier config.yml

Le fichier config.yml est l’endroit où la magie CI / CD se produit. Ci-dessous le fichier utilisé comme exemple et nous allons expliquer brièvement ce qu’il se passe au niveau de la syntaxe.

Code :

version: 2 jobs: build: docker: - image: circleci/python:2.7.14 environment: FLASK_CONFIG: testing steps: - checkout - run: name: Setup VirtualEnv command: | echo 'export TAG=0.1.${CIRCLE_BUILD_NUM}' >> $BASH_ENV echo 'export IMAGE_NAME=python-circleci-docker' >> $BASH_ENV virtualenv helloworld . helloworld/bin/activate pip install --no-cache-dir -r requirements.txt - run: name: Run Tests command: | . helloworld/bin/activate python test_hello_world.py - setup_remote_docker: docker_layer_caching: true - run: name: Build and push Docker image command: | . helloworld/bin/activate pyinstaller -F hello_world.py docker build -t ariv3ra/$IMAGE_NAME:$TAG . echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin docker push ariv3ra/$IMAGE_NAME:$TAG - run: name: Deploy app to Digital Ocean Server via Docker command: | ssh -o StrictHostKeyChecking=no root@hello.dpunks.org "/bin/bash ./deploy_app.sh ariv3ra/$IMAGE_NAME:$TAG"

La clé « jobs: » représente une liste des jobs qui seront exécutés. Un job encapsule les actions à exécuter. Si vous n’avez qu’un seul job à exécuter, vous devez lui donner un nom de clé « build: ». Vous pouvez obtenir plus de détails sur les jobs et les builds ici.

La clé « build: » est composée de plusieurs éléments :

  • « docker: »
  • « steps: »

La clé  « docker: » indique à CircleCI d’utiliser un exécuteur Docker, ce qui signifie que notre build sera exécuté à l’aide de conteneurs Docker.

« image: circleci/python:2.7.14 » spécifie l’image Docker que le build doit utiliser.

Les Étapes d’un CI/CD avec Docker

La clé « steps: »est une collection qui spécifie toutes les commandes qui seront exécutées dans cette version. La première action qui se produit est la commande « – checkout » qui effectue essentiellement un clone git de votre code dans l’environnement de construction.

Les clés « – run: » spécifient les commandes à exécuter dans le build. Les clés d’exécution ont un paramètre « name: » où vous pouvez étiqueter un regroupement de commandes. Par exemple, « name: Run Tests » regroupe les actions liées aux tests qui aident à organiser et à afficher les données de construction dans le tableau de bord CircleCI.

Remarque importante : chaque bloc « run » équivaut à des shells ou des terminaux séparés / individuels. Les commandes configurées ou exécutées ne persisteront pas dans les blocs d’exécution ultérieurs. Utilisez la solution de contournement « $BASH_ENV ».

Code :

- run: name: Setup VirtualEnv command: | echo 'export TAG=0.1.${CIRCLE_BUILD_NUM}' >> $BASH_ENV echo 'export IMAGE_NAME=python-circleci-docker' >> $BASH_ENV virtualenv helloworld . helloworld/bin/activate pip install --no-cache-dir – r requirements.txt

La clé « command: »  pour ce bloc d’exécution a une liste de commandes à exécuter. Ces commandes définissent les variables d’environnement personnalisé « $TAG & IMAGE_NAME » qui seront utilisées tout au long de ce build. Les commandes restantes configurent python virtualenv et installent les dépendances Python spécifiées dans le fichier requirements.txt.

Code :

- run: name: Run Tests command: | . helloworld/bin/activate python test_hello_world.py

Dans ce bloc d’exécution, la commande exécute des tests sur notre application et si ces tests échouent, la construction entière échouera.

Code :

- setup_remote_docker: docker_layer_caching: true

Ce bloc d’exécution spécifie la clé « setup_remote_docker: » qui est une fonctionnalité qui permet de créer, d’exécuter et de pousser des images vers des registres Docker à partir d’un travail d’exécuteur Docker. Lorsque « docker_layer_caching » est défini sur « true », CircleCI essaie de réutiliser les images Docker (couches) créées lors d’un travail ou d’un « workflow » précédent. Autrement dit, chaque couche que vous avez créée dans un travail précédent sera accessible dans l’environnement distant. Cependant, dans certains cas, votre travail peut s’exécuter dans un environnement propre, même si la configuration spécifie « docker_layer_caching: true ».

Étant donné que nous créons une image Docker pour notre application et que nous la poussons vers Docker Hub, « setup_remote_docker: » est requis.

Code :

- run: name: Build and push Docker image command: | . helloworld/bin/activate pyinstaller -F hello_world.py docker build -t ariv3ra/$IMAGE_NAME:$TAG . echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin docker push ariv3ra/$IMAGE_NAME:$TAG

Le bloc d’exécution d’image Builder et push Docker spécifie les commandes qui regroupent l’application dans un seul fichier binaire à l’aide de pyinstaller, puis poursuit le processus de construction d’image Docker.

Code :

docker build -t ariv3ra/$IMAGE_NAME:$TAG . echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin docker push ariv3ra/$IMAGE_NAME:$TAG

Ces commandes créent l’image de docker en fonction de Dockerfile inclus dans le repo. Dockerfile est l’instruction sur la façon de construire l’image Docker.

Code :

FROM python:2.7.14 RUN mkdir /opt/hello_word/ WORKDIR /opt/hello_word/ COPY requirements.txt . COPY dist/hello_world /opt/hello_word/ EXPOSE 80 CMD [ "./hello_world" ]

La commande « echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN –password-stdin »  utilise les variables env « $ DOCKER_LOGIN et $ DOCKER_PWD » définies dans le tableau de bord CircleCI comme informations d’identification pour se connecter et pousser cette image vers Docker Hub.

Code :

- run: name: Deploy app to Digital Ocean Server via Docker command: | ssh -o StrictHostKeyChecking=no root@hello.dpunks.org "/bin/bash ./deploy_app.sh ariv3ra/$IMAGE_NAME:$TAG"

Le dernier « run » bloc déploie notre nouveau code sur un serveur en direct fonctionnant sur la plate-forme Digital Ocean. Assurez-vous que vous avez créé un script de déploiement sur le serveur distant. La commande SSH accède au serveur distant et exécute le script « deploy_app.sh » sur le serveur et spécifie : « ariv3ra / $ IMAGE_NAME: $ TAG » qui spécifie l’image à extraire et à déployer à partir de Docker Hub.

Une fois le travail terminé, la nouvelle application doit s’exécuter sur le serveur cible que vous avez spécifié dans config.yml.

Vous pouvez également retrouver le code utilisé dans cet article ici.

Pour aller plus loin

Cet article a éveillé votre curiosité et vous souhaitez vous initier à l’automatisation du déploiement d’applications dans des conteneurs logiciels et plus généralement à apprendre à utiliser de façon intensive la technologie Docker & Ansible ?

Nous vous invitons à participer à :

Des questions ? Contactez-nous via notre formulaire de contact ou bien appelez-nous au 01 40 34 11 53 !

Des commentaires ? C’est juste en dessous ! 😉

UNE QUESTION ? UN PROJET ? UN AUDIT DE CODE / D'INFRASTRUCTURE ?

Pour vos besoins d’expertise que vous ne trouvez nulle part ailleurs, n’hésitez pas à nous contacter.