Mise en pratique - exemple de A à Z¶
Support de présentation : https://soleil-docker.jrobert-orleans.fr/06-Ecosysteme_et_Orchestration
Démo complète¶
Nous allons créer une application avec :
Un serveur backend à partir du code : https://gitlab.com/formation-docker_jr/backend_springboot.git
Un serveur frontend à partir du code : https://gitlab.com/formation-docker_jr/frontend.git
Un serveur de base de données en postgres.
Un reverse proxy qui transferera les requêtes vers /api/ vers le backend et toutes les autres vers le frontend.
Commençons par un dessin de l’architecture visée.
Ensuite, nous allons créer des images pour chaque service. Nous allons tester ces images indépendamment du reste et noter ce qu’on a fait au format docker compose pour pouvoir ensuite le reproduire.
Je propose cet ordre (qui me semble aller du plus simple au moins simple) qui sera détaillé dans la suite :
Faire tourner le frontend seul en exposant sur le port 8080
Faire tourner une base de données seule, noter son ip,
Faire tourner le backend seul en exposant le port 8081 puis le brancher à la BD (en utilisant son ip)
Ajouter des données à la base de données
Vérifier que tout cela continue de fonctionner avec un docker compose.
On verra ensuite comment ajouter le reverse proxy pour que tout passe par un port unique puis comment sécuriser davantage les réseaux.
Frontend¶
Cloner le dépôt : git clone https://gitlab.com/formation-docker_jr/frontend.git
Créer une image (docker build -t frontend_exemple) et la lancer.
On peut noter tout ça :
services:
frontend:
image: fontend_exemple
ports:
- "8080:80"
Base de données¶
docker run --rm -d -e POSTGRES_PASSWORD=toto --name pgtest postgres
Noter l’ip du conteneur :
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' pgtest
(ici on est un peu embêtés car il n’y a pas comme dans le docker compose la résolution de l’IP par le nom du conteneur)
Backend¶
D’abord sans docker :
export POSTGRES_SERVER=`docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' pgtest`
export POSTGRES_DB="postgres"
export POSTGRES_USER="postgres"
export POSTGRES_PASSWORD="toto"
java -jar target/spring-boot-docker-0.0.1-SNAPSHOT.jar
Puis créer un Dockerfile en utilisant l’image openjdk:11-jdk :
FROM openjdk:11-jdk
COPY target/*.jar /opt/app.jar
ENTRYPOINT ["java","-jar","/opt/app.jar"]
Créez l’image en l’appelant backend_exemple
Ajout de données en BD¶
Il faudrait exécuter le code sql suivant dans la base de données :
CREATE TABLE utilisateur( nom TEXT );
INSERT INTO utilisateur VALUES ('bonjour');
INSERT INTO utilisateur VALUES ('tout');
INSERT INTO utilisateur VALUES ('le');
INSERT INTO utilisateur VALUES ('monde');
Pour cela, on peut par exemple exécuter postgres directement dans le conteneur pgtest :
docker exec -it pgtest psql -U postgres
Notez qu’on aurait aussi pu lancer un conteneur rien que pour cette opération. Un conteneur qui aurait psql …
Résumé¶
version: '3.9'
services:
frontend:
image: frontend_exemple
ports:
- "8080:80"
backend:
image: backend_exemple
ports:
- "8081:8080"
environment: &env_db
- POSTGRES_SERVER=database
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=toto
- PGPASSWORD=toto
database:
image: postgres
environment:
- POSTGRES_PASSWORD=toto
init_db:
image: postgres
environment:
- POSTGRES_SERVER=database
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=toto
- PGPASSWORD=toto
- |
INIT_DB=
CREATE TABLE utilisateur( nom TEXT );
INSERT INTO utilisateur VALUES ('bonjour');
INSERT INTO utilisateur VALUES ('tout');
INSERT INTO utilisateur VALUES ('le');
INSERT INTO utilisateur VALUES ('monde');
command: sh -c "echo $$INIT_DB | psql -U $$POSTGRES_USER -h $$POSTGRES_SERVER $$POSTGRES_DB"
Améliorations¶
A partir de là, on peut :
segmenter mieux nos réseau
indiquer les dépendances entre conteneurs
documenter les ports ouverts
choisir des versions en dur pour nos conteneurs / paquets
relire nos dockerfile pour voir si on applique les bonnes pratiques
ajouter un volume pour que la base de données soit persistante
Initialisation base de données¶
Pour automatiser la création de la base de données plusieurs options sont possibles :
Créer un script coté springboot (cf https://www.baeldung.com/spring-boot-data-sql-and-schema-sql <https://www.baeldung.com/spring-boot-data-sql-and-schema-sql>_)
Créer un script coté base de données (l’image postgres permet de lancer quelque chose au premier lancement)
Utiliser un outil de gestion de migration de base de données (flyway par exemple pour springboot)
Définir un containeur supplémentaire dans le docker compose qui sera lancé sur demande
Pour aller plus loin¶
Dans la suite, nous pouvons approfondir :
l’utilisation de traefik comme reverse proxy
l’utilisation de gitlab pour déposer nos images.
l’automatisation de la compilation du code (ici le code du serveur devait être compilé à la main avant de pouvoir créer l’image)
l’utilisation de gitlab pour la compilation (et le déploiement) automatique des images
Traefik¶
Avec docker compose, vous avez vu comment déclarer des services, les « scaler », .. Pour pouvoir déployer une application, vous avez croisé haproxy pour :
l’équilibrage de charge,
un routage qui dépend de la requête http (le conteneur questionné dépend de la route demandée),
la gestion des certificats TLS (souvent un peu compliqué) ..
- Traefik répond à tous ces besoins, en un peu plus simple.
Vous allez avoir ici un petit aperçu de ce qui est possible et de son fonctionnement.
Écrivez le docker-compose suivant :
services:
reverse-proxy:
# The official v2 Traefik docker image
image: traefik:v2.7
# Enables the web UI and tells Traefik to listen to docker
command: --api.insecure=true --providers.docker
ports:
# The HTTP port
- "80:80"
# The Web UI (enabled by --api.insecure=true)
- "8080:8080"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
Lancez le service reverse-proxy : docker compose up -d reverse-proxy
Ce faisant, vous avez lancé le conteneur traefik qui :
écoute sur le port 80 de votre machine et sur le port 8080.
a un accès à l’API docker de votre machine (via le montage de /var/run/docker.sock)
Pour le moment, rien d’intéressant sur le port 80. Et sur le port 8080, vous avez une interface de gestion de traefik (rien d’intéressant non plus).
Traefik écoute tout ce qui se passe sur l’API docker et détecte lorsque vous lancez de nouveaux conteneurs. Il regarde alors les labels associés, et se configure en conséquence.
Par exemple, ajoutez les lignes suivantes à votre docker-compose.yml:
whoami:
# A container that exposes an API to show its IP address
image: traefik/whoami
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)"
Remarquez la partie « labels », elle permettra à traefik de se configurer. Lancez le service : docker compose up -d whoami
Rendez-vous à l’adresse whoami.docker.localhost : le proxy traefik transmet les requêtes qui lui sont faites vers whoami. Pas mal déjà !
Modifiez le service whoami pour qu’il y ait plusieurs répliques :
whoami:
# A container that exposes an API to show its IP address
image: traefik/whoami
labels:
- "traefik.http.routers.whoami.rule=Host(`whoami.docker.localhost`)"
deploy:
replicas: 2
Lancez à nouveau les services. Constatez le fonctionnement « round robin » de l’équilibrage de charge !
Et ce n’est qu’une toute petite partie de ce que permet Traefik..
Pour essayer, mettez en place une application web dont le contenu dynamique est servi par un conteneur et le contenu static est servi par un autre conteneur. Pour cela, vous utiliserez les règles traefik de la forme
"(Host(`example.org`) && Path(`/machin`))"
Voir également :
https://doc.traefik.io/traefik/getting-started/quick-start/ pour la doc.
https://www.digitalocean.com/community/tutorials/how-to-use-traefik-v2-as-a-reverse-proxy-for-docker-containers-on-ubuntu-20-04 pour un exemple d’usage en reverse-proxy avec un Wordpress.
Traefik dans un compose séparé¶
Dans le cas où vous avez plusieurs applications dockerisées, vous souhaiterez avoir un reverse proxy commun à toutes les applications.
Pour cela, vous aurez un docker compose pour traefik, puis un docker compose par « application »
services:
traefik:
image: "traefik:v2.9"
container_name: "traefik"
command:
#- "--log.level=DEBUG"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "8001:80"
- "8087:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
networks:
- frontends
networks:
frontends:
name: public
Puis par exemple pour une application whoami :
services:
whoami:
image: "traefik/whoami"
container_name: "simple-service"
networks:
- public
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`whoami.localhost`)"
- "traefik.http.routers.whoami.entrypoints=web"
networks:
public:
external: true
Et pour une deuxième application :
services:
whoami:
image: "traefik/whoami"
container_name: "simple-service"
networks:
- public
labels:
- "traefik.enable=true"
- "traefik.http.routers.whoami.rule=Host(`deuxieme.localhost`)"
- "traefik.http.routers.whoami.entrypoints=web"
networks:
public:
external: true
Traefik pour le code de la démo précédente¶
services:
reverse-proxy:
# The official v2 Traefik docker image
image: traefik:v2.7
# Enables the web UI and tells Traefik to listen to docker
command: --api.insecure=true --providers.docker
ports:
- "80:80"
- "8080:8080"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- public
frontend: # <- le nom du conteneur
image: image_frontend_demo
ports:
- "80"
labels:
- "traefik.http.routers.frontend.rule=Host(`demo.localhost`) && PathPrefix(`/`)"
networks:
- public
database:
image: postgres:15
ports:
- "5432"
environment:
- POSTGRES_PASSWORD=toto
networks:
- db_network
backend:
image: image_backend_demo
ports:
- "8080"
environment:
- POSTGRES_SERVER=database
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${MACHIN}
labels:
- "traefik.http.routers.backend.rule=Host(`demo.localhost`) && PathPrefix(`/api`)"
networks:
- public
- db_network
init_db:
image: postgres
environment:
- POSTGRES_SERVER=database
- POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=toto
- PGPASSWORD=toto
- |
INIT_DB=
CREATE TABLE utilisateur( nom TEXT );
INSERT INTO utilisateur VALUES ('bonjour');
INSERT INTO utilisateur VALUES ('tout');
INSERT INTO utilisateur VALUES ('le');
INSERT INTO utilisateur VALUES ('monde');
command: sh -c "echo $$INIT_DB | psql -U $$POSTGRES_USER -h $$POSTGRES_SERVER $$POSTGRES_DB"
profiles:
- tache_administration
networks:
- public
networks:
db_network:
public:
Bonnes pratiques & Securité¶
Sécurité¶
Docker propose un outil de détection de vulnérabilité (Common Vulnerabilities and Exposures -CVEs ) des images. C’est aussi simple que de lancer :
docker scan <nom_de_l_image>
Essayez par exemple en créant une image basée sur nginx:1.18.0 :
FROM nginx:1.18.0
RUN apt update
RUN apt install -y vim
puis en faisant un build :
docker build -t essai_scan .
puis le scan :
docker scan essai_scan
Vous obtiendrez un rapport plus détaillé en spécifiant le Dockerfile utilisé :
docker scan --file Dockerfile <nom_de_l_image>
.dockerignore¶
Ca ne vous a peut être pas échappé, au moment de lancer un build docker affiche « Uploading build context » . Le client docker à ce moment envoie l’intégralité du répertoire courant au serveur docker. Si ce répertoire est volumineux, cela peut prendre du temps.
Une bonne pratique consiste à écrire un fichier .dockerignore contenant des expressions régulières de fichiers à exclure du contexte de build.
Bonnes pratiques diverses¶
Ces bonnes pratiques sont tirées de la documentation : https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
Each container should have only one responsibility.
Containers should be immutable, lightweight, and fast.
Don’t store data in your container. Use a shared data store instead.
Containers should be easy to destroy and rebuild.
Use a small base image (such as Linux Alpine). Smaller images are easier to distribute.
Avoid installing unnecessary packages. This keeps the image clean and safe.
Avoid cache hits when building.
Auto-scan your image before deploying to avoid pushing vulnerable containers to production.
Scan your images daily both during development and production for vulnerabilities Based on that, automate the rebuild of images if necessary.
apt-get : privilégiez l’installation de paquets sous la forme suivante :
RUN apt-get update && apt-get install -y \
package-bar \
package-baz \
package-foo \
&& rm -rf /var/lib/apt/lists/*
Isolation des réseaux¶
Dans le docker compose suivant, on crée 3 services et 2 réseaux. Lancez ce docker compose et vérifiez observez la communication depuis chaque conteneur : * service1 peut-il communiquer (ping) avec service2 ? avec service3 ? avec yahoo.fr ? * Idem avec service2 et service3
services:
service1:
image: alpine
command: tail -f /dev/null
networks:
- internal_net
- isolated_net
service2:
image: alpine
command: tail -f /dev/null
networks:
- internal_net
service3:
image: alpine
command: tail -f /dev/null
networks:
- isolated_net
networks:
internal_net:
driver: bridge
isolated_net:
driver: bridge
internal: true
Utilisation du registry de gitlab¶
Nous allons ici voir comment utiliser gitlab pour partager vos images docker.
Prérequis¶
Créez un acces token pour gitlab
Se logguer sur gitlab
Choisir « edit profile » en haut à gauche
Choisir le menu « Access Token »
Donner un nom à votre token (par exemple formation_docker)
Choisir « read_repository » et « write_repository »
Générer le token et noter le code dans « new personnal access token »
Enregistrer ce token dans docker : docker login
Créez un projet « public » sur gitlab pour les besoins de cette expérimentation
Push/pull image¶
Créez un fichier Dockerfile contenant :
FROM hello-world
Ensuite :
docker build . -t registry.XXXX.fr/VOTRENOM/NOM_PROJET/image_hello:latest
Puis :
docker push registry.XXXX.fr/VOTRENOM/NOM_PROJET/image_hello:latest
On peut ensuite récupérer l’image sur un autre poste avec :
docker pull registry.XXXX.fr/VOTRENOM/NOM_PROJET/image_hello:latest
et la lancer avec :
docker run registry.XXXX.fr/VOTRENOM/NOM_PROJET/image_hello:latest
(Bonus) Utilisation du CI/CD: rendez vous sur gitlab puis dans le projet et dans CI/CD -> Pipelines puis choisissez Docker puis validez.
Build multistage¶
FROM alpine as base
RUN echo mot_de_pass_secret > /opt/mot_de_passe_secret
RUN echo toto > /opt/code_compile
CMD ["sh", "-c", "echo Bonjour depuis base && ls /opt"]
FROM alpine as dev
COPY --from=base /opt/code_compile /opt/
CMD ["sh", "-c", "echo Bonjour depuis dev && ls /opt"]
FROM alpine as prod
COPY --from=base /opt/code_compile /opt/
CMD ["sh", "-c", "echo Bonjour depuis prod && ls /opt"]
FROM alpine as rien
CMD ["sh", "-c", "echo Bonjour depuis rien && ls /opt"]
# syntax=docker/dockerfile:1
FROM node:14-alpine AS builder
WORKDIR /opt/
COPY . .
RUN npm install
RUN npm run build -- --mode production
FROM nginx:1.23
WORKDIR /opt/
COPY --from=builder /opt/dist/ /usr/share/nginx/html/
Pour springboot
# syntax=docker/dockerfile:1
FROM openjdk:11-jdk AS builder
WORKDIR /opt/
COPY . .
RUN ./mvnw package -Dmaven.test.skip=true
FROM openjdk:11-jdk
COPY --from=builder /opt/target/*.jar /opt/app.jar
ENTRYPOINT ["java","-jar","/opt/app.jar"]
Intérêts¶
Images allégées, permet de ne pas stocker de données sensibles dans l’image, ..
Process de compilation automatisé
Possibilité de viser un « stage » et ainsi avec un seul Dockerfile pouvoir créer des images différentes (par exemple prod / dev).
Avec docker build : docker build –target builder
Avec docker compose en ajoutant une option “target” à la partie build.