First commit
This commit is contained in:
307
.github/workflows/ci-cd.yml
vendored
Normal file
307
.github/workflows/ci-cd.yml
vendored
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
name: Déploiement sur AWS Elastic Beanstalk
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
# Déclenche le workflow uniquement sur des pushs vers la branche master
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: CI - Tests et vérifications
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
env:
|
||||||
|
SECRET_KEY: "key_for_testing"
|
||||||
|
steps:
|
||||||
|
# 1. Récupération du code
|
||||||
|
- name: 1. Récupérer le code
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
|
# 2. Installation de Python
|
||||||
|
- name: 2. Mettre en place Python 3.13
|
||||||
|
uses: actions/setup-python@v6.0.0
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
# 3. Installation de Poetry
|
||||||
|
- name: 3. Installer Poetry
|
||||||
|
run: pip install poetry==1.8.3
|
||||||
|
|
||||||
|
# 4. Installation des dépendances
|
||||||
|
- name: 4. Installer les dépendances
|
||||||
|
run: |
|
||||||
|
echo "Synchronisation du fichier poetry.lock..."
|
||||||
|
poetry lock --no-update
|
||||||
|
echo "Exportation des dépendances (y compris dev) vers requirements-dev.txt..."
|
||||||
|
poetry export --with dev -f requirements.txt --output requirements-dev.txt --without-hashes
|
||||||
|
echo "Installation via pip à partir de requirements-dev.txt..."
|
||||||
|
pip install -r requirements-dev.txt
|
||||||
|
|
||||||
|
# 5. Lancement des tests unitaires
|
||||||
|
- name: 5. Lancer les tests unitaires
|
||||||
|
run: poetry run pytest
|
||||||
|
|
||||||
|
# 6. DevSecOps - Vérifie la qualité et le style du code
|
||||||
|
- name: 6. Linter et Formater (Ruff)
|
||||||
|
run: |
|
||||||
|
echo "Vérification de la qualité du code..."
|
||||||
|
poetry run ruff check .
|
||||||
|
echo "Vérification du formatage du code..."
|
||||||
|
poetry run ruff format --check .
|
||||||
|
|
||||||
|
# 7. DevSecOps - Scanne le code Python pour les failles de sécurité courantes.
|
||||||
|
- name: 7. SAST - Analyse statique
|
||||||
|
run: |
|
||||||
|
echo "Analyse de sécurité statique (SAST) du code source (src/)..."
|
||||||
|
# Scan strict sur le code de l'application (src)
|
||||||
|
poetry run bandit -r src/
|
||||||
|
|
||||||
|
echo "Analyse de sécurité statique (SAST) du code de test (tests/)..."
|
||||||
|
# Scan des tests, en ignorant la règle B101 (assert_used)
|
||||||
|
# L'option -s (ou --skip) liste les règles à ignorer.
|
||||||
|
poetry run bandit -r tests/ -s B101
|
||||||
|
|
||||||
|
# 8. DevSecOps - Scanne les dépendances pour les vulnérabilités connues.
|
||||||
|
- name: 8. SCA - Scan des dépendances (Trivy)
|
||||||
|
# Utilise Trivy pour scanner le fichier poetry.lock à la recherche de CVEs
|
||||||
|
# et le code pour des secrets ou mauvaises configurations.
|
||||||
|
uses: aquasecurity/trivy-action@0.33.1
|
||||||
|
with:
|
||||||
|
scan-type: 'fs' # Scan du système de fichiers
|
||||||
|
scan-ref: '.' # Scanner le répertoire courant
|
||||||
|
ignore-unfixed: true # Ignorer les CVEs sans correctif connu
|
||||||
|
format: 'table' # Sortie facile à lire dans les logs
|
||||||
|
scanners: 'vuln,secret' # Activer le scan de vulnérabilités ET de secrets
|
||||||
|
# Fait échouer le build si une vulnérabilité HAUTE ou CRITIQUE est trouvée
|
||||||
|
severity: 'HIGH,CRITICAL'
|
||||||
|
|
||||||
|
# 9. DevSecOps - Scanne l'historique Git pour des secrets accidentellement commités.
|
||||||
|
- name: 9. Scan des secrets de l'historique Git (Gitleaks)
|
||||||
|
# Scanne tout l'historique Git pour des secrets (clés, mots de passe)
|
||||||
|
# qui auraient pu être commités puis supprimés.
|
||||||
|
uses: gitleaks/gitleaks-action@v2.3.9
|
||||||
|
|
||||||
|
deploy-python-aws-eb:
|
||||||
|
name: CD - Déploiement Python sur AWS EB
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
env:
|
||||||
|
AWS_REGION: us-east-1 # Mettre la région de son environnement AWS Elastic Beanstalk "HelloWorldAPI-env"
|
||||||
|
EB_APPLICATION_NAME: "Hello World API" # doit matcher avec le nom de l'application dans l'environnement EB
|
||||||
|
EB_ENVIRONMENT_NAME: "HelloWorldAPI-env" # doit matcher avec le nom de l'environnement EB
|
||||||
|
|
||||||
|
needs: test
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 1. Récupération du code
|
||||||
|
- name: 1. Récupérer le code
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
|
# 2. Installation de Python
|
||||||
|
- name: 2. Mettre en place Python 3.13
|
||||||
|
uses: actions/setup-python@v6.0.0
|
||||||
|
with:
|
||||||
|
python-version: '3.13'
|
||||||
|
|
||||||
|
# 3. Installation de nos outils
|
||||||
|
- name: 3. Installer Poetry et AWS CLI
|
||||||
|
run: |
|
||||||
|
pip install poetry==1.8.3
|
||||||
|
pip install awscli==1.42.59
|
||||||
|
|
||||||
|
# 4. Création du 'requirements.txt' dont EB a besoin
|
||||||
|
- name: 4. Exporter les dépendances (Poetry -> requirements.txt)
|
||||||
|
run: poetry export -f requirements.txt --output requirements.txt --without-hashes
|
||||||
|
|
||||||
|
# 5. Authentification auprès d'AWS
|
||||||
|
- name: 5. Configurer les identifiants AWS
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5.1.0
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
aws-region: ${{ env.AWS_REGION }}
|
||||||
|
|
||||||
|
# 6. Récupération dynamique de l'ID de compte
|
||||||
|
- name: 6. Récupérer l'ID de compte AWS
|
||||||
|
# Maintenant que nous sommes authentifiés, nous demandons à AWS "Qui suis-je ?".
|
||||||
|
# La commande 'aws sts get-caller-identity' renvoie l'ID du compte.
|
||||||
|
# '--query Account' filtre la réponse pour ne garder que l'ID.
|
||||||
|
# '--output text' le renvoie en texte brut.
|
||||||
|
run: |
|
||||||
|
echo "Récupération de l'ID de compte..."
|
||||||
|
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
|
||||||
|
echo "ID de compte trouvé : $ACCOUNT_ID"
|
||||||
|
# On stocke cet ID dans l'environnement GitHub pour les étapes suivantes
|
||||||
|
echo "AWS_ACCOUNT_ID=$ACCOUNT_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 7. Préparation des noms uniques
|
||||||
|
- name: 7. Définir les noms de version et de fichier
|
||||||
|
# On crée des variables uniques pour ce déploiement spécifique
|
||||||
|
run: |
|
||||||
|
# Crée une étiquette de version (ex: v-ffc6c4c...)
|
||||||
|
echo "VERSION_LABEL=v-${{ github.sha }}" >> $GITHUB_ENV
|
||||||
|
# Crée un nom de fichier unique (ex: deploy-ffc6c4c.zip)
|
||||||
|
echo "ZIP_FILE_NAME=deploy-${{ github.sha }}.zip" >> $GITHUB_ENV
|
||||||
|
# Construit le nom du bucket S3 d'EB en utilisant l'ID de compte de l'étape 6
|
||||||
|
echo "S3_BUCKET_NAME=elasticbeanstalk-${{ env.AWS_REGION }}-${{ env.AWS_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 8. Zippage du projet
|
||||||
|
- name: 8. Créer le paquet de déploiement (ZIP)
|
||||||
|
# On zippe tout le code (Procfile, src/, requirements.txt)
|
||||||
|
# On exclut les dossiers qui ne servent à rien sur le serveur
|
||||||
|
run: |
|
||||||
|
echo "Création du fichier ${{ env.ZIP_FILE_NAME }}..."
|
||||||
|
zip -r ${{ env.ZIP_FILE_NAME }} . -x ".git/*" ".github/*" "*.idea/*" "*.vscode/*"
|
||||||
|
|
||||||
|
# 9. Envoi du code sur S3
|
||||||
|
- name: 9. Envoyer le paquet sur S3
|
||||||
|
# Elastic Beanstalk déploie depuis S3. On doit y copier notre ZIP.
|
||||||
|
run: |
|
||||||
|
echo "Envoi sur s3://${{ env.S3_BUCKET_NAME }}/${{ env.ZIP_FILE_NAME }}"
|
||||||
|
aws s3 cp ${{ env.ZIP_FILE_NAME }} s3://${{ env.S3_BUCKET_NAME }}/${{ env.ZIP_FILE_NAME }}
|
||||||
|
|
||||||
|
# 10. Informer EB qu'une nouvelle version existe
|
||||||
|
- name: 10. Créer la nouvelle version de l'application
|
||||||
|
# On dit à EB : "Voici une nouvelle version (VERSION_LABEL),
|
||||||
|
# son code source est à cet emplacement S3 (source-bundle)"
|
||||||
|
run: |
|
||||||
|
echo "Création de la version ${{ env.VERSION_LABEL }}..."
|
||||||
|
aws elasticbeanstalk create-application-version \
|
||||||
|
--application-name "${{ env.EB_APPLICATION_NAME }}" \
|
||||||
|
--version-label "${{ env.VERSION_LABEL }}" \
|
||||||
|
--source-bundle S3Bucket="${{ env.S3_BUCKET_NAME }}",S3Key="${{ env.ZIP_FILE_NAME }}" \
|
||||||
|
--description "Déploiement depuis GitHub Actions (SHA: ${{ github.sha }})"
|
||||||
|
|
||||||
|
# 11. Donner l'ordre de déploiement
|
||||||
|
- name: 11. Lancer la mise à jour de l'environnement
|
||||||
|
# C'est l'ordre final. On dit à EB :
|
||||||
|
# "Prends cette nouvelle version (VERSION_LABEL) et applique-la
|
||||||
|
# à cet environnement (EB_ENVIRONMENT_NAME)"
|
||||||
|
run: |
|
||||||
|
echo "Mise à jour de l'environnement ${{ env.EB_ENVIRONMENT_NAME }}..."
|
||||||
|
aws elasticbeanstalk update-environment \
|
||||||
|
--environment-name "${{ env.EB_ENVIRONMENT_NAME }}" \
|
||||||
|
--version-label "${{ env.VERSION_LABEL }}"
|
||||||
|
|
||||||
|
deploy-docker-aws-eb:
|
||||||
|
name: CD - Déploiement Docker sur AWS EB
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
env:
|
||||||
|
AWS_REGION: us-east-1 # Mettre la même région
|
||||||
|
EB_APPLICATION_NAME: "Hello World API" # doit matcher avec le nom de l'application dans l'environnement EB
|
||||||
|
EB_ENVIRONMENT_NAME: "HelloWorldAPI-env-docker" # doit matcher avec le nom du NOUVEL environnement EB
|
||||||
|
ECR_REPOSITORY: "mynamespace/hello-world-api" # Le nom du dépôt ECR
|
||||||
|
|
||||||
|
needs: test # Dépend aussi du job 'test'
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# 1. Récupération du code
|
||||||
|
- name: 1. Récupérer le code
|
||||||
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
|
# 2. Configurer les identifiants AWS (Nécessaire pour ECR et EB)
|
||||||
|
- name: 2. Configurer les identifiants AWS
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5.1.0
|
||||||
|
with:
|
||||||
|
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||||
|
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||||
|
aws-region: ${{ env.AWS_REGION }}
|
||||||
|
|
||||||
|
# 3. Récupération dynamique de l'ID de compte
|
||||||
|
- name: 3. Récupérer l'ID de compte AWS
|
||||||
|
run: |
|
||||||
|
echo "Récupération de l'ID de compte..."
|
||||||
|
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
|
||||||
|
echo "ID de compte trouvé : $ACCOUNT_ID"
|
||||||
|
echo "AWS_ACCOUNT_ID=$ACCOUNT_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 4. Se connecter à Amazon ECR
|
||||||
|
- name: 4. Se connecter à Amazon ECR
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v2.0.1
|
||||||
|
|
||||||
|
# 5. Définir les variables de l'image Docker
|
||||||
|
- name: 5. Définir les variables de l'image
|
||||||
|
run: |
|
||||||
|
# Récupère l'URI du registre ECR (ex: 123456789.dkr.ecr.us-east-1.amazonaws.com)
|
||||||
|
echo "ECR_REGISTRY=${{ steps.login-ecr.outputs.registry }}" >> $GITHUB_ENV
|
||||||
|
# Construit le nom complet de l'image avec le tag (SHA du commit)
|
||||||
|
echo "ECR_IMAGE_NAME=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 6. Builder, tagger et pousser l'image Docker sur ECR
|
||||||
|
- name: 6. Builder et Pousser l'image Docker
|
||||||
|
run: |
|
||||||
|
echo "Build de l'image : ${{ env.ECR_IMAGE_NAME }}"
|
||||||
|
docker build -t ${{ env.ECR_IMAGE_NAME }} .
|
||||||
|
|
||||||
|
echo "Push de l'image vers ECR..."
|
||||||
|
docker push ${{ env.ECR_IMAGE_NAME }}
|
||||||
|
|
||||||
|
# 7. Générer le fichier Dockerrun.aws.json
|
||||||
|
# Ce fichier dit à EB quelle image ECR utiliser et quel port exposer
|
||||||
|
- name: 7. Générer le Dockerrun.aws.json
|
||||||
|
run: |
|
||||||
|
echo "Génération du Dockerrun.aws.json..."
|
||||||
|
# Crée le fichier JSON
|
||||||
|
echo '{' > Dockerrun.aws.json
|
||||||
|
echo ' "AWSEBDockerrunVersion": "1",' >> Dockerrun.aws.json
|
||||||
|
echo ' "Image": {' >> Dockerrun.aws.json
|
||||||
|
echo ' "Name": "${{ env.ECR_IMAGE_NAME }}",' >> Dockerrun.aws.json
|
||||||
|
echo ' "Update": "true"' >> Dockerrun.aws.json
|
||||||
|
echo ' },' >> Dockerrun.aws.json
|
||||||
|
echo ' "Ports": [' >> Dockerrun.aws.json
|
||||||
|
echo ' {' >> Dockerrun.aws.json
|
||||||
|
echo ' "ContainerPort": 8000' >> Dockerrun.aws.json
|
||||||
|
echo ' }' >> Dockerrun.aws.json
|
||||||
|
echo ' ]' >> Dockerrun.aws.json
|
||||||
|
echo '}' >> Dockerrun.aws.json
|
||||||
|
|
||||||
|
echo "Contenu du Dockerrun.aws.json :"
|
||||||
|
cat Dockerrun.aws.json
|
||||||
|
|
||||||
|
# 8. Préparation des noms uniques (pour le zip S3)
|
||||||
|
- name: 8. Définir les noms de version et de fichier
|
||||||
|
run: |
|
||||||
|
echo "VERSION_LABEL=v-docker-${{ github.sha }}" >> $GITHUB_ENV
|
||||||
|
# Le zip ne contiendra QUE le Dockerrun.aws.json
|
||||||
|
echo "ZIP_FILE_NAME=deploy-docker-${{ github.sha }}.zip" >> $GITHUB_ENV
|
||||||
|
echo "S3_BUCKET_NAME=elasticbeanstalk-${{ env.AWS_REGION }}-${{ env.AWS_ACCOUNT_ID }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
# 9. Zippage du Dockerrun.aws.json
|
||||||
|
- name: 9. Créer le paquet de déploiement (ZIP)
|
||||||
|
# Pour la plateforme Docker, le "source bundle" est juste le Dockerrun.aws.json
|
||||||
|
run: |
|
||||||
|
echo "Création du fichier ${{ env.ZIP_FILE_NAME }}..."
|
||||||
|
zip -r ${{ env.ZIP_FILE_NAME }} Dockerrun.aws.json
|
||||||
|
|
||||||
|
# 10. Envoi du paquet sur S3
|
||||||
|
- name: 10. Envoyer le paquet sur S3
|
||||||
|
run: |
|
||||||
|
echo "Envoi sur s3://${{ env.S3_BUCKET_NAME }}/${{ env.ZIP_FILE_NAME }}"
|
||||||
|
aws s3 cp ${{ env.ZIP_FILE_NAME }} s3://${{ env.S3_BUCKET_NAME }}/${{ env.ZIP_FILE_NAME }}
|
||||||
|
|
||||||
|
# 11. Informer EB qu'une nouvelle version existe
|
||||||
|
- name: 11. Créer la nouvelle version de l'application
|
||||||
|
run: |
|
||||||
|
echo "Création de la version ${{ env.VERSION_LABEL }}..."
|
||||||
|
aws elasticbeanstalk create-application-version \
|
||||||
|
--application-name "${{ env.EB_APPLICATION_NAME }}" \
|
||||||
|
--version-label "${{ env.VERSION_LABEL }}" \
|
||||||
|
--source-bundle S3Bucket="${{ env.S3_BUCKET_NAME }}",S3Key="${{ env.ZIP_FILE_NAME }}" \
|
||||||
|
--description "Déploiement Docker depuis GitHub Actions (SHA: ${{ github.sha }})"
|
||||||
|
|
||||||
|
# 12. Attendre qu'EB traite la nouvelle version
|
||||||
|
- name: 12. Attendre le traitement de la version par AWS
|
||||||
|
run: |
|
||||||
|
echo "Pause de 60 secondes pour laisser à AWS le temps de traiter la version..."
|
||||||
|
sleep 60
|
||||||
|
|
||||||
|
# 12. Donner l'ordre de déploiement
|
||||||
|
- name: 12. Lancer la mise à jour de l'environnement
|
||||||
|
run: |
|
||||||
|
echo "Mise à jour de l'environnement DOCKER ${{ env.EB_ENVIRONMENT_NAME }}..."
|
||||||
|
aws elasticbeanstalk update-environment \
|
||||||
|
--environment-name "${{ env.EB_ENVIRONMENT_NAME }}" \
|
||||||
|
--version-label "${{ env.VERSION_LABEL }}"
|
||||||
|
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
||||||
46
Dockerfile
Normal file
46
Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# --- Phase 1: build des dépendances ---
|
||||||
|
# Utilise une image Python complète avec Poetry
|
||||||
|
FROM python:3.13-slim AS builder
|
||||||
|
|
||||||
|
# Définir le répertoire de travail
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Installer Poetry
|
||||||
|
RUN pip install poetry==1.8.3
|
||||||
|
|
||||||
|
# Copier uniquement les fichiers de définition de projet
|
||||||
|
COPY pyproject.toml poetry.lock ./
|
||||||
|
|
||||||
|
# Créer un environnement virtuel dans un emplacement spécifique
|
||||||
|
RUN python -m venv /venv
|
||||||
|
|
||||||
|
# Activer le venv et installer les dépendances (sans dev)
|
||||||
|
# Cela permet de mettre en cache cette couche si les dépendances ne changent pas
|
||||||
|
RUN . /venv/bin/activate && \
|
||||||
|
poetry export -f requirements.txt --output requirements.txt --without-hashes && \
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# --- Phase 2: image finale d'éxécution ---
|
||||||
|
# Utiliser une image slim car nous n'avons plus besoin des outils de build
|
||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
# Définir le répertoire de travail
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copier l'environnement virtuel complet de la phase de build
|
||||||
|
COPY --from=builder /venv /venv
|
||||||
|
|
||||||
|
# Copier le code source de l'application
|
||||||
|
# Assumant que le code FastAPI est dans src/
|
||||||
|
COPY src/ ./src
|
||||||
|
|
||||||
|
# Ajouter /app/src au PYTHONPATH pour que Python trouve le module "app"
|
||||||
|
ENV PYTHONPATH="${PYTHONPATH}:/app/src"
|
||||||
|
|
||||||
|
# Exposer le port sur lequel FastAPI (uvicorn) va tourner
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
# Commande pour lancer l'application
|
||||||
|
# On utilise le binaire uvicorn du venv
|
||||||
|
# EB (plateforme Docker) s'attend par défaut à ce que l'app tourne sur le port 8000
|
||||||
|
CMD ["/venv/bin/uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
1
Procfile
Normal file
1
Procfile
Normal file
@@ -0,0 +1 @@
|
|||||||
|
web: gunicorn --chdir src -w 2 -k uvicorn.workers.UvicornWorker app.main:app
|
||||||
462
README.md
Normal file
462
README.md
Normal file
@@ -0,0 +1,462 @@
|
|||||||
|
# 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<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 :
|
||||||
|
> ```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<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 :
|
||||||
|
|
||||||
|
> ```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<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](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
|
||||||
|
```
|
||||||
33
k8s/deploiement.yaml
Normal file
33
k8s/deploiement.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: hello-world-app-deploy
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: hello-world-app
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: hello-world-app
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: hello-world-app-conteneur
|
||||||
|
# C'est l'image que vous avez buildée localement
|
||||||
|
image: hello-world-app:1.0
|
||||||
|
# Très important : indique à K8s de ne PAS essayer de télécharger l'image
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
# Le port sur lequel notre app (Uvicorn) écoute DANS le conteneur
|
||||||
|
- containerPort: 8000
|
||||||
|
env:
|
||||||
|
# Définit une variable d'environnement pour le conteneur
|
||||||
|
- name: SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
# Indique de prendre la valeur depuis un objet Secret
|
||||||
|
secretKeyRef:
|
||||||
|
# Le nom du secret créé à l'étape 1
|
||||||
|
name: hello-world-secret
|
||||||
|
# La clé spécifique à utiliser dans ce secret
|
||||||
|
key: SECRET_KEY
|
||||||
16
k8s/service.yaml
Normal file
16
k8s/service.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: hello-world-app-service
|
||||||
|
spec:
|
||||||
|
# LoadBalancer expose le service sur une IP externe (localhost sur Docker Desktop)
|
||||||
|
type: LoadBalancer
|
||||||
|
selector:
|
||||||
|
# Cible tous les pods ayant l'étiquette "app: hello-world-app"
|
||||||
|
app: hello-world-app
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
# Le port sur lequel le service est exposé sur votre machine physique (celui de votre navigateur)
|
||||||
|
port: 8080
|
||||||
|
# Le port sur lequel le trafic doit être envoyé (le port de votre conteneur)
|
||||||
|
targetPort: 8000
|
||||||
1288
poetry.lock
generated
Normal file
1288
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
64
pyproject.toml
Normal file
64
pyproject.toml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
[project]
|
||||||
|
name = "hello-world-fastapi"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Hello World FastAPI"
|
||||||
|
authors = [
|
||||||
|
{name = "Your Name",email = "you@example.com"}
|
||||||
|
]
|
||||||
|
readme = "README.md"
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
]
|
||||||
|
keywords = ["fastapi", "web"]
|
||||||
|
|
||||||
|
exclude = [
|
||||||
|
{ path = "tests", format = "wheel" }
|
||||||
|
]
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.13"
|
||||||
|
fastapi = "^0.116.1"
|
||||||
|
uvicorn = { version = "^0.35.0", extras = [ "standard" ] }
|
||||||
|
gunicorn = "^23.0.0"
|
||||||
|
pydantic = {extras = ["email"], version = "^2.11.7"}
|
||||||
|
python-dotenv = "^1.0.1"
|
||||||
|
pydantic-settings = "^2.10.1"
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
pytest = "^8.4.1"
|
||||||
|
pytest-cov = "^6.2.1"
|
||||||
|
pytest-asyncio = "^1.1.0"
|
||||||
|
pytest-mock = "^3.14.1"
|
||||||
|
httpx = "^0.28.1"
|
||||||
|
aiosqlite = "^0.21.0"
|
||||||
|
coverage = { version="*", extras=["toml"]}
|
||||||
|
ruff = "^0.14.2"
|
||||||
|
bandit = "^1.8.6"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
pythonpath = "src"
|
||||||
|
testpaths = "tests"
|
||||||
|
addopts = "-v -s"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.pycln]
|
||||||
|
all = true
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
line_length = 120
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
force_grid_wrap = 0
|
||||||
|
use_parentheses = true
|
||||||
|
ensure_newline_before_comments = true
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
0
src/app/__init__.py
Normal file
0
src/app/__init__.py
Normal file
0
src/app/core/__init__.py
Normal file
0
src/app/core/__init__.py
Normal file
33
src/app/core/config.py
Normal file
33
src/app/core/config.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pydantic import AnyHttpUrl, SecretStr
|
||||||
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""
|
||||||
|
Classe de configuration qui charge les variables d'environnement.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Configuration du modèle Pydantic
|
||||||
|
model_config = SettingsConfigDict(
|
||||||
|
env_file=".env",
|
||||||
|
env_file_encoding="utf-8",
|
||||||
|
case_sensitive=True, # Respecte la casse des variables
|
||||||
|
)
|
||||||
|
|
||||||
|
# Paramètres du projet
|
||||||
|
PROJECT_NAME: str = "FastAPI Project"
|
||||||
|
API_V1_STR: str = "/api/v1"
|
||||||
|
|
||||||
|
# Configuration de la sécurité (JWT)
|
||||||
|
SECRET_KEY: SecretStr
|
||||||
|
|
||||||
|
# Configuration CORS
|
||||||
|
# Pydantic va automatiquement convertir la chaîne de caractères séparée par des virgules
|
||||||
|
# en une liste de chaînes de caractères.
|
||||||
|
BACKEND_CORS_ORIGINS: List[AnyHttpUrl] = []
|
||||||
|
|
||||||
|
|
||||||
|
# Création d'une instance unique des paramètres qui sera importée dans le reste de l'application
|
||||||
|
settings = Settings()
|
||||||
27
src/app/main.py
Normal file
27
src/app/main.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
app = FastAPI(title="Hello World API", description="Hello World API.", version="1.0.0")
|
||||||
|
|
||||||
|
if settings.BACKEND_CORS_ORIGINS:
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=[
|
||||||
|
str(origin).rstrip("/") for origin in settings.BACKEND_CORS_ORIGINS
|
||||||
|
],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", tags=["Root"])
|
||||||
|
def read_root():
|
||||||
|
"""
|
||||||
|
Un endpoint simple pour vérifier que l'API est en ligne.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"message": "Welcome to this fantastic API! Config Secret Key : "
|
||||||
|
+ str(settings.SECRET_KEY)
|
||||||
|
}
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
0
tests/api/__init__.py
Normal file
0
tests/api/__init__.py
Normal file
20
tests/api/test_api.py
Normal file
20
tests/api/test_api.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_read_root_success(test_client: AsyncClient):
|
||||||
|
"""
|
||||||
|
Vérifie que l'endpoint racine ("/") fonctionne et renvoie le message attendu
|
||||||
|
"""
|
||||||
|
# 1. Action (appel de l'API)
|
||||||
|
response = await test_client.get("/")
|
||||||
|
|
||||||
|
# 2. Assertions (vérifications)
|
||||||
|
|
||||||
|
# Vérifie que la requête a réussi
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Vérifie le contenu de la réponse
|
||||||
|
data = response.json()
|
||||||
|
assert "fantastic" in data["message"]
|
||||||
15
tests/conftest.py
Normal file
15
tests/conftest.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from httpx import AsyncClient, ASGITransport
|
||||||
|
|
||||||
|
from app.main import app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
async def test_client() -> AsyncGenerator[AsyncClient, None]:
|
||||||
|
# On crée un "transport" pour l'application ASGI
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
yield client
|
||||||
|
app.dependency_overrides.clear()
|
||||||
Reference in New Issue
Block a user