Déployer une application Spring Boot sur Kubernetes : guide complet
Guide pas-à-pas pour déployer Spring Boot sur Kubernetes : Dockerfile optimisé, manifests K8s, health checks, ConfigMaps et bonnes pratiques.
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.
org.springframework.boot
spring-boot-starter-actuator
# 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: 30sCette 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 ?
| Optimisation | Bénéfice |
|---|---|
| Multi-stage | Image finale ~200MB au lieu de ~500MB |
| Layers Spring Boot | Rebuild rapide (seule la couche application change) |
| JRE Alpine | Image légère et sécurisée |
| User non-root | Sécurité renforcée |
UseContainerSupport | JVM 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: productionConfigMap
# 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' | base64Important : 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/hostnameExplication des paramètres critiques
Probes :
| Paramètre | Valeur | Explication |
|---|---|---|
initialDelaySeconds | 60 | Spring Boot démarre lentement, attendre |
periodSeconds | 10 | Fréquence de vérification |
failureThreshold | 3 | Nombre 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-apiServiceAccount (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 -wVé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-apiRollback 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=2Bonnes 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-api3. 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: 80Checklist 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 disponiblesL'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/healthProblè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