Drain API pods before Kubernetes shutdown
Dependency Audit / dependency-audit (push) Successful in 1m51s
Deployment Installer / deployment-installer (push) Successful in 6s
Security Audit / security-audit (push) Successful in 11m37s

This commit is contained in:
2026-08-05 21:55:32 +02:00
parent 3b9ae901dd
commit 389df7c3d5
3 changed files with 39 additions and 0 deletions
@@ -270,6 +270,10 @@ record under the installation evidence directory and never retains the API key.
Use `--exercise-api-pod-loss` in an approved drill window to delete one API pod, Use `--exercise-api-pod-loss` in an approved drill window to delete one API pod,
observe the public readiness path continuously, and record its replacement. observe the public readiness path continuously, and record its replacement.
Generated API workloads use a ten-second pre-stop drain so Kubernetes can remove
the terminating endpoint from ingress and service routing before Uvicorn exits.
Do not remove or shorten this drain without repeating the public-path pod-loss
test against the target ingress controller and network implementation.
This proves the bounded stateless-node-loss slice only. Session continuity, This proves the bounded stateless-node-loss slice only. Session continuity,
accepted-job redelivery, state-service failover, and coordinated restore remain accepted-job redelivery, state-service failover, and coordinated restore remain
separate target exercises whose signed evidence is governed by separate target exercises whose signed evidence is governed by
+16
View File
@@ -460,6 +460,22 @@ class DeploymentInstallerTests(unittest.TestCase):
"containers" "containers"
][0]["readinessProbe"]["httpGet"]["httpHeaders"], ][0]["readinessProbe"]["httpGet"]["httpHeaders"],
) )
api_pod_spec = deployments["govoplan-cluster-api"]["spec"]["template"][
"spec"
]
self.assertEqual(30, api_pod_spec["terminationGracePeriodSeconds"])
self.assertEqual(
["/bin/sh", "-c", "sleep 10"],
api_pod_spec["containers"][0]["lifecycle"]["preStop"]["exec"][
"command"
],
)
self.assertNotIn(
"lifecycle",
deployments["govoplan-cluster-worker"]["spec"]["template"]["spec"][
"containers"
][0],
)
worker_command = deployments["govoplan-cluster-worker"]["spec"]["template"][ worker_command = deployments["govoplan-cluster-worker"]["spec"]["template"][
"spec" "spec"
]["containers"][0]["command"] ]["containers"][0]["command"]
@@ -168,6 +168,7 @@ def render_kubernetes(
readiness_path="/health/ready", readiness_path="/health/ready",
liveness_path="/health", liveness_path="/health",
probe_host=public_host, probe_host=public_host,
graceful_shutdown_seconds=10,
extra_environment=_role_database_environment(environment, "API"), extra_environment=_role_database_environment(environment, "API"),
), ),
_service( _service(
@@ -658,6 +659,7 @@ def _deployment(
readiness_path: str | None = None, readiness_path: str | None = None,
liveness_path: str | None = None, liveness_path: str | None = None,
probe_host: str | None = None, probe_host: str | None = None,
graceful_shutdown_seconds: int = 0,
extra_environment: Mapping[str, str] | None = None, extra_environment: Mapping[str, str] | None = None,
selector_labels: Mapping[str, str] | None = None, selector_labels: Mapping[str, str] | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
@@ -715,6 +717,18 @@ def _deployment(
container_port or 8000, container_port or 8000,
host=probe_host, host=probe_host,
) )
if graceful_shutdown_seconds:
container["lifecycle"] = {
"preStop": {
"exec": {
"command": [
"/bin/sh",
"-c",
f"sleep {graceful_shutdown_seconds}",
]
}
}
}
pod_spec: dict[str, Any] = { pod_spec: dict[str, Any] = {
"serviceAccountName": service_account, "serviceAccountName": service_account,
"automountServiceAccountToken": False, "automountServiceAccountToken": False,
@@ -734,6 +748,11 @@ def _deployment(
} }
], ],
} }
if graceful_shutdown_seconds:
pod_spec["terminationGracePeriodSeconds"] = max(
30,
graceful_shutdown_seconds + 20,
)
container["volumeMounts"] = [{"name": "tmp", "mountPath": "/tmp"}] container["volumeMounts"] = [{"name": "tmp", "mountPath": "/tmp"}]
if s3_ca_secret_name: if s3_ca_secret_name:
pod_spec["volumes"].append(_s3_ca_volume(s3_ca_secret_name)) pod_spec["volumes"].append(_s3_ca_volume(s3_ca_secret_name))