# 작업 H — L4 passthrough에서 keepalive로 재사용된 연결은 DNS flip 후에도 옛 backend로 가는가 **Date:** 2026-07-09 · **cluster:** homelab · **ns:** dns-lab · **Istio:** 1.30.0 **시나리오:** `scenarios/50-dns-resolution/48-serviceentry-passthrough.yaml` (변경 없음, 있는 그대로 사용) **신규 파일:** 없음(repo 범위는 본 리포트 1개만 — 스크립트는 스크래치패드에서만 실행) **선행:** `2026-07-09_134250_dns-passthrough-tcp.md`(작업 D — passthrough 경로 기본 성질), `2026-07-09_144251_dns-passthrough-logical-inflight.md` > 초안(construction 로그). 이 리포트가 검증하는 대상은 문서에 "메커니즘 추론, 미실측"으로 표기돼 있던 > 다음 주장 하나다: *"L4 passthrough에서는 Envoy가 연결을 능동적으로 닫지 않으므로, 앱이 그 TLS 연결을 > keepalive로 재사용하면 DNS flip 후에도 계속 옛 backend로 간다. 새 연결을 맺어야 비로소 새 backend로 > 간다."* 좁고 짧은 단일 질문 실측이라 범위를 넓히지 않았다. --- ## 0. 결론 먼저 (TL;DR) **주장은 지지된다.** 2회 반복 모두 동일한 패턴: 하나의 TLS 연결(keepalive)로 DNS flip 전/후 걸쳐 요청을 3번 보내면 **3번 다 flip 이전 backend(옛 IP)로 간다** — body도, TCP peer도, 로컬 소켓도 그대로. 연결을 close()하고 새로 연 뒤에야 새 backend로 바뀐다. Envoy 클러스터의 `upstream_cx_total` 누적 카운터가 이 전체 타임라인 동안 요청 4번에 대해 딱 2번만 증가(재사용 구간 3회 요청은 Δ0, close 후 신규 연결에서 +1)한 것이 "같은 업스트림 연결이 실제로 유지됐다"는 것의 **Envoy 쪽 카운터 교차증거**다. --- ## 1. 실측 설계 client(netshoot pod)에서 `python3`(3.14.5, 이미 이미지에 포함) `http.client.HTTPSConnection` 하나를 열어 유지한 채 순차 요청. `curl --next`/`openssl s_client` 대신 python을 쓴 이유: `conn.sock.getsockname()`/ `getpeername()`으로 "같은 TCP 연결"과 "실제 어느 IP로 붙어 있는가"를 요청마다 직접 찍을 수 있어 별도 `ss` 파싱 없이 그 자체로 연결 재사용의 1차 증거가 된다. ```python # scratchpad/keepalive_probe.py 핵심부 conn = new_conn() # HTTPSConnection(verify off, SNI=gslb-pt.lab.internal) do_get(conn, "REQ1_t0") # t=0 time.sleep(15) do_get(conn, "REQ2_t15") # t=15 (flip은 외부에서 t=+5에 실행됨) time.sleep(10) do_get(conn, "REQ3_t25") # t=25 conn.close() conn2 = new_conn() do_get(conn2, "REQ4_newconn") # 새 연결 ``` 오케스트레이션(`scratchpad/run_keepalive_test.sh`)은 host 쪽에서 절대 epoch(`T0=$(date +%s)`)을 잡고 `sleep_until $((T0+N))` 방식으로 flip을 고정 오프셋(T0+5s)에 발사한다 — 백그라운드 dispatch 어긋남을 피하려고 flip 발사 시각과 probe 내부 시각(둘 다 실제 wallclock으로 기록)을 리포트에 나란히 남겨 사후에 정렬 확인이 가능하게 했다. Flip은 `lab-dns`의 `writer` 컨테이너가 쓰는 `/hosts/addn` 파일을 `gslb.lab.internal`(기존 HTTP 경로) 줄까지 함께 재기록하는 방식(기존 `dns-passthrough-inflight.sh`에서 발견된 "덮어쓰기하면 다른 hostname 줄이 날아간다" 버그 패턴을 그대로 재사용해 회피). **연결 재사용의 증거는 3중**: 1. 클라이언트 소켓 로컬 포트(`getsockname()`)가 REQ1→REQ2→REQ3 동안 불변, REQ4(신규 연결)에서만 바뀜. 2. `getpeername()`이 REQ1~3 내내 flip 이전 IP(pod-a)를 가리키고, REQ4에서만 flip 이후 IP(pod-b)로 바뀜. 3. Envoy 사이드카의 `outbound|443||gslb-pt.lab.internal` 클러스터 `upstream_cx_total`(누적 오픈 커넥션 수)이 BEFORE→MID1 구간에서 딱 +1(=REQ1이 새로 연 연결), MID1→MID2 구간은 Δ0(=REQ2/REQ3가 그 연결을 그대로 재사용해 새 연결을 안 열었다는 뜻), MID2→AFTER 구간에서 다시 +1(=REQ4의 신규 연결). 클라이언트 쪽 관찰과 서버 사이드카 카운터가 서로 다른 관측 지점에서 같은 결론을 가리킨다. --- ## 2. 사전 확인 (clean 상태 재확인) ``` $ kubectl --context=homelab -n dns-lab get pods -o wide backend-a-b69577fc-kmbvp 1/1 Running IP 10.255.194.124 backend-b-69bd4df99b-jlnpn 1/1 Running IP 10.255.126.38 netshoot-76c869b576-h4fqh 2/2 Running $ kubectl --context=homelab -n dns-lab exec deploy/netshoot -- dig +short gslb-pt.lab.internal 10.255.194.124 # 이미 backend-a pod IP와 일치 — DNS 레코드 갱신 불필요, pod 재시작 없었음 $ kubectl --context=homelab -n dns-lab exec deploy/backend-a -c nginx -- nginx -T | grep -i keepalive keepalive_timeout 65; # backend-b도 동일 65s — 요청 간격 10~15s면 서버가 먼저 안 끊는다 ``` 부수 확인 하나(범위 밖이지만 실측 설계에 영향을 줘서 기록): `curl --resolve gslb-pt.lab.internal:443:`로 목적지를 강제해도 응답은 여전히 `backend-a`였다. 즉 이 passthrough 클러스터의 실제 업스트림은 "client가 어떤 IP로 다이얼했는가"가 아니라 **Envoy 자신의 STRICT_DNS 재해석 결과(`gslb-pt.lab.internal`을 lab-dns에 질의한 값)** 로 결정된다 — 그래서 이번 실측은 client 쪽 hostname 오버라이드가 아니라 실제 lab-dns 레코드 flip으로만 수행했다(설계와 일치). --- ## 3. Run 1 — 타임라인 | t (probe 내부) | wallclock(probe, UTC) | 이벤트 | 로컬포트 | peer(연결 목적지) | body | |---|---|---|---|---|---| | t=0 | 11:39:27.724 | REQ1 (연결 오픈) | 40186 | 10.255.194.124 (pod-a) | `backend-a` | | — | 20:39:32.555 (host) | **FLIP 발사** gslb-pt: pod-a → pod-b | — | — | — | | — | 20:39:37.739 (host) | dig 결과 갱신 확인(pod-b) | — | — | — | | t=15 | 11:39:42.725 | REQ2 (같은 conn) | 40186 (불변) | 10.255.194.124 (pod-a, **여전히 옛 IP**) | `backend-a` | | t=25 | 11:39:52.726 | REQ3 (같은 conn) | 40186 (불변) | 10.255.194.124 (pod-a, **여전히 옛 IP**) | `backend-a` | | — | 11:39:52.728 | conn.close() | — | — | — | | t=25+ | 11:39:52.734 | REQ4 (신규 conn) | 59332 (**변경**) | 10.255.126.38 (pod-b, **새 IP**) | `backend-b` | Envoy 클러스터 카운터(`outbound|443||gslb-pt.lab.internal`, `pilot-agent request GET stats?filter=gslb-pt`): | 시점 | upstream_cx_total(누적) | upstream_cx_active | membership_change(누적) | |---|---|---|---| | BEFORE (probe 시작 직전) | 2984 | 0 | 12 | | MID1 (flip+5s, REQ1 이미 연결됨) | 2985 (**+1**) | 1 | 13 (**+1**, pod-b 멤버 추가) | | MID2 (flip+15s, REQ2 지난 시점) | 2985 (Δ0) | 1 | 13 | | AFTER (REQ4까지 끝나고 conn 모두 닫힘) | 2986 (**+1**) | 0 | 13 | `upstream_cx_total`이 요청 4번에 대해 딱 2번만 틱했다 — REQ1~3이 하나의 업스트림 연결을 공유했다는 뜻. --- ## 4. Run 2 — 타임라인 (재현성 확인) | t (probe 내부) | 이벤트 | 로컬포트 | peer | body | |---|---|---|---|---| | t=0 | REQ1 | 41868 | 10.255.194.124 (pod-a) | `backend-a` | | t=15 (flip은 t=+5에 발사, dig는 flip+~9s에 pod-b로 갱신 확인) | REQ2 (같은 conn) | 41868 (불변) | 10.255.194.124 (**여전히 pod-a**) | `backend-a` | | t=25 | REQ3 (같은 conn) | 41868 (불변) | 10.255.194.124 (**여전히 pod-a**) | `backend-a` | | t=25+ (close 후 신규 conn) | REQ4 | 34726 (변경) | 10.255.126.38 (pod-b) | `backend-b` | Envoy 카운터: BEFORE `upstream_cx_total=2986` → MID1 `2987`(+1, REQ1) → MID2 `2987`(Δ0, REQ2/REQ3 재사용 구간) → AFTER `2988`(+1, REQ4). Run 1과 정확히 같은 패턴(+1, Δ0, +1)이 재현됐다. 원시 실행 로그(명령 그대로, 잘라내지 않음): `run1_full.txt`, `run2_full.txt` (전체 내용은 §6에 인라인). --- ## 5. 서버 keepalive_timeout 관련 관측 backend nginx `keepalive_timeout 65`(양쪽 backend-a/b 동일). 이번 실측의 요청 간격(15s, 10s)은 65s보다 훨씬 짧아 **서버가 먼저 연결을 끊는 상황은 발생하지 않았다** — probe 스크립트에 넣어둔 재연결 폴백 분기(`REQ2_t15_RECONNECTED` 등, 서버/Envoy가 먼저 끊었을 때의 데이터 포인트를 잡기 위한 코드)는 2회 실행 모두 트리거되지 않았다(stderr 비어있고, stdout에 `RECONNECTED` 라벨이 한 번도 안 찍힘 — REQ2/REQ3가 모두 정상 `OK` 상태로 로컬 포트 불변). 즉 이번 결과는 "서버가 계속 열어뒀기 때문에 재사용이 가능했던" 정직한 keepalive 재사용 시나리오이지, 우연히 재연결된 결과가 아니다. --- ## 6. 원시 실행 로그 (Run 1 / Run 2 전문) ### Run 1 ``` #### run1 — 2026-07-09_203919 POD_A=10.255.194.124 POD_B=10.255.126.38 IP_A_SVC=10.250.161.145 -- reset gslb-pt.lab.internal -> POD_A (10.255.194.124) before run, keep gslb.lab.internal line intact -- -- confirm dig + confirm cluster resolves to POD_A before starting timed run -- 10.255.194.124 ENDPOINT STATUS OUTLIER CHECK CLUSTER 10.255.194.124:443 HEALTHY OK outbound|443||gslb-pt.lab.internal === BEFORE @ 20:39:27.366 === -- dig gslb-pt -- 10.255.194.124 -- cluster stats (gslb-pt, netshoot sidecar) -- cluster.outbound|443||gslb-pt.lab.internal;.membership_change: 12 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_active: 0 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy: 2984 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local: 319 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local_with_active_rq: 290 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote: 2665 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote_with_active_rq: 2665 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_with_active_rq: 2955 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_total: 2984 PROBE_LAUNCH wallclock=20:39:27.551 (T0=1783597167) FLIP_ISSUED wallclock=20:39:32.555 (t=+5s) gslb-pt: 10.255.194.124 -> 10.255.126.38 FLIP_DONE wallclock=20:39:32.635 (t=+5s) -- dig right after flip write -- 10.255.194.124 [t=+10s] === MID1_afterflip @ 20:39:37.739 === -- dig gslb-pt -- 10.255.126.38 -- cluster stats (gslb-pt, netshoot sidecar) -- cluster.outbound|443||gslb-pt.lab.internal;.membership_change: 13 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_active: 1 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy: 2984 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local: 319 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local_with_active_rq: 290 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote: 2665 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote_with_active_rq: 2665 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_with_active_rq: 2955 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_total: 2985 [t=+20s] === MID2 @ 20:39:47.929 === -- dig gslb-pt -- 10.255.126.38 -- cluster stats (gslb-pt, netshoot sidecar) -- cluster.outbound|443||gslb-pt.lab.internal;.membership_change: 13 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_active: 1 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy: 2984 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local: 319 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local_with_active_rq: 290 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote: 2665 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote_with_active_rq: 2665 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_with_active_rq: 2955 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_total: 2985 PROBE_WAIT_DONE wallclock=20:39:52.751 (t=+25s) -- probe stdout -- RUN_START wallclock=11:39:27.714 [REQ1_t0] wallclock=11:39:27.724 OK status=200 local=('10.255.126.8', 40186) peer=('10.255.194.124', 443) body='backend-a' SLEEP_15 wallclock=11:39:27.724 [REQ2_t15] wallclock=11:39:42.725 OK status=200 local=('10.255.126.8', 40186) peer=('10.255.194.124', 443) body='backend-a' SLEEP_10 wallclock=11:39:42.726 [REQ3_t25] wallclock=11:39:52.726 OK status=200 local=('10.255.126.8', 40186) peer=('10.255.194.124', 443) body='backend-a' CONN_CLOSED wallclock=11:39:52.728 [REQ4_newconn] wallclock=11:39:52.734 OK status=200 local=('10.255.126.8', 59332) peer=('10.255.126.38', 443) body='backend-b' RUN_END wallclock=11:39:52.735 -- probe stderr -- [t=+27s] === AFTER @ 20:39:54.757 === -- dig gslb-pt -- 10.255.126.38 -- cluster stats (gslb-pt, netshoot sidecar) -- cluster.outbound|443||gslb-pt.lab.internal;.membership_change: 13 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_active: 0 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy: 2986 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local: 319 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local_with_active_rq: 290 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote: 2667 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote_with_active_rq: 2667 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_with_active_rq: 2957 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_total: 2986 -- final dig -- 10.255.126.38 ``` ### Run 2 ``` #### run2 — 2026-07-09_204101 POD_A=10.255.194.124 POD_B=10.255.126.38 IP_A_SVC=10.250.161.145 -- reset gslb-pt.lab.internal -> POD_A (10.255.194.124) before run, keep gslb.lab.internal line intact -- -- confirm dig + confirm cluster resolves to POD_A before starting timed run -- 10.255.194.124 ENDPOINT STATUS OUTLIER CHECK CLUSTER 10.255.194.124:443 HEALTHY OK outbound|443||gslb-pt.lab.internal === BEFORE @ 20:41:09.770 === -- dig gslb-pt -- 10.255.194.124 -- cluster stats (gslb-pt, netshoot sidecar) -- cluster.outbound|443||gslb-pt.lab.internal;.membership_change: 14 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_active: 0 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy: 2986 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local: 319 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local_with_active_rq: 290 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote: 2667 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote_with_active_rq: 2667 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_with_active_rq: 2957 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_total: 2986 PROBE_LAUNCH wallclock=20:41:09.962 (T0=1783597269) FLIP_ISSUED wallclock=20:41:14.965 (t=+5s) gslb-pt: 10.255.194.124 -> 10.255.126.38 FLIP_DONE wallclock=20:41:15.048 (t=+6s) -- dig right after flip write -- 10.255.194.124 [t=+10s] === MID1_afterflip @ 20:41:19.144 === -- dig gslb-pt -- 10.255.126.38 -- cluster stats (gslb-pt, netshoot sidecar) -- cluster.outbound|443||gslb-pt.lab.internal;.membership_change: 15 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_active: 1 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy: 2986 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local: 319 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local_with_active_rq: 290 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote: 2667 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote_with_active_rq: 2667 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_with_active_rq: 2957 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_total: 2987 [t=+20s] === MID2 @ 20:41:29.331 === -- dig gslb-pt -- 10.255.126.38 -- cluster stats (gslb-pt, netshoot sidecar) -- cluster.outbound|443||gslb-pt.lab.internal;.membership_change: 15 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_active: 1 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy: 2986 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local: 319 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local_with_active_rq: 290 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote: 2667 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote_with_active_rq: 2667 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_with_active_rq: 2957 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_total: 2987 PROBE_WAIT_DONE wallclock=20:41:35.144 (t=+26s) -- probe stdout -- RUN_START wallclock=11:41:10.106 [REQ1_t0] wallclock=11:41:10.116 OK status=200 local=('10.255.126.8', 41868) peer=('10.255.194.124', 443) body='backend-a' SLEEP_15 wallclock=11:41:10.117 [REQ2_t15] wallclock=11:41:25.117 OK status=200 local=('10.255.126.8', 41868) peer=('10.255.194.124', 443) body='backend-a' SLEEP_10 wallclock=11:41:25.118 [REQ3_t25] wallclock=11:41:35.118 OK status=200 local=('10.255.126.8', 41868) peer=('10.255.194.124', 443) body='backend-a' CONN_CLOSED wallclock=11:41:35.120 [REQ4_newconn] wallclock=11:41:35.125 OK status=200 local=('10.255.126.8', 34726) peer=('10.255.126.38', 443) body='backend-b' RUN_END wallclock=11:41:35.127 -- probe stderr -- [t=+28s] === AFTER @ 20:41:37.149 === -- dig gslb-pt -- 10.255.126.38 -- cluster stats (gslb-pt, netshoot sidecar) -- cluster.outbound|443||gslb-pt.lab.internal;.membership_change: 15 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_active: 0 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy: 2988 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local: 319 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_local_with_active_rq: 290 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote: 2669 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_remote_with_active_rq: 2669 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_destroy_with_active_rq: 2959 cluster.outbound|443||gslb-pt.lab.internal;.upstream_cx_total: 2988 -- final dig -- 10.255.126.38 ``` --- ## 7. 판정 **주장 지지.** L4 SNI passthrough 클러스터(`outbound|443||gslb-pt.lab.internal`, resolution DNS, VS/DR 없음)에서 이미 맺어진 TLS 연결은 DNS flip 이후에도 Envoy가 능동적으로 끊지 않으며, 그 연결을 keepalive로 재사용하는 client는 **연결을 닫고 새로 맺을 때까지 계속 flip 이전 backend로 간다.** 2회 반복 모두 동일: 재사용 구간(REQ2, REQ3) 3회 요청 전부 옛 IP·옛 body, close 후 신규 연결(REQ4)만 새 IP·새 body. Envoy 클러스터의 `upstream_cx_total` 누적 카운터가 재사용 구간에서 Δ0였다는 것이 "요청마다 새로 연결한 게 아니라 진짜 하나의 소켓을 계속 썼다"는 것의 독립적 교차증거이기도 하다. 이 결과가 갖는 실무 함의(추론, 이번 실측 범위 밖): HTTP 경로(A·B, report A/B/D)는 connection pool이 DNS 재해석과 맞물려 어느 정도 drain 되지만, L4 passthrough는 pool이라는 개념 자체가 없어 "새 연결을 강제로 유도"하는 장치(예: 앱 레벨 커넥션 수명 제한, `idleTimeout`류 설정) 없이는 **client가 알아서 재접속하기 전까지 무한정 옛 backend에 고정(pin)된다** — 이번 실측(65s보다 훨씬 짧은 간격)에서는 서버 keepalive timeout이 끊는 일도 없었으므로, 그 "무한정"을 끊는 유일한 장치는 client 쪽 애플리케이션의 연결 재사용 정책뿐이라는 뜻이 된다. --- ## 8. 종료 상태 ``` $ kubectl --context=homelab -n dns-lab exec deploy/lab-dns -c writer -- cat /hosts/addn 10.250.161.145 gslb.lab.internal 10.255.194.124 gslb-pt.lab.internal $ kubectl --context=homelab -n dns-lab exec deploy/netshoot -- curl -sk -m 5 -w ' [http_code=%{http_code}]\n' https://gslb-pt.lab.internal/ backend-a [http_code=200] $ kubectl --context=homelab -n dns-lab exec deploy/netshoot -- curl -s -m 5 -w ' [http_code=%{http_code}]\n' http://gslb.lab.internal/ backend-a [http_code=200] $ kubectl --context=homelab -n dns-lab get pods -o wide (전 파드 READY, RESTARTS 0 — 변경 없음) ``` DNS는 pod-a로 원복, 두 경로(passthrough·기존 HTTP) 모두 sanity 200. SE/VS/DR 등 manifest는 전 과정 동안 무변경(있는 그대로 사용). repo에는 이 리포트 파일 1개만 추가.