3.06 Déployer avec docker compose créer et lancer ses applications
Objectifs pédagogiques
- Savoir lancer une application multi-conteneur avec docker compose
- Savoir créer une application multi-conteneur
Mise en pratique : écrire un fichier compose pas à pas
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.
Le Dockerfile
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.
Construire l'application, pour l'instant avec docker build
, la lancer.
Vérifiez avec ps aux
que le serveur est bien lancé.
Poussez l'image sur le registry local.
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
Notez que 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 "dnmonster" déjà existante sur docker hub. Elle 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 :
services:
identidock:
build: .
ports:
- "5000:5000"
dnmonster:
image: amouat/dnmonster:1.0
Mettre nos conteneurs dans un réseau dédié
Enfin, nous déclarons aussi un réseau appelé identinet
pour y mettre les deux conteneurs de notre application.
Il faut d'abord déclarer le réseau à la fin du fichier.
networks:
identinet:
driver: bridge
Sans spécifier le driver réseau, bridge
celui utilisé par défaut, donc la 3e ligne est facultative ici.
Il faut ensuite aussi mettre nos deux services identidock
et dnmonster
sur le même réseau en ajoutant deux fois le bout de code suivant pour chaque service/conteneur :
networks:
- identinet
Un conteneur de stockage pour nos données
Ajoutons également un conteneur redis
.
Cette "base de données" sert à mettre en cache les images et à ne pas les recalculer à chaque fois.
redis:
image: redis
networks:
- identinet
Résultat final
docker-compose.yml
final :
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 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 Docker Compose de microblog
Créons un fichier Docker Compose pour faire fonctionner l'application Microblog avec redis sous forme de docker-compose.
Quelles étapes faut-il ?
Avancé : Comment pourrait-on faire pour avoir du "Hot Reload", c'est à dire voir les modifications du code immédiates ?
Indice : chercher "flask hot reload" et penser aux volumes
Avancé : Trouver comment configurer une base de données Postgres pour une app Flask (c'est une option de SQLAlchemy).
D'autres services
On lancer un pad HedgeDoc (ou autre logiciel de votre choix).
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.
Assemblez à partir d'Internet un fichier docker-compose.yml
permettant de lancer un Wordpress et un Nextcloud déjà pré-configurés (pour l'accès à la base de données notamment).
Ajoutez-y un pad CodiMD / HackMD (toujours grâce à du code trouvé sur Internet).
Faire varier la configuration en fonction de l'environnement
Finalement le serveur de développement flask est bien pratique pour debugger en situation de développement, mais il n'est pas adapté à la production.
Nous pourrions créer deux images pour les deux situations mais ce serait aller contre l'imperatif DevOps de rapprochement du dév et de la production.
Créons un script bash boot.sh
pour adapter le lancement de l'application au contexte.
#!/bin/bash
set -e
if [ "$CONTEXT" = 'DEV' ]; then
echo "Running Development Server"
exec python3 "/app/identidock.py"
else
echo "Running Production Server"
exec uwsgi --http 0.0.0.0:5000 --wsgi-file /app/identidock.py --callable app --stats 0.0.0.0:9191
fi
Ajoutez au Dockerfile une deuxième instruction COPY
en dessous de la précédente pour mettre le script dans le conteneur.
Ajoutez un RUN chmod a+x /boot.sh
pour le rendre executable.
Modifiez l'instruction ENTRYPOINT
pour lancer le script de boot plutôt que uwsgi
directement.
Modifiez l'instruction EXPOSE
pour déclarer le port 5000 en plus.
Ajoutez au dessus une instruction ENV CONTEXT PROD
pour définir la variable d'environnement ENV
à la valeur PROD
par défaut.
Testez votre conteneur en mode DEV avec docker run --env CONTEXT=DEV -p 5000:5000 identidock
, visitez localhost:5000
Et en mode PROD
?
Conclusions:
- On peut faire des images multicontextes qui s'adaptent au contexte.
- Les variables d'environnement sont souvent utilisée pour configurer les conteneurs au moment de leur lancement. (plus dynamique qu'un fichier de configuration)
FROM python:3.7
RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
RUN pip install Flask uWSGI requests redis
WORKDIR /app
COPY app /app
COPY boot.sh /
RUN chmod a+x /boot.sh
ENV CONTEXT PROD
EXPOSE 9191 5000
USER uwsgi
CMD ["/boot.sh"]
Un docker-compose.prod.yml
pour identicon
Créez un deuxième fichier Compose docker-compose.prod.yml
(à compléter) pour lancer l'application identicon
en configuration de production.
On veut ajouter les fonctionnalités suivantes :
- configurer les variables d'environnement via un fichier d'environnement
- voir la documentation
- LOGLEVEL
- CONTEXT
- Tester d'autres variables Flask
- Un volume pour la base redis
- un service redis-commander pour afficher le contenu de la base redis
- disponible sur le port 8191
- le connecter via des variables d'environnement