DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Zero-downtime Kubernetes deploys for 8 Spring Boot services: maxUnavailable 0, preStop drain, graceful shutdown

Day 46 of building OrderHub in public. The stack already runs on Kubernetes with a Helm chart (Days 43-44) and a CI/CD pipeline (Day 45). Today: make a deploy drop zero requests.

A naive rollout kills old pods and starts new ones — and for a few seconds in between, requests hit pods that are shutting down or not yet ready. Fixing that is three coordinated pieces.

1. RollingUpdate with maxUnavailable: 0

The Deployment strategy controls how pods are swapped:

spec:
  minReadySeconds: 10
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # add one new pod before removing any old one
      maxUnavailable: 0    # never drop below the desired count
Enter fullscreen mode Exit fullscreen mode

maxUnavailable: 0 is the key: Kubernetes brings a new pod up (and waits for its readiness probe — wired since Day 43) before it takes an old one down. Capacity never dips below 100%. minReadySeconds makes it wait a beat after Ready so a flapping pod doesn't get traffic.

2. preStop drain + termination grace

When a pod is told to stop, Kubernetes removes it from the Service endpoints and sends SIGTERM — but those two happen concurrently, so an in-flight request (or a proxy that hasn't seen the endpoint removal yet) can still arrive during shutdown. A preStop sleep bridges that race:

    terminationGracePeriodSeconds: 45
    containers:
      - name: order-service
        lifecycle:
          preStop:
            exec: { command: ["sh", "-c", "sleep 10"] }
Enter fullscreen mode Exit fullscreen mode

The pod sleeps 10s before the app gets SIGTERM — long enough for endpoint removal to propagate so no new requests are routed to it.

3. Spring Boot graceful shutdown

Now the app must finish the requests it already has. Gated behind the k8s profile so local/test/compose behaviour is unchanged:

spring:
  config:
    activate:
      on-profile: k8s
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
Enter fullscreen mode Exit fullscreen mode

On SIGTERM Spring stops accepting new connections and lets in-flight requests complete (up to 30s) before the JVM exits — comfortably inside the 45s grace period.

Together: new pod Ready → old pod de-registered → 10s drain → graceful in-flight completion → exit. No 502s.

Test the rollout config, don't hope

Like Days 43-45, a JUnit + SnakeYAML test (ZeroDowntimeDeployTest) parses every manifest and asserts: each Deployment has RollingUpdate with maxUnavailable: 0, a preStop hook + terminationGracePeriodSeconds coherent with it, and the k8s-profile config enables graceful shutdown. Weaken any of it and a unit test goes red.

mvn clean test: 166 green across 9 modules (163 → 166).

Roll forward with kubectl rollout status, and back with kubectl rollout undorevisionHistoryLimit: 5 keeps the last five.

Free + open. Repo in comments. Next: k6 load testing.

kubernetes #devops #springboot #java #sre #buildinpublic

Top comments (0)