Autour du codeDevelopper toujours mieux
Posté le

Utiliser Docker pour déployer un environnement de test

Le but est d'utiliser Docker pour tester le fonctionnement d'une application web WSGI utilisant une base de données MySql. Docker est un logiciel qui gère des environnements isolés de l'OS, appelés containers, il les crée, les versionnent et facilite la communication entre eux. L'intérêt d'utiliser des containers et de bénéficier d'un environnement vierge à chaque lancement de test et d'éviter les effets de bord suite à des informations restées en BDD. Cela permet également de s'assurer que l'application est capable de fonctionner hors d'un environnement de développement. Dans cet article, on va utiliser Docker pour créer deux containers. L'un qui va héberger la base de données et l'autre notre application.

Installation de Docker sur Debian Jessie

Enregistrez puis installez la clé du dépôt Docker.

# apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D

Ajoutez le dépot docker à l'index des paquets.

# echo "deb https://apt.dockerproject.org/repo debian-jessie main" > /etc/apt/sources.list.d/docker.list

Mettez à jour.

# apt-get update

# apt-get upgrate

# apt-get install docker-engine

Démarrez le daemon docker.

# service docker start

Création du container MySQL

Récupérer une image MySQL sur Docker Hub

Comme je vous l'ai dit dans l'introduction, Docker permet de versionner des containers. On peut donc facilement récupérer l'image d'un container via un dépôt. A l'instar de GitHub pour le code source, DocherHub regroupe plusieurs dépôts de container.

La commande docker pull permet de récupérer des images. On va lui passer en paramètre le nom du dépôt où sont stockés les images MySQL avec pour tag 5.7 qui est actuellement la dernière version de MySQL.

# docker pull mysql:5.7

On va maintenant utiliser la commande docker run pour créer et lancer un container à partir de l'image.

# docker run --name mysql_db -d -P -e MYSQL_ROOT_PASSWORD=root_password -e MYSQL_DATABASE=wsgi_database -e MYSQL_USER=wsgi_user -e MYSQL_PASSWORD=wsgi_password mysql:5.7

On utilise le paramètre --name pour donner comme nom à notre container mysql_db -d et pour le lancer en daemon -P permet de lier les ports exposés du container à des ports libre de l'hôte. Dans le cas du container MySQL, c'est le port 3306 qui est exposé. On définit via des variables le mot de passe administrateur et utilisateur puis le nom de l'utilisateur et de la base de données.

Création du container de l'application WSGI

Voici notre application web. C'est une application WSGI qui affiche une citation qu'elle va récupérer dans une base de données MySQL en fonction de la date donnée dans l'URL. Pour fonctionner, notre application à besoin d'un fichier JSON dans lequel se trouve les paramètres de connexion à la base de données. Le module python qui contient notre application WSGI est également conçu pour peupler la base de donné lorsqu'il est exécuté directement.

#!/usr/bin/env python3

import pymysql
import os, sys
from datetime import datetime, date
import json


conf_file_name = os.path.join(os.path.dirname(__file__), 'conf.json')
with open(conf_file_name) as conf_file:
    CONF = json.load(conf_file)


def application(environ, start_response):
    headers = [('Content-type', 'text/plain')]
    try:
        date = datetime.strptime(
            environ['PATH_INFO'], '/%d-%m-%Y').date()
        print("DATE", date)
    except ValueError:
        start_response('404 Not Found', headers)
        return ["page not found".encode('utf-8')]

    try:
        connection = pymysql.connect(
            host=CONF['host'], port=CONF['port'],
            user=CONF['user'], passwd=CONF['passwd'],
            db=CONF['db'])

        with connection.cursor() as cursor:
            cursor.execute("SELECT cit_text FROM citations WHERE cit_date = %s", date)
            line = cursor.fetchone()

        if line is None:
            status = '404 Not Found'
            body = "page not found"
        else:
           status = '200 OK'
           body = line[0]

    except pymysql.MySQLError as error:
       status = '500 Error'
       body = str(error)

    finally:
        connection.close()

    start_response(status, headers)
    return [body.encode('utf-8')]


if __name__ == '__main__':
    connection = pymysql.connect(host=CONF['host'], port=CONF['port'],
                                 user=CONF['user'], passwd=CONF['passwd'],
                                 db=CONF['db'])

    with connection.cursor() as cursor:
        cursor.execute("CREATE TABLE citations(cit_date DATE, cit_text VARCHAR(255), PRIMARY KEY(cit_date));")
        sql = "INSERT INTO citations(cit_date, cit_text) VALUES (%s, %s)"
        cursor.execute(sql, (date(2015, 12, 24), "Joyeux Noel"))
    connection.commit()
    connection.close()

Création d'une image via un Dockerfile

Pour hébergé notre application on va partir d'une image Debian sur lequel on va installer python 3. On va faire cela en utilisant un Dockerfile. C'est un fichier dans lequel on utilise des directives pour permettre à docker de créer une images. On utilisera ainsi la directive FROM pour donner une image de départ puis la commande RUN pour installer python3 et pip via apt-get. La commande COPY va nous permettre de copier le fichier application.py dans notre container. On pourrait également utiliser COPY pour copier le fichier JSON de configuration, mais le problème et que nous ne connaissons pas l'adresse IP de notre container MySQL ni le port sur lequel il est lié.

Docker offre un moyen de connaitre l'adresse et le port d'un container à partir d'un autre. Il va lui transmettre ces informations via des variables d'environnements. Ces variables sont préfixées par le nom du container en majuscule suivi d'un underscore. Ainsi toutes les informations concernant notre container mysql_db seront accessibles dans d'autre container liée à celui-ci par des variables d'environnement commençant par MYSQL_DB_. Pour connaitre l'IP et le port lié au port exposé on utilisera respectivement les suffixe PORT_<port-exposé>_TCP_ADDR et DB_PORT_<port-exposé>_TCP_PORT où <port-exposé> sera remplacé par le numéro de port exposé.

On va donc créer un script bash launch_sercer.sh qui lorsqu'il sera exécuté sur le container WSGI va créer le fichier conf.json en utilisant les variables d'environnements et lancer notre application python. Dans notre Dockerfile, on va utiliser l'instruction COPY pour copier launch_sercer.sh dans notre container. On va ensuite utiliser la directive CMD. CMD est un peu spéciale, il ne peut y en avoir qu'une seule dans un Dockerfile elle permet d'indiquer une commande qui sera exécutée à chaque lancement de notre container. On va lui demander d'exécuter notre script launch_server.sh

FROM debian:jessie

RUN apt-get update && apt-get install -y python3 python3-pip
RUN pip3 install PyMySQL==0.6.7

COPY ./application.py ./
COPY ./launch_server.sh ./

CMD ["/bin/bash", "launch_server.sh"]

#!/bin/bash

echo '{"host": "'$MYSQL_DB_PORT_3306_TCP_ADDR'", ' \
      '"port": '$MYSQL_DB_PORT_3306_TCP_PORT', ' \
      '"passwd" : "'$MYSQL_DB_ENV_MYSQL_PASSWORD'", '\
      '"db": "'$MYSQL_DB_ENV_MYSQL_DATABASE'", '\
      '"user": "'$MYSQL_DB_ENV_MYSQL_USER'"}' > conf.json


cat conf.json

python3 application.py

python3 -c "from wsgiref.simple_server import make_server; \
            from application import application; \
            make_server('', 8000, application).serve_forever()"

Maintenant que notre Dockerfile est terminé, on va exécuter la commande docker build pour créer notre image.

# docker build -f wsgi.dockerfile .

La commande se termine en nous indiquant l'ID de l'image nouvellement créée.

Sending build context to Docker daemon 20.48 kB
...
Successfully built 0c9a5d5a0c4c

Lier notre container WSGI au container MySQL

On va ensuite lancer notre container en utilisant la commande run. Le paramètre --link avec la valeur mysql_db va indiquer à docker qu'il doit créer à l'intérieure du nouveau container, des variables d'environnement concernant le container mysql_db.

# docker run --name wsgi -p 127.0.0.1:80:8000 --link mysql_db 0c9a5d5a0c4c

A la place du paramètre -P on a utilisé -p pour donner le port de l'hôte sur lequel lier le port exposé de container. Maintenant, vous pouvez directement jeter un coup d'oeil avec votre navigateur à l'adresse 127.0.0.1/24-12-2015. Après avoir vérifié que tout fonctionne, vous pourrez arrêter et supprimer vos containers avec les commandes.

# docker stop wsgi mysql_db # docker rm wsgi mysql_db

Sans supprimer les containers, impossible de recréer d'autre avec le même nom. Vous allez voir que grâce au système de cache de docker, la création de container prend beaucoup moins de temps la deuxième fois.

L'automatisation

Bon c'est bien beau, mais pour le moment, ça nous simplifie pas la vie, on va donc faire un bash pour automatiser tout ça. Il y a plusieurs astuces la première, c'est de quitter le bash dès que quelque chose échoue. Il n'est pas utile, de lancer le container wsgi si celui hébergeant la BDD crash. on va donc utiliser le pattern suivant après chaque commande:

if [ $? != 0 ]; then
    exit 1
fi

La deuxième astuce, c'est de stopper les containers déjà lancés et de les supprimer avant de quitter le bash. Sans cela on ne pourra lancer le bash une seconde fois car il y aura un conflit avec les noms des containers. Pour ne pas écrire en dur les container à supprimer selon le succès ou l'échec d'une étape, on va écrire une fonction de nettoyage.

function clean_container {
    names="^(mysql_db|wsgi)$"
    docker ps --format "{{.Names}}" -f status=running | grep -P "$names" | xargs -I % docker stop %
    docker ps --format "{{.Names}}" -f status=exited | grep -P "$names" | xargs -I % docker rm %
}

On utilise dans cette fonction la commande docker ps pour lister les containers le paramètre status nous permet de lister seulement ceux qui sont en fonctionnement ou arrêté. Docker ps propose une option -f name=valeur pour filtrer sur les noms des container. Malheureusement cette option liste tous les containers dont le nom contient la valeur. Pour lister les container sur le nom exacte, on va utiliser grep. L'option --format nous permet d'afficher seulement le nom.

La troisième astuce qui est l'un des plus important, lorsque docker vous dit qu'il vient de démarrer un container avec succès rien ne vous prouve que le serveur dans le container et près à recevoir des requêtes. On va donc mettre un sleep après chaque docker run pour laisser le temps au serveur d'être à l'écoute du réseau. Une autre solution et d'utiliser la commande wget avec l'option spider pour ne pas télécharger la page.

$ wget --tries 10 --spider -q --retry-connrefused <address-ip>:<port>

Si vous prenez cette option, vous pouvez récupérer le port de la machine mysql_db via la commande

# docker inspect --format '{{ (index (index .NetworkSettings.Ports "3306/tcp") 0).HostPort }}' mysql_db

Voici le bash final. J'ai mis un simple curl à l'endroit où il faut appeler votre framework de test

#!/bin/bash

function clean_container {
    names="^(mysql_db|wsgi)$"
    docker ps --format "{{.Names}}" -f status=running | grep -P "$names" | xargs -I % docker stop %
    docker ps --format "{{.Names}}" -f status=exited | grep -P "$names" | xargs -I % docker rm %
}


docker run --name mysql_db -d -P -e MYSQL_ROOT_PASSWORD=root_password -e MYSQL_DATABASE=wsgi_database -e MYSQL_USER=wsgi_user -e MYSQL_PASSWORD=wsgi_password  mysql:5.7
if [ $? != 0 ];
    then exit;
else
    echo "mysql_db running"
fi

output=$(mktemp)
echo 'tmpfile' $output

docker build -f wsgi.dockerfile . > "$output"
if [ $? != 0 ]; then
    rm $output
    clean_container
    exit 1
else
    echo "WSGI built"
fi

wsgi_img_id=$(grep "Successfully built" "$output" | cut -d ' ' -f 3)

mysql_port=$(docker inspect --format '{{ (index (index .NetworkSettings.Ports "3306/tcp") 0).HostPort }}'  mysql_db)
sleep 10

docker run -d --name wsgi -p 127.0.0.1:80:8000 --link mysql_db $wsgi_img_id
if [ $? != 0 ]; then
    rm $output
    clean_container
    exit 1;
else
    echo "WSGI running"
fi
sleep 10

curl 127.0.0.1/24-12-2015 | grep 'Joyeux Noel'
if [ $? == 0 ]; then
    echo TEST OK;
    clean_container
else
    echo TEST FAILD;
    clean_container
    exit 1
fi

Voilou j'espère que vous avez aimé cet article alor à vos IDE et bon code laughing