2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00
2025-12-19 10:07:55 +01:00

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 src
  • uvicorn 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 Dockerfile qui 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 :

  1. Ouvrir et lire le contenu du Dockerfile. Essayez de comprendre ce que chaque ligne de commande (par exemple FROM, COPY, RUN, CMD) est censée faire.
  2. Ouvrir un terminal sous PyCharm dans ce projet.
  3. Vous assurez que vous êtes bien positionné dans le répertoire qui contient le Dockerfile et le code source de l'application. Vous pouvez utiliser la commande dir sur 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 nommons hello-world-app et 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 premier 8080) au port interne du conteneur (le second 8080). C'est ce qui rend votre application accessible depuis votre machine.
  • -e SECRET_KEY : définit une variable d'environnement SECRET_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 :

  1. Ouvrez Docker Desktop : Allez dans l'onglet "Containers". Vous devriez voir votre conteneur nommé mon-conteneur avec le statut "Running".
  2. 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.
  3. (Optionnel) Commande de vérification : Vous pouvez aussi taper docker ps dans 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 :

  1. Docker Desktop avec l'option Kubernetes activé (Settings > Kubernetes > Enable Kubernetes).
  2. De l'image hello-world-app:1.0 que nous avons "buildée" localement.
  3. Un dossier k8s/ contenant nos deux fichiers : deploiement.yaml et service.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 :

  1. 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: IfNotPresent pour 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'environnement SECRET_KEY.
    • Donne au pod l'étiquette (label) app: hello-world-app.
  2. service.yaml :

    • Crée un service réseau.
    • type: LoadBalancer pour l'exposer sur notre localhost.
    • selector: app: hello-world-app pour qu'il sache quels pods cibler (ceux de notre déploiement).
    • Mappe le port 8080 (navigateur) au port 8000 (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
  1. Votre Navigateur parle à localhost:8080.
  2. Docker Desktop intercepte et envoie au Service (hello-world-app-service).
  3. Le Service voit quels Pods ont l'étiquette app: hello-world-app.
  4. Il envoie la requête au Pod (et donc à notre conteneur hello-world-app:1.0).
  5. 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 :

  1. CI (Intégration) : lancer les tests, le "linting" (qualité de code), et les scans de sécurité (DevSecOps).
  2. 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 à chaque push sur la branche master.
  • jobs: : il y a 3 "gros" travaux (jobs) :
    1. 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).
    2. deploy-python-aws-eb : c'est la CD. Il déploie l'application sur AWS en mode "Python".
    3. 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.

  1. Allez dans le menu VCS (Version Control System).
  2. Cliquez sur Enable Version Control Integration....
  3. Choisissez Git dans la liste.

3. Créer le Dépôt Distant (GitHub)

  1. Allez sur GitHub.com et connectez-vous.
  2. Créez un Nouveau Dépôt (New Repository).
  3. Nommez-le (ex: hello-world-fastapi).
  4. Sélectionnez Private (privé).
  5. IMPORTANT : ne cochez AUCUNE case (ni README, ni .gitignore). Laissez-le vide.
  6. 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.

  1. Sur la page de votre nouveau dépôt GitHub, allez dans l'onglet Settings (Paramètres).
  2. Dans le menu de gauche, naviguez vers Secrets and variables > Actions.
  3. Cliquez sur New repository secret.
  4. Créez le premier secret :
    • Nom : AWS_ACCESS_KEY_ID
    • Valeur : (insérez la valeur fournie dans le canal Teams de votre cours)
  5. Créez le second secret :
    • Nom : AWS_SECRET_ACCESS_KEY
    • Valeur : (insérez la valeur fournie dans le canal Teams de votre cours)

Attention : Les noms AWS_ACCESS_KEY_ID et AWS_SECRET_ACCESS_KEY doivent être exactement les mêmes que ceux utilisés dans le fichier ci-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.

  1. 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 sur OK.
  2. 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 Commit et choisissez Commit and Push....
    • Confirmez le push vers la branche origin/master.

6. Observer la magie (CI/CD)

  1. Retournez sur votre dépôt GitHub.
  2. Cliquez sur l'onglet Actions.
  3. Vous devriez voir votre pipeline, avec votre message "Initial commit", en train de s'exécuter (une icône jaune).
  4. 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.

  1. Retournez dans PyCharm.
  2. Ouvrez n'importe quel fichier, par exemple ce README.md.
  3. Ajoutez un simple commentaire ou un espace vide quelque part.
  4. Ouvrez la fenêtre Commit dans la barre latérale gauche.
  5. Écrivez un message (ex: Fake commit).
  6. Faites Commit and Push... à nouveau.
  7. Retournez sur l'onglet Actions de 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 :

  1. Branche master (ou main) : contient le code en production. Personne n'y touche directement.
  2. Branche develop : contient la version en cours de développement.
  3. Branches de fonctionnalité (ou bug) : pour chaque nouvelle tâche ou bug (ex: feature/add-login), vous créez une branche à partir de develop, 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
Description
No description provided
Readme 80 KiB
Languages
Python 62.7%
Dockerfile 35.5%
Procfile 1.8%