CI/CD avec GitHub Actions : automatiser le déploiement Kubernetes
Vous avez votre application conteneurisée et vos manifests Kubernetes prêts. Maintenant, il faut automatiser : chaque push doit déclencher les tests, builder l'image et déployer sur le cluster. GitHub Actions rend cela simple et gratuit pour les projets open source.
Architecture du pipeline
Un pipeline CI/CD complet pour Kubernetes comprend plusieurs étapes :
Push → Build → Test → Scan → Push Image → Deploy → Verify
| Étape | Objectif | Outils |
|---|---|---|
| Build | Compiler l'application | Maven, Gradle, npm |
| Test | Valider le code | JUnit, pytest, Jest |
| Scan | Détecter les vulnérabilités | Trivy, Snyk |
| Push Image | Stocker l'image Docker | Docker Hub, GHCR, ECR |
| Deploy | Appliquer sur Kubernetes | kubectl, Helm |
| Verify | Valider le déploiement | Smoke tests |
Configuration initiale
Structure du projet
.github/
└── workflows/
├── ci.yaml # Tests sur chaque PR
├── cd.yaml # Deploy sur main
└── security.yaml # Scan quotidien
k8s/
├── base/
│ ├── deployment.yaml
│ └── service.yaml
└── overlays/
├── dev/
│ └── kustomization.yaml
└── prod/
└── kustomization.yamlSecrets GitHub à configurer
Dans Settings > Secrets and variables > Actions :
| Secret | Description |
|---|---|
DOCKERHUB_USERNAME | Utilisateur Docker Hub |
DOCKERHUB_TOKEN | Token d'accès Docker Hub |
KUBE_CONFIG | Fichier kubeconfig encodé en base64 |
SLACK_WEBHOOK | URL webhook Slack (optionnel) |
# Générer le KUBE_CONFIG encodé
cat ~/.kube/config | base64 -w 0Pipeline CI : tests et qualité
Workflow complet
# .github/workflows/ci.yaml
name: CI Pipeline
on:
pull_request:
branches: [main, develop]
push:
branches: [main, develop]
env:
JAVA_VERSION: '21'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# Job 1 : Build et tests
build-test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B compile
- name: Run tests
run: mvn -B test
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: target/surefire-reports/
- name: Generate coverage report
run: mvn -B jacoco:report
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: target/site/jacoco/jacoco.xml
fail_ci_if_error: true
# Job 2 : Analyse de code
code-quality:
name: Code Quality
runs-on: ubuntu-latest
needs: build-test
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Nécessaire pour SonarQube
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Cache SonarQube packages
uses: actions/cache@v4
with:
path: ~/.sonar/cache
key: ${{ runner.os }}-sonar
- name: SonarQube Scan
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: |
mvn -B verify sonar:sonar \
-Dsonar.projectKey=mon-api \
-Dsonar.host.url=https://sonarcloud.io
# Job 3 : Build Docker image
build-image:
name: Build Docker Image
runs-on: ubuntu-latest
needs: build-test
if: github.event_name == 'push'
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: 'temurin'
cache: maven
- name: Build JAR
run: mvn -B package -DskipTests
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
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=sha,prefix=
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Job 4 : Scan de sécurité
security-scan:
name: Security Scan
runs-on: ubuntu-latest
needs: build-image
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'Pipeline CD : déploiement Kubernetes
Workflow de déploiement
# .github/workflows/cd.yaml
name: CD Pipeline
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'staging'
type: choice
options:
- staging
- production
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# Job 1 : Deploy to staging
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
environment: staging
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v4
with:
version: 'v1.29.0'
- name: Configure kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG_STAGING }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Update image tag in manifests
run: |
cd k8s/overlays/staging
kustomize edit set image \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
kubectl apply -k k8s/overlays/staging
kubectl -n staging rollout status deployment/mon-api --timeout=300s
- name: Verify deployment
run: |
kubectl -n staging get pods -l app=mon-api
kubectl -n staging get svc mon-api
- name: Run smoke tests
run: |
ENDPOINT=$(kubectl -n staging get svc mon-api -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl -f http://$ENDPOINT/actuator/health || exit 1
- name: Notify Slack on success
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "✅ Deployment to staging successful!",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Staging deployment successful*\nCommit: `${{ github.sha }}`\nBy: ${{ github.actor }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "❌ Deployment to staging failed!",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Staging deployment FAILED*\nCommit: `${{ github.sha }}`\nCheck: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
# Job 2 : Deploy to production (manual approval)
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: deploy-staging
environment: production
if: github.event.inputs.environment == 'production' || github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v4
- name: Configure kubectl
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBE_CONFIG_PROD }}" | base64 -d > ~/.kube/config
- name: Deploy with canary strategy
run: |
# Deploy canary (10% traffic)
cd k8s/overlays/production
kustomize edit set image \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
kubectl apply -k .
kubectl -n production rollout status deployment/mon-api --timeout=600s
- name: Health check
run: |
sleep 30
HEALTHY=$(kubectl -n production get pods -l app=mon-api \
-o jsonpath='{.items[*].status.conditions[?(@.type=="Ready")].status}' | grep -c True)
TOTAL=$(kubectl -n production get pods -l app=mon-api --no-headers | wc -l)
if [ "$HEALTHY" -lt "$TOTAL" ]; then
echo "Health check failed: $HEALTHY/$TOTAL pods ready"
kubectl -n production rollout undo deployment/mon-api
exit 1
fi
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
generate_release_notes: trueConfiguration Kubernetes avec Kustomize
Base commune
# k8s/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
commonLabels:
app: mon-api
managed-by: kustomizeOverlay staging
# k8s/overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
resources:
- ../../base
replicas:
- name: mon-api
count: 2
images:
- name: ghcr.io/org/mon-api
newTag: latest
patches:
- patch: |
- op: replace
path: /spec/template/spec/containers/0/resources/limits/memory
value: 512Mi
target:
kind: Deployment
name: mon-apiOverlay production
# k8s/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- ../../base
- hpa.yaml
- pdb.yaml
replicas:
- name: mon-api
count: 5
images:
- name: ghcr.io/org/mon-api
newTag: latest
patches:
- patch: |
- op: replace
path: /spec/template/spec/containers/0/resources/limits/memory
value: 2Gi
- op: replace
path: /spec/template/spec/containers/0/resources/limits/cpu
value: "2"
target:
kind: Deployment
name: mon-apiSécurité du pipeline
Gestion des secrets
# Utiliser OIDC au lieu de credentials statiques
permissions:
id-token: write
contents: read
steps:
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions
aws-region: eu-west-1Scanner les dépendances
# .github/workflows/security.yaml
name: Security Scan
on:
schedule:
- cron: '0 6 * * *' # Tous les jours à 6h
push:
branches: [main]
jobs:
dependency-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/maven@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=highSigner les images
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign the image
run: |
cosign sign --yes \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
env:
COSIGN_EXPERIMENTAL: 1Rollback automatique
Détection d'échec et rollback
- name: Deploy and monitor
run: |
kubectl apply -k k8s/overlays/production
# Attendre le rollout avec timeout
if ! kubectl -n production rollout status deployment/mon-api --timeout=300s; then
echo "Deployment failed, rolling back..."
kubectl -n production rollout undo deployment/mon-api
exit 1
fi
- name: Post-deployment validation
run: |
# Vérifier les métriques pendant 2 minutes
for i in {1..12}; do
ERROR_RATE=$(curl -s http://prometheus:9090/api/v1/query \
--data-urlencode 'query=rate(http_requests_total{status=~"5.."}[1m])' \
| jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Error rate too high: $ERROR_RATE"
kubectl -n production rollout undo deployment/mon-api
exit 1
fi
sleep 10
doneOptimisations
Cache des dépendances
- name: Cache Maven packages
uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache Docker layers
uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=maxParallélisation
jobs:
test:
strategy:
matrix:
java: [17, 21]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-java@v4
with:
java-version: ${{ matrix.java }}Réutilisation de workflows
# .github/workflows/reusable-deploy.yaml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
KUBE_CONFIG:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy
run: kubectl apply -k k8s/overlays/${{ inputs.environment }}# .github/workflows/cd.yaml
jobs:
deploy-staging:
uses: ./.github/workflows/reusable-deploy.yaml
with:
environment: staging
secrets:
KUBE_CONFIG: ${{ secrets.KUBE_CONFIG_STAGING }}Monitoring du pipeline
Métriques GitHub Actions
- name: Send metrics to Datadog
run: |
curl -X POST "https://api.datadoghq.com/api/v1/series" \
-H "Content-Type: application/json" \
-H "DD-API-KEY: ${{ secrets.DD_API_KEY }}" \
-d '{
"series": [{
"metric": "github.actions.deployment.duration",
"points": [['"$(date +%s)"', '"${{ steps.deploy.outputs.duration }}"']],
"tags": ["env:production", "repo:${{ github.repository }}"]
}]
}'Checklist CI/CD
Avant de mettre en production votre pipeline :
- ☐ Tests unitaires et d'intégration automatisés
- ☐ Scan de vulnérabilités (code + images)
- ☐ Secrets stockés dans GitHub Secrets (pas en clair)
- ☐ OIDC configuré pour les cloud providers
- ☐ Environnements GitHub avec approbations
- ☐ Rollback automatique en cas d'échec
- ☐ Notifications (Slack, email) configurées
- ☐ Cache des dépendances activé
- ☐ Images Docker signées
- ☐ Métriques de déploiement collectées
Conclusion
Un pipeline CI/CD bien configuré avec GitHub Actions transforme votre workflow : chaque commit est testé, scanné, et déployé automatiquement. Les clés du succès sont la sécurité (secrets, scans, signatures), la résilience (rollback automatique) et l'observabilité (notifications, métriques).
Commencez simple avec un pipeline basique, puis ajoutez progressivement les fonctionnalités avancées selon vos besoins.
Pour déployer votre application : Déployer une application Spring Boot sur Kubernetes
Pour les fondamentaux : Kubernetes pour développeurs : ce qu'il faut vraiment maîtriser