Spring Boot 3.x + Spring Cloud Kubernetes: Native Service Discovery, Config Injection & Graceful Shutdown
This production-hardened guide shows how to replace Eureka/Nacos with Kubernetes-native Service/EndpointSlice for service discovery, use ConfigMap/Secret for configuration with dynamic refresh, and implement zero-downtime deployments via preStop hooks and Spring Boot graceful shutdown — complete with RBAC hardening, fault-drill baselines, and a 7-point production checklist.
1. Architecture Evolution: Why Hand Registry/Config to Kubernetes
Traditional microservice stacks relied on Eureka or Nacos for service governance, but after full containerization these external clusters introduce three pains:
Architectural redundancy — separate registry/config clusters consume resources and require dedicated ops.
Time-gap inconsistency — K8s schedules a Pod, assigns an IP, then the app registers; during this window the registry either lacks the instance or holds stale metadata, causing traffic to hit dead endpoints.
Extra network hop — every service call first queries the registry for metadata, lengthening the call chain and expanding the failure surface.
Kubernetes already solves this natively: Service + EndpointSlice + CoreDNS provide declarative service discovery, while kube-proxy (IPVS mode) maintains per-node L4 load-balancing rules so DNS returns actual Pod IPs. Spring Cloud Kubernetes preserves the Spring Cloud programming model while wiring registration and configuration directly to the K8s API Server via its watch mechanism, eliminating middleware and cutting ops overhead.
2. Core Component Integration: Dependencies, Configuration & Property Mapping
2.1 Dependencies (Maven)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-kubernetes-client-discovery</artifactId>
</dependency>
<!-- Replace Ribbon, must be explicit -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>Use the official kubernetes-client starters; the legacy fabric8 client is unmaintained and breaks on Boot 3.x.
2.2 Core Configuration (application.yml)
spring:
application:
name: order-service
cloud:
kubernetes:
config:
enabled: true
name: ${spring.application.name}
namespace: ${POD_NAMESPACE:default}
discovery:
enabled: true
all-namespaces: false
primary-service-only: true
lifecycle:
timeout-per-shutdown-phase: 30s
server:
shutdown: graceful POD_NAMESPACEis injected via the Downward API — never hard-code it. primary-service-only: true filters out headless-service noise, keeping only standard Service endpoints.
2.3 ConfigMap Mapping & Strongly-Typed Binding
ConfigMap data is fed straight into Spring's Environment. The safest pattern is @ConfigurationProperties:
# K8s ConfigMap: order-service
data:
app:
rate-limit: 1000
db-timeout: 5000ms @ConfigurationProperties(prefix = "app")
@Component
public class AppProperties {
private int rateLimit;
private Duration dbTimeout;
// getters/setters omitted
}Service discovery requires zero registration code. KubernetesDiscoveryClient periodically fetches EndpointSlice IPs; simply annotate RestClient or WebClient with @LoadBalanced to enable client-side load balancing.
3. Dynamic Config Refresh: EVENT Listening & Hot-Reload Pitfalls
Spring Cloud Kubernetes supports two watch modes; EVENT (default) is the only production-grade choice. POLLING is a fallback for exotic network constraints.
The event-driven flow: client opens a Watch connection to the API Server; on ConfigMap ADDED/MODIFIED events, KubernetesPropertySourceLocator re-fetches data, merges into Environment, then ContextRefresher fires EnvironmentChangeEvent and RefreshScopeRefreshedEvent. Beans annotated with @RefreshScope are destroyed and recreated on next access.
Production pitfalls: @RefreshScope uses AOP proxies with local caches; bean rebuild discards caches, scheduled tasks, thread-pool state. If a config change hits during peak traffic, state loss triggers business errors. Fix: isolate volatile settings in a dedicated @ConfigurationProperties class, decoupled from core lifecycle beans.
DataSource pools, Redis clients, etc. carry high hot-reload risk. Either keep them out of ConfigMap or implement custom smooth-switch logic in @EventListener(RefreshScopeRefreshedEvent.class) — don't rely on framework auto-close.
Control-plane hiccups may fire duplicate events. Although the framework de-duplicates, add a simple debounce or version check on the business side to avoid CPU-spinning refresh storms.
4. Graceful Shutdown: PreStop, Connection Draining & Traffic Switch
K8s rolling updates send SIGTERM, but without proper tuning you'll see 502/503 spikes. server.shutdown: graceful alone is insufficient.
Real draining sequence:
K8s marks the Pod Terminating and removes it from EndpointSlice. kube-proxy syncs the change to node iptables/IPVS — 5–15 s async delay .
If the container starts stopping Tomcat/Undertow before the LB layer finishes draining, new requests are rejected.
Industry-standard fix: add a preStop sleep in the Deployment:
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 10
periodSeconds: 5The 15 s pause lets the LB layer purge the old IP. During this window the container stays alive, readiness returns 200, and traffic naturally shifts to new Pods. After the pause, Spring Boot's graceful shutdown begins: marks the web container PAUSE (rejects new connections), finishes in-flight requests, then forces exit after timeout-per-shutdown-phase (30 s). Do not tweak tomcat.connection-timeout — it conflicts with the graceful-shutdown logic; defaults are safe.
5. Security & Permissions: RBAC, ServiceAccount & Secret Handling
K8s enables RBAC by default. Pods needing ConfigMap reads or Service watches must bind a least-privilege ServiceAccount — never use the default sa.
apiVersion: v1
kind: ServiceAccount
metadata:
name: order-sa
namespace: prod
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: order-config-reader
namespace: prod
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets", "endpoints"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: order-rb
namespace: prod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: order-config-reader
subjects:
- kind: ServiceAccount
name: order-sa
namespace: prodSecret injection: never stuff secrets into environment variables — /proc/1/environ is world-readable inside the container. Mount via volume with 0400 permissions. For rotation, adopt Secrets Store CSI Driver backed by Vault or cloud KMS; the app rotates transparently without reload logic.
6. Fault Drills & Stress-Test Baselines
Configuration review isn't enough — validate with production-grade chaos:
Pod kill test: kubectl delete --force — verify preStop + graceful shutdown + Resilience4j Retry absorb requests.
Network partition / DNS slowdown: EndpointSlice sync stalls; mitigate with topologySpreadConstraints to spread Pods across AZs. Keep readiness timeouts tight; configure LoadBalancer retries so transient blips self-heal.
ConfigMap corruption: set spring.cloud.kubernetes.config.fail-fast=false so the app starts with local defaults and stays alive while alerts fire.
Hard metrics to hit:
Rolling-update 5xx rate < 0.05% (with maxSurge: 25%, maxUnavailable: 0).
EVENT-mode config refresh latency 1–2 s.
Connection-pool drain success > 99.8% at 500 QPS.
Monitor via Micrometer: Spring Cloud cache metrics, HTTP 5xx counters, graceful-shutdown duration.
7. Production Checklist & Experience Summary
Version alignment: Boot 3.2+ / Spring Cloud 2023.0.x / kubernetes-client 6.8+. No cross-major mixing; follow the compatibility matrix strictly.
Resource quotas: set adequate requests/limits. API Server QPS throttling + starved Pods = config-fetch timeout = startup failure.
JVM DNS cache: Java defaults to infinite TTL in some versions. Add -Dsun.net.inetaddr.ttl=30 or stale Pod IPs linger locally, defeating even perfect draining.
ConfigMap has no undo: K8s doesn't version ConfigMaps. Enforce GitOps (e.g., ArgoCD) or periodic etcd snapshots. Validate YAML syntax in a pre-prod env before applying.
Multi-environment isolation: no hard-coded env flags. Separate by Namespace, overlay with spring.profiles.active for clean, safe promotion.
Converging service discovery and config governance onto the K8s control plane saves massive middleware ops effort, but stability comes from mastering EndpointSlice sync mechanics, LB draining strategies, and Spring lifecycle details — not from "cloud-native" magic. This baseline has run in 10M+ QPS production clusters for over a year; follow it to dodge 90% of startup jitter and release interruption issues.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Xiaolin Talks Programming
Focuses on sharing original technical insights. Senior architect at a top tech company with years of experience in technical architecture and management, and extensive interview experience. Offers one-on-one technical coaching, guiding you from beginner to architecture design to technical management. Follow for free learning resources. Free one-on-one interview coaching to help you land offers quickly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
