18 KiB
Backend Python : démonstration conteneurisation, orchestration, CI/CD, DevSecOps et déploiement Cloud AWS
Avant-propos
Ce projet est une démonstration complète des pratiques modernes de développement et de déploiement d'applications backend en Python.
A titre informatif, l'application est une simple API web construite avec FastAPI, qui expose un endpoint renvoyant un message de bienvenue.
Elle est lançable en local via :
cd srcuvicorn app.main:app --reload --host 0.0.0.0 --port 8000
Cependant, il n'est pas nécessaire de lancer l'application en local pour suivre cette démonstration.
De la même façon, à noter que cette commande est utilisé pour formatter le code source avec ruff en local :
poetry run ruff format .
Elle n'est pas non plus nécessaire, dans la mesure où le code source est déjà formaté.
Première partie : conteneurisation et déploiement avec Docker Desktop
Contexte
Vous avez à votre disposition les éléments suivants :
- Le code source d'une application Python backend de type Web API.
- Un
Dockerfilequi définit comment l'application doit être "empaquetée". - Docker Desktop est installé et en cours d'exécution sur votre machine.
Votre mission est de suivre les étapes ci-dessous pour construire l'image de l'application et la lancer.
Étapes à réaliser
1. Prise de connaissance
Avant d'exécuter les commandes, prenez une minute pour :
- Ouvrir et lire le contenu du
Dockerfile. Essayez de comprendre ce que chaque ligne de commande (par exempleFROM,COPY,RUN,CMD) est censée faire. - Ouvrir un terminal sous PyCharm dans ce projet.
- Vous assurez que vous êtes bien positionné dans le répertoire qui contient le
Dockerfileet le code source de l'application. Vous pouvez utiliser la commandedirsur Windows pour vérifier la présence des fichiers.
2. Étape 1 : construire l'image Docker
La première étape consiste à demander à Docker de lire le Dockerfile et de construire l'image.
Exécutez la commande suivante dans votre terminal :
docker build -t hello-world-app:1.0 .
Ce que fait cette commande :
docker build: c'est la commande principale pour construire une image.-t hello-world-app:1.0: l'option-t(pour tag) permet de nommer votre image. Ici, nous la nommonshello-world-appet lui donnons la version (tag)1.0..: le point final est très important. Il indique à Docker où se trouve le contexte de build (c'est-à-dire les fichiers à utiliser), dans notre cas, le répertoire courant.
Attendez que le processus de build se termine. Vous devriez voir Docker exécuter les différentes étapes définies dans le Dockerfile.
Vous pouvez vérifier que l'image a bien été créée en exécutant la commande suivante :
docker images
3. Étape 2 : lancer le conteneur (déploiement)
Maintenant que l'image est construite, vous pouvez la "lancer" pour créer une instance de votre application : un conteneur.
Note : Nous allons mapper le port externe 8080 (machine physique) sur le port 8080 interne au conteneur. Si votre Dockerfile expose un port différent, ajustez la commande en conséquence.
Exécutez la commande suivante :
docker run -d -p 8080:8000 -e SECRET_KEY="123456" --name mon-conteneur hello-world-app:1.0
Ce que fait cette commande :
docker run: la commande pour démarrer un conteneur.-d: (pour detached) lance le conteneur en arrière-plan, pour que le terminal reste disponible.-p 8080:8000: (pour publish) mappe le port de votre machine (le premier8080) au port interne du conteneur (le second8080). C'est ce qui rend votre application accessible depuis votre machine.-e SECRET_KEY: définit une variable d'environnementSECRET_KEYà l'intérieur du conteneur avec la valeur"123456". Cela peut être utile pour configurer des paramètres sensibles, et dépend de votre application.--name mon-conteneur: donne un nom facile à retenir à votre conteneur.hello-world-app:1.0: le nom de l'image que vous avez construite à l'étape précédente.
Vous pouvez vérifier que le conteneur est bien en cours d'exécution en utilisant la commande suivante :
docker ps
Si elle n'est pas listée, utilisez docker ps -a pour voir tous les conteneurs, y compris ceux qui sont arrêtés.
Ainsi que pour voir les logs du conteneur :
docker logs mon-conteneur
Si tout est fonctionnel, votre application devrait maintenant être en cours d'exécution à l'intérieur du conteneur Docker !
Vous pouvez tester l'URL http://localhost:8080 dans votre navigateur ou utiliser un outil comme curl ou Postman pour interagir avec l'API.
graph LR
Utilisateur[Navigateur/Client] -- Requête --> MachineHote[Votre Machine Hôte<br/>localhost:8080]
subgraph MachineHote
direction LR
PortHote(Port 8080)
end
subgraph ConteneurDocker [Conteneur: mon-conteneur]
direction LR
PortConteneur(Port 8000) --- App[Application FastAPI]
end
PortHote -- Mappage de port -p 8080:8000 --> PortConteneur
Vérification
Vous avez terminé ! Pour vérifier que tout fonctionne :
- Ouvrez Docker Desktop : Allez dans l'onglet "Containers". Vous devriez voir votre conteneur nommé
mon-conteneuravec le statut "Running". - Vérifiez l'application : Si l'application est une application web, ouvrez votre navigateur et rendez-vous sur
http://localhost:8080. Vous devriez voir votre application s'afficher. - (Optionnel) Commande de vérification : Vous pouvez aussi taper
docker psdans votre terminal pour lister les conteneurs en cours d'exécution.
Nettoyage
Pour arrêter et supprimer le conteneur, exécutez les commandes suivantes :
docker stop mon-conteneur docker rm mon-conteneur
Seconde partie : orchestration avec Kubernetes (Docker Desktop)
Nous allons maintenant prendre notre application, hello-world-app, et la déployer sur un vrai cluster Kubernetes (celui fourni par Docker Desktop).
Objectif : démontrer le passage de Docker à Kubernetes d'une application en utilisant des manifestes YAML.
Prérequis
Pour cette démonstration, nous avons besoin de :
- Docker Desktop avec l'option Kubernetes activé (Settings > Kubernetes >
Enable Kubernetes). - De l'image
hello-world-app:1.0que nous avons "buildée" localement. - Un dossier
k8s/contenant nos deux fichiers :deploiement.yamletservice.yaml. Ils sont fournis. N'hésitez pas à les ouvrir pour voir leur contenu.
Démonstration
Étape 1 : vérifier l'état initial
D'abord, assurons-nous que notre cluster est vide.
# Affiche les pods (conteneurs), les services (réseaux), et les déploiements kubectl get pods,svc,deploy
Vous verrez, il n'y a rien à part le service "kubernetes" par défaut.
Étape 2 : le Secret
Notre application a besoin d'une SECRET_KEY. On ne la met pas en clair dans nos fichiers de configuration. On crée un objet "Secret" dans Kubernetes.
# Je crée un secret nommé 'hello-world-secret' # Ce nom doit correspondre à ce qui est attendu dans votre deploiement.yaml kubectl create secret generic hello-world-secret --from-literal=SECRET_KEY='123456'# On peut vérifier qu'il existe kubectl get secrets
Étape 3 : explication des fichiers YAML
Les "recettes" sont déjà prêtes dans notre dossier k8s/.
Rapidement, ce qu'ils contiennent :
-
deploiement.yaml:- Demande 1 copie (
replicas: 1) de notre application (en production, on pourrait avoir plusieurs instances pour supporter une charge accrue). - Utilise notre image
image: hello-world-app:1.0. - Crucial :
imagePullPolicy: IfNotPresentpour qu'il utilise notre image locale, et ne tente pas de la télécharger depuis un registre distant. - Injecte notre secret (
123456) en tant que variable d'environnementSECRET_KEY. - Donne au pod l'étiquette (label)
app: hello-world-app.
- Demande 1 copie (
-
service.yaml:- Crée un service réseau.
type: LoadBalancerpour l'exposer sur notrelocalhost.selector: app: hello-world-apppour qu'il sache quels pods cibler (ceux de notre déploiement).- Mappe le port
8080(navigateur) au port8000(conteneur).
Étape 4 : appliquer la configuration
Maintenant, appliquons tout le contenu du dossier k8s/ en une seule commande.
kubectl apply -f k8s/
Kubernetes va lire les deux fichiers et créer/mettre à jour les ressources.
Étape 5 : observer le déploiement
Voyons ce que Kubernetes est en train de faire.
# -w signifie "watch", la commande va se rafraîchir toute seule kubectl get pods -w
Vous voyez ? Il est en ContainerCreating... et voilà, Running ! Notre application est lancée.
Étape 6 : vérification finale
Vérifions que notre service est prêt.
# Remplacez 'hello-world-app-service' si votre service a un autre nom dans le YAML kubectl get service hello-world-app-service
Vous voyez, il a une EXTERNAL-IP (IP Externe) : localhost.
Cela signifie que si vous allez sur... http://localhost:8080
Vous verrez un retour de notre application (API) qui tourne sur Kubernetes.
Schéma de ce qui se passe
graph LR
Utilisateur["1. Navigateur"] -- "Requête http://localhost:8080" --> ServiceK8S["2. Service: hello-world-app-service<br/>(type: LoadBalancer)"]
ServiceK8S -- "3. Sélectionne les pods<br/>(selector: app: hello-world-app)" --> PodK8S
subgraph PodK8S ["4. Pod: hello-world-app-..."]
Conteneur["5. Conteneur: hello-world-app:1.0<br/>(Port 8000)"]
end
PodK8S -- "Réponse" --> ServiceK8S
ServiceK8S -- "Réponse" --> Utilisateur
- Votre Navigateur parle à
localhost:8080. - Docker Desktop intercepte et envoie au Service (
hello-world-app-service). - Le Service voit quels Pods ont l'étiquette
app: hello-world-app. - Il envoie la requête au Pod (et donc à notre conteneur
hello-world-app:1.0). - L'application répond.
Nettoyage
Pour tout arrêter et supprimer, c'est aussi simple que d'appliquer la configuration :
# Supprime toutes les ressources créées via le dossier k8s/ kubectl delete -f k8s/ # N'oubliez pas le secret ! kubectl delete secret hello-world-secret# Si on vérifie à nouveau... kubectl get pods,svc,deploy
Tout a disparu. C'est la puissance de la configuration déclarative.
Troisième partie : CI/CD et DevSecOps avec GitHub Actions, et déploiement Cloud AWS
Nous avons construit et testé notre application localement avec Docker et Kubernetes. Il est temps d'automatiser tout ce processus.
Objectif : mettre en place un pipeline de CI/CD (Intégration Continue / Déploiement Continu) complet. À chaque fois que nous pousserons du code, GitHub Actions va automatiquement :
- CI (Intégration) : lancer les tests, le "linting" (qualité de code), et les scans de sécurité (DevSecOps).
- CD (Déploiement) : si la CI passe, déployer automatiquement notre application sur le cloud AWS.
Contexte
- Vous avez un projet fonctionnel sur votre machine.
- Vous disposez d'un fichier de workflow :
.github/workflows/ci-cd.yaml. - Concernant AWS : un environnement (Elastic Beanstalk) est déjà configuré pour vous sur le cloud. Vous n'avez PAS besoin de vous connecter à la console AWS. Votre seule mission est de fournir à GitHub les "clés" (Secrets) pour qu'il puisse s'y connecter à votre place.
Étapes à réaliser
1. Prise de connaissance du pipeline CI/CD
Ouvrez le fichier .github/workflows/ci-cd.yaml dans PyCharm.
Prenez 5 minutes pour le lire. Vous n'avez pas besoin de tout comprendre, mais identifiez sa structure :
on: push: branches: [ master ]: le pipeline se déclenche à chaquepushsur la branchemaster.jobs:: il y a 3 "gros" travaux (jobs) :test: c'est la partie CI / DevSecOps. Ce job installe Python, lance les tests (pytest), vérifie la qualité du code (ruff), et scanne les failles de sécurité (bandit,trivy).deploy-python-aws-eb: c'est la CD. Il déploie l'application sur AWS en mode "Python".deploy-docker-aws-eb: un deuxième déploiement, cette fois en mode "Docker", sur un autre environnement.
graph TD
A["Développeur: git push<br/>(branche master)"] --> B{"GitHub Actions déclenchés"}
B --> C["Job 1 : CI / DevSecOps"]
subgraph C["Job 1 : CI / DevSecOps"]
C1["pytest"]
C2["ruff"]
C3["bandit"]
C4["trivy"]
end
C -- "Si succès" --> D["Job 2: deploy-python-aws-eb"]
C -- "Si succès" --> E["Job 3: deploy-docker-aws-eb"]
D --> F["Déploiement Python sur AWS"]
E --> G["Déploiement Docker sur AWS"]
2. Initialiser Git dans PyCharm
Si ce n'est pas déjà fait, vous devez "dire" à PyCharm que ce projet est suivi par Git.
- Allez dans le menu
VCS(Version Control System). - Cliquez sur
Enable Version Control Integration.... - Choisissez Git dans la liste.
3. Créer le Dépôt Distant (GitHub)
- Allez sur GitHub.com et connectez-vous.
- Créez un Nouveau Dépôt (New Repository).
- Nommez-le (ex:
hello-world-fastapi). - Sélectionnez
Private(privé). - IMPORTANT : ne cochez AUCUNE case (ni
README, ni.gitignore). Laissez-le vide. - Cliquez sur
Create repository.
4. Configurer les Secrets AWS dans GitHub
C'est l'étape la plus sensible. GitHub a besoin des "clés" AWS pour s'y connecter.
- Sur la page de votre nouveau dépôt GitHub, allez dans l'onglet
Settings(Paramètres). - Dans le menu de gauche, naviguez vers
Secrets and variables>Actions. - Cliquez sur
New repository secret. - Créez le premier secret :
- Nom :
AWS_ACCESS_KEY_ID - Valeur : (insérez la valeur fournie dans le canal Teams de votre cours)
- Nom :
- Créez le second secret :
- Nom :
AWS_SECRET_ACCESS_KEY - Valeur : (insérez la valeur fournie dans le canal Teams de votre cours)
- Nom :
Attention : Les noms
AWS_ACCESS_KEY_IDetAWS_SECRET_ACCESS_KEYdoivent être exactement les mêmes que ceux utilisés dans le fichierci-cd.yaml.
5. Lier PyCharm et GitHub (Le "Push")
Votre code est en local, le dépôt est sur GitHub. Il faut maintenant les lier.
-
Ajouter la "remote" :
- Dans PyCharm, allez dans
Git>Manage Remotes.... - Cliquez sur le
+. - Pour l'URL, collez l'URL fournie par GitHub (celle en
https:// .../... .git). - Laissez le nom
origin. Cliquez surOK.
- Dans PyCharm, allez dans
-
Faire le premier Commit & Push :
- Ouvrez la fenêtre
Commit(souvent à gauche). - Sélectionnez tous vos fichiers pour les "commiter" (les "Staged").
- Écrivez un message de commit (ex:
Initial commit). - Cliquez sur la flèche à côté de
Commitet choisissezCommit and Push.... - Confirmez le
pushvers la brancheorigin/master.
- Ouvrez la fenêtre
6. Observer la magie (CI/CD)
- Retournez sur votre dépôt GitHub.
- Cliquez sur l'onglet
Actions. - Vous devriez voir votre pipeline, avec votre message "Initial commit", en train de s'exécuter (une icône jaune).
- Cliquez dessus pour voir les
jobs(test,deploy-docker,deploy-python) s'exécuter en direct. Si tout se passe bien, ils passeront au vert.
7. Provoquer un nouveau déclenchement
Votre pipeline fonctionne ! Faisons un petit changement pour le voir se re-déclencher.
- Retournez dans PyCharm.
- Ouvrez n'importe quel fichier, par exemple ce
README.md. - Ajoutez un simple commentaire ou un espace vide quelque part.
- Ouvrez la fenêtre
Commitdans la barre latérale gauche. - Écrivez un message (ex:
Fake commit). - Faites
Commit and Push...à nouveau. - Retournez sur l'onglet
Actionsde GitHub : un nouveau pipeline s'est automatiquement lancé !
Note importante : comment cela se passe en entreprise ?
Ce que nous avons fait est une simplification pour l'exercice.
Trunk Based Development
Avec la stratégie Trunk Based Development, on s'autorise à travailler directement sur la branche master (ou main), mais on s'autorise tout de même à créer des branches de fonctionnalité avec une durée de vie très courte (quelques heures). Une telle stratégie est recommandée pour exécuter la CI/CD plus souvent.
graph TD
M1["master (c1)"] --> M2["master (c2)"]
M2 --> F1["feature/A (fA1)"]
F1 --> M3["master (m1 - merge)"]
M3 --> F2["feature/B (fB1)"]
F2 --> M4["master (m2 - merge)"]
M4 --> M5["master (c3)"]
Git Flow
En entreprise, vous verrez également la stratégie Git Flow, qui est un peu plus formelle :
- Branche
master(oumain) : contient le code en production. Personne n'y touche directement. - Branche
develop: contient la version en cours de développement. - Branches de fonctionnalité (ou bug) : pour chaque nouvelle tâche ou bug (ex:
feature/add-login), vous créez une branche à partir dedevelop, avec une durée de vie plus longue (quelques jours).
Le pipeline de CI (Tests) :
Le pipeline de test (comme celui de notre job test) se déclencherait automatiquement lorsque vous essayez de "merger" (fusionner) votre branche de fonctionnalité dans develop (via une Pull Request).
Le pipeline de CD (Déploiement) :
Le déploiement en production (notre job deploy-...) ne se déclencherait PAS sur un simple commit, mais seulement lorsque le chef de projet décide de créer une nouvelle "version" (un tag Git, ex: v1.0.1).
graph TD
%% Définir les lignes principales
subgraph Ligne master
M0("init") --> M1("v1.0.0") --> M2("v1.0.1")
end
subgraph Ligne develop
D1("d1") --> D2("d2 (merge login)") --> D3("d3") --> D4("d4 (merge release)") --> D5("d5 (merge hotfix)")
end
%% Créer les liens entre les lignes
M0 --> D1;
%% Branche de fonctionnalité
D1 --> F1("feature/login")
F1 --> D2
%% Branche de release
D3 --> R1("release/v1.0")
R1 --> M1
R1 --> D4
%% Branche de hotfix
M1 --> H1("hotfix/auth-bug")
H1 --> M2
H1 --> D5