# 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 : ```bash 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 : > ```bash > 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 : > ```bash > 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 : > ```bash > 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 : > ```bash > 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 : > ```bash > 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. ```mermaid graph LR Utilisateur[Navigateur/Client] -- Requête --> MachineHote[Votre Machine Hôte
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 : > ```bash > 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. > ```bash > # 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. > ```bash > # 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' > ``` > > ```bash > # 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. > ```bash > 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. > ```bash > # -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. > ```bash > # 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](http://localhost:8080) Vous verrez un retour de notre application (API) qui tourne sur Kubernetes. #### Schéma de ce qui se passe ```mermaid graph LR Utilisateur["1. Navigateur"] -- "Requête http://localhost:8080" --> ServiceK8S["2. Service: hello-world-app-service
(type: LoadBalancer)"] ServiceK8S -- "3. Sélectionne les pods
(selector: app: hello-world-app)" --> PodK8S subgraph PodK8S ["4. Pod: hello-world-app-..."] Conteneur["5. Conteneur: hello-world-app:1.0
(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 : > ```bash > # 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 > ``` > > ```bash > # 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. ```mermaid graph TD A["Développeur: git push
(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](https://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. ```mermaid 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`). ```mermaid 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 ```