⚙️ CI/CD — Pipelines de A à Z
Formation complète CI/CD : GitHub Actions, GitLab CI, testing, GitOps. Des fondamentaux aux pipelines de production avec exemples concrets.
Introduction — CI/CD en 2026, c’est non négociable
En 2026, si tu déploies encore à la main, tu vis dans le passé. Le CI/CD n’est plus un luxe ou un “nice to have” — c’est le socle de toute équipe qui livre du logiciel sérieusement.
Les faits sont là :
- Les équipes avec du CI/CD mature déploient 100x plus souvent que celles sans (DORA metrics)
- Le temps de recovery après incident chute de jours à minutes
- Les bugs sont détectés en minutes au lieu de semaines
- Le coût d’un déploiement tend vers zéro
Cette formation te donne tout ce qu’il faut pour construire des pipelines solides, de zéro à la prod. Pas de théorie creuse — du YAML, du code, des patterns qui marchent.
Ce que tu vas apprendre :
- Les fondamentaux du CI/CD et du trunk-based development
- GitHub Actions en profondeur avec des workflows complets
- GitLab CI avec des pipelines multi-stages
- Les stratégies de test automatisé (unit → E2E)
- Le GitOps avec ArgoCD et Flux
Module 1 — Fondamentaux CI/CD
CI vs CD vs CD — Mettons les choses au clair
Trois acronymes, deux qui partagent les mêmes lettres. Clarifions :
CI — Continuous Integration Chaque commit déclenche un build + tests. Le code est intégré en continu dans la branche principale. L’objectif : détecter les conflits et bugs le plus tôt possible.
CD — Continuous Delivery Le code est toujours dans un état déployable. Chaque commit qui passe le pipeline peut aller en production. Le déploiement reste un acte manuel (un clic, une approbation).
CD — Continuous Deployment Tout commit qui passe le pipeline va automatiquement en production. Zéro intervention humaine. C’est le graal, mais ça demande une confiance totale dans tes tests.
Commit → Build → Test → [Continuous Integration]
→ Package → Staging → [Continuous Delivery]
→ Production → [Continuous Deployment]
Trunk-Based Development
Oublie les branches feature/JIRA-1234-refacto-du-service-machin qui vivent 3 semaines. Le trunk-based development, c’est :
- Une branche principale (
mainoutrunk) - Des branches courtes (< 24h idéalement, 48h max)
- Des feature flags pour le code pas encore prêt
- Des merges fréquents vers main
Pourquoi ? Parce que plus une branche vit longtemps, plus le merge est douloureux. Et plus le merge est douloureux, moins tu merges. C’est un cercle vicieux.
# Workflow trunk-based typique
git checkout -b feat/add-health-endpoint
# ... travail de quelques heures ...
git add -A && git commit -m "feat: add /healthz endpoint"
git push origin feat/add-health-endpoint
# PR → review → merge → delete branch
# Tout ça dans la journée
Les 7 principes d’un bon pipeline
-
Rapide — Moins de 10 minutes pour le feedback. Si ton pipeline prend 45 minutes, personne ne va attendre.
-
Fiable — Un pipeline flaky (qui échoue aléatoirement) est pire qu’un pipeline absent. Les devs vont ignorer les échecs.
-
Reproductible — Même commit = même résultat. Pas de “ça marchait sur ma machine”.
-
Incrémental — Ne rebuild que ce qui a changé. Cache les dépendances, les layers Docker, les artefacts.
-
Parallélisé — Les tests unitaires n’ont pas besoin d’attendre le linting. Exécute en parallèle tout ce qui peut l’être.
-
Sécurisé — Les secrets sont dans un vault, pas dans le code. Les images sont scannées. Les dépendances sont auditées.
-
Observable — Tu dois savoir pourquoi ça a cassé en 30 secondes. Logs structurés, artefacts de test, notifications ciblées.
Anatomie d’un pipeline moderne
graph LR
A[Lint + Format] --> B[Build + Cache]
B --> C[Test Unit/Int]
C --> D[Security Scan]
D --> E[Deploy Stage]
E --> F[Deploy Prod - approval]
Module 2 — GitHub Actions en profondeur
Structure d’un workflow
Les workflows GitHub Actions vivent dans .github/workflows/. Chaque fichier YAML = un workflow.
# .github/workflows/ci.yml
name: CI Pipeline
# Déclencheurs
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
# Possibilité de déclencher manuellement
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
# Variables d'environnement globales
env:
NODE_VERSION: '20'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
# Les jobs
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run format:check
Concepts clés
Runners — Les machines qui exécutent tes jobs :
ubuntu-latest— Le plus courant, Linuxmacos-latest— Pour les builds iOS/macOSwindows-latest— Pour .NET et consorts- Self-hosted — Tes propres machines pour plus de contrôle
Contextes — Les variables disponibles :
${{ github.sha }}— Le SHA du commit${{ github.ref_name }}— Le nom de la branche${{ secrets.MY_SECRET }}— Un secret du repo${{ vars.MY_VAR }}— Une variable de configuration${{ needs.job_id.outputs.value }}— Output d’un job précédent
Cache — Crucial pour la performance :
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
Workflow complet — Build Node.js + Tests
# .github/workflows/ci.yml
name: Node.js CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
# ─── Lint & Format ───
quality:
name: Code Quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: ESLint
run: npm run lint -- --format=json --output-file=eslint-report.json
continue-on-error: true
- name: Prettier check
run: npm run format:check
- name: Upload lint report
if: always()
uses: actions/upload-artifact@v4
with:
name: eslint-report
path: eslint-report.json
# ─── Tests unitaires ───
test-unit:
name: Unit Tests
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm run test:unit -- --coverage
- name: Upload coverage
if: matrix.node-version == 20
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
# ─── Tests d'intégration ───
test-integration:
name: Integration Tests
runs-on: ubuntu-latest
needs: [quality]
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
# ─── Build ───
build:
name: Build
runs-on: ubuntu-latest
needs: [test-unit, test-integration]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
retention-days: 7
Workflow complet — Docker Build + Push
# .github/workflows/docker.yml
name: Docker Build & Push
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
docker:
name: Build & Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # Pour cosign
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=git-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
- name: Sign image with Cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@v3
- name: Sign the image
if: github.event_name != 'pull_request'
run: |
cosign sign --yes \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
Workflow complet — Deploy Kubernetes
# .github/workflows/deploy.yml
name: Deploy to Kubernetes
on:
workflow_run:
workflows: ["Docker Build & Push"]
types: [completed]
branches: [main]
jobs:
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
environment:
name: staging
url: https://staging.monapp.ch
steps:
- uses: actions/checkout@v4
- name: Install kubectl
uses: azure/setup-kubectl@v4
with:
version: 'v1.30.0'
- name: Configure kubeconfig
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBE_CONFIG_STAGING }}" | base64 -d > $HOME/.kube/config
- name: Install Kustomize
uses: imranismail/setup-kustomize@v2
- name: Update image tag
working-directory: k8s/overlays/staging
run: |
kustomize edit set image \
app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:git-${GITHUB_SHA::7}
- name: Deploy to staging
run: |
kustomize build k8s/overlays/staging | kubectl apply -f -
kubectl rollout status deployment/app -n staging --timeout=300s
- name: Run smoke tests
run: |
# Attendre que le service soit prêt
sleep 10
curl -sf https://staging.monapp.ch/healthz || exit 1
curl -sf https://staging.monapp.ch/readyz || exit 1
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [deploy-staging]
environment:
name: production
url: https://monapp.ch
steps:
- uses: actions/checkout@v4
- name: Install kubectl
uses: azure/setup-kubectl@v4
- name: Configure kubeconfig
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBE_CONFIG_PROD }}" | base64 -d > $HOME/.kube/config
- name: Install Kustomize
uses: imranismail/setup-kustomize@v2
- name: Deploy with canary
working-directory: k8s/overlays/production
run: |
# D'abord déployer le canary (10% du trafic)
kustomize edit set image \
app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:git-${GITHUB_SHA::7}
kustomize build . | kubectl apply -f -
kubectl rollout status deployment/app -n production --timeout=300s
- name: Post-deploy verification
run: |
# Vérifier les métriques pendant 5 minutes
for i in $(seq 1 30); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" https://monapp.ch/healthz)
if [ "$HTTP_CODE" != "200" ]; then
echo "Health check failed with $HTTP_CODE, rolling back..."
kubectl rollout undo deployment/app -n production
exit 1
fi
sleep 10
done
echo "Deploy verified successfully"
Actions réutilisables (Composite Actions)
Quand tu répètes la même logique dans plusieurs workflows, crée une action composite :
# .github/actions/setup-node-project/action.yml
name: 'Setup Node.js Project'
description: 'Checkout, setup Node.js, install deps'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
runs:
using: 'composite'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
shell: bash
run: npm ci
- name: Cache build
uses: actions/cache@v4
with:
path: |
.next/cache
dist/
key: build-${{ runner.os }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
Utilisation :
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: ./.github/actions/setup-node-project
with:
node-version: '20'
- run: npm test
Module 3 — GitLab CI
Structure d’un .gitlab-ci.yml
GitLab CI utilise un seul fichier .gitlab-ci.yml à la racine du repo. La philosophie est différente de GitHub Actions — tout est plus centralisé.
# .gitlab-ci.yml
# Image Docker par défaut pour tous les jobs
default:
image: node:20-alpine
# Retry automatique en cas d'échec infra
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
# Variables globales
variables:
NPM_CONFIG_CACHE: "$CI_PROJECT_DIR/.npm"
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
# Définition des stages (ordre d'exécution)
stages:
- quality
- test
- build
- security
- deploy
# Cache global
cache:
key:
files:
- package-lock.json
paths:
- .npm/
- node_modules/
# ─── QUALITY ───
lint:
stage: quality
script:
- npm ci --cache .npm --prefer-offline
- npm run lint
- npm run format:check
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# ─── TESTS ───
test:unit:
stage: test
script:
- npm ci --cache .npm --prefer-offline
- npm run test:unit -- --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
when: always
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
test:integration:
stage: test
services:
- name: postgres:16-alpine
alias: db
variables:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
- name: redis:7-alpine
alias: cache
variables:
DATABASE_URL: "postgresql://test:test@db:5432/testdb"
REDIS_URL: "redis://cache:6379"
script:
- npm ci --cache .npm --prefer-offline
- npm run test:integration
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# ─── BUILD ───
build:app:
stage: build
script:
- npm ci --cache .npm --prefer-offline
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
build:docker:
stage: build
image: docker:24-dind
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- |
docker build \
--cache-from $CI_REGISTRY_IMAGE:latest \
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
--tag $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG \
--tag $CI_REGISTRY_IMAGE:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
.
- docker push $CI_REGISTRY_IMAGE --all-tags
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
# ─── SECURITY ───
sast:
stage: security
image: returntocorp/semgrep
script:
- semgrep --config=auto --json --output=semgrep-report.json .
artifacts:
reports:
sast: semgrep-report.json
allow_failure: true
container_scan:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
needs: ["build:docker"]
# ─── DEPLOY ───
deploy:staging:
stage: deploy
image: bitnami/kubectl:1.30
environment:
name: staging
url: https://staging.monapp.ch
script:
- kubectl config use-context $KUBE_CONTEXT_STAGING
- |
kubectl set image deployment/app \
app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
-n staging
- kubectl rollout status deployment/app -n staging --timeout=300s
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
needs: ["build:docker", "test:unit", "test:integration"]
deploy:production:
stage: deploy
image: bitnami/kubectl:1.30
environment:
name: production
url: https://monapp.ch
script:
- kubectl config use-context $KUBE_CONTEXT_PROD
- |
kubectl set image deployment/app \
app=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA \
-n production
- kubectl rollout status deployment/app -n production --timeout=300s
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
needs: ["deploy:staging"]
GitLab CI — Fonctionnalités avancées
Les includes — Pour modulariser tes pipelines :
# .gitlab-ci.yml
include:
# Template local
- local: '.gitlab/ci/tests.yml'
# Template distant
- remote: 'https://gitlab.com/company/ci-templates/-/raw/main/docker.yml'
# Template du projet
- project: 'devops/ci-templates'
ref: main
file: '/templates/deploy.yml'
# Templates GitLab intégrés
- template: Security/SAST.gitlab-ci.yml
Les règles conditionnelles :
deploy:
rules:
# Seulement sur main
- if: $CI_COMMIT_BRANCH == "main"
when: manual
# Auto sur les tags
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
when: on_success
# Jamais sur les MR
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
Les environments dynamiques — Parfait pour les review apps :
review:
stage: deploy
environment:
name: review/$CI_COMMIT_REF_SLUG
url: https://$CI_COMMIT_REF_SLUG.review.monapp.ch
on_stop: stop_review
auto_stop_in: 1 week
script:
- helm upgrade --install review-$CI_COMMIT_REF_SLUG ./chart \
--set image.tag=$CI_COMMIT_SHA \
--set ingress.host=$CI_COMMIT_REF_SLUG.review.monapp.ch
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
stop_review:
stage: deploy
environment:
name: review/$CI_COMMIT_REF_SLUG
action: stop
script:
- helm uninstall review-$CI_COMMIT_REF_SLUG
when: manual
Parent-child pipelines — Pour les monorepos :
# .gitlab-ci.yml (parent)
stages:
- triggers
frontend:
stage: triggers
trigger:
include: frontend/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- frontend/**/*
backend:
stage: triggers
trigger:
include: backend/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- backend/**/*
Module 4 — Testing dans le pipeline
La pyramide de tests
╱╲
╱ E2E ╲ Peu, lents, coûteux
╱────────╲ mais haute confiance
╱Integration╲
╱──────────────╲ Modérés
╱ Unit Tests ╲
╱──────────────────╲ Beaucoup, rapides, pas chers
Tests unitaires
Rapides, isolés, nombreux. Ils couvrent la logique métier.
# Dans ton CI
test:unit:
stage: test
script:
- npm run test:unit -- --coverage --ci --reporters=default --reporters=jest-junit
artifacts:
when: always
reports:
junit: junit.xml
Exemple de config Jest :
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
'!src/types/**',
],
coverageThresholds: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
reporters: [
'default',
['jest-junit', {
outputDirectory: '.',
outputName: 'junit.xml',
}],
],
};
Tests d’intégration
Ils testent l’interaction entre composants — API + base de données, par exemple.
# GitHub Actions avec services
test-integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: app_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgresql://test:test@localhost:5432/app_test
Tests E2E avec Playwright
# .github/workflows/e2e.yml
name: E2E Tests
on:
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Build app
run: npm run build
- name: Start app
run: npm run start &
env:
PORT: 3000
- name: Wait for app
run: npx wait-on http://localhost:3000 --timeout 30000
- name: Run Playwright tests
run: npx playwright test
env:
BASE_URL: http://localhost:3000
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
Code quality — SonarQube
# GitLab CI avec SonarQube
sonarqube:
stage: quality
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
GIT_DEPTH: "0" # Important pour l'analyse de blame
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- |
sonar-scanner \
-Dsonar.projectKey=$CI_PROJECT_PATH_SLUG \
-Dsonar.sources=src \
-Dsonar.tests=src \
-Dsonar.test.inclusions=**/*.test.ts,**/*.spec.ts \
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info \
-Dsonar.host.url=$SONAR_HOST_URL \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.qualitygate.wait=true
Security scanning dans le CI
# Scan des dépendances avec npm audit
security:deps:
stage: security
script:
- npm audit --audit-level=high
- npx better-npm-audit audit --level high
allow_failure: false
# Scan avec Trivy (filesystem mode)
security:trivy:
stage: security
image:
name: aquasec/trivy:latest
entrypoint: [""]
script:
- trivy fs --exit-code 1 --severity HIGH,CRITICAL .
- trivy fs --format json --output trivy-report.json .
artifacts:
reports:
dependency_scanning: trivy-report.json
# SAST avec Semgrep
security:sast:
stage: security
image: returntocorp/semgrep
script:
- semgrep --config=auto --config=p/security-audit --json --output=semgrep.json .
artifacts:
reports:
sast: semgrep.json
Module 5 — GitOps
C’est quoi le GitOps ?
Le GitOps, c’est simple : Git est la source de vérité pour ton infrastructure. Tout changement passe par un commit. Pas de kubectl apply à la main, pas de click dans une console.
Les 4 principes du GitOps (OpenGitOps) :
- Déclaratif — L’état désiré est décrit, pas les étapes pour y arriver
- Versionné et immuable — Git stocke tout l’historique
- Automatiquement appliqué — Un agent réconcilie l’état réel avec l’état désiré
- Continuellement réconcilié — L’agent vérifie en permanence, pas juste au deploy
Push vs Pull — Deux approches
Push-based (traditionnel) :
- Le CI pousse les changements vers le cluster
- Le CI a besoin d’accès au cluster (kubeconfig)
- Problème : credentials du cluster dans le CI = surface d’attaque
Pull-based (GitOps pur) :
- Un agent dans le cluster pull les changements depuis Git
- Le CI ne touche jamais au cluster directement
- L’agent a un accès en lecture seule à Git
- Plus sécurisé, plus fiable
Push-based:
Dev → Git → CI → Build → Push → Cluster
↑ CI a les credentials
Pull-based (GitOps):
Dev → Git → CI → Build → Registry
↓
Git (manifests) ← Agent (ArgoCD/Flux) → Cluster
↑ L'agent vit dans le cluster
ArgoCD — Le standard de facto
Installation :
# Installer ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Récupérer le mot de passe initial
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
# Installer le CLI
brew install argocd # macOS
# ou
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd && sudo mv argocd /usr/local/bin/
Définir une Application ArgoCD :
# argocd/applications/my-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/company/k8s-manifests.git
targetRevision: main
path: apps/my-app/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Supprime les ressources qui ne sont plus dans Git
selfHeal: true # Corrige les drifts manuels
allowEmpty: false # Empêche de tout supprimer par accident
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ApplicationSet pour le multi-environnement :
# argocd/applicationsets/my-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app
namespace: argocd
spec:
generators:
- list:
elements:
- env: staging
cluster: in-cluster
namespace: staging
values:
replicas: "1"
- env: production
cluster: in-cluster
namespace: production
values:
replicas: "3"
template:
metadata:
name: 'my-app-{{env}}'
spec:
project: default
source:
repoURL: https://github.com/company/k8s-manifests.git
targetRevision: main
path: 'apps/my-app/overlays/{{env}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{namespace}}'
syncPolicy:
automated:
prune: true
selfHeal: true
Flux CD — L’alternative CNCF
# Installer Flux
flux install
# Bootstrap avec GitHub
flux bootstrap github \
--owner=company \
--repository=fleet-infra \
--branch=main \
--path=./clusters/production \
--personal
Définir un déploiement Flux :
# clusters/production/my-app/kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: my-app
namespace: flux-system
spec:
interval: 5m
path: ./apps/my-app/overlays/production
prune: true
sourceRef:
kind: GitRepository
name: fleet-infra
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: my-app
namespace: production
timeout: 3m
retryInterval: 1m
---
# Image automation (met à jour automatiquement le tag dans Git)
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: my-app
namespace: flux-system
spec:
imageRepositoryRef:
name: my-app
policy:
semver:
range: '>=1.0.0'
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: my-app
namespace: flux-system
spec:
image: ghcr.io/company/my-app
interval: 1m
Structure d’un repo GitOps
k8s-manifests/
├── apps/
│ ├── my-app/
│ │ ├── base/
│ │ │ ├── kustomization.yaml
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── hpa.yaml
│ │ └── overlays/
│ │ ├── staging/
│ │ │ ├── kustomization.yaml
│ │ │ └── patch-replicas.yaml
│ │ └── production/
│ │ ├── kustomization.yaml
│ │ ├── patch-replicas.yaml
│ │ └── patch-resources.yaml
│ └── another-app/
│ └── ...
├── infrastructure/
│ ├── cert-manager/
│ ├── ingress-nginx/
│ └── monitoring/
└── clusters/
├── staging/
│ └── kustomization.yaml
└── production/
└── kustomization.yaml
Comparatif — GitHub Actions vs GitLab CI
Pas de tableau, comme promis. Voici les différences qui comptent :
Configuration
- GitHub Actions : Un fichier par workflow dans
.github/workflows/. Tu peux en avoir autant que tu veux. Chaque workflow est indépendant. - GitLab CI : Un seul
.gitlab-ci.ymlà la racine (avec possibilité d’includes). Plus centralisé, mais peut devenir un monstre sur un gros projet.
Runners
- GitHub Actions : Runners hébergés par GitHub (gratuit pour les repos publics, minutes limitées pour les privés). Self-hosted runners disponibles. Les runners GitHub sont frais à chaque job.
- GitLab CI : Runners auto-hébergés par défaut (gitlab.com offre des shared runners avec minutes). Plus de contrôle, mais tu gères l’infra.
Marketplace / Écosystème
- GitHub Actions : Marketplace gigantesque avec des milliers d’actions. C’est un avantage et un risque (supply chain attacks — utilise toujours un SHA, pas un tag).
- GitLab CI : Pas de marketplace, mais des templates intégrés (SAST, DAST, container scanning). Moins de risque de supply chain, mais tu codes plus toi-même.
Container Registry
- GitHub Actions : ghcr.io intégré. Gratuit pour les repos publics.
- GitLab CI : Registry intégré par projet. Variables
$CI_REGISTRY*prêtes à l’emploi. Très bien intégré.
Environnements et review apps
- GitHub Actions : Support des environments avec protection rules et secrets par environnement. Pas de review apps native.
- GitLab CI : Environments dynamiques natifs, review apps intégrées, auto-stop. Plus mature sur ce point.
Monorepo
- GitHub Actions : Path filters dans les triggers (
on.push.paths). Fonctionne, mais pas de pipeline parent-enfant natif. - GitLab CI : Parent-child pipelines,
rules:changespar chemin. Meilleur support monorepo.
Prix (en 2026)
- GitHub Actions : 2000 min/mois gratuit (privé), illimité en public. Après, ~0.008$/min (Linux).
- GitLab CI : 400 min/mois gratuit (shared runners). Self-hosted = coût de ton infra.
Verdict
- Choisis GitHub Actions si tu es déjà sur GitHub, que tu veux un écosystème riche, et que tes projets sont petits à moyens.
- Choisis GitLab CI si tu as besoin de tout-en-un (registry, environments, review apps), de self-hosting, ou si tu travailles en monorepo.
- Les deux sont excellents. Le meilleur CI/CD, c’est celui que ton équipe maîtrise.
Bonnes pratiques — Checklist
Avant de partir, vérifie que ton pipeline coche ces cases :
Vitesse
- Le pipeline complet tourne en moins de 15 minutes
- Les tests unitaires finissent en moins de 5 minutes
- Tu utilises le cache pour les dépendances
- Les jobs indépendants tournent en parallèle
Fiabilité
- Pas de tests flaky (ou ils sont identifiés et quarantinés)
- Les retry sont configurés pour les échecs infra
- Les timeouts sont explicites
Sécurité
- Les secrets sont dans un vault / secrets manager
- Les images Docker sont scannées (Trivy, Snyk)
- Les dépendances sont auditées
- Tu pin les versions des actions/images (SHA, pas
latest) - Cosign signe tes images
GitOps
- L’état désiré est dans Git
- Les deployments sont automatiques (staging) ou semi-auto (prod)
- Le drift est détecté et corrigé
- Les rollbacks sont un
git revert
Observabilité
- Tu sais pourquoi un pipeline a échoué en 30 secondes
- Les métriques de pipeline sont trackées (durée, taux de succès)
- Les notifications sont ciblées (pas de spam)
Pour aller plus loin
- GitHub Actions Documentation
- GitLab CI/CD Documentation
- ArgoCD — Getting Started
- Flux CD — Getting Started
- DORA Metrics — Mesure la performance de ta delivery
Tu veux aller plus loin ? Consulte nos autres formations sur Kubernetes, Docker, et Sécurité DevSecOps.