MFormations
Modern DevOps Engineering

Chapitre 19

19 - Corrections des Exercices

19 - Corrections des Exercices

Cours 19 : Corrections Détaillées des 40 Exercices

Exercice 1 : Dockerfile basique (Node.js)

Solution :

FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

Explication :

  • node:18-alpine : Image légère (≈ 120MB vs 900MB pour node:18)
  • WORKDIR : Définit le répertoire de travail
  • Copie séparée de package.json : Optimisation du cache Docker (re-build plus rapide si seul le code change)
  • npm ci : Installation déterministe basée sur package-lock.json
  • COPY . . : Copie le code source en dernier (car change le plus souvent)
  • EXPOSE : Documentation du port (ne publie pas automatiquement)

Validation :

docker build -t myapp .
docker run -d -p 3000:3000 myapp
curl http://localhost:3000

Exercice 2 : Premier déploiement Kubernetes (Nginx)

Solution :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80

Explication :

  • apiVersion apps/v1 : Version stable de l'API Deployment
  • replicas: 3 : Haute disponibilité
  • selector.matchLabels : Doit correspondre aux labels du template
  • matchLabels : Indispensable pour que le Deployment trouve ses Pods

Validation :

kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get pods -l app=nginx

Exercice 3 : Terraform - Provider AWS (Bucket S3)

Solution :

provider "aws" {
  region = "eu-west-3"
}

resource "aws_s3_bucket" "this" {
  bucket = "devops-exercice-03-${random_id.suffix.hex}"
}

resource "random_id" "suffix" {
  byte_length = 4
}

resource "aws_s3_bucket_tagging" "this" {
  bucket = aws_s3_bucket.this.id
  tags = {
    Environment = "dev"
    ManagedBy   = "Terraform"
  }
}

Explication :

  • Nom unique : Utilisation de random_id pour garantir l'unicité
  • Tags : Ajoutés via ressource séparée aws_s3_bucket_tagging
  • Provider : Configuration de la région Paris (eu-west-3)

Validation :

terraform init
terraform validate
terraform plan
terraform apply -auto-approve

Exercice 4 : Pipeline CI simple (GitHub Actions)

Solution :

name: CI Pipeline
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'npm'
      - run: npm ci
      - run: npm test

Explication :

  • actions/checkout@v4 : Récupère le code source
  • actions/setup-node@v4 : Configure Node 18 avec cache npm
  • npm ci : Installation propre (plus rapide et déterministe que npm install)
  • Le cache réduit le temps d'installation des dépendances

Exercice 5 : Docker Compose basique

Solution :

version: "3.9"
services:
  web:
    image: nginx:alpine
    ports:
      - "80:80"
    networks:
      - app-network
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks:
      - app-network
networks:
  app-network:
    driver: bridge
volumes:
  pgdata:

Explication :

  • Variable d'environnement avec valeur par défaut : ${DB_PASSWORD:-secret}
  • Volume nommé pgdata pour persister les données
  • Réseau partagé app-network pour la communication inter-services

Validation :

docker-compose up -d
docker-compose ps

Exercice 6 : Configuration Prometheus

Solution :

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'my-app'
    scrape_interval: 15s
    scrape_timeout: 10s
    static_configs:
      - targets: ['localhost:9090']

Explication :

  • scrape_interval : Fréquence de collecte globale (15s)
  • scrape_timeout : Timeout de 10s (doit être < intervalle)
  • static_configs : Targets définies statiquement

Exercice 7 : Git Workflow basique

Solution (git-workflow.md) :

# 1. Créer une branche feature
git checkout -b feature/add-auth

# 2. Ajouter un fichier
echo "def authenticate(): pass" > auth.py
git add auth.py

# 3. Committer avec message conventionnel
git commit -m "feat(auth): add authentication module"

# 4. Pousser et créer PR
git push -u origin feature/add-auth
# Créer PR sur GitHub

Explication :

  • Conventionnal Commits : type(scope): description
  • Branche feature isolée de main
  • Push avec -u pour lier la branche locale à la distante

Exercice 8 : Configuration NGINX Reverse Proxy

Solution :

events {
    worker_connections 1024;
}

http {
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;

    server {
        listen 80;
        server_name example.com;

        location / {
            proxy_pass http://localhost:3000;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;

            limit_req zone=mylimit burst=20 nodelay;
        }
    }
}

Explication :

  • limit_req_zone : Définit la zone de rate limiting (10 req/s, mémoire 10MB)
  • proxy_pass : Reverse proxy vers l'application backend
  • X-Forwarded-For : Préserve l'IP du client
  • burst=20 nodelay : Permet des bursts de 20 requêtes

Exercice 9 : Script de backup PostgreSQL

Solution (backup.sh) :

#!/bin/bash
set -euo pipefail

DB_NAME="${DB_NAME:-myapp}"
DB_USER="${DB_USER:-postgres}"
DB_PASSWORD="${DB_PASSWORD:-postgres}"
BACKUP_DIR="/var/backups/postgres"
RETENTION_DAYS=7
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"

export PGPASSWORD="${DB_PASSWORD}"

mkdir -p "${BACKUP_DIR}"

pg_dump -U "${DB_USER}" -h localhost "${DB_NAME}" | gzip > "${BACKUP_FILE}"

if [ $? -eq 0 ]; then
    echo "Backup successful: ${BACKUP_FILE}"
    # Rotation : supprimer les backups de plus de 7 jours
    find "${BACKUP_DIR}" -name "${DB_NAME}_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
else
    echo "Backup failed!"
    exit 1
fi

unset PGPASSWORD

Explication :

  • set -euo pipefail : Arrêt sur erreur, variable non définie, échec de pipe
  • Rotation : find avec -mtime +7 supprime les backups de plus de 7 jours
  • PGPASSWORD : Transmis via variable d'environnement (définie puis unset)

Exercice 10 : Healthcheck API

Solution (server.js) :

const http = require('http');

const server = http.createServer((req, res) => {
  if (req.url === '/health') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ status: 'ok' }));
  } else {
    res.writeHead(200);
    res.end('Hello World');
  }
});

server.listen(3000);

Solution (Dockerfile) :

FROM node:18-alpine
WORKDIR /usr/src/app
COPY server.js .
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "server.js"]

Explication :

  • HEALTHCHECK : Docker vérifie périodiquement la santé du conteneur
    • --interval=30s : Toutes les 30s
    • --timeout=3s : Timeout de 3s
    • --start-period=5s : Délai avant la première vérification
    • --retries=3 : 3 échecs avant de marquer unhealthy
  • wget --spider : Vérifie l'URL sans télécharger

Exercice 11 : Multi-stage Dockerfile (Java/Spring Boot)

Solution :

# Stage 1 : Build
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests

# Stage 2 : Runtime
FROM eclipse-temurin:17-jre-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

Explication :

  • Multi-stage : Le JDK lourd (stage 1) n'est pas dans l'image finale
  • dependency:go-offline : Télécharge les dépendances avant le code source (cache)
  • Utilisateur non-root : spring:spring (meilleure sécurité)
  • jre-alpine : Image JRE légère (≈ 40MB vs 300MB pour JDK)

Exercice 12 : Service Kubernetes avec ConfigMap

Solution (configmap.yaml) :

apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  APP_ENV: production
  LOG_LEVEL: info

Solution (deployment.yaml) :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  labels:
    app: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: myapp:v1
        ports:
        - containerPort: 8080
        envFrom:
        - configMapRef:
            name: app-config

Solution (service.yaml) :

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  type: ClusterIP
  selector:
    app: myapp
  ports:
  - port: 8080
    targetPort: 8080

Exercice 13 : Module Terraform VPC

Solution (modules/vpc/main.tf) :

variable "cidr_block" {
  type = string
}
variable "name" {
  type = string
}
variable "environment" {
  type = string
}

output "vpc_id" {
  value = aws_vpc.this.id
}
output "public_subnets" {
  value = aws_subnet.public[*].id
}
output "private_subnets" {
  value = aws_subnet.private[*].id
}

resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true
  enable_dns_support   = true
  tags = {
    Name        = var.name
    Environment = var.environment
  }
}

resource "aws_subnet" "public" {
  count                   = 2
  vpc_id                  = aws_vpc.this.id
  cidr_block              = cidrsubnet(var.cidr_block, 8, count.index)
  map_public_ip_on_launch = true
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  tags = {
    Name        = "${var.name}-public-${count.index}"
    Environment = var.environment
  }
}

resource "aws_subnet" "private" {
  count             = 2
  vpc_id            = aws_vpc.this.id
  cidr_block        = cidrsubnet(var.cidr_block, 8, count.index + 2)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  tags = {
    Name        = "${var.name}-private-${count.index}"
    Environment = var.environment
  }
}

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
  tags = {
    Name        = "${var.name}-igw"
    Environment = var.environment
  }
}

resource "aws_nat_gateway" "this" {
  count         = 1
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id
  tags = {
    Name        = "${var.name}-nat"
    Environment = var.environment
  }
}

resource "aws_eip" "nat" {
  count = 1
  domain = "vpc"
  tags = {
    Name        = "${var.name}-nat-eip"
    Environment = var.environment
  }
}

Explication :

  • Module réutilisable avec variables d'entrée et outputs
  • cidrsubnet : Calcule automatiquement les CIDR des sous-réseaux
  • Comptage : 2 sous-réseaux publics, 2 privés (HA multi-AZ)
  • NAT Gateway pour l'accès Internet des sous-réseaux privés

Exercice 14 : Pipeline CI/CD complète

Solution (.github/workflows/ci-cd.yml) :

name: CI/CD Pipeline
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
          cache: 'npm'
      - run: npm ci
      - run: npm test
  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build
          path: dist/
  docker:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.ref == 'refs/heads/main' }}
          tags: ${{ secrets.DOCKER_USERNAME }}/myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Exercice 15 : Stack Prometheus + Grafana

Solution (docker-compose.yml) :

version: "3.9"
services:
  prometheus:
    image: prom/prometheus:v2.49
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    ports:
      - "9090:9090"
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
  grafana:
    image: grafana/grafana:10.2
    volumes:
      - ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml
      - grafana_data:/var/lib/grafana
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
  node-exporter:
    image: prom/node-exporter:v1.6
    volumes:
      - /proc:/host/proc:ro
      - /sys:/host/sys:ro
      - /:/rootfs:ro
    command:
      - '--path.procfs=/host/proc'
      - '--path.sysfs=/host/sys'
      - '--path.rootfs=/rootfs'
    ports:
      - "9100:9100"
volumes:
  prometheus_data:
  grafana_data:

Exercice 16 : Déploiement Blue/Green

Solution (switch.sh) :

#!/bin/bash
set -euo pipefail

ACTIVE=$(kubectl get svc app-service -o jsonpath='{.spec.selector.version}')

if [ "$ACTIVE" = "blue" ]; then
  echo "Switching from BLUE to GREEN"
  kubectl patch service app-service -p '{"spec":{"selector":{"version":"green"}}}'
else
  echo "Switching from GREEN to BLUE"
  kubectl patch service app-service -p '{"spec":{"selector":{"version":"blue"}}}'
fi

echo "Rollout status:"
kubectl rollout status deployment/app-$ACTIVE --timeout=120s

Exercice 17 : ArgoCD Application

Solution (argocd-application.yaml) :

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/user/repo.git
    targetRevision: HEAD
    path: k8s/
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - ApplyOutOfSyncOnly=true
  info:
    - name: Description
      value: "Application déployée avec GitOps"

Exercice 18 : Helm Chart basique

Solution (Chart.yaml) :

apiVersion: v2
name: myapp
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.0.0"

Solution (values.yaml) :

replicaCount: 2
image:
  repository: nginx
  tag: stable
  pullPolicy: IfNotPresent
service:
  type: ClusterIP
  port: 80
ingress:
  enabled: false
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 128Mi

Solution (templates/_helpers.tpl) :

{{- define "myapp.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}

Exercice 19 : Backup etcd

Solution (etcd-backup.sh) :

#!/bin/bash
set -euo pipefail

ETCDCTL_API=3
ENDPOINT="https://127.0.0.1:2379"
CACERT="/etc/kubernetes/pki/etcd/ca.crt"
CERT="/etc/kubernetes/pki/etcd/server.crt"
KEY="/etc/kubernetes/pki/etcd/server.key"
BACKUP_DIR="/backup/etcd"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/etcd-snapshot-${TIMESTAMP}.db"
S3_BUCKET="s3://my-backups/etcd/"

mkdir -p "${BACKUP_DIR}"

ETCDCTL_API=3 etcdctl \
  --endpoints="${ENDPOINT}" \
  --cacert="${CACERT}" \
  --cert="${CERT}" \
  --key="${KEY}" \
  snapshot save "${BACKUP_FILE}"

# Vérification
ETCDCTL_API=3 etcdctl --write-out=table snapshot status "${BACKUP_FILE}"

# Upload vers S3
aws s3 cp "${BACKUP_FILE}" "${S3_BUCKET}"

# Rotation locale : garder 7 jours
find "${BACKUP_DIR}" -name "etcd-snapshot-*.db" -mtime +7 -delete

Exercice 20 : Alerting avec Alertmanager

Solution (rules.yml) :

groups:
- name: infrastructure
  rules:
  - alert: HighCPUUsage
    expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "CPU > 80% sur {{ $labels.instance }}"
  - alert: PodCrashLooping
    expr: kube_pod_status_phase{phase="CrashLoopBackOff"} > 0
    for: 2m
    labels:
      severity: warning
    annotations:
      summary: "Pod {{ $labels.pod }} en CrashLoopBackOff"
  - alert: DiskSpaceLow
    expr: (node_filesystem_avail_bytes{fstype!="",mountpoint="/"} / node_filesystem_size_bytes{fstype!="",mountpoint="/"}) * 100 < 15
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "Espace disque < 15% sur {{ $labels.instance }}"

Solution (alertmanager.yml) :

route:
  receiver: team-infra
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h
  routes:
  - match:
      severity: critical
    receiver: team-infra
    repeat_interval: 1h
receivers:
- name: team-infra
  slack_configs:
  - api_url: 'https://hooks.slack.com/services/T000000/B000000/XXXXXXXX'
    channel: '#alerts'
    send_resolved: true
    title: '{{ .GroupLabels.alertname }}'
    text: '{{ .CommonAnnotations.summary }}'
  email_configs:
  - to: 'team@company.com'
    from: 'alertmanager@company.com'
    smarthost: 'smtp.company.com:587'
inhibit_rules:
- source_match:
    severity: critical
  target_match:
    severity: warning
  equal: ['instance']

Exercice 21 : Service Mesh Istio - Canary

Solution (virtual-service.yaml) :

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: myapp
spec:
  hosts:
  - myapp
  http:
  - route:
    - destination:
        host: myapp
        subset: v1
      weight: 90
    - destination:
        host: myapp
        subset: v2
      weight: 10

Solution (destination-rule.yaml) :

apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: myapp
spec:
  host: myapp
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 10
        maxRequestsPerConnection: 10
    outlierDetection:
      consecutive5xxErrors: 5
      interval: 30s
      baseEjectionTime: 60s
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2

Exercice 22 : Infrastructure AWS complète (Terraform)

Solution (infra/main.tf) :

module "vpc" {
  source      = "../modules/vpc"
  cidr_block  = "10.0.0.0/16"
  name        = "devops-cluster"
  environment = "production"
}

module "eks" {
  source          = "terraform-aws-modules/eks/aws"
  version         = "19.19.1"
  cluster_name    = "devops-cluster"
  cluster_version = "1.28"
  vpc_id          = module.vpc.vpc_id
  subnet_ids      = module.vpc.private_subnets
  node_groups = {
    main = {
      desired_capacity = 3
      max_capacity     = 10
      min_capacity     = 2
      instance_types   = ["t3.medium"]
    }
  }
}

resource "aws_db_instance" "postgres" {
  identifier        = "devops-db"
  engine            = "postgres"
  engine_version    = "15.3"
  instance_class    = "db.t3.small"
  allocated_storage = 100
  db_name           = "appdb"
  username          = "admin"
  password          = random_password.db_password.result
  skip_final_snapshot = false
  vpc_security_group_ids = [module.vpc.vpc_id]
  db_subnet_group_name   = aws_db_subnet_group.main.name
}

resource "random_password" "db_password" {
  length  = 16
  special = false
}

resource "aws_elasticache_cluster" "redis" {
  cluster_id           = "devops-cache"
  engine               = "redis"
  node_type            = "cache.t3.micro"
  num_cache_nodes      = 1
  parameter_group_name = "default.redis7"
  subnet_group_name    = aws_elasticache_subnet_group.main.name
  security_group_ids   = [module.vpc.vpc_id]
}

output "kubeconfig_command" {
  value = "aws eks update-kubeconfig --name ${module.eks.cluster_name} --region eu-west-3"
}

Exercice 23 : Chaos Engineering avec Chaos Mesh

Solution (chaos/pod-kill.yaml) :

apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
  name: pod-kill-example
  namespace: chaos-mesh
spec:
  action: pod-kill
  mode: one
  selector:
    namespaces: [production]
    labelSelectors:
      app: myapp
  duration: 60s
  scheduler:
    cron: "@every 5m"

Solution (chaos/network-delay.yaml) :

apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
  name: network-delay
  namespace: chaos-mesh
spec:
  action: delay
  mode: all
  selector:
    namespaces: [production]
    labelSelectors:
      app: myapp
  delay:
    latency: 200ms
    jitter: 50ms
  duration: 120s
  scheduler:
    cron: "@every 10m"

Solution (chaos/cpu-stress.yaml) :

apiVersion: chaos-mesh.org/v1alpha1
kind: StressChaos
metadata:
  name: cpu-stress
  namespace: chaos-mesh
spec:
  mode: one
  selector:
    namespaces: [production]
    labelSelectors:
      app: myapp
  stressors:
    cpu:
      workers: 2
      load: 80
  duration: 60s

Exercice 24 : Pipeline GitOps avancé

Solution (.github/workflows/gitops-pipeline.yml) :

name: GitOps Pipeline
on:
  push:
    branches: [dev, staging, prod]
  pull_request:
    branches: [staging, prod]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Validate K8s manifests
        run: |
          kubectl apply --dry-run=client -f k8s/
  security-scan:
    needs: validate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          format: sarif
          output: trivy-results.sarif
  build:
    needs: security-scan
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.tag.outputs.tag }}
    steps:
      - uses: actions/checkout@v4
      - id: tag
        run: echo "tag=${{ github.sha }}" >> $GITHUB_OUTPUT
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: myapp:${{ github.sha }}
  update-manifests:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          repository: myorg/gitops-config
          token: ${{ secrets.GIT_TOKEN }}
      - name: Update image tag
        run: |
          sed -i "s|image: myapp:.*|image: myapp:${{ needs.build.outputs.tag }}|" k8s/deployment.yaml
          git config user.name "GitOps Bot"
          git config user.email "bot@company.com"
          git add .
          git commit -m "chore: update image to ${{ needs.build.outputs.tag }}"
          git push

Exercice 25 : Stack d'observabilité LGTM

Solution (docker-compose.yml - partie LGTM) :

version: "3.9"
services:
  loki:
    image: grafana/loki:2.9
    volumes:
      - ./loki-config.yaml:/etc/loki/local-config.yaml
      - loki_data:/loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yaml
  tempo:
    image: grafana/tempo:2.2
    volumes:
      - ./tempo-config.yaml:/etc/tempo.yaml
      - tempo_data:/tmp/tempo
    ports:
      - "3200:3200"
      - "4317:4317"
    command: -config.file=/etc/tempo.yaml
  mimir:
    image: grafana/mimir:2.9
    volumes:
      - mimir_data:/data
    ports:
      - "9009:9009"
  grafana:
    image: grafana/grafana:10.2
    volumes:
      - grafana_data:/var/lib/grafana
    ports:
      - "4000:3000"
    environment:
      GF_AUTH_ANONYMOUS_ENABLED: "true"
volumes:
  loki_data:
  tempo_data:
  mimir_data:
  grafana_data:

Exercice 26 : Security Scanning dans CI

Solution (.github/workflows/security.yml) :

name: Security Scan
on:
  push:
    branches: [main]
  schedule:
    - cron: "0 6 * * 1"  # Chaque lundi à 6h
jobs:
  trivy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Trivy FS scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          format: table
          exit-code: 1
          severity: CRITICAL
  snyk:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Snyk test
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high
  opa:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: OPA eval
        uses: docker://openpolicyagent/opa:latest
        with:
          args: eval --format pretty --data policy/ --input k8s/deployment.yaml "data.kubernetes.deny"
  report:
    needs: [trivy, snyk, opa]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/upload-artifact@v4
        with:
          name: security-reports
          path: reports/

Exercice 27 : Auto-scaling avancé (HPA + VPA + KEDA)

Solution (hpa.yaml) :

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: myapp-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Solution (keda-scaledobject.yaml) :

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: myapp-scaledobject
spec:
  scaleTargetRef:
    name: myapp
  triggers:
  - type: rabbitmq
    metadata:
      queueName: tasks
      queueLength: "100"
      host: amqp://guest:guest@rabbitmq:5672
  - type: cron
    metadata:
      timezone: Europe/Paris
      start: "0 8 * * *"
      end: "0 20 * * *"
      desiredReplicas: "10"

Exercice 28 : Network Policies Kubernetes

Solution (network-policies/default-deny.yaml) :

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: frontend
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Solution (network-policies/allow-frontend-to-backend.yaml) :

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: backend
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: frontend
    ports:
    - port: 8080
      protocol: TCP

Exercice 29 : Disaster Recovery Plan

Solution (velero-schedule.yaml) :

apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: daily-backup
  namespace: velero
spec:
  schedule: "0 2 * * *"
  template:
    includedNamespaces:
    - production
    - staging
    excludedResources:
    - events
    - events.events.k8s.io
    ttl: 168h  # 7 jours

Solution (restore-script.sh) :

#!/bin/bash
set -euo pipefail
BACKUP_NAME=$1
NAMESPACE=$2
echo "Restoring backup: ${BACKUP_NAME} to namespace: ${NAMESPACE}"
velero restore create \
  --from-backup "${BACKUP_NAME}" \
  --namespace-mappings production:"${NAMESPACE}" \
  --restore-volumes=true
echo "Waiting for restore to complete..."
velero restore describe $(velero restore get --output=json | jq -r '.items[0].metadata.name')

Exercice 30 : FinOps - Cost Optimization

Solution (finops/analysis.py) :

#!/usr/bin/env python3
import boto3
import json
from datetime import datetime, timedelta

client = boto3.client('ce', region_name='us-east-1')

def get_daily_costs(start_date, end_date):
    response = client.get_cost_and_usage(
        TimePeriod={'Start': start_date, 'End': end_date},
        Granularity='DAILY',
        Metrics=['UnblendedCost'],
        GroupBy=[{'Type': 'TAG', 'Key': 'Environment'}]
    )
    return response['ResultsByTime']

def get_rightsizing_recommendations():
    response = client.get_rightsizing_recommendation(
        Service='AmazonEC2',
        Filter={'Dimensions': {'Key': 'RECOMMENDED_ACTION', 'Values': ['Buy']}}
    )
    return response['RightsizingRecommendations']

def generate_report():
    today = datetime.now().strftime('%Y-%m-%d')
    week_ago = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')

    costs = get_daily_costs(week_ago, today)
    recommendations = get_rightsizing_recommendations()

    report = {
        'period': f'{week_ago} to {today}',
        'daily_costs': costs,
        'savings_opportunities': recommendations,
        'total_potential_savings': sum(
            r['ModifyRecommendationDetail']['EstimatedMonthlySavings']
            for r in recommendations
            if 'ModifyRecommendationDetail' in r
        )
    }

    with open('cost-report.json', 'w') as f:
        json.dump(report, f, indent=2, default=str)
    print(f"Report generated: cost-report.json")
    return report

if __name__ == '__main__':
    generate_report()

Exercice 31 : Custom Kubernetes Operator (Kubebuilder)

Solution (operator/) :

// api/v1/database_types.go
package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type DatabaseSpec struct {
	Engine  string `json:"engine"`
	Version string `json:"version"`
	Storage string `json:"storage"`
}

type DatabaseStatus struct {
	Ready bool   `json:"ready"`
	Phase string `json:"phase"`
}

type Database struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`
	Spec              DatabaseSpec   `json:"spec,omitempty"`
	Status            DatabaseStatus `json:"status,omitempty"`
}

type DatabaseList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty"`
	Items           []Database `json:"items"`
}

Solution (controllers/database_controller.go - extraits) :

package controllers

import (
	"context"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	db := &v1.Database{}
	if err := r.Get(ctx, req.NamespacedName, db); err != nil {
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// Create StatefulSet
	sts := &appsv1.StatefulSet{}
	err := r.Get(ctx, types.NamespacedName{Name: db.Name, Namespace: db.Namespace}, sts)
	if errors.IsNotFound(err) {
		sts = r.buildStatefulSet(db)
		if err := r.Create(ctx, sts); err != nil {
			return ctrl.Result{}, err
		}
	}

	// Create Service
	svc := &corev1.Service{}
	err = r.Get(ctx, types.NamespacedName{Name: db.Name, Namespace: db.Namespace}, svc)
	if errors.IsNotFound(err) {
		svc = r.buildService(db)
		if err := r.Create(ctx, svc); err != nil {
			return ctrl.Result{}, err
		}
	}

	return ctrl.Result{}, nil
}

Exercice 32 : Multi-cluster Cilium ClusterMesh

Solution (cilium-clustermesh.yaml) :

apiVersion: v1
kind: ConfigMap
metadata:
  name: cilium-config
  namespace: kube-system
data:
  cluster-name: cluster-eu
  cluster-id: "1"
---
apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: cross-cluster-allow
spec:
  endpointSelector:
    matchLabels:
      app: myapp
  ingress:
  - fromEndpoints:
    - matchLabels:
        app: myapp
        "k8s:io.kubernetes.pod.namespace": production
    toPorts:
    - ports:
      - port: "8080"
        protocol: TCP

Exercice 33 : Backstage IDP

Solution (backstage/app-config.yaml) :

app:
  title: DevOps Platform
  baseUrl: http://localhost:3000
organization:
  name: My Company
backend:
  baseUrl: http://localhost:7007
  listen:
    port: 7007
  database:
    client: better-sqlite3
    connection: ':memory:'
integrations:
  github:
    - host: github.com
      token: ${GITHUB_TOKEN}
techdocs:
  builder: local
  publisher:
    type: local
catalog:
  rules:
    - allow: [Component, System, API, Resource, Location]
  providers:
    github:
      discovery:
        schedule:
          frequency: { minutes: 30 }
          timeout: { minutes: 3 }
scaffolder:
  github:
    visibility: public
    repoUrl: github.com

Exercice 34 : eBPF XDP Program

Solution (xdp-drop.c) :

#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>

SEC("xdp_drop")
int xdp_drop_prog(struct xdp_md *ctx) {
    void *data = (void *)(long)ctx->data;
    void *data_end = (void *)(long)ctx->data_end;

    struct ethhdr *eth = data;
    if ((void *)(eth + 1) > data_end)
        return XDP_PASS;

    if (eth->h_proto != __constant_htons(ETH_P_IP))
        return XDP_PASS;

    struct iphdr *ip = data + sizeof(*eth);
    if ((void *)(ip + 1) > data_end)
        return XDP_PASS;

    if (ip->protocol == IPPROTO_TCP) {
        struct tcphdr *tcp = (void *)ip + sizeof(*ip);
        if ((void *)(tcp + 1) > data_end)
            return XDP_PASS;

        if (tcp->dest == __constant_htons(22)) {
            return XDP_DROP;
        }
    }

    return XDP_PASS;
}

char _license[] SEC("license") = "GPL";

Solution (load.sh) :

#!/bin/bash
clang -O2 -target bpf -c xdp-drop.c -o xdp-drop.o
ip link set dev eth0 xdpgeneric obj xdp-drop.o sec xdp_drop
echo "XDP program loaded on eth0"

Exercice 35 : Policy as Code OPA/Gatekeeper

Solution (gatekeeper/constraints/require-resources.yaml) :

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
  name: require-pod-resources
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["Pod"]
  parameters:
    limits:
      - "cpu"
      - "memory"
    requests:
      - "cpu"
      - "memory"
---
# ConstraintTemplate
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredresources
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredResources
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package k8srequiredresources
      violation[{"msg": msg}] {
        container := input.review.object.spec.containers[_]
        not container.resources
        msg := sprintf("Container %v has no resource limits/requests", [container.name])
      }

Exercice 36 : WebAssembly sur Kubernetes

Solution (wasm-deployment.yaml) :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: wasm-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: wasm-app
  template:
    metadata:
      labels:
        app: wasm-app
    spec:
      runtimeClassName: wasmtime
      containers:
      - name: wasm-app
        image: myregistry/wasm-app:latest
        ports:
        - containerPort: 8080

Exercice 37 : AIOps - Anomaly Detection

Solution (aiops/anomaly-detector.py) :

#!/usr/bin/env python3
import numpy as np
import requests
import json
from datetime import datetime

PROMETHEUS_URL = "http://localhost:9090"

def fetch_metric(query, duration="1h"):
    response = requests.get(f"{PROMETHEUS_URL}/api/v1/query_range", params={
        'query': query,
        'start': f"now-{duration}",
        'end': 'now',
        'step': '60s'
    })
    return response.json()['data']['result']

def z_score_detection(values, threshold=3):
    values = np.array(values)
    mean = np.mean(values)
    std = np.std(values)
    z_scores = np.abs((values - mean) / std)
    anomalies = np.where(z_scores > threshold)[0]
    return anomalies.tolist(), z_scores.tolist()

def moving_average_detection(values, window=5, threshold=2):
    values = np.array(values)
    ma = np.convolve(values, np.ones(window)/window, mode='valid')
    residuals = np.abs(values[window-1:] - ma)
    anomalies = np.where(residuals > threshold * np.std(residuals))[0]
    return (anomalies + window - 1).tolist()

def analyze():
    cpu_query = '100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)'
    result = fetch_metric(cpu_query)

    for series in result:
        values = [float(v[1]) for v in series['values']]
        timestamps = [v[0] for v in series['values']]

        z_anomalies = z_score_detection(values)
        ma_anomalies = moving_average_detection(values)

        if z_anomalies[0]:
            print(f"Anomalies detectees sur {series['metric']['instance']}:")
            for idx in z_anomalies[0]:
                ts = datetime.fromtimestamp(timestamps[idx])
                print(f"  {ts}: {values[idx]:.2f}% CPU (Z-score: {z_anomalies[1][idx]:.2f})")

if __name__ == '__main__':
    analyze()

Exercice 38 : Zero-downtime Migration

Solution (migration/migration-plan.md) :

# Plan de migration zéro-downtime

## Stratégie : Expansion-Contraction (Dual Writes)

### Phase 1 : Préparation
1. Ajouter les nouvelles colonnes dans la table existante
2. Configurer la réplication logique PostgreSQL
3. Préparer les scripts de rollback

### Phase 2 : Dual Writes
1. L'application écrit sur l'ancienne ET la nouvelle table
2. Backfill des données historiques par batches
3. Monitoring des erreurs d'écriture

### Phase 3 : Bascule
1. Lire depuis la nouvelle table uniquement
2. Valider l'intégrité des données
3. Supprimer les colonnes/migrations obsolètes

### Rollback
- Revenir à l'étape 2 (dual writes) si anomalie
- Scripts de revert automatisés
- Budget de downtime : < 5s

Exercice 39 : Supply Chain Security (SLSA + Sigstore)

Solution (supply-chain/slsa.yml) :

name: SLSA Level 3
on:
  workflow_dispatch:
  push:
    branches: [main]
jobs:
  build:
    permissions:
      id-token: write
      contents: read
      attestations: write
    uses: slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@v1
    with:
      go-version: "1.21"
      evaluated-env: "true"
---
# Signer l'image avec Cosign
cosign sign --key cosign.key ghcr.io/myorg/myapp@sha256:abc123...
# Générer SBOM
syft ghcr.io/myorg/myapp:latest -o spdx-json=sbom.spdx.json

Exercice 40 : Plateforme Multi-Cloud complète

Solution (multi-cloud/crossplane/provider-config.yaml) :

apiVersion: aws.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: aws-provider
spec:
  credentials:
    source: Secret
    secretRef:
      name: aws-creds
      namespace: crossplane-system
---
apiVersion: gcp.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: gcp-provider
spec:
  credentials:
    source: Secret
    secretRef:
      name: gcp-creds
      namespace: crossplane-system
---
apiVersion: azure.upbound.io/v1beta1
kind: ProviderConfig
metadata:
  name: azure-provider
spec:
  credentials:
    source: Secret
    secretRef:
      name: azure-creds
      namespace: crossplane-system

Solution (multi-cloud/composition.yaml) :

apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
  name: xclusters.devops.platform.example.org
spec:
  group: devops.platform.example.org
  names:
    kind: XCluster
    plural: xclusters
  claimNames:
    kind: Cluster
    plural: clusters
  versions:
  - name: v1alpha1
    served: true
    referenceable: true
    schema:
      openAPIV3Schema:
        type: object
        properties:
          spec:
            type: object
            properties:
              provider:
                type: string
                enum: [aws, gcp, azure]
              region:
                type: string
              nodeCount:
                type: integer

Résumé des bonnes pratiques

  1. Docker : Multi-stage, .dockerignore, utilisateur non-root
  2. Kubernetes : Resources limits, health probes, namespaces, network policies
  3. Terraform : Modules, remote state, locking, variables typées
  4. CI/CD : Cache, jobs parallèles, secrets sécurisés, artefacts
  5. Monitoring : Trois piliers (logs, metrics, traces), SLOs, alerting pertinent
  6. GitOps : Git comme source de vérité, sync auto, self-heal, PR review
  7. Security : Least privilege, scanning automatique, runtime security
  8. Disaster Recovery : Backups réguliers, DR testé, runbooks documentés