Modern Backend Engineering
Chapitre 12
Chapitre 12 — DevOps Backend
Chapitre 12 — DevOps Backend
Cours — DevOps Backend
1. Docker Multi-stage et Optimisation
Multi-stage build
# Stage 1: Build
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM node:22-alpine AS runner
WORKDIR /app
RUN addgroup --system app && adduser --system --ingroup app app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER app
EXPOSE 3000
CMD ["node", "dist/index.js"]
Optimisation de l'image
# Utiliser des images légères
FROM node:22-alpine # ~120MB vs 350MB pour node:22
# Réduire les layers
RUN apt-get update && apt-get install -y \
package1 \
package2 \
&& rm -rf /var/lib/apt/lists/*
# Utiliser .dockerignore
# .dockerignore
node_modules
.git
*.md
Dockerfile
docker-compose.yml
.gitignore
Docker Compose (développement)
# docker-compose.yml
version: '3.8'
services:
api:
build:
context: .
target: runner
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/app
- REDIS_URL=redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
volumes:
- .:/app
- /app/node_modules
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
pgdata:
2. Kubernetes
Pod
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: api-pod
labels:
app: api
spec:
containers:
- name: api
image: registry.example.com/api:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
Service
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: api-service
spec:
selector:
app: api
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
Ingress
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
annotations:
nginx.ingress.kubernetes.io/rate-limit: "100r/m"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
tls:
- hosts:
- api.example.com
secretName: api-tls
Deployment avec rolling update
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-deployment
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: registry.example.com/api:v2
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secret
3. GitHub Actions CI/CD
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run test:integration
- uses: codecov/codecov-action@v3
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
deploy:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/api-deployment \
api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
4. Terraform (Infrastructure as Code)
# main.tf
provider "aws" {
region = "eu-west-3"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
name = "app-vpc"
cidr = "10.0.0.0/16"
azs = ["eu-west-3a", "eu-west-3b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
}
module "eks" {
source = "terraform-aws-modules/eks/aws"
cluster_name = "app-cluster"
subnet_ids = module.vpc.private_subnets
vpc_id = module.vpc.vpc_id
node_groups = {
main = {
desired_capacity = 3
max_capacity = 10
min_capacity = 1
instance_types = ["t3.medium"]
}
}
}
5. Ansible
# playbook.yml
- name: Configure application server
hosts: webservers
become: yes
vars:
app_version: "1.2.3"
db_host: "localhost"
tasks:
- name: Install Docker
apt:
name: docker.io
state: present
- name: Pull application image
docker_image:
name: "registry.example.com/app:{{ app_version }}"
source: pull
- name: Run application container
docker_container:
name: app
image: "registry.example.com/app:{{ app_version }}"
state: started
ports:
- "3000:3000"
env:
DATABASE_URL: "postgresql://user:pass@{{ db_host }}/app"
6. Prometheus & Grafana
Configuration Prometheus
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'api'
static_configs:
- targets: ['api:3000']
metrics_path: '/metrics'
- job_name: 'node'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'postgres'
static_configs:
- targets: ['postgres-exporter:9187']
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
Règles d'alerte
# alerts.yml
groups:
- name: api
rules:
- alert: HighErrorRate
expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate > 5%"
- alert: HighLatency
expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
for: 5m
labels:
severity: warning
7. ELK Stack (Elasticsearch + Logstash + Kibana)
Filebeat configuration
# filebeat.yml
filebeat.inputs:
- type: container
paths:
- /var/log/containers/*.log
output.elasticsearch:
hosts: ["elasticsearch:9200"]
setup.kibana:
host: "kibana:5601"
Logging structuré (application)
// logger.js
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level(label) {
return { level: label };
},
},
serializers: {
req: (req) => ({
method: req.method,
url: req.url,
headers: {
'user-agent': req.headers['user-agent'],
'x-request-id': req.headers['x-request-id'],
},
}),
res: (res) => ({
statusCode: res.statusCode,
}),
err: pino.stdSerializers.err,
},
});
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
req,
res,
responseTime: Date.now() - start,
});
});
next();
});
8. Sentry (Error Tracking)
import * as Sentry from '@sentry/node';
import { nodeProfilingIntegration } from '@sentry/profiling-node';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
profilesSampleRate: 1.0,
integrations: [nodeProfilingIntegration()],
});
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
app.use(Sentry.Handlers.errorHandler());
9. Bonnes Pratiques DevOps
GitOps
- Déclaration d'état dans Git (single source of truth)
- ArgoCD / Flux pour la synchronisation automatique
- Pull-based deployment
12-Factor App
- Codebase unique
- Dépendances explicites
- Configuration dans l'environnement
- Services attachés
- Build, release, run séparés
- Processes stateless
- Port binding
- Concurrency (scale-out)
- Disposability (démarrage/arrêt rapide)
- Dev/Prod parity
- Logs comme event streams
- Admin processes (migrations, etc.)