Déployer une application Spring Boot sur Kubernetes : guide complet

Déployer une application Spring Boot sur Kubernetes : guide complet

Vous avez une application Spring Boot qui tourne parfaitement en local. Maintenant, il faut la déployer sur Kubernetes. Ce guide vous accompagne de A à Z, du Dockerfile optimisé jusqu'au déploiement production-ready.

Prérequis

  • Une application Spring Boot fonctionnelle
  • Docker installé
  • Accès à un cluster Kubernetes (minikube, kind, ou cloud)
  • kubectl configuré

Étape 1 : Préparer l'application Spring Boot

Configuration des health checks

Kubernetes a besoin de savoir si votre application est vivante et prête. Spring Boot Actuator fournit ces endpoints automatiquement.

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
# application.yaml
management:
  endpoints:
    web:
      exposure:
        include: health,info,prometheus
  endpoint:
    health:
      probes:
        enabled: true
      show-details: when_authorized
  health:
    livenessstate:
      enabled: true
    readinessstate:
      enabled: true

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Cette configuration expose :

  • /actuator/health/liveness : l'application est-elle vivante ?
  • /actuator/health/readiness : l'application peut-elle recevoir du trafic ?

Externaliser la configuration

Ne hardcodez jamais les valeurs de configuration. Utilisez des variables d'environnement.

# application.yaml
spring:
  datasource:
    url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:myapp}
    username: ${DB_USERNAME:postgres}
    password: ${DB_PASSWORD:postgres}

app:
  feature:
    new-ui: ${FEATURE_NEW_UI:false}
  api:
    external-url: ${EXTERNAL_API_URL:http://localhost:8081}

Étape 2 : Créer un Dockerfile optimisé

Dockerfile multi-stage

Un Dockerfile optimisé réduit la taille de l'image et accélère les déploiements.

# Dockerfile
# Stage 1 : Build
FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /app

# Copier les fichiers de dépendances d'abord (cache Docker)
COPY pom.xml mvnw ./
COPY .mvn .mvn

# Télécharger les dépendances (couche cachée)
RUN ./mvnw dependency:go-offline -B

# Copier le code source
COPY src src

# Build l'application
RUN ./mvnw package -DskipTests -B

# Extraire les layers Spring Boot (optimisation)
RUN java -Djarmode=layertools -jar target/*.jar extract

# Stage 2 : Runtime
FROM eclipse-temurin:21-jre-alpine

# Sécurité : ne pas exécuter en root
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

WORKDIR /app

# Copier les layers dans l'ordre de fréquence de changement
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./

# Port exposé (documentation)
EXPOSE 8080

# Health check Docker natif
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
    CMD wget -qO- http://localhost:8080/actuator/health/liveness || exit 1

# Démarrage avec optimisations JVM pour containers
ENTRYPOINT ["java", \
    "-XX:+UseContainerSupport", \
    "-XX:MaxRAMPercentage=75.0", \
    "-Djava.security.egd=file:/dev/./urandom", \
    "org.springframework.boot.loader.launch.JarLauncher"]

Pourquoi cette structure ?

OptimisationBénéfice
Multi-stageImage finale ~200MB au lieu de ~500MB
Layers Spring BootRebuild rapide (seule la couche application change)
JRE AlpineImage légère et sécurisée
User non-rootSécurité renforcée
UseContainerSupportJVM respecte les limites CPU/RAM du container

Build et test local

# Build l'image
docker build -t mon-api:1.0.0 .

# Test local
docker run -p 8080:8080 \
    -e DB_HOST=host.docker.internal \
    -e DB_PASSWORD=secret \
    mon-api:1.0.0

# Vérifier les health checks
curl http://localhost:8080/actuator/health

Étape 3 : Créer les manifests Kubernetes

Structure recommandée

k8s/
├── namespace.yaml
├── configmap.yaml
├── secret.yaml
├── deployment.yaml
├── service.yaml
└── ingress.yaml (optionnel)

Namespace

# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: mon-api
  labels:
    app: mon-api
    environment: production

ConfigMap

# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mon-api-config
  namespace: mon-api
data:
  DB_HOST: "postgres.database.svc.cluster.local"
  DB_PORT: "5432"
  DB_NAME: "myapp"
  FEATURE_NEW_UI: "true"
  EXTERNAL_API_URL: "http://external-api.external.svc.cluster.local"
  JAVA_OPTS: "-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"

Secret

# k8s/secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: mon-api-secrets
  namespace: mon-api
type: Opaque
data:
  DB_USERNAME: cG9zdGdyZXM=        # echo -n 'postgres' | base64
  DB_PASSWORD: c3VwZXJzZWNyZXQ=    # echo -n 'supersecret' | base64

Important : Ne committez jamais les Secrets en clair. Utilisez :

  • Sealed Secrets
  • External Secrets Operator
  • Vault

Deployment

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mon-api
  namespace: mon-api
  labels:
    app: mon-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: mon-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: mon-api
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/actuator/prometheus"
    spec:
      serviceAccountName: mon-api
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
        - name: api
          image: mon-registry.io/mon-api:1.0.0
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          envFrom:
            - configMapRef:
                name: mon-api-config
            - secretRef:
                name: mon-api-secrets
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "1Gi"
              cpu: "1000m"
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 10
            timeoutSeconds: 5
            failureThreshold: 3
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 5
            timeoutSeconds: 3
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]
      terminationGracePeriodSeconds: 45
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchLabels:
                    app: mon-api
                topologyKey: kubernetes.io/hostname

Explication des paramètres critiques

Probes :

ParamètreValeurExplication
initialDelaySeconds60Spring Boot démarre lentement, attendre
periodSeconds10Fréquence de vérification
failureThreshold3Nombre d'échecs avant action

Resources :

resources:
  requests:    # Minimum garanti
    memory: "512Mi"
    cpu: "250m"
  limits:      # Maximum autorisé
    memory: "1Gi"
    cpu: "1000m"
  • requests : utilisé pour le scheduling (où placer le Pod)
  • limits : protection contre les fuites mémoire

Lifecycle preStop :

lifecycle:
  preStop:
    exec:
      command: ["/bin/sh", "-c", "sleep 10"]

Laisse le temps au load balancer de retirer le Pod avant l'arrêt.

Service

# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mon-api
  namespace: mon-api
  labels:
    app: mon-api
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
      name: http
  selector:
    app: mon-api

ServiceAccount (sécurité)

# k8s/serviceaccount.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: mon-api
  namespace: mon-api
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: mon-api-role
  namespace: mon-api
rules: []  # Aucune permission par défaut
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: mon-api-rolebinding
  namespace: mon-api
subjects:
  - kind: ServiceAccount
    name: mon-api
roleRef:
  kind: Role
  name: mon-api-role
  apiGroup: rbac.authorization.k8s.io

Étape 4 : Déployer

Ordre de déploiement

# Créer le namespace
kubectl apply -f k8s/namespace.yaml

# Créer les configurations
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.yaml
kubectl apply -f k8s/serviceaccount.yaml

# Déployer l'application
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml

# Vérifier le déploiement
kubectl -n mon-api get pods -w

Vérifications

# État des pods
kubectl -n mon-api get pods

# Logs
kubectl -n mon-api logs -l app=mon-api -f

# Décrire un pod (debugging)
kubectl -n mon-api describe pod mon-api-xxxxx

# Test de connectivité interne
kubectl -n mon-api run curl --rm -it --image=curlimages/curl -- \
    curl http://mon-api/actuator/health

Étape 5 : Mise à jour de l'application

Déploiement d'une nouvelle version

# Option 1 : Modifier le Deployment
kubectl -n mon-api set image deployment/mon-api \
    api=mon-registry.io/mon-api:1.1.0

# Option 2 : Modifier le fichier et apply
kubectl apply -f k8s/deployment.yaml

# Suivre le rollout
kubectl -n mon-api rollout status deployment/mon-api

Rollback si problème

# Voir l'historique
kubectl -n mon-api rollout history deployment/mon-api

# Rollback à la version précédente
kubectl -n mon-api rollout undo deployment/mon-api

# Rollback à une version spécifique
kubectl -n mon-api rollout undo deployment/mon-api --to-revision=2

Bonnes pratiques production

1. Resource Quotas

Limitez les ressources par namespace :

apiVersion: v1
kind: ResourceQuota
metadata:
  name: mon-api-quota
  namespace: mon-api
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    pods: "20"

2. Pod Disruption Budget

Garantissez une disponibilité minimale :

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: mon-api-pdb
  namespace: mon-api
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: mon-api

3. Horizontal Pod Autoscaler

Scalez automatiquement selon la charge :

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

Checklist déploiement

Avant de déployer en production :

  • ☐ Health checks configurés (liveness + readiness)
  • ☐ Graceful shutdown activé
  • ☐ Dockerfile multi-stage optimisé
  • ☐ User non-root dans le container
  • ☐ Resources (requests/limits) définies
  • ☐ Secrets non committés
  • ☐ Pod anti-affinity configuré
  • ☐ PodDisruptionBudget défini
  • ☐ HPA configuré si nécessaire
  • ☐ Logs structurés (JSON)
  • ☐ Métriques Prometheus exposées

Debugging des problèmes courants

Le Pod ne démarre pas

# Vérifier les événements
kubectl -n mon-api describe pod mon-api-xxxxx

# Erreurs courantes :
# - ImagePullBackOff : vérifier le nom de l'image et les credentials
# - CrashLoopBackOff : voir les logs avec --previous
# - Pending : vérifier les resources disponibles

L'application ne répond pas

# Vérifier les probes
kubectl -n mon-api get pods -o wide

# Port-forward pour test local
kubectl -n mon-api port-forward pod/mon-api-xxxxx 8080:8080

# Test direct
curl http://localhost:8080/actuator/health

Problèmes de performance

# Vérifier l'utilisation des resources
kubectl -n mon-api top pods

# Vérifier les limites
kubectl -n mon-api describe pod mon-api-xxxxx | grep -A 5 "Limits:"

Conclusion

Déployer Spring Boot sur Kubernetes demande de la préparation : health checks, Dockerfile optimisé, manifests bien configurés. Mais une fois en place, vous bénéficiez de la scalabilité, de la résilience et de l'automatisation de Kubernetes.

Les points clés à retenir :

  • Toujours configurer liveness et readiness probes
  • Utiliser un Dockerfile multi-stage avec les layers Spring Boot
  • Définir les resources requests et limits
  • Externaliser toute la configuration
  • Prévoir le graceful shutdown

Pour les fondamentaux Kubernetes : Kubernetes pour développeurs : ce qu'il faut vraiment maîtriser

Pour les breaking changes Spring Boot 4 : Spring Boot 4 : breaking changes à connaître