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,
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,
accepted-job redelivery, state-service failover, and coordinated restore remain
separate target exercises whose signed evidence is governed by
+16
View File
@@ -460,6 +460,22 @@ class DeploymentInstallerTests(unittest.TestCase):
"containers"
][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"][
"spec"
]["containers"][0]["command"]
@@ -168,6 +168,7 @@ def render_kubernetes(
readiness_path="/health/ready",
liveness_path="/health",
probe_host=public_host,
graceful_shutdown_seconds=10,
extra_environment=_role_database_environment(environment, "API"),
),
_service(
@@ -658,6 +659,7 @@ def _deployment(
readiness_path: str | None = None,
liveness_path: str | None = None,
probe_host: str | None = None,
graceful_shutdown_seconds: int = 0,
extra_environment: Mapping[str, str] | None = None,
selector_labels: Mapping[str, str] | None = None,
) -> dict[str, Any]:
@@ -715,6 +717,18 @@ def _deployment(
container_port or 8000,
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] = {
"serviceAccountName": service_account,
"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"}]
if s3_ca_secret_name:
pod_spec["volumes"].append(_s3_ca_volume(s3_ca_secret_name))