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 surpackage-lock.jsonCOPY . .: 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 Deploymentreplicas: 3: Haute disponibilitéselector.matchLabels: Doit correspondre aux labels du templatematchLabels: 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_idpour 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 sourceactions/setup-node@v4: Configure Node 18 avec cache npmnpm ci: Installation propre (plus rapide et déterministe quenpm 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é
pgdatapour persister les données - Réseau partagé
app-networkpour 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
-upour 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 backendX-Forwarded-For: Préserve l'IP du clientburst=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 :
findavec-mtime +7supprime 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
- Docker : Multi-stage, .dockerignore, utilisateur non-root
- Kubernetes : Resources limits, health probes, namespaces, network policies
- Terraform : Modules, remote state, locking, variables typées
- CI/CD : Cache, jobs parallèles, secrets sécurisés, artefacts
- Monitoring : Trois piliers (logs, metrics, traces), SLOs, alerting pertinent
- GitOps : Git comme source de vérité, sync auto, self-heal, PR review
- Security : Least privilege, scanning automatique, runtime security
- Disaster Recovery : Backups réguliers, DR testé, runbooks documentés