Chapitre 1
Vue d'ensemble et philosophie CI/CD
Qu'est-ce que CI/CD ?
CI (Intégration Continue) : chaque push de code déclenche automatiquement les tests et le build, garantissant que le code intégré ne casse rien.
CD (Déploiement Continu) : si le CI passe, le code est automatiquement déployé en production (ou staging), sans intervention manuelle.
Le flux complet
💻
git push
Developer
→
🔍
Lint + Tests
CI Runner
→
🐳
Build Docker
Registry
→
🚀
Deploy
VPS / K8s
→
✅
Health Check
Monitoring
→
🔔
Notification
Slack / Email
Les trois environnements
- Development — branche
develop, déployé automatiquement sur un serveur de test - Staging — branche
staging, miroir de la production pour validation finale - Production — branche
main, déploiement après validation manuelle (approval gate)
💡 Règle d'or : Le pipeline doit échouer rapidement (fast fail). Si les tests ne passent pas, le déploiement n'a pas lieu, peu importe l'urgence.
Chapitre 2
Structure du projet et prérequis
Arborescence recommandée
# Structure de votre dépôt mon-projet/ ├── .github/ │ └── workflows/ │ ├── ci.yml # Tests + build (sur tous les pushes) │ ├── deploy-staging.yml # Deploy staging (branche staging) │ └── deploy-prod.yml # Deploy production (branche main) ├── docker/ │ ├── Dockerfile │ └── nginx.conf ├── docker-compose.yml # Composition locale ├── docker-compose.prod.yml # Composition production └── src/
Secrets GitHub à configurer
Dans votre dépôt GitHub → Settings → Secrets and variables → Actions :
PROD_SSH_HOST
PROD_SSH_USER
PROD_SSH_KEY
REGISTRY_TOKEN
SLACK_WEBHOOK_URL
APP_ENV_PROD
Générer et configurer la clé SSH de déploiement
# Sur votre machine locale : générer une clé dédiée au CI ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/deploy_key -N "" # Copier la clé publique sur le VPS ssh-copy-id -i ~/.ssh/deploy_key.pub deployer@votre-vps.com # Ajouter la clé privée dans GitHub Secrets # PROD_SSH_KEY = contenu de ~/.ssh/deploy_key (clé PRIVÉE) cat ~/.ssh/deploy_key
⚠️ La clé privée SSH va dans les Secrets GitHub (chiffrés). Ne jamais la committer dans le code.
Chapitre 3
Pipeline CI — Tests et build
.github/workflows/ci.yml
name: CI — Tests & Build on: push: branches: ['**'] # Sur toutes les branches pull_request: branches: [main, staging] jobs: test: name: Tests & Lint runs-on: ubuntu-latest services: postgres: image: postgres:16-alpine env: POSTGRES_DB: test_db POSTGRES_USER: postgres POSTGRES_PASSWORD: secret options: >- --health-cmd pg_isready --health-interval 5s --health-timeout 3s --health-retries 5 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup PHP 8.3 uses: shivammathur/setup-php@v2 with: php-version: '8.3' extensions: pdo_pgsql, redis, bcmath coverage: xdebug - name: Install Composer dependencies run: composer install --no-interaction --prefer-dist --optimize-autoloader - name: Prepare Laravel environment run: | cp .env.testing .env php artisan key:generate php artisan migrate --force - name: Run PHPUnit tests run: php artisan test --parallel --coverage-clover coverage.xml - name: PHP CS Fixer (Lint) run: vendor/bin/php-cs-fixer fix --dry-run --diff build: name: Build & Push Docker image needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging' steps: - uses: actions/checkout@v4 - name: Log in to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v5 with: push: true tags: | ghcr.io/${{ github.repository }}:${{ github.sha }} ghcr.io/${{ github.repository }}:latest cache-from: type=gha cache-to: type=gha,mode=max
💡 Cache Docker layers —
cache-from/cache-to: type=gha utilise le cache GitHub Actions pour accélérer les builds répétés de 60 à 80%.Chapitre 4
Déploiement sur VPS — Zero-downtime
docker-compose.prod.yml
services: app: image: ghcr.io/monorg/mon-app:${IMAGE_TAG} restart: unless-stopped env_file: .env.production depends_on: db: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 10s timeout: 5s retries: 3 start_period: 30s labels: - "traefik.enable=true" - "traefik.http.routers.app.rule=Host(`app.example.com`)" - "traefik.http.routers.app.tls.certresolver=letsencrypt" db: image: postgres:16-alpine restart: unless-stopped volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"] interval: 5s volumes: pgdata:
.github/workflows/deploy-prod.yml
name: Deploy — Production on: push: branches: [main] jobs: deploy: name: Deploy to VPS (zero-downtime) runs-on: ubuntu-latest environment: name: production url: https://app.example.com steps: - uses: actions/checkout@v4 - name: Deploy via SSH uses: appleboy/ssh-action@v1 with: host: ${{ secrets.PROD_SSH_HOST }} username: ${{ secrets.PROD_SSH_USER }} key: ${{ secrets.PROD_SSH_KEY }} script: | # Pull la nouvelle image docker pull ghcr.io/${{ github.repository }}:${{ github.sha }} # Mise à jour zero-downtime avec Docker Compose export IMAGE_TAG=${{ github.sha }} docker compose -f /opt/mon-app/docker-compose.prod.yml up -d --no-deps app # Attendre le health check echo "Waiting for health check..." timeout 60 bash -c 'until docker inspect --format="{{.State.Health.Status}}" mon-app-app-1 | grep -q healthy; do sleep 3; done' # Migrations Laravel docker compose -f /opt/mon-app/docker-compose.prod.yml exec -T app php artisan migrate --force # Nettoyage des anciennes images docker image prune -f echo "✅ Deployment successful: ${{ github.sha }}"
✅ Zero-downtime garanti : Docker Compose recrée uniquement le container
app, la base de données et les volumes restent intacts. Le health check confirme que le nouveau container répond avant de considérer le déploiement réussi.Rollback en un seul commit
# En cas de problème, revenir au commit précédent git revert HEAD --no-edit git push origin main # Le pipeline se relance automatiquement avec la version précédente
Chapitre 5
Déploiement sur Kubernetes
Workflow complet GitHub Actions → K8s
name: Deploy — Kubernetes on: push: branches: [main] jobs: deploy-k8s: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Configure kubectl uses: azure/k8s-set-context@v3 with: kubeconfig: ${{ secrets.KUBECONFIG }} - name: Inject image tag into manifests run: sed -i "s|IMAGE_TAG|${{ github.sha }}|g" k8s/deployment.yaml - name: Apply manifests run: kubectl apply -f k8s/ -n production - name: Wait for rollout run: kubectl rollout status deployment/mon-app -n production --timeout=120s - name: Run post-deploy migrations run: | kubectl exec deployment/mon-app -n production -- php artisan migrate --force - name: Rollback on failure if: failure() run: kubectl rollout undo deployment/mon-app -n production
💡 L'étape
Rollback on failure s'exécute automatiquement si une étape précédente échoue, grâce à if: failure().Chapitre 6
Gestion des environnements
Stratégie de branches recommandée
# Gitflow simplifié pour CI/CD
main → Production (protégée, PR obligatoire, approval gate)
staging → Staging (déploiement automatique)
develop → Dev (déploiement automatique)
feature/* → CI uniquement (tests + build, pas de déploiement)
Variables d'environnement par env
# .env.staging APP_ENV=staging APP_URL=https://staging.example.com DB_DATABASE=app_staging LOG_LEVEL=debug CACHE_DRIVER=redis --- # .env.production APP_ENV=production APP_URL=https://app.example.com APP_DEBUG=false LOG_LEVEL=error CACHE_DRIVER=redis
Approval gate pour la production
Dans GitHub → Settings → Environments → production : activer Required reviewers. Le déploiement en production ne peut se lancer qu'après approbation manuelle d'un responsable.
⚠️ Ne jamais déployer directement en production depuis une branche feature. Toujours passer par staging d'abord.
Chapitre 7
Notifications et observabilité
Notification Slack sur succès/échec
# Ajouter en fin de workflow - name: Notify Slack — Success if: success() uses: slackapi/slack-github-action@v1 with: channel-id: '#deployments' slack-message: | ✅ *${{ github.repository }}* déployé en production Commit: `${{ github.sha }}` par @${{ github.actor }} ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} - name: Notify Slack — Failure if: failure() uses: slackapi/slack-github-action@v1 with: channel-id: '#deployments' slack-message: | 🚨 *ÉCHEC DÉPLOIEMENT* — ${{ github.repository }} Commit: `${{ github.sha }}` — rollback automatique déclenché env: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
Badge de statut dans le README
# Ajouter dans README.md


Chapitre 8
Checklist de mise en place
Avant d'activer le pipeline
- Secrets GitHub configurés : SSH key, tokens registry, variables d'env production
- Branche
mainprotégée : PR obligatoire, CI doit passer avant le merge - Fichier
.env.testingprésent et fonctionnel pour les tests en CI - Route
/healthimplémentée dans l'application (retourne 200 si OK) - Health check configuré dans
docker-compose.prod.yml - Approval gate activé sur l'environnement
productionGitHub - Rollback testé manuellement :
kubectl rollout undoou revert Git - Notification Slack ou email configurée sur échec
- Logs centralisés et accessibles post-déploiement
- Pipeline testé sur une branche feature avant de l'activer sur
main
Commandes de vérification post-déploiement
# Vérifier l'état du container docker compose -f docker-compose.prod.yml ps # Logs en temps réel docker compose -f docker-compose.prod.yml logs -f app --tail=50 # Health check manuel curl -f https://app.example.com/health && echo "✅ OK" || echo "❌ FAIL" # Vérifier la version déployée curl https://app.example.com/health | jq '.version'
