Spring Boot on Kubernetes
Kubernetes is the standard platform for running containerized microservices. Spring Boot integrates naturally with Kubernetes — Actuator probes map directly to Kubernetes probes, and Spring configuration maps to ConfigMaps and Secrets.
Core Kubernetes Resources
Deployment
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
version: "1.2.3"
spec:
containers:
- name: order-service
image: devopsmonk/order-service:1.2.3
ports:
- containerPort: 8080
name: http
- containerPort: 8081
name: management
# Resource limits — always set these
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
# Environment from ConfigMap and Secret
envFrom:
- configMapRef:
name: order-service-config
- secretRef:
name: order-service-secrets
# Individual env vars
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
# Probes
startupProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
failureThreshold: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8081
initialDelaySeconds: 0
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8081
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 3
# Graceful shutdown
lifecycle:
preStop:
exec:
command: ["sleep", "5"]
# Allow enough time for graceful shutdown
terminationGracePeriodSeconds: 60
# Rolling update — never take down all pods at once
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # never have less than desired replicas
maxSurge: 1 # allow 1 extra pod during update
Service
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- name: http
port: 80
targetPort: 8080
- name: management
port: 8081
targetPort: 8081
type: ClusterIP # internal only — expose via Ingress for external traffic
ConfigMap and Secrets
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
data:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/orders
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
SPRING_DATA_REDIS_HOST: redis
MANAGEMENT_SERVER_PORT: "8081"
SERVER_SHUTDOWN: graceful
SPRING_LIFECYCLE_TIMEOUT_PER_SHUTDOWN_PHASE: 30s
# secret.yaml — values are base64-encoded
apiVersion: v1
kind: Secret
metadata:
name: order-service-secrets
type: Opaque
stringData: # stringData auto-encodes to base64
SPRING_DATASOURCE_PASSWORD: "${DB_PASSWORD}"
PAYMENT_API_KEY: "${PAYMENT_API_KEY}"
JWT_SECRET: "${JWT_SECRET}"
Never commit real secrets to Git. Use:
kubectl create secret genericfrom environment variables- External Secrets Operator (syncs from Vault, AWS Secrets Manager)
- Sealed Secrets (encrypted, safe to commit)
Horizontal Pod Autoscaler
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: order-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: order-service
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # scale up when avg CPU > 70%
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Custom metrics-based autoscaling (scale on Kafka consumer lag):
metrics:
- type: External
external:
metric:
name: kafka_consumer_lag
selector:
matchLabels:
topic: order-events
consumer_group: inventory-service
target:
type: AverageValue
averageValue: "1000" # scale if lag > 1000 messages
Ingress
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: order-service-ingress
annotations:
nginx.ingress.kubernetes.io/rate-limit: "100"
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- api.devopsmonk.com
secretName: devopsmonk-tls
rules:
- host: api.devopsmonk.com
http:
paths:
- path: /api/orders
pathType: Prefix
backend:
service:
name: order-service
port:
number: 80
Kubernetes-Native Configuration
Enable Spring to read from ConfigMaps and Secrets directly (no Config Server needed):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>
spring:
cloud:
kubernetes:
config:
enabled: true
name: order-service-config # reads this ConfigMap
namespace: default
secrets:
enabled: true
name: order-service-secrets
reload:
enabled: true
mode: polling
period: 15000 # reload every 15s if ConfigMap changes
Now you can update order-service-config ConfigMap, and the application reloads automatically — no restart needed.
RBAC for Spring Cloud Kubernetes
Spring Cloud Kubernetes reads ConfigMaps and Secrets via the Kubernetes API — the pod needs permissions:
# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: order-service
namespace: default
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: order-service-role
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: order-service-binding
subjects:
- kind: ServiceAccount
name: order-service
roleRef:
kind: Role
name: order-service-role
apiGroup: rbac.authorization.k8s.io
# In Deployment spec:
spec:
serviceAccountName: order-service
Namespace Isolation
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: order-platform
labels:
env: production
Separate namespaces per environment (dev, staging, prod) or per team. Apply NetworkPolicies to prevent cross-namespace communication:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: order-service-netpol
namespace: order-platform
spec:
podSelector:
matchLabels:
app: order-service
ingress:
- from:
- namespaceSelector:
matchLabels:
name: api-gateway
ports:
- port: 8080
- from: [] # allow any for management port from monitoring namespace
ports:
- port: 8081
Only the API Gateway namespace can reach port 8080. The management port (8081) is accessible for Prometheus scraping.
PodDisruptionBudget
Prevent accidental outages during node maintenance or rolling updates:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: order-service-pdb
spec:
minAvailable: 2 # always keep at least 2 pods running
selector:
matchLabels:
app: order-service
kubectl drain (node maintenance) respects PDBs — it won’t evict pods if it would violate this budget.
Deployment Commands
# Apply all manifests
kubectl apply -f k8s/
# Rolling update — update the image tag
kubectl set image deployment/order-service order-service=devopsmonk/order-service:1.3.0
# Monitor rollout
kubectl rollout status deployment/order-service
# Roll back if something goes wrong
kubectl rollout undo deployment/order-service
# Scale manually
kubectl scale deployment/order-service --replicas=5
# Check pod logs
kubectl logs -l app=order-service -f --tail=100
# Execute into a running pod
kubectl exec -it $(kubectl get pod -l app=order-service -o name | head -1) -- /bin/sh
# Check events (good for debugging probe failures)
kubectl describe pod $(kubectl get pod -l app=order-service -o name | head -1)
What You’ve Learned
- Kubernetes Deployment manages replicas, rolling updates, and self-healing
startupProbeprevents liveness/readiness probes from killing slow-starting appspreStop: sleep 5gives the load balancer time to deregister before shutdown beginsterminationGracePeriodSecondsmust exceed Spring Boot’s shutdown timeout- ConfigMaps and Secrets inject configuration without baking it into the image
- HPA scales based on CPU, memory, or custom metrics (Kafka lag, request rate)
- PodDisruptionBudget ensures minimum availability during maintenance
- Spring Cloud Kubernetes reads ConfigMaps/Secrets directly from the API — no Config Server needed
Next: Article 54 — Full Observability: Prometheus + Grafana + Tempo + Loki — the complete observability stack for production Spring Boot.