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: 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