Esc
 Naviguer  Ouvrir Esc Fermer
Aller au contenu

14.4 — Conteneurs en production

🎯 Objectif : passer du docker run local à des conteneurs robustes en prod. Docker pour packager, Kubernetes pour orchestrer (quand tu en as besoin) — sans tomber dans le piège du « K8s pour tout, tout de suite ».

À l'issue de cet axe, tu sauras :

  • Écrire un Dockerfile multi-stage propre, petit, non-root et reproductible
  • Signer et scanner ses images avant de les déployer
  • Comprendre les briques K8s : pod, deployment, service, ingress, HPA, secret
  • Choisir entre K8s managé et alternative plus simple
  • Mettre en place du GitOps avec Argo CD ou Flux

Confirmé 12 min prérequis : axes 4 et 8 lus

D’abord — as-tu vraiment besoin de Kubernetes ?

Section intitulée « D’abord — as-tu vraiment besoin de Kubernetes ? »

K8s est un excellent outil. Il est aussi excessivement coûteux en complexité pour 80 % des projets.

Tu asTu n’as pas besoin de K8s
1-3 services à scalerECS Fargate, Cloud Run, Render, Fly suffisent
Une équipe < 5 devsLe coût cognitif K8s ralentit l’équipe
Un produit en MVPK8s = 3 mois de setup au lieu de feature
< 100 req/sUn VPS bien configuré tient

Tu as besoin de K8s si :

  • 20+ microservices avec interdépendances complexes.
  • Multi-tenant avec isolation forte par namespace.
  • Compliance qui exige des contrôles fins (NetworkPolicies, RBAC granulaire).
  • Équipe ops dédiée.
  • Charge variable et critique avec auto-scaling fin.

« Kubernetes est un système distribué qui transforme un autre système distribué en un troisième système distribué. » — un SRE anonyme

Cette section couvre les deux mondes : Docker industrialisé (la base utile à tout le monde), puis Kubernetes (quand le besoin est avéré).


# ❌ — image massive, root, npm install full
FROM node:24
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

Problèmes :

  • Image > 1 GB (image Node full).
  • Root par défaut (escalation possible).
  • npm install installe les devDependencies.
  • COPY . . casse le cache à chaque modif.
1.7
# ----- builder -----
FROM node:24-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
# ----- runtime -----
FROM node:24-alpine AS runtime
WORKDIR /app
# Utilisateur non-root
RUN addgroup -S app && adduser -S app -G app
USER app
ENV NODE_ENV=production \
NODE_OPTIONS="--enable-source-maps"
COPY --from=builder --chown=app:app /app/node_modules ./node_modules
COPY --from=builder --chown=app:app /app/dist ./dist
COPY --from=builder --chown=app:app /app/package.json ./
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
CMD wget -qO- http://127.0.0.1:3000/health || exit 1
CMD ["node", "dist/server.js"]

Bénéfices :

LevierEffet
Multi-stageL’image finale ne contient pas les outils de build
alpine-200 à -800 MB
--mount=type=cacheCache npm/pnpm partagé entre builds
Layer order : package*.json avant COPY . .Cache hit sur dépendances inchangées
USER appNon-root → moins de surface d’attaque
HEALTHCHECKK8s / Render savent quand le container est prêt
npm ci + --omit=devPas de devDependencies en prod
node_modules
.git
.env
.env.*
dist
*.log
.DS_Store
coverage
.next

Sans lui, COPY . . envoie 500 MB de node_modules local au build context.

FROM gcr.io/distroless/nodejs24-debian12:nonroot
# ou
FROM cgr.dev/chainguard/node:latest

Images minimales sans shell, sans apt, sans utilitaires. Aucune surface d’attaque, image typiquement < 100 MB.

Inconvénient : pas de sh pour debug → tu utilises kubectl debug avec une image éphémère.


RegistreForce
GitHub Container Registry (ghcr.io)Gratuit, intégré CI, par repo
Docker HubLe plus connu, mais quotas pull serrés
Amazon ECRIntégré IAM, pas d’egress depuis EC2
GCP Artifact RegistryIdem côté GCP
Cloudflare RegistryPull depuis edge mondial
Self-hosted (Harbor)Pour compliance fort
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64

platforms: linux/amd64,linux/arm64 : multi-arch dès le départ — utile pour les Mac M-series locaux et serveurs ARM (Hetzner ARM Ampere, Graviton).


OutilQuoi
TrivyOSS de référence, scan deps + OS + secrets + IaC
GrypeIdem, par Anchore
Snyk ContainerCommercial, intégré DevSecOps
Docker ScoutIntégré Docker Desktop
Fenêtre de terminal
trivy image ghcr.io/myorg/myapp:1.2.0
# CRITICAL: 0
# HIGH: 2
# ...

Bloque la PR si un CRITICAL est détecté :

- run: trivy image --severity CRITICAL --exit-code 1 ghcr.io/myorg/myapp:${{ github.sha }}

Signer une image prouve qu’elle vient bien de toi (et pas d’un attaquant qui a compromis le registre).

Fenêtre de terminal
cosign sign --yes ghcr.io/myorg/myapp:1.2.0
cosign verify --certificate-identity=... ghcr.io/myorg/myapp:1.2.0

Avec OIDC GitHub Actions, la signature est faite sans clé long-lived — la chaîne keyless Sigstore est l’option moderne.

Liste de toutes les dépendances de ton image. Obligatoire pour certains contextes (Cyber Resilience Act EU, US Executive Order).

Fenêtre de terminal
syft ghcr.io/myorg/myapp:1.2.0 -o spdx-json > sbom.json

flowchart TD
    Ingress -->|HTTP| Service
    Service -->|TCP| Pod
    Deployment -->|gère| Pod
    Pod -->|consomme| ConfigMap
    Pod -->|consomme| Secret
    HPA -->|scale| Deployment
    PVC -->|monté dans| Pod
Hiérarchie des objets K8s utiles à 95 % des cas

L’unité de base : 1+ conteneurs co-localisés (même IP, même volume éphémère).

apiVersion: v1
kind: Pod
metadata: { name: myapp-xyz }
spec:
containers:
- name: app
image: ghcr.io/myorg/myapp:1.2.0
ports: [{ containerPort: 3000 }]
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }

Tu n’écris jamais des pods directement en prod. Tu écris des Deployments qui les gèrent.

apiVersion: apps/v1
kind: Deployment
metadata: { name: myapp }
spec:
replicas: 3
selector: { matchLabels: { app: myapp } }
template:
metadata: { labels: { app: myapp } }
spec:
containers:
- name: app
image: ghcr.io/myorg/myapp:1.2.0
ports: [{ containerPort: 3000 }]
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: 500m, memory: 512Mi }
readinessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet: { path: /health, port: 3000 }
initialDelaySeconds: 30
periodSeconds: 30
ProbeEffet
readinessProbeTant que ❌ → le pod ne reçoit pas de trafic
livenessProbeSi ❌ persistant → kill et redémarre le pod
startupProbePour apps longues à démarrer (évite que liveness kill avant ready)
apiVersion: v1
kind: Service
metadata: { name: myapp }
spec:
type: ClusterIP
selector: { app: myapp }
ports:
- port: 80
targetPort: 3000

Le service expose myapp.namespace.svc.cluster.local, load-balance les pods sains. type: LoadBalancer provisionne un LB cloud (AWS NLB, GCP LB, etc.).

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts: [api.example.com]
secretName: myapp-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend: { service: { name: myapp, port: { number: 80 } } }

Composant indispensable : un ingress controller installé (NGINX, Traefik, HAProxy, ALB Controller). Et cert-manager pour TLS auto.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: myapp }
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: myapp }
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }

Plus avancé : KEDA scale aussi sur des métriques custom (queue length, requêtes Prometheus).

apiVersion: v1
kind: ConfigMap
metadata: { name: myapp-config }
data:
LOG_LEVEL: info
---
apiVersion: v1
kind: Secret
metadata: { name: myapp-secrets }
type: Opaque
stringData:
DATABASE_URL: postgres://user:pw@db/myapp

Les Secrets K8s ne sont pas chiffrés par défaut, juste base64-encodés. Pour vraiment chiffrer : Sealed Secrets (Bitnami), External Secrets Operator (lit depuis Vault/AWS Secrets Manager), ou SOPS.


ServiceForce
EKS (AWS)Mature, intégré IAM/VPC, cher
GKE (GCP)Le plus mûr (K8s vient de Google), Autopilot mode
AKS (Azure)Bien intégré AD, support entreprise
DigitalOcean KubernetesSimple, ~30 €/mois pour un petit cluster
CivoCluster en 90 s, bon prix
Scaleway KapsuleSouverain FR/UE

Évite d’auto-héberger ton cluster K8s en 2026 sauf besoin précis. Le control plane managé coûte ~70 $/mois et te fait gagner des semaines.


Fenêtre de terminal
# Installer
helm install myapp ./chart --values values-prod.yaml
# Mettre à jour
helm upgrade myapp ./chart --values values-prod.yaml --atomic
# Rollback
helm rollback myapp 1

Un chart est un dossier YAML templaté + values.yaml :

chart/
├── Chart.yaml
├── values.yaml
└── templates/
├── deployment.yaml
├── service.yaml
├── ingress.yaml
└── hpa.yaml
# templates/deployment.yaml (extrait)
spec:
replicas: {{ .Values.replicas }}
template:
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"

Helm est standard pour distribuer des apps réutilisables (ingress controllers, monitoring stacks). Pour ta propre app, kustomize est souvent plus simple — pas de templating, juste des overlays.


Modèle :

  1. Le état désiré vit dans un repo Git (manifests YAML / Helm).
  2. Argo CD / Flux observe ce repo et réconcilie le cluster vers cet état.
  3. Tout déploiement est un commit — auditable, reversible.
┌──────────────┐ git push ┌────────────┐
│ Dev/CI │ ───────────> │ Repo Git │
└──────────────┘ │ (manifests)│
└─────┬──────┘
│ pull (toutes les 3 min)
┌────────────┐
│ Argo CD │
└─────┬──────┘
│ kubectl apply
┌────────────┐
│ Cluster │
└────────────┘

Avantages :

  • Observabilité : drift visible dans le UI Argo CD.
  • Rollback = git revert.
  • Multi-cluster sans recréer de pipelines.
  • Aucun secret cloud côté CI (le cluster pulle).
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata: { name: myapp, namespace: argocd }
spec:
project: default
source:
repoURL: https://github.com/myorg/myapp-manifests
targetRevision: HEAD
path: chart
helm:
valueFiles: [values-prod.yaml]
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated: { prune: true, selfHeal: true }

Sans requests/limits, un pod peut consommer toute la RAM d’un node et tuer ses voisins. Toujours définir :

RessourceEffet
requests.cpuRéservé pour le scheduler
requests.memoryIdem, + déclenche eviction si dépassé
limits.cpuPlafond — le pod est throttle si dépassé
limits.memoryPlafond — OOMKilled si dépassé

Trop bas → ton app rame. Trop haut → tu paies pour rien. Mesure avant de fixer (Prometheus + Grafana, Goldilocks).


StratégieQuoi
Rolling update (défaut Deployment)Rollout pod par pod
Blue-Green2 versions en parallèle, switch ingress
Canary5 % du trafic sur la nouvelle, monitorer, ramp up
Feature flagsDécouplage déploiement / activation (LaunchDarkly, Unleash)

Pour faire du canary sans Istio, Argo Rollouts est l’outil de référence — déploiement piloté par métriques.


Tu lances un nouveau projet B2B avec 2 services et une équipe de 4 devs sans expérience K8s. Quelle plateforme privilégier ?
Ton Dockerfile fait 1.4 GB. Tu mets `node:24-alpine`, l'image fait 380 MB. Quelle prochaine optimisation simple ?
Sur K8s, ton pod a `resources.limits.memory: 256Mi`. Ton app dépasse 256 MB en charge. Que se passe-t-il ?
Pourquoi le GitOps avec Argo CD est-il préféré au `kubectl apply` depuis la CI sur des clusters de prod ?


Suite : 14.5 — Observabilité pour voir ce qui se passe en prod.