Stage 7 · Master
Phase 19 — Production Deployment
Rolling Updates
Make a rollout keep full capacity throughout by pairing maxSurge/maxUnavailable with readiness-gated draining tied to /readyz, size terminationGracePeriodSeconds against the real shutdown budget, add a PodDisruptionBudget, and prove zero dropped requests by load-testing through a live rollout — then recover a bad one with kubectl rollout undo.
maxSurge and maxUnavailable Decide Whether a Rollout Ever Drops Below Full Capacity
The Deployment written in the Kubernetes lesson has no strategy block, which means it inherits Kubernetes's defaults: maxUnavailable: 25%, maxSurge: 25%. At organization-api's 3 replicas, 25% rounds down to 0 unavailable and up to 1 surge — workable by accident, but nowhere written down, and silently wrong the day someone changes replicas to a number where the same percentages round differently. The fix is to state the intent explicitly: a new pod must be Ready before an old one is removed, so the fleet never has fewer than 3 healthy replicas serving traffic during a deploy.
spec:
replicas: 3
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 30
containers:
- name: api
# image, ports, envFrom, probes unchanged from the Kubernetes lesson
lifecycle:
preStop:
sleep: { seconds: 5 }minReadySeconds: 10 makes a just-started pod prove it stays Ready for 10 full seconds — not just pass one readiness check — before Kubernetes considers it available and proceeds to the next pod, catching a pod that passes its first probe and then immediately fails under real traffic.
terminationGracePeriodSeconds Must Cover preStop Plus the App's Own Shutdown Timeout, Not Guess
services/organization/internal/config/config.go already reads a SHUTDOWN_TIMEOUT of 15 seconds — the budget http.Server.Shutdown gets to drain in-flight requests before the process exits. terminationGracePeriodSeconds: 30 is not a round number: 5 seconds for the preStop sleep, plus the 15-second shutdown budget, plus 10 seconds of margin for scheduling jitter — sized against the actual components of shutdown, not chosen because 30 sounded reasonable.
func Run(ctx context.Context, cfg Config, srv *http.Server, ready *atomic.Bool) error {
ready.Store(true) // flips /readyz to 200 once startup dependencies are confirmed
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM)
errCh := make(chan error, 1)
go func() { errCh <- srv.ListenAndServe() }()
select {
case err := <-errCh:
return err
case <-stop:
// Immediately fail /readyz — this pod stops receiving new traffic from
// the Service well before the process actually exits, because kube-proxy
// on every node needs a moment to notice the Endpoints removal. The
// preStop sleep below buys that propagation window.
ready.Store(false)
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
return srv.Shutdown(shutdownCtx) // drains in-flight requests, does not accept new ones
}Setting ready.Store(false) is the entire mechanism behind 'readiness-gated draining' — the process does not stop listening the instant SIGTERM arrives; it stops being routable, drains what is already in flight, and only then shuts the listener down, in that order.
A PodDisruptionBudget Protects Availability During Node Drains, Not Just Application Rollouts
maxUnavailable: 0 on the Deployment's rolling-update strategy only governs organization-api's own deploys. A cluster-autoscaler node replacement or a manual kubectl drain during a rollout is a separate, voluntary disruption Kubernetes does not automatically coordinate with the rollout in progress — a PodDisruptionBudget is what stops both events from happening to the same pods at once and dropping below serving capacity.
spec:
- minAvailable: 1
+ minAvailable: 2
selector:
- matchLabels: { app: gateway }
+ matchLabels: { app: organization-api }The object is the one API Gateway introduced when it became the single public entry point; only the floor changes. minAvailable: 2 of 3 means the eviction API refuses a drain that would drop the fleet below two — and choosing 2 rather than 1 matters precisely because a rollout is already consuming one pod's worth of headroom. A PDB of 1 would permit a drain and a rollout to coincide at exactly one serving replica.
A Broken Image Cannot Silently Finish a Rollout With maxUnavailable: 0
A deploy ships organization-api@sha256:9f2a..., built from a commit that reads a new required config key without a fallback default — the key was added to the Go struct but its ConfigMap entry from Lesson 8 was never updated in the same pull request. Every new pod fails to start, /readyz never turns healthy, and — because maxUnavailable: 0 refuses to remove any working old pod until a new one is confirmed Ready — the rollout stalls instead of completing broken.
The exact same broken commit deployed under the Kubernetes lesson's original default strategy could have removed a healthy old pod before discovering the new one was broken, actively reducing capacity during the incident instead of just failing to add it. The strategy choice is what determined whether this was an outage or a stalled rollout with zero customer impact.
Prove Zero Dropped Requests, Not Just a Green Rollout Status
A rollout reported as successful by kubectl is not the same claim as 'no request was dropped while it happened' — the only way to verify the latter is to generate real traffic through the exact window the rollout is running and check its error rate, the same discipline the load-testing lesson established for finding the saturation knee.
A successful verification shows the rollout completing, kubectl get pods transitioning from 3 old pods to 3 new pods with a brief 4-pod surge in between, and the k6 error rate for the entire run staying under the platform's existing 0.1% SLO with no visible spike correlated to the rollout's timestamps — the readiness-gated draining pattern is proven by absence of dropped requests, not by the rollout command's own exit code.
Applied exercise
Size gateway's own rollout strategy instead of copying organization's numbers
gateway currently has no explicit strategy, terminationGracePeriodSeconds, or preStop hook, and reads its own SHUTDOWN_TIMEOUT of 10 seconds (shorter than organization's 15, since gateway holds no database connections to drain).
- State gateway's current replica count (from the autoscaling lesson's HPA) and propose maxSurge/maxUnavailable values, justifying them the same way this lesson justified organization's — not by copying its numbers.
- Compute gateway's terminationGracePeriodSeconds from its own SHUTDOWN_TIMEOUT and preStop sleep, showing the arithmetic.
- Write the readiness-gated SIGTERM handling gateway's own signal-handling code needs, noting any difference from organization's version given gateway proxies to other services rather than querying a database directly.
- Describe the verification you would run — referencing the specific k6 script and metric this lesson used — to confirm gateway's rollout drops zero requests.
Deliverable
A gateway-specific rollout strategy and terminationGracePeriodSeconds with shown arithmetic, updated signal-handling code, and a verification plan referencing a concrete command and threshold.
Completion checks
- The proposed strategy values are justified against gateway's own replica count and role, not copied from organization's.
- terminationGracePeriodSeconds is computed from gateway's actual SHUTDOWN_TIMEOUT and preStop sleep, matching this lesson's arithmetic style.
- The verification plan names a specific k6 command and a specific metric threshold, not a general description of 'testing it'.
Why does the SIGTERM handler call ready.Store(false) before calling srv.Shutdown, rather than shutting the server down immediately on signal receipt?
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.