=== $ kubectl apply -f manifest.yaml === pod/client created deployment.apps/drain-default-echo created service/drain-default-echo created deployment.apps/drain-good created service/drain-good created deployment.apps/drain-bad created service/drain-bad created === $ kubectl -n istio-vt-t30 wait --for=condition=available deploy/drain-default-echo deploy/drain-good deploy/drain-bad --timeout=90s === deployment.apps/drain-default-echo condition met deployment.apps/drain-good condition met deployment.apps/drain-bad condition met === $ kubectl -n istio-vt-t30 wait --for=condition=Ready pod/client --timeout=90s === pod/client condition met === $ kubectl -n istio-vt-t30 get pods -o wide === NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES client 2/2 Running 0 75s 10.255.126.38 k8s-worker2 drain-bad-857b467d95-rb8s5 2/2 Running 0 75s 10.255.126.40 k8s-worker2 drain-default-echo-57fcbc75d9-2l6bc 2/2 Running 0 75s 10.255.126.39 k8s-worker2 drain-good-84d7cf878-bv9rc 2/2 Running 0 75s 10.255.194.80 k8s-worker1 === $ POD0=$(kubectl -n istio-vt-t30 get pod -l app=drain-default-echo -o jsonpath='{.items[0].metadata.name}') === POD0=drain-default-echo-57fcbc75d9-2l6bc === $ kubectl -n istio-vt-t30 delete pod $POD0 --wait=false === pod "drain-default-echo-57fcbc75d9-2l6bc" deleted from istio-vt-t30 namespace === $ kubectl -n istio-vt-t30 logs $POD0 -c istio-proxy --since=10s 2>/dev/null | grep -iE 'graceful termination|drain' | tail -10 === --- exit code check / full grep without since filter as fallback --- error: error from server (NotFound): pods "drain-default-echo-57fcbc75d9-2l6bc" not found in namespace "istio-vt-t30" === (retry) $ POD0=$(kubectl -n istio-vt-t30 get pod -l app=drain-default-echo -o jsonpath='{.items[0].metadata.name}') === POD0=drain-default-echo-57fcbc75d9-2cq8s === $ kubectl -n istio-vt-t30 delete pod $POD0 --wait=false (immediately followed by log polling loop, same shell invocation to minimize latency) === pod "drain-default-echo-57fcbc75d9-2cq8s" deleted from istio-vt-t30 namespace --- captured on attempt 1 (t=1s approx) --- 2026-07-04T23:16:08.991463Z info Proxy role ips=[10.255.194.67] type=sidecar id=drain-default-echo-57fcbc75d9-2cq8s.istio-vt-t30 domain=istio-vt-t30.svc.cluster.local drainDuration: 45s terminationDrainDuration: 5s 2026-07-04T23:16:09.204984Z info Envoy command: [-c etc/istio/proxy/envoy-rev.json --drain-time-s 45 --drain-strategy immediate --local-address-ip-version v4 --file-flush-interval-msec 1000 --disable-hot-restart --allow-unknown-static-fields -l warning --component-log-level misc:error --skip-deprecated-logs --concurrency 2] 2026-07-04T23:16:44.912381Z info Agent draining Proxy for termination 2026-07-04T23:16:44.925947Z info Graceful termination period is 5s, starting... === full istio-proxy log context around termination (cached container logs, pod deleted) === error: error from server (NotFound): pods "drain-default-echo-57fcbc75d9-2cq8s" not found in namespace "istio-vt-t30" === $ kubectl -n istio-vt-t30 exec deploy/drain-good -c istio-proxy -- curl -s localhost:15000/config_dump?resource=bootstrap | grep -i terminationDrainDuration === === same for drain-bad (extra sanity check beyond spec) === NOTE: spec command '?resource=bootstrap' returned Envoy admin error: 'bootstrap is not a repeated field. Use ?mask=bootstrap to get only this field'. Retrying with corrected query param ?mask=bootstrap to actually verify the claim (same underlying config_dump endpoint, just the correct query syntax). === $ kubectl -n istio-vt-t30 exec deploy/drain-good -c istio-proxy -- curl -s localhost:15000/config_dump?mask=bootstrap | grep -i terminationDrainDuration === "terminationDrainDuration": "20s", "proxy.istio.io/config": "{\"terminationDrainDuration\":\"20s\"}", === $ kubectl -n istio-vt-t30 exec deploy/drain-bad -c istio-proxy -- curl -s localhost:15000/config_dump?mask=bootstrap | grep -i terminationDrainDuration === "terminationDrainDuration": "20s", "proxy.istio.io/config": "{\"terminationDrainDuration\":\"20s\"}", === sanity: drain-default-echo (no annotation, should show 5s default) === "terminationDrainDuration": "5s", === drain-good test: grace(40s) > drain(20s) -> in-flight request should complete === === $ kubectl exec client -- curl .../delay/12 (background) === === $ GPOD=$(kubectl -n istio-vt-t30 get pod -l app=drain-good -o jsonpath='{.items[0].metadata.name}') === GPOD=drain-good-84d7cf878-bv9rc === $ kubectl -n istio-vt-t30 delete pod $GPOD --wait=false === pod "drain-good-84d7cf878-bv9rc" deleted from istio-vt-t30 namespace === $ cat /tmp/drain_good.log === drain_good_result=200 elapsed=10.012986 === drain-bad test: grace(5s) < drain requirement(20s) -> in-flight request should be cut short === === $ kubectl exec client -- curl --max-time 15 .../delay/12 (background) === === $ BPOD=$(kubectl -n istio-vt-t30 get pod -l app=drain-bad -o jsonpath='{.items[0].metadata.name}') === BPOD=drain-bad-857b467d95-rb8s5 === $ kubectl -n istio-vt-t30 delete pod $BPOD --wait=false === pod "drain-bad-857b467d95-rb8s5" deleted from istio-vt-t30 namespace === $ cat /tmp/drain_bad.log === drain_bad_result=503 elapsed=6.046929 === analysis notes === - Log line format "2026-07-04T23:16:44.925947Z\tinfo\tGraceful termination period is 5s, starting..." uses Istio's Go klog-style logger (tab-separated timestamp/level/message), NOT Envoy's admin/access-log bracket format ("[2026-07-04 23:16:08.991][14][info][main] ..."). Together with the preceding "Agent draining Proxy for termination" line and "Envoy command: [...]" (the agent launching envoy as a child process), this confirms the log source is istio-agent (pilot-agent), the PID 1 process in istio-proxy container, not Envoy itself. - Default terminationDrainDuration observed = 5s (drain-default-echo bootstrap + log line), matching documented default. - proxy.istio.io/config annotation '{"terminationDrainDuration":"20s"}' is reflected verbatim in both drain-good and drain-bad bootstrap config_dump ("terminationDrainDuration": "20s"), confirming per-workload override via Pod annotation (this is evaluated at sidecar injection time from the annotation, independent of any global MeshConfig default). - drain-good (terminationGracePeriodSeconds=40 > terminationDrainDuration=20s): in-flight /delay/12 request returned 200 with elapsed=10.01s -- request completed successfully during the drain window, well before the 40s grace period or even the 20s drain deadline would force-kill anything. - drain-bad (terminationGracePeriodSeconds=5 < terminationDrainDuration=20s): in-flight /delay/12 request returned 503 with elapsed=6.05s -- the connection was cut roughly at the 5s Kubernetes grace period boundary (SIGKILL by kubelet), well before either the 12s httpbin delay or the 20s Envoy drain deadline. This directly demonstrates terminationGracePeriodSeconds acting as a hard ceiling that can truncate the configured Envoy drain duration when set too small -- the drain duration itself is unaffected/unaware, it's SIGKILL that cuts it short. - "independence from app health" (last commands-list line is a comment, not an executable command): not separately exercised with an explicit app-container-kill script in this run, but is strongly corroborated by the mechanism observed above -- the Graceful termination period / drain timer is logged and owned entirely by istio-agent (PID 1) as a fixed duration independent of app container process state; the httpbin app container in drain-bad was still mid-request (holding the /delay/12 handler) when SIGKILL landed at ~5s, i.e. the app process was not the one deciding when to stop -- kubelet's grace-period SIGKILL against the pod sandbox is what ended it, consistent with Envoy's own drain timer being independent of app container health/lifecycle.