TP 4 - Créer une application multiconteneur

Articuler trois images avec Docker Compose

Si Docker Compose est pas installé

identidock : une application Flask qui se connecte à redis

  • Démarrez un nouveau projet dans VSCode (créez un dossier appelé identidock et chargez-le avec la fonction Add folder to workspace)
  • Dans un sous-dossier app, ajoutez une petite application python en créant ce fichier identidock.py :
from flask import Flask, Response, request, abort
import requests
import hashlib
import redis
import os
import logging

LOGLEVEL = os.environ.get('LOGLEVEL', 'INFO').upper()
logging.basicConfig(level=LOGLEVEL)

app = Flask(__name__)
cache = redis.StrictRedis(host='redis', port=6379, db=0)
salt = "UNIQUE_SALT"
default_name = 'toi'

@app.route('/', methods=['GET', 'POST'])
def mainpage():

    name = default_name
    if request.method == 'POST':
        name = request.form['name']

    salted_name = salt + name
    name_hash = hashlib.sha256(salted_name.encode()).hexdigest()
    header = '<html><head><title>Identidock</title></head><body>'
    body = '''<form method="POST">
                Salut <input type="text" name="name" value="{0}"> !
                <input type="submit" value="submit">
                </form>
                <p>Tu ressembles a ca :
                <img src="/monster/{1}"/>
            '''.format(name, name_hash)
    footer = '</body></html>'
    return header + body + footer


@app.route('/monster/<name>')
def get_identicon(name):
    found_in_cache = False

    try:
        image = cache.get(name)
        redis_unreachable = False
        if image is not None:
            found_in_cache = True
            logging.info("Image trouvee dans le cache")
    except:
        redis_unreachable = True
        logging.warning("Cache redis injoignable")

    if not found_in_cache:
        logging.info("Image non trouvee dans le cache")
        try:
            r = requests.get('http://dnmonster:8080/monster/' + name + '?size=80')
            image = r.content
            logging.info("Image generee grace au service dnmonster")

            if not redis_unreachable:
                cache.set(name, image)
                logging.info("Image enregistree dans le cache redis")
        except:
            logging.critical("Le service dnmonster est injoignable !")
            abort(503)

    return Response(image, mimetype='image/png')

if __name__ == '__main__':
  app.run(debug=True, host='0.0.0.0', port=5000)
  • uWSGI est un serveur python de production très adapté pour servir notre serveur intégré Flask, nous allons l’utiliser.

  • Dockerisons maintenant cette nouvelle application avec le Dockerfile suivant :

FROM python:3.7

RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
RUN pip3 install Flask uWSGI requests redis
WORKDIR /app
COPY app/identidock.py /app
ENV FLASK_APP identidock.py

EXPOSE 5000 9191
USER uwsgi
CMD ["uwsgi", "--http", "0.0.0.0:5000", "--wsgi-file", "/app/identidock.py", \
"--callable", "app", "--stats", "0.0.0.0:9191"]
  • Observons le code du Dockerfile ensemble s’il n’est pas clair pour vous. Juste avant de lancer l’application, nous avons changé d’utilisateur avec l’instruction USER, pourquoi ?.

Le fichier Docker Compose

  • A la racine de notre projet identidock (à côté du Dockerfile), créez un fichier de déclaration de notre application appelé docker-compose.yml avec à l’intérieur :
services:
  identidock:
    build: .
    ports:
      - "5000:5000"
  • Plusieurs remarques :

    • la première ligne après services déclare le conteneur de notre application
    • les lignes suivantes permettent de décrire comment lancer notre conteneur
    • build: . indique que l’image d’origine de notre conteneur est le résultat de la construction d’une image à partir du répertoire courant (équivaut à docker build -t identidock .)
    • la ligne suivante décrit le mapping de ports entre l’extérieur du conteneur et l’intérieur.
  • Lancez le service (pour le moment mono-conteneur) avec docker compose up (cette commande sous-entend docker compose build)

  • Visitez la page web de l’app.

  • Ajoutons maintenant un deuxième conteneur. Nous allons tirer parti d’une image déjà créée qui permet de récupérer une “identicon”. Ajoutez à la suite du fichier Compose (attention aux indentations !) un service dnmonster utilisant l’image amouat/dnmonster:1.0.

Solution :
  • Enfin, nous déclarons aussi un réseau appelé identinet pour y mettre les deux conteneurs de notre application.
Solution :
  • Il faut aussi mettre nos deux services identidock et dnmonster sur le même réseau en ajoutant deux fois ce bout de code où c’est nécessaire (attention aux indentations !) :
  networks:
    - identinet
  • Ajoutons également un conteneur redis (attention aux indentations !). Cette base de données sert à mettre en cache les images et à ne pas les recalculer à chaque fois.
Solution :
`docker-compose.yml` final :
  • Lancez l’application et vérifiez que le cache fonctionne en cherchant les messages dans les logs de l’application.

  • N’hésitez pas à passer du temps à explorer les options et commandes de docker-compose, ainsi que la documentation officielle du langage des Compose files.

Le Hot Code Reloading (rechargement du code à chaud)

Modifions le docker-compose.yml pour y inclure des instructions pour lancer le serveur python en mode debug.

Notre image est codée pour lancer le serveur de production appelé uWSGI (CMD ["uwsgi", "--http", "0.0.0.0:5000", "--wsgi-file", "/app/identidock.py", \ "--callable", "app", "--stats", "0.0.0.0:9191"]). Nous voulons plutôt lancer le serveur de debug qui se lance avec :

  • la variable d’environnement FLASK_ENV=development
  • le processus lancé avec la commande flask run -h 0.0.0.0

En réfléchissant à comment utiliser les volumes, le but est de trouver comment la modification du code source devrait immédiatement être répercutée dans les logs d'identidock : recharger la page devrait nous montrer la nouvelle version du code de l’application.

Solution :

(facultatif) Monter un script d’entrypoint

En vous inspirant de ce fichier, créez un script d’entrypoint et déclarez un volume pour l’utiliser.

#!/bin/sh

# cette partie sert à effectuer des opérations sur la base de données si nécessaires
while true; do
    if flask db upgrade; then
        break
    fi
    echo Deploy command failed, retrying in 5 secs...
    sleep 5
done

# cette partie permet de faire varier l'environnement du container
set -e
if [ "$CONTEXT" = 'DEV' ]; then
    echo "Running Development Server"
    FLASK_ENV=development exec flask run -h 0.0.0.0
else
    echo "Running Production Server"
    exec gunicorn -b :5000 --access-logfile - --error-logfile - microblog:app
fi

(facultatif) Le Docker Compose de microblog

Créons un fichier Docker Compose pour faire fonctionner l’application Microblog du TP précédent avec Postgres.

  • Quelles étapes faut-il ?
  • Trouver comment configurer une base de données Postgres pour une app Flask (c’est une option de SQLAlchemy)

D’autres services

Exercices de google-fu

ex: un pad HedgeDoc

On se propose ici d’essayer de déployer plusieurs services pré-configurés comme Wordpress, Nextcloud, Sentry ou votre logiciel préféré.

  • Récupérez (et adaptez si besoin) à partir d’Internet un fichier docker-compose.yml permettant de lancer un pad HedgeDoc ou autre avec sa base de données. Je vous conseille de toujours chercher dans la documentation officielle ou le repository officiel (souvent sur Github) en premier.

  • Vérifiez que le service est bien accessible sur le port donné.

  • Si besoin, lisez les logs en quête bug et adaptez les variables d’environnement.