Développement d’exemples et points spécifiques¶
Support de présentation : https://soleil-docker.jrobert-orleans.fr/06-Ecosysteme_et_Orchestration
(et en pdf : 06-Ecosysteme_et_Orchestration-export)
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.
Ecrivez 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 scout cves <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 scout cves essai_scan
Suivez les recommendations pour améliorer la sécurité de votre 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.
Essayez !
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 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
Exercice¶
Dans l’exemple suivant la personne qui fournit le Dockerfile a pour intention de créer une application qui qui exécute un serveur web Nginx et un serveur SSH pour qu’un utilisateur “user” puisse se connecter en SSH et modifier le contenu du serveur web. Cependant, ce qu’il propose ne respecte pas les bonnes pratiques énoncées ci-dessus.
Essayez de proposer des améliorations.
.
├── Dockerfile
└── index.html
FROM ubuntu:latest
MAINTAINER John Doe <ohn.doe@example.com>
RUN apt-get update
RUN apt-get install -y nginx vim git openssh-server
COPY index.html /var/www/html/index.html
RUN mkdir /run/sshd
RUN useradd -m -s /bin/bash user
RUN echo 'user:password' | chpasswd
EXPOSE 80
EXPOSE 22
ENTRYPOINT ["bash", "-c"]
CMD ["/sbin/sshd && nginx && tail -f /var/log/nginx/access.log"]
version: '3'
services:
web:
build: .
ports:
- "80:80"
- "22:22"
volumes:
- ./index.html:/var/www/html/index.html
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.
Exercices¶
Exercice Build multistage GO¶
Créer une application simple en Go
Dans un nouveau dossier, créez un fichier main.go qui met en place un serveur web basique affichant « Hello, World! ».
Créez également le fichier go.mod pour la gestion des dépendances.
go.mod :
module example.com/hello go 1.18
main.go :
package main import ( "fmt" "log" "net/http" ) func handler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, World! Ceci est une application GO.\n") } func main() { http.HandleFunc("/", handler) log.Println("Serveur démarré sur http://localhost:8080") log.Fatal(http.ListenAndServe(":8080", nil)) }
Créer un Dockerfile naif
Pour bien voir la différence, commencez par écrire un Dockerfile simple qui :
Part de l’image golang:1.18.
Copie le code source.
Compile l’application. (
CGO_ENABLED=0 GOOS=linux go build -o /app/main .)Expose le port et lance l’exécutable.
Construisez cette image et notez sa taille avec docker images.
Modifier le Dockerfile pour utiliser le multi-stage
Modifiez votre Dockerfile pour introduire deux étapes :
Étape 1 (le `builder`) : Basée sur golang:1.18, cette étape sera responsable de la compilation du code source pour produire un binaire statique.
Étape 2 (l’image finale) : Basée sur une image minimale comme alpine:latest ou même scratch (une image vide). Copiez uniquement le binaire compilé depuis l’étape builder dans cette nouvelle étape.
La commande finale (CMD) doit lancer ce binaire.
Construire et comparer
Construisez la nouvelle image multi-stage.
Comparez sa taille avec celle de l’image naïve. La différence devrait être spectaculaire !
Lancez un conteneur à partir de l’image optimisée pour vérifier qu’elle fonctionne toujours.
Exercice Environnement de développement JS¶
Créer une application Node.js simple
Dans un nouveau dossier, initialisez un projet Node.js (npm init -y).
Installez Express et nodemon (en tant que dépendance de développement) : npm install express et npm install –save-dev nodemon.
Créez un fichier app.js qui démarre un serveur Express simple :
app.js :
const express = require('express'); const app = express(); const port = 3000; app.get('/', (req, res) => { res.send('Hello, Docker Dev World!'); // Modifiez ce message ! }); app.listen(port, () => { console.log(`Application en écoute sur http://localhost:${port}`); });
Ajoutez un script dev dans votre package.json pour lancer l’application avec nodemon :
package.json :
... "scripts": { ... "dev": "nodemon app.js" } ...
Créer le Dockerfile
Créez un Dockerfile qui prépare l’image pour la production.
Il doit : - Partir d’une image node alpine. - Copier package.json et package-lock.json. - Installer uniquement les dépendances de production (npm ci –only=production). - Copier le reste du code source. - La commande par défaut (CMD) doit lancer l’application avec node (ex: CMD [« node », « app.js »]).
Créer le fichier Docker Compose pour le développement
Créez un fichier docker-compose.yml.
Définissez un service pour votre application pour le développement :
Utilisez la directive build pour construire l’image à partir de votre Dockerfile.
Mappez le port de l’application (ex: 3000:3000).
Utilisez un volume pour monter le répertoire de votre code source local dans le répertoire de travail du conteneur. Cela écrasera le code copié lors du build.
Remplacez la commande par défaut (CMD) avec la commande de développement qui utilise nodemon (ex: command: npm run dev).
Testez !
Exercice Reverse Proxy¶
Créer deux applications web simples
Créez deux dossiers distincts, app1 et app2.
Dans chaque dossier, créez une application web très simple qui affiche un message distinct (ex: « Je suis l’application 1 » et « Je suis l’application 2 »). Dans ce chaque dossier, mettez le Dockerfile qui permet de générer l’image de l’application. Par exemple un Dockerfile basé sur Nginx avec une simple page HTML est suffisant.
Créer le fichier Docker Compose
À la racine du projet, créez un fichier docker-compose.yml qui définira trois services : traefik, app1, et app2.
Service `traefik` : Définissez un service traefik (en suivant la documentation ici https://doc.traefik.io/traefik/expose/docker/)
Services `app1` et `app2` :
Configurez chaque service pour qu’il se construise à partir de son Dockerfile.
N’exposez pas les ports de ces applications sur la machine hôte.
Ajoutez des labels Docker à chaque service pour que Traefik sache comment router le trafic vers eux. Vous devrez définir la règle de routage basée sur le nom d’hôte (ex: Host(`app1.fbi.com)`).
Tester le routage
Lancez l’ensemble avec docker-compose up.
Ouvrez votre navigateur et visitez http://app1.fbi.com et http://app2.fbi.com. Vous devriez voir les pages des applications correspondantes.
Visitez http://localhost:8080 pour voir le tableau de bord de Traefik et observer les routes qu’il a créées.