CI/CD avec GitHub Actions : automatiser le déploiement Kubernetes

Pipeline CI/CD complet avec GitHub Actions pour Kubernetes : build, test, scan sécurité, deploy. Workflow YAML prêt à l'emploi avec bonnes pratiques.

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
ÉtapeObjectifOutils
BuildCompiler l'applicationMaven, Gradle, npm
TestValider le codeJUnit, pytest, Jest
ScanDétecter les vulnérabilitésTrivy, Snyk
Push ImageStocker l'image DockerDocker Hub, GHCR, ECR
DeployAppliquer sur Kuberneteskubectl, Helm
VerifyValider le déploiementSmoke 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.yaml

Secrets GitHub à configurer

Dans Settings > Secrets and variables > Actions :

SecretDescription
DOCKERHUB_USERNAMEUtilisateur Docker Hub
DOCKERHUB_TOKENToken d'accès Docker Hub
KUBE_CONFIGFichier kubeconfig encodé en base64
SLACK_WEBHOOKURL webhook Slack (optionnel)
# Générer le KUBE_CONFIG encodé
cat ~/.kube/config | base64 -w 0

Pipeline 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: true

Configuration 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: kustomize

Overlay 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-api

Overlay 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-api

Sé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-1

Scanner 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=high

Signer 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: 1

Rollback 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
    done

Optimisations

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=max

Parallé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