TP 4 - Créer une application multiconteneur

Articuler deux images avec Docker compose

  • Installez docker-compose avec sudo apt install docker-compose.

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 :
                <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 trouvée dans le cache")
    except:
        redis_unreachable = True
        logging.warning("Cache redis injoignable")

    if not found_in_cache:
        logging.info("Image non trouvée dans le cache")
        try:
            r = requests.get('http://dnmonster:8080/monster/' + name + '?size=80')
            image = r.content
            logging.info("Image générée grâce au service dnmonster")

            if not redis_unreachable:
                cache.set(name, image)
                logging.info("Image enregistrée 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 pip install Flask uWSGI requests redis
WORKDIR /app
COPY app/identidock.py /app

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 ?.

  • Construire l’application, pour l’instant avec docker build, la lancer et vérifier avec docker exec, whoami et id l’utilisateur avec lequel tourne le conteneur.

Réponse :

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 :
version: "3.7"
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 !) :

dnmonster:
  image: amouat/dnmonster:1.0

Le docker-compose.yml doit pour l’instant ressembler à ça :

version: "3.7"
services:
  identidock:
    build: .
    ports:
      - "5000:5000"

  dnmonster:
    image: amouat/dnmonster:1.0

Enfin, nous déclarons aussi un réseau appelé identinet pour y mettre les deux conteneurs de notre application.

  • Il faut déclarer ce réseau à la fin du fichier (notez que l’on doit spécifier le driver réseau) :
networks:
  identinet:
    driver: bridge
  • 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.
redis:
  image: redis
  networks:
    - identinet

docker-compose.yml final :

version: "3.7"
services:
  identidock:
    build: .
    ports:
      - "5000:5000"
      - "9191:9191" # port pour les stats
    networks:
      - identinet

  dnmonster:
    image: amouat/dnmonster:1.0
    networks:
      - identinet

  redis:
    image: redis
    networks:
      - identinet

networks:
  identinet:
    driver: bridge
  • Lancez l’application et vérifiez que le cache fonctionne en chercheant les cache miss 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. Cette documentation indique aussi les différences entre la version 2 et la version 3 des fichiers Docker Compose.

D’autres services

Exerciced 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é.

  • Le cas échéant, lisez les logs s’il y a un bug et adaptez les variables d’environnement.

Une stack Elastic

Centraliser les logs

L’utilité d’Elasticsearch est que, grâce à une configuration très simple de son module Filebeat, nous allons pouvoir centraliser les logs de tous nos conteneurs Docker. Pour ce faire, il suffit d’abord de télécharger une configuration de Filebeat prévue à cet effet :

curl -L -O https://raw.githubusercontent.com/elastic/beats/7.10/deploy/docker/filebeat.docker.yml

Renommons cette configuration et rectifions qui possède ce fichier pour satisfaire une contrainte de sécurité de Filebeat :

mv filebeat.docker.yml filebeat.yml
sudo chown root filebeat.yml
sudo chmod go-w filebeat.yml

Enfin, créons un fichier docker-compose.yml pour lancer une stack Elasticsearch :

version: "3"

services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.5.0
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
    networks:
      - logging-network

  filebeat:
    image: docker.elastic.co/beats/filebeat:7.5.0
    user: root
    depends_on:
      - elasticsearch
    volumes:
      - ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - logging-network
    environment:
      - -strict.perms=false

  kibana:
    image: docker.elastic.co/kibana/kibana:7.5.0
    depends_on:
      - elasticsearch
    ports:
      - 5601:5601
    networks:
      - logging-network

networks:
  logging-network:
    driver: bridge

Il suffit ensuite de se rendre sur Kibana (port 5601) et de configurer l’index en tapant * dans le champ indiqué, de valider et de sélectionner le champ @timestamp, puis de valider. L’index nécessaire à Kibana est créé, vous pouvez vous rendre dans la partie Discover à gauche (l’icône boussole 🧭) pour lire vos logs.

Facultatif : Ajouter un nœud Elasticsearch

Puis, à l’aide de la documentation Elasticsearch et/ou en adaptant de bouts de code Docker Compose trouvés sur internet, ajoutez et configurez un nœud Elastic. Toujours à l’aide de la documentation Elasticsearch, vérifiez que ce nouveau nœud communique bien avec le premier.

Facultatif : ajouter une stack ELK à microblog

Dans la dernière version de l’app microblog, Elasticsearch est utilisé pour fournir une fonctionnalité de recherche puissante dans les posts de l’app. Avec l’aide du tutoriel de Miguel Grinberg, écrivez le docker-compose.yml qui permet de lancer une stack entière pour microblog. Elle devra contenir un conteneur microblog, un conteneur mysql, un conteneur elasticsearch et un conteneur kibana.

Facultatif : Utiliser Traefik

Vous pouvez désormais faire l’exercice 1 du TP7 pour configurer un serveur web qui permet d’accéder à vos services via des domaines.