feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다
기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만 있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문 종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다. SSOT 결함 둘을 고쳤다. - kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이 「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽 `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다. - virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데 원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다. 기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은 둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더 든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다). 계약을 셋 고쳤다. - kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/** 28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는 것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온 것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 커밋도 28개가 어긋났다). - virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고 재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다. - kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고 넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다. style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져 (실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고, engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고 있었다(Question 기록에서 11.94 → 3.86). verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d473609e0a
commit
2109f726fe
+137
@@ -0,0 +1,137 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: commands-written-as-prose-do-not-run
|
||||
title: 산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다
|
||||
topic: when-the-measurement-lies
|
||||
topicName: 주입이 걸렸는지 무엇으로 아는가
|
||||
project: keycloak-session-store
|
||||
status: 게시 전
|
||||
lastVerifiedOn:
|
||||
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
source:
|
||||
- final/document.md#결정이-지켜지는지-확인하는-방법-재현-가능성
|
||||
- final/document.md#검토한-선택지와-막힌-지점-스크립트를-쓰지-않는다
|
||||
assets:
|
||||
- key: reproducibility-gap
|
||||
file: ../../../final/assets/reproducibility-gap/reproducibility-gap.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a6-latency-injection__04-pool-under-load.txt
|
||||
- ../../../final/evidence/raw/followup__05-command-reproducibility.txt
|
||||
---
|
||||
|
||||
# 산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다
|
||||
|
||||
산문으로 적힌 측정 장치 넷을 셸 표현식으로 바꿔 실제로 돌렸더니 한 건이 깨졌다. 22.2초를 낸 A-6 의 부하 생성기였고, 일회성 파드의 stdout 이 유실돼 동시 20건 중 일부만 도착했다. 상주 탐침 안 파일로 모아 회수하는 방식으로 고치고 20/20 을 다시 확인했다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다**
|
||||
여기서 깨진 부하 생성기가 그 아홉 건의 마지막 항목이다. 고치고 나서야 아홉 번째 실패로 확정됐다.
|
||||
- **예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다**
|
||||
주입을 확인하는 명령도 절차의 일부라서, 그 명령이 실제로 도는지까지 확인해야 이 기준이 성립한다.
|
||||
|
||||
## 문제
|
||||
|
||||
절차를 스크립트로 감싸면 무엇을 했는지가 스크립트 안으로 숨는다. 그래서 모든 절차를 셸에 그대로 붙여넣을 수 있는 명령으로 적었다.
|
||||
|
||||
명령을 적는 것만으로는 부족했다. 측정 장치 자체를 괄호와 설명으로 적어 둔 곳이 넷 있었고, 그 넷이 헤드라인 수치를 만든 명령이었다.
|
||||
|
||||
## 결론
|
||||
|
||||
산문으로 적혀 있던 측정 장치 : 4곳 (A-6 · A-3 · A-8 · A-1)
|
||||
셸 표현식으로 바꾼 뒤 실제로 돌렸을 때 깨진 것 : 1건 (A-6 의 부하 생성기)
|
||||
깨진 이유 : 일회성 파드의 stdout 이 유실된다
|
||||
고친 방법 : 상주 탐침 + 파드 안 파일 수집
|
||||
고친 뒤 : 동시 20건에서 20/20 도착
|
||||
고친 장치에서 나온 값 : 가장 느린 요청 22.230871초 · 커넥션 획득 대기 최대 20000.0ms
|
||||
|
||||
명령을 실행 가능하게 고쳤다는 것과 고친 명령이 동작한다는 것은 다른 주장이다. 넷 중 셋은 고치자마자 돌았고 하나는 돌지 않았는데, 실제로 실행해 보기 전에는 넷이 같아 보였다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
대상 실험 : A-6 지연 주입 (200ms 를 걸고 동시 20건 로그인)
|
||||
부하 대상 : keycloak-1
|
||||
측정한 것 : 상태코드 · 응답 시간 · agroal 커넥션 풀 지표 · readiness 이벤트
|
||||
처음 쓴 방법 : kubectl run --rm -i 로 일회성 파드를 띄워 stdout 수집
|
||||
고친 방법 : 상주 탐침 안에서 실행하고 파드 안 파일로 모은 뒤 회수
|
||||
스크립트 사용 : x
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 절차 문서에서 괄호와 설명으로 적힌 명령을 찾는다.
|
||||
실행할 수 없는 형태로 적힌 측정 장치를 골라낸다.
|
||||
|
||||
2. 그것을 셸에 붙여넣을 수 있는 표현식으로 바꾼다.
|
||||
|
||||
3. 바꾼 표현식을 실제로 실행한다. 문법 검사로 대신하지 않는다.
|
||||
|
||||
4. 기대한 만큼의 줄이 도착했는지 센다.
|
||||
동시 20건이면 20줄이 있어야 한다.
|
||||
|
||||
5. 줄이 모자라면 출력 경로를 바꾼다.
|
||||
일회성 파드의 stdout 대신 상주 탐침 안 파일에 쓰고 회수한다.
|
||||
|
||||
6. 고친 장치로 다시 측정해 원래 수치가 재현되는지 본다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
## 스크립트를 쓰지 않기로 했다
|
||||
|
||||
절차를 스크립트로 감싸면 무엇을 했는지가 스크립트 안으로 숨는다. 파일 이름만 남고 그 안에서 어떤 명령이 어떤 순서로 돌았는지는 다른 파일을 열어야 알 수 있다. 그래서 이 실험대는 모든 절차를 셸에 그대로 붙여넣을 수 있는 명령으로 적었다.
|
||||
|
||||
그런데 나중에 재현 절차를 점검해 보니 셸에 붙여넣을 수 없는 형태로 남아 있는 곳이 넷 있었다.
|
||||
|
||||
## 명령으로 적히지 않은 곳이 넷 있었다
|
||||
|
||||
문서의 명령을 하나씩 봤더니 절차 대부분은 붙여넣으면 도는 형태였고, 걸린 넷은 전부 측정 장치 자체를 적어 둔 곳이었다.
|
||||
|
||||
| 어디 | 무엇이 산문으로 적혀 있었나 |
|
||||
|---|---|
|
||||
| A-6 | `( curl ... ) & 를 20개 띄우고 wait` ← 22.2초의 출처 |
|
||||
| A-3 | `<로그인 반복, sid 를 /tmp/sids 에>` ← RPO 측정 전체 |
|
||||
| A-8 | `/tmp/tok` 에 쓰고 `/tmp/rt` 를 읽는다 ← 빈 토큰을 보내고 있었다 |
|
||||
| A-1 | conntrack 튜플을 손으로 적는다 ← 방향이 재시작마다 바뀐다 |
|
||||
|
||||
넷 다 이 실험대의 헤드라인 수치를 만든 명령이다. A-6 의 한 줄이 22.2초를 냈고, A-3 의 한 줄은 RPO(복구 시점 목표 — 장애로 잃을 수 있는 데이터의 시간 범위) 측정 전체를 냈다. A-8 은 적힌 대로 읽으면 토큰을 쓴 파일과 읽는 파일이 달라서 빈 토큰을 보내고 있었고, A-1 은 재시작마다 바뀌는 방향을 사람이 그때그때 적는 형태였다.
|
||||
|
||||
## 바꾼 다음 실제로 돌렸고, 거기서 한 건이 깨졌다
|
||||
|
||||
넷을 전부 셸 표현식으로 바꾸고 문법이 맞는지가 아니라 실제로 실행해서 확인했다. 돌려 보지 않고 「재현 가능하게 고쳤다」고 적는 것은 측정하지 않고 단언하는 일이라, 애초에 이 넷을 걸러 낸 점검이 잡아낸 실수를 그대로 되풀이하는 것이 된다. 그래서 실행 기록을 따로 남겼다. 셋은 그대로 돌았고 A-6 의 부하 생성기가 깨졌다.
|
||||
|
||||
깨진 이유는 표현식이 아니라 출력 경로였다. `kubectl run --rm -i` 로 띄운 일회성 파드는 명령이 끝나면 곧바로 지워지는데, 그 과정에서 stdout 이 유실돼 동시 20건의 결과가 일부만 도착하거나 아예 끊겼다. 두 번 시도해서 두 번 다 출력이 도착하지 않고 세션이 그대로 끊겼다. 셸은 오류를 내지 않고 종료 코드도 0 이므로, 도착한 줄 수를 세기 전에는 실패한 것으로 보이지 않는다.
|
||||
|
||||
같은 함정을 이 실험 시리즈에서 이미 한 번 겪었는데, 재현 절차를 실행 가능하게 고치면서 그 깨진 형태를 그대로 다시 써넣었다.
|
||||
|
||||
출력을 파드 밖으로 흘려보내는 대신 파드 안에 모으는 쪽으로 바꿨다. 상주 탐침 하나를 계속 띄워 두고 그 안에서 요청을 보내 결과를 파일에 쓴 뒤, 측정이 끝나고 파일을 통째로 회수했다.
|
||||
|
||||

|
||||
|
||||
두 화살표가 각각 다른 것을 걸러낸다. 첫 번째를 지나면 붙여넣어 실행할 수 있는 명령이 되고, 두 번째를 지나야 그 명령이 기대한 출력을 내는지 확인된다. 이 실험대에서 넷은 첫 번째를 다 지났고 한 건이 두 번째에서 멈췄다.
|
||||
|
||||
## 고친 장치로 다시 잰 값
|
||||
|
||||
바꾼 탐침으로 동시 20건을 다시 돌렸다. 20줄이 전부 도착했고 상태코드는 모두 200 이었다.
|
||||
|
||||
```text label="고친 부하 생성기로 다시 돌린 동시 20건과 직후의 커넥션 풀"
|
||||
1 200 1.911191
|
||||
1 200 19.053724
|
||||
1 200 22.228466
|
||||
1 200 22.230871
|
||||
agroal_blocking_time_max_milliseconds 20000.0
|
||||
agroal_max_used_count 19.0
|
||||
agroal_acquire_count_total 672.0
|
||||
```
|
||||
|
||||
가장 느린 요청이 22.230871초였고 이것이 A-6 이 보고한 22.2초다. 커넥션 획득 대기 최댓값은 20000.0ms 로 찍혔다. 일회성 파드로 재던 때에는 스무 줄 중 몇 줄이 도착했는지 셀 수 없었으므로, 같은 수치가 스무 건 전부에서 나온다는 것은 고친 장치로 돌린 다음에 확인됐다.
|
||||
|
||||
부하 직후에 함께 수집한 이벤트에는 `keycloak-1` 의 readiness 프로브가 503 과 타임아웃으로 실패한 기록이 남아 있다. 지연을 건 노드가 느려지다 로드밸런서에서 빠지는 구간이고, 응답 시간 20줄과 커넥션 풀 지표와 이 이벤트가 한 파일에 함께 들어 있다.
|
||||
|
||||
## 이 확인이 닿지 않은 곳
|
||||
|
||||
고친 명령을 다른 환경에서 돌려 보지 않았다. 이 실험대의 k3s 와 이 Keycloak 구성에서만 확인했고, 상주 탐침이 필요한 이유였던 일회성 파드의 출력 유실이 다른 런타임에서도 같은 모양으로 나타나는지는 재지 않았다.
|
||||
|
||||
넷 중 셋은 바꾼 표현식이 한 번에 돌았다는 것까지만 확인했다. A-3 과 A-8 과 A-1 의 측정값을 고친 명령으로 처음부터 다시 만들어 원래 수치와 맞춰 보지는 않았다.
|
||||
|
||||
실행 확인을 하면서 A-6 의 단일 요청 지연도 같이 쟀는데, 그것은 지연을 걸지 않은 평시 값이라 본문의 22.2초와 나란히 놓으면 안 된다. 거기서 확인한 것은 명령이 돈다는 것뿐이다.
|
||||
<!-- body:end -->
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: nine-injections-that-silently-did-nothing
|
||||
title: 주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다
|
||||
topic: when-the-measurement-lies
|
||||
topicName: 주입이 걸렸는지 무엇으로 아는가
|
||||
project: keycloak-session-store
|
||||
status: 게시 전
|
||||
lastVerifiedOn:
|
||||
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
source:
|
||||
- final/document.md#문제를-어렵게-만든-제약-주입이-먹지-않는다
|
||||
assets:
|
||||
- key: injection-verification
|
||||
file: ../../../final/assets/injection-verification/injection-verification.svg
|
||||
- key: a5-partition-asymmetry
|
||||
file: ../../../final/assets/a5-partition-asymmetry/a5-partition-asymmetry.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/a1-jgroups-transport-block__05-conntrack-problem.txt
|
||||
- ../../../final/evidence/raw/a6-latency-injection__03-flannel-injection.txt
|
||||
---
|
||||
|
||||
# 주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다
|
||||
|
||||
주입 명령이 아홉 번 걸리지 않았고, 걸리지 않은 주입은 관측에서 영향이 없는 것과 구별되지 않았다. 아홉 건의 원인은 네트워크·시그널·권한·출력 경로로 매번 달랐다. 공통점은 주입과 결과 관측 사이에 대상 상태를 보는 단계가 없었다는 것 하나였다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다**
|
||||
그 기준의 두 번째 규칙이 이 아홉 건에서 나왔다.
|
||||
- **실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다**
|
||||
주입이 걸린 것을 확인한 다음에도 관측은 틀릴 수 있고, 그 다음 단계를 대조군이 맡는다.
|
||||
- **up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다**
|
||||
주입이 제대로 걸린 A-2 에서도 이 지표는 1 이었다. 주입 확인과 지표 읽기는 서로를 대신하지 못한다.
|
||||
|
||||
## 문제
|
||||
|
||||
A층 여덟 실험은 예측을 먼저 적어 두고 주입한 뒤 관측하는 순서로 돌렸다. 주입 명령이 오류 없이 끝나면 걸린 것으로 보고 결과를 읽었다.
|
||||
|
||||
주입이 걸리지 않으면 관측에 아무 변화도 나타나지 않는다. 영향이 없어서 변화가 없는 경우와 같은 모습이므로, 이 실험대에서는 두 경우를 가를 방법이 없었다.
|
||||
|
||||
## 결론
|
||||
|
||||
조용히 실패한 주입 : 9건
|
||||
원인이 겹치는 건 : 없음. 아홉 건이 각각 다른 이유로 걸리지 않았다
|
||||
한 실험에서 세 번 실패한 곳 : A-5 비대칭 분단 (표의 4·5·6번)
|
||||
공통 원인 : 주입과 관측 사이에 대상 상태를 보는 단계가 없었다
|
||||
그 뒤 바꾼 것 : 주입한 다음 결과를 보기 전에 대상 상태를 따로 확인한다
|
||||
상태를 보는 수단 : cluster_size · 워커 PID · conntrack 표 · 패킷 카운터
|
||||
|
||||
## 검증 환경
|
||||
|
||||
실험대 : 베어메탈 한 대 위에 VM 두 대
|
||||
test-server : Arch Linux, 12GB, WiFi only
|
||||
kc-lab-1 : k3s server (컨트롤 플레인) · keycloak-1
|
||||
kc-lab-2 : k3s agent · keycloak-0 · PostgreSQL · Redis
|
||||
게스트 sudo : 무암호
|
||||
호스트 sudo : 비밀번호 요구
|
||||
주입에 쓴 것 : NetworkPolicy · iptables · tc · conntrack · kubectl delete · kill
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 예측을 먼저 문서에 적는다. 무엇이 깨지고 무엇이 안 깨질지를 주입 전에 적어 둔다.
|
||||
|
||||
2. 주입 명령을 넣는다. NetworkPolicy 로 포트를 빼거나, iptables 규칙을 끼워 넣거나, tc 로 지연을 걸거나, 파드를 강제 종료한다.
|
||||
|
||||
3. 결과를 보기 전에 대상 상태를 조회한다.
|
||||
conntrack 표에 그 포트의 ESTABLISHED 항목이 남아 있는지, iptables 규칙의 패킷 카운터가 0 인지, tc 필터에 패킷이 걸렸는지를 본다.
|
||||
|
||||
4. 상태가 바뀌지 않았으면 주입이 걸리지 않은 것으로 보고 결과를 읽지 않는다.
|
||||
|
||||
5. 주입 방법을 바꾸고 3번을 다시 한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
## 여덟 실험이 같은 모양으로 돌았다
|
||||
|
||||
A층 실험은 예측을 먼저 문서에 적어 두고 주입한 뒤 관측하고 마지막에 그 예측과 대조하는 순서였다. 주입은 상태를 일부러 망가뜨리는 명령이다 — NetworkPolicy 에서 포트를 빼 통신을 막고, `tc` 로 지연을 걸고, 파드를 강제 종료하고, `iptables` 규칙을 앞에 끼워 넣는다.
|
||||
|
||||
이 순서에는 확인이 하나 빠져 있었다. 주입 명령이 오류 없이 끝나도 대상이 실제로 그 상태가 됐는지는 보지 않고 바로 결과를 읽었기 때문에, 걸리지 않은 주입은 관측에서 「아무 일도 없었다」로 나타났다. 영향이 없어서 아무 일도 없는 것과 화면에서 같아 보인다. 이 실험대가 시간을 가장 많이 쓴 곳이 여기다.
|
||||
|
||||
## 아홉 건이 각각 다른 이유로 걸리지 않았다
|
||||
|
||||
| # | 무엇을 했나 | 왜 안 먹었나 |
|
||||
|---|---|---|
|
||||
| 1 | NetworkPolicy 로 7800 차단 | conntrack — ESTABLISHED 연결은 규칙 평가를 건너뛴다. `cluster_size` 가 25분간 2 로 남았다 |
|
||||
| 2 | `kubectl delete --grace-period=0 --force` | 크래시가 아니다. 런타임이 SIGTERM 을 보내 PostgreSQL 이 정상 플러시했다 |
|
||||
| 3 | `kill -9 1` | PID 1 은 자기 네임스페이스의 SIGKILL 을 무시한다 |
|
||||
| 4 | `iptables -I FORWARD 1` | kube-router 가 자기 체인을 FORWARD 맨 위에 다시 끼워 넣는다 (패킷 0) |
|
||||
| 5 | raw 규칙을 한쪽 노드에 | 방향이 뒤집혀 있었다. JGroups 의 client/server 역할은 재시작마다 바뀐다 |
|
||||
| 6 | `tc ... dev eth0` | Debian 은 `enp1s0` 이고, flannel VXLAN 이 이미 캡슐화해 파드 IP 가 안 보인다 |
|
||||
| 7 | `spring.sql.init` 로 스키마 생성 | 기본 DDL 이 `blob` 인데 PostgreSQL 은 `bytea` 다. `continue-on-error: true` 가 삼켰다 |
|
||||
| 8 | 호스트에서 `sudo` | 비밀번호를 요구한다. 빈 출력이 곧 실패였다 |
|
||||
| 9 | `kubectl run --rm -i` 로 동시 20건 | 일회성 파드의 stdout 이 유실된다. 20줄 중 일부만 도착하거나 아예 끊긴다 |
|
||||
|
||||
아홉 건을 원인으로 묶어 보려 했지만 묶이지 않았다. 네트워크 계층이 명령을 다르게 해석한 것도 있고, 종료 신호가 크래시가 아니었던 것도 있고, 오류를 삼키는 설정과 권한과 출력 경로도 하나씩 있었다. 아홉 건에 같은 대책을 걸 수 없었다.
|
||||
|
||||
## 게스트에서는 되고 호스트에서는 되지 않던 것
|
||||
|
||||
kc-lab-1 과 kc-lab-2 는 무암호 `sudo` 라 `conntrack`·`tc`·`iptables` 를 그대로 썼지만 호스트인 test-server 는 비밀번호를 요구한다. 이 차이를 모르고 한동안 호스트의 nginx 설정을 읽으려 했는데 계속 빈 출력이 돌아왔고, 그 빈 출력은 명령이 낸 답이 아니라 `sudo` 가 비밀번호를 받지 못해 멈춘 결과였다. 하마터면 빈 로그를 「아무 일도 없음」으로 읽을 뻔했다.
|
||||
|
||||
호스트에서 해야 하는 일은 인증서 강제 갱신과 nginx reload 인데 둘 다 결국 사람이 직접 쳐야 했으므로, D-4 에서는 명령 한 줄을 헛되이 쓰지 않는 것이 설계의 일부가 됐다.
|
||||
|
||||
## 한 실험에서 세 번 — A-5 비대칭 분단
|
||||
|
||||
표의 4·5·6번은 모두 A-5 한 실험에서 나왔다. 세 번 모두 다른 이유였고, 셋 다 화면에서는 「아무 일도 없었다」로 보였다.
|
||||
|
||||
A-5 가 물은 것은 한쪽 방향만 막았을 때 클러스터가 갈라지는가였다. 한 방향만 막으면 JGroups 가 열린 방향으로 재연결하므로 클러스터가 갈라지지 않는다. 양방향을 다 막으면 갈라지기는 하는데 한쪽만 DOWN 이 되어서, 코디네이터 쪽이 살아남고 분단된 쪽은 스스로 로드밸런서에서 빠지며 서비스는 이어진다.
|
||||
|
||||

|
||||
|
||||
그래서 이 실험에서는 주입이 걸리지 않은 것과 비대칭 차단이 원래 클러스터를 가르지 못하는 것이 같은 관측으로 나온다. 세 번의 실패를 하나씩 분리하려면 규칙이 실제로 패킷을 잡았는지부터 따로 봐야 했다.
|
||||
|
||||
## conntrack 표와 패킷 카운터를 직접 조회했다
|
||||
|
||||
1번에서 무엇이 걸리지 않았는지는 conntrack 표를 조회해서 확정했다. conntrack 은 리눅스 커널이 진행 중인 연결을 기억해 두는 표이고, 여기 등록된 흐름의 패킷은 방화벽 규칙을 다시 평가하지 않고 통과한다. NetworkPolicy 를 적용한 뒤 두 노드에서 7800 흐름을 뽑았다.
|
||||
|
||||
```text label="NetworkPolicy 적용 후에도 7800 연결이 conntrack 에 살아 있다"
|
||||
--- kc-lab-1 ---
|
||||
tcp 6 86398 ESTABLISHED src=10.42.0.35 dst=10.42.1.43 sport=40023 dport=7800 src=10.42.1.43 dst=10.42.0.35 sport=7800 dport=40023 [ASSURED] mark=0 use=1
|
||||
--- kc-lab-2 ---
|
||||
tcp 6 33 SYN_SENT src=10.42.1.58 dst=10.42.0.35 sport=34824 dport=7800 [UNREPLIED] src=10.42.0.35 dst=10.42.1.58 sport=7800 dport=34824 mark=0 use=1
|
||||
```
|
||||
|
||||
두 노드 모두 7800 으로 가는 ESTABLISHED 항목을 들고 있었다. 쿠버네티스가 넣어 둔 FORWARD 규칙은 전부 `NEW` 상태에만 걸려 있어서, 이미 성립한 연결의 패킷은 NetworkPolicy 평가에 도달하지도 않는다. 규칙이 안 걸린 것이 아니라 규칙은 정확히 걸렸고 패킷이 그 앞에서 지나갔다. `nf_conntrack_tcp_timeout_established` 는 86400 이고 오가는 패킷이 있으면 타이머가 계속 갱신되므로, JGroups 처럼 주기적으로 통신하는 연결은 사실상 영원히 표에 남는다. `cluster_size` 가 25분간 2 로 남은 것은 이상한 일이 아니라 정상 동작이었다.
|
||||
|
||||
항목을 지우는 것도 한 번에 되지 않았다. 두 노드에서 7800 흐름을 지웠는데 돌아온 것은 `0 flow entries have been deleted` 였고 삭제 뒤에도 항목은 두 건씩 남아 있었으며 `cluster_size` 도 2 그대로였다. 클러스터가 실제로 갈라진 것은 정책이 걸린 상태에서 파드를 재시작했을 때다. 처음 쓴 A-1 기록은 4초 전 파드 재시작이 만든 분단을 conntrack 공으로 돌렸고, 증거와 대조하면서 그 귀속을 고쳤다.
|
||||
|
||||
6번은 인터페이스를 바꿔 우회했다. Debian 게스트의 물리 인터페이스는 `enp1s0` 이고 flannel VXLAN 이 파드 트래픽을 이미 캡슐화하므로 거기서는 파드 IP 가 보이지 않는다. 캡슐화 전 구간인 `flannel.1` 에 같은 `netem` 을 걸고, 결과를 읽기 전에 필터에 패킷이 걸렸는지를 카운터로 확인했다.
|
||||
|
||||
```text label="flannel.1 에 건 netem 필터의 패킷 카운터와 두 노드 응답 시간"
|
||||
=== [검증] 필터에 패킷이 걸리는가 ===
|
||||
qdisc netem 30: parent 1:3 limit 1000 delay 200ms
|
||||
Sent 18388 bytes 150 pkt (dropped 0, overlimits 0 requeues 0)
|
||||
|
||||
=== 두 노드 지연 비교 (기준선: k0=70ms k1=66ms) ===
|
||||
keycloak-0 평균 41 ms 최대 57 ms
|
||||
keycloak-1 평균 1872 ms 최대 1887 ms
|
||||
```
|
||||
|
||||
카운터가 150 패킷으로 올라간 것을 보고 나서 응답 시간을 읽었다. 지연을 걸기 전 두 노드의 로그인 응답은 70ms 와 66ms 였는데, 주입한 지연이 200ms 인데도 `keycloak-1` 의 평균 응답은 1872ms 로 나왔다. 지연을 걸지 않은 `keycloak-0` 은 41ms 였다.
|
||||
|
||||
## 주입한 다음, 결과가 아니라 상태를 본다
|
||||
|
||||
아홉 건은 원인이 제각각이었고 공통점은 한 곳이었다. 주입 명령과 결과 관측 사이에 대상이 실제로 그 상태인지 보는 단계가 없었다. 그래서 그 단계를 넣고 이후 모든 실험에 적용했다. 확인하는 대상은 결과가 아니라 상태다 — `cluster_size`, 워커 PID, conntrack 표, 패킷 카운터.
|
||||
|
||||

|
||||
|
||||
가운데 단계를 건너뛰면 걸리지 않은 주입과 영향이 없는 주입을 결과만 보고 가를 수 없다.
|
||||
|
||||
## 이 아홉 건에서 확인하지 않은 것
|
||||
|
||||
아홉 건을 모두 고친 뒤 처음부터 다시 돌리지는 않았다. 일부는 원래 방법을 고치지 않고 다른 주입 방법으로 우회했다 — 6번은 `eth0` 대신 `flannel.1` 에 걸었고, 9번은 일회성 파드 대신 상주 탐침으로 바꿨다. 원래 방법이 왜 안 걸렸는지까지만 확정했고, 그 방법 자체를 동작하게 만든 것은 아니다.
|
||||
<!-- body:end -->
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: seventy-six-failures-that-were-not-the-servers
|
||||
title: 실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다
|
||||
topic: when-the-measurement-lies
|
||||
topicName: 주입이 걸렸는지 무엇으로 아는가
|
||||
project: keycloak-session-store
|
||||
status: 게시 전
|
||||
lastVerifiedOn: 2026-09-04
|
||||
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
source:
|
||||
- final/document.md#결정이-지켜지는지-확인하는-방법-대조군-없이는
|
||||
assets:
|
||||
- key: measurement-control
|
||||
file: ../../../final/assets/measurement-control/measurement-control.svg
|
||||
evidence:
|
||||
- ../../../final/evidence/raw/d4-certificate-renewal__05-control-no-injection.txt
|
||||
- ../../../final/evidence/raw/d4-certificate-renewal__11-inflight-full.txt
|
||||
---
|
||||
|
||||
# 실패 76 건이 서버 탓이 아니었다 — 대조군이 오보를 막았다
|
||||
|
||||
nginx reload 중 감시 로그에 찍힌 실패 76건은 서버에 닿지 않은 요청이었다. 연결수가 0 이고 소요 시간이 50µs 였다. 같은 순간 대조 폴링 49건은 전부 200 이었고, 같은 조건을 100번 반복해도 재현되지 않았다. 주입 전에 대조군 900건을 잡아 두지 않았다면 「갱신 중 대규모 요청 실패」로 적었다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다**
|
||||
그 기준의 세 번째 규칙을 이 확인이 실제로 써 본 결과다.
|
||||
- **up 지표는 살아 있지만 쓸모없는 상태를 보지 못한다**
|
||||
이 실험대에서 한 지표만 보고 판정한 다른 경우이고, 방향이 반대다. 그쪽은 이상을 못 봤고 이쪽은 없는 이상을 봤다.
|
||||
- **두 시계에서 온 값을 빼지 않는다**
|
||||
같은 D-4 에서 나온 다른 측정 결함이다. 감시를 돌린 기계와 실험대의 시계가 106초 달랐다.
|
||||
|
||||
## 문제
|
||||
|
||||
인증서 갱신 중 nginx reload 가 진행 중이던 요청을 끊는지 확인해야 했다. 새 연결만 폴링하면 TLS 핸드셰이크가 매번 새로 일어나서 「새 연결을 받아주는가」만 재게 되므로, reload 순간에 실제로 전송 중인 요청을 따로 만들어 감시했다.
|
||||
|
||||
그 감시 로그에 실패가 76건 남았다. 이 값만 보면 reload 가 진행 중 요청을 대량으로 끊은 것으로 읽힌다.
|
||||
|
||||
## 결론
|
||||
|
||||
감시 로그의 실패 : 76건
|
||||
같은 순간 대조 폴링 : 49건 전부 200
|
||||
실패한 요청의 연결수 : 0
|
||||
실패한 요청의 소요 시간 : 50µs
|
||||
같은 조건 재현 : 0/100
|
||||
주입 전 대조군 : 900건 전부 200, 오류 0
|
||||
서버 쪽 원인으로 귀속 : x
|
||||
|
||||
TCP 연결 시도조차 없었고 소요 시간이 DNS 조회보다 짧았으므로 요청이 서버에 닿지 않았다. 같은 순간 다른 경로로 들어간 요청은 전부 200 을 받았다. 훅을 넣고 다시 검증한 D-4a 에서 reload 자체는 새 연결 8856건 전부 200 이었고, 전송 12초째에 reload 를 맞은 42초짜리 요청은 845361바이트를 온전히 받았다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
실험 : D-4 인증서 갱신 중 nginx reload
|
||||
호스트 TLS 종단 : nginx, Let's Encrypt 인증서
|
||||
주입 전 대조군 : 0.2초 간격 900회 = 180초
|
||||
대조군 응답시간 : 최소 67 중앙 98 p95 195 최대 1121 평균 106.9 (ms)
|
||||
진행 중 요청 : 845KB 번들을 20k/s 로 받아 한 요청을 42초 동안 유지
|
||||
관측 지점 : 외부 curl · Prometheus 지표 · PostgreSQL 직접 조회
|
||||
시계 : test-server 는 NTP 가 꺼져 있어 106초 빠르다
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 주입 전에 평시 오류율을 잰다.
|
||||
0.2초 간격으로 900회 폴링하고 상태코드 분포와 응답시간, TLS 핸드셰이크 횟수를 기록한다.
|
||||
|
||||
2. reload 순간에 전송 중인 요청이 있도록 만든다.
|
||||
845KB 번들을 20k/s 로 내려받아 요청 하나를 42초 동안 유지한다.
|
||||
|
||||
3. 새 연결 폴링과 진행 중 요청 감시를 각각 다른 프로세스로 띄우고 같은 시간대에 돌린다.
|
||||
|
||||
4. 인증서를 강제 갱신하고 nginx 를 reload 한다.
|
||||
|
||||
5. 실패가 나오면 그 순간의 대조 폴링 결과, 연결수, 소요 시간을 함께 읽는다.
|
||||
|
||||
6. 같은 조건을 100회 반복해 재현되는지 본다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
## 주입 전에 평시를 먼저 쟀다
|
||||
|
||||
D-4 의 물음은 nginx reload 중 진행 중이던 요청이 어떻게 되는가였다. 주입에 쓸 강제 갱신은 진짜 인증서를 발급하는 명령이라 되돌릴 수 없고 Let's Encrypt 의 발급 한도(주당 중복 인증서 5장)를 한 장 깎는다. 그래서 이 실험 전체에서 한 번만 쓰기로 했고, 그 한 번을 헛되게 쓰지 않으려면 잴 것을 주입 전에 전부 재 둬야 했다. 답하기 전에 주입 없는 상태의 오류율부터 잡은 것이 그래서다 — 0.2초 간격으로 900회, 180초 동안 폴링해서 900건 전부 200 이고 오류가 0 이라는 것을 확인했다. 중앙값은 98ms, p95 는 195ms 였다.
|
||||
|
||||
이 폴링에는 한계가 하나 있었다. TLS 핸드셰이크가 900회 전부 일어나서 매 요청이 새 연결이었고, 그래서 이 폴링이 재는 것은 「새 연결을 받아주는가」다. 계획서가 물은 것은 진행 중이던 요청이므로 reload 순간에 실제로 전송 중인 요청이 있어야 했다. 845KB 짜리 번들을 20k/s 로 일부러 느리게 받아 요청 하나를 42초 동안 유지하는 감시를 따로 띄웠다.
|
||||
|
||||
## 76건이 한 초에 몰려 찍혔다
|
||||
|
||||
감시 로그는 146줄이었고 그중 76줄이 `코드=000` 이었다. 나머지 70줄은 `코드=200` 에 `바이트=845361` 이었다.
|
||||
|
||||
```text label="진행 중 요청 감시 — 76줄이 같은 초에 찍혔다"
|
||||
08:14:22 코드=200 바이트=845361 시간=42.338482 연결수=1
|
||||
08:15:04 코드=000 바이트=0 시간=0.001148 연결수=0
|
||||
08:15:04 코드=000 바이트=0 시간=0.000051 연결수=0
|
||||
08:15:04 코드=000 바이트=0 시간=0.000056 연결수=0
|
||||
08:15:04 코드=200 바이트=845361 시간=42.236496 연결수=1
|
||||
```
|
||||
|
||||
76줄의 시각이 전부 `08:15:04` 로 같고, 같은 초의 다음 줄은 다시 `코드=200` 에 42.236496초다. 42초씩 걸리던 요청이 한 초 안에 76번 끝났다. 실패가 즉시 돌아오니 다음 요청을 바로 띄우는 감시 루프가 그 1초 동안 폭주했다.
|
||||
|
||||
## 76건을 서버 탓으로 적지 않은 근거 네 가지
|
||||
|
||||
76건을 reload 탓으로 적기 전에 네 가지를 봤다.
|
||||
|
||||
| 근거 | 값 |
|
||||
|---|---|
|
||||
| 같은 순간 폴링 | 49건 전부 200 |
|
||||
| 연결수 | 0 — TCP 연결 시도조차 못 했다 |
|
||||
| 소요 시간 | 50µs — DNS 조회보다 짧다 |
|
||||
| 재현 | 0/100 |
|
||||
|
||||
연결수가 0 이므로 서버까지 패킷이 가지 않았다. 소요 시간 50µs 는 이름 풀이 한 번보다 짧아서 요청이 네트워크로 나간 시간이 아니다. 같은 순간 다른 프로세스가 보낸 49건은 전부 200 을 받았으므로 그 시각에 서버는 요청을 처리하고 있었다. 같은 조건으로 100번 더 돌려도 한 번도 다시 나오지 않았다.
|
||||
|
||||
## 대조군이 없었다면 무엇이 됐나
|
||||
|
||||

|
||||
|
||||
같은 관측이 두 갈래로 갈린다. 평시 오류율을 아는 쪽에서는 76건을 대조군 900건과 견주어 서버 바깥 원인으로 돌릴 수 있고, 모르는 쪽에서는 「갱신 중 대규모 요청 실패」로 적게 된다.
|
||||
|
||||
이 실험대에서 대조군 규칙은 이미 두 번 어겨졌다. A-6 에서는 대조군이 −41% 인데도 「영향 없음」이라고 적었고, A-8 에서는 표본 9개로 무중단을 주장했다. 둘 다 나중에 고쳤다. 세 번째가 이 76건인데, 이번에는 대조군을 먼저 잡아 둔 덕분에 기록에 들어가기 전에 걸렸다.
|
||||
|
||||
## 같은 대조를 문서 전체에 돌렸다
|
||||
|
||||
기록을 다 쓴 뒤 본문의 주장과 증거 파일을 하나씩 맞춰 봤다. 어긋난 곳이 여섯 군데 나왔다.
|
||||
|
||||
| 어디 | 무엇이 어긋났나 |
|
||||
|---|---|
|
||||
| C-1 | 본문은 「세션 0」인데 증거는 4 |
|
||||
| C-2 | `exit code 1` 인 명령의 성공 읽기를 실었다 |
|
||||
| A-1 | 4초 전 파드 재시작이 만든 분단을 conntrack 공으로 돌렸다 |
|
||||
| A-2 | 첫 측정의 `000000{"error":"HTTP 401"}401` 을 숨겼다 |
|
||||
| A-3 | `wal_writer_delay` 를 재지 않고 단언했다 (실측 200ms, 로그인율도 19/s 가 아니라 14/s) |
|
||||
| D-1 | 본문은 RTO 30초, 자기 타임라인은 41초 |
|
||||
|
||||
전부 고치면서 무엇이 어긋났는지를 표로 남겼다. 76건과 이 여섯 건은 같은 방향으로 틀린다 — 숫자 하나를 그 숫자가 나온 조건과 떼어 놓고 읽으면 본문이 자기 증거와 다른 말을 하게 된다.
|
||||
|
||||
## 이 확인이 말하지 않는 것
|
||||
|
||||
76건이 서버 탓이 아니라는 것까지가 이 확인의 범위다. 감시를 돌린 쪽에서 무엇이 그 한 초 동안 76번 즉시 실패했는지는 특정하지 못했다. 감시를 `-s` 로 돌려 curl 의 오류 메시지를 버렸고 종료 코드도 남기지 않았기 때문이고, 재현이 0/100 이라 같은 조건을 다시 만들어 좁힐 수도 없었다. 장치가 왜 실패했는지를 남기지 않은 탓에, 이 76건을 서버 밖으로 돌린 근거는 같은 시각 대조 폴링이 멀쩡했다는 사실 하나였다.
|
||||
|
||||
그래서 감시 자체를 바꿨다. curl 의 종료 코드까지 함께 적고 실패하면 1초 쉬어 루프 폭주를 막는 형태로 갈아끼웠고, 같은 로그의 뒷부분에는 그 뒤로 `curl종료=0` 이 줄마다 붙어 있다. 다음에 같은 일이 생기면 6이 이름 풀이, 7이 연결, 35가 TLS 라는 식으로 종료 코드가 바로 답한다.
|
||||
<!-- body:end -->
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
---
|
||||
kind: CASE
|
||||
slug: the-guides-broke-at-the-first-command
|
||||
title: 가이드 26편을 순서대로 따라가니 첫 명령부터 막혔다
|
||||
topic: when-the-measurement-lies
|
||||
topicName: 주입이 걸렸는지 무엇으로 아는가
|
||||
project: keycloak-session-store
|
||||
status: 게시 전
|
||||
lastVerifiedOn: 2026-09-11
|
||||
sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
source:
|
||||
- final/document.md#재현-가이드-26편과-그것을-따라가다-드러난-결함
|
||||
---
|
||||
|
||||
# 가이드 26편을 순서대로 따라가니 첫 명령부터 막혔다
|
||||
|
||||
기반 7단계를 끝낸 독자가 실험 가이드의 첫 명령에서 막혔다. 2026-09-11 에 가이드 26편을 순서대로 따라가 보니 sudo kubectl 905건 가운데 게스트에서 돌아야 하는 명령은 0건이었다. sudo 는 root 환경이라 lab host 의 ~/.kube/config 를 못 본다. 명령 자체가 아니라 그 명령이 놓인 위치가 틀렸다.
|
||||
|
||||
## 관계
|
||||
|
||||
- **주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다**
|
||||
가이드마다 주입 검증 단계를 따로 둔 근거가 이 아홉 건이다.
|
||||
- **산문으로 적힌 측정 장치를 실행 가능하게 고쳤더니 한 건이 깨졌다**
|
||||
그 기록은 명령이 산문이라 돌지 않았고, 이 기록은 명령이 도는데 놓인 위치가 틀렸다.
|
||||
- **예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다**
|
||||
가이드의 다섯 단계 가운데 주입 검증이 이 규칙을 절차로 옮긴 단계다.
|
||||
|
||||
## 문제
|
||||
|
||||
발견을 적은 문서와 별개로, 직접 쳐서 다시 만드는 가이드가 26편 있다. 각 편은 같은 다섯 단계로 적혀 있고 그중 주입 검증이 핵심인 편이 많다. 이 실험대에서 주입은 아홉 번 조용히 실패했고, 실패한 주입은 아무 일도 없었던 것처럼 보여서 영향이 없는 주입과 구별되지 않기 때문이다.
|
||||
|
||||
가이드를 쓴 다음 그것을 처음부터 순서대로 따라가 본 적은 없었다. 2026-09-11 에 실제로 쳐 보니 기반 7단계를 끝낸 독자가 실험 가이드의 첫 명령에서 막혔다.
|
||||
|
||||
## 결론
|
||||
|
||||
2026-09-11 감사에서 재현을 막는 결함이 계열로 나왔다.
|
||||
|
||||
sudo kubectl 을 쓰는 명령 : 905건
|
||||
그중 게스트에서 도는 것 : 0건
|
||||
저장소 클론 단계가 없는 편 : 05·06
|
||||
그 단계에 없는 리소스를 조회하는 편 : 05
|
||||
|
||||
kubeconfig 는 lab host 의 ~/.kube/config 에 있고 sudo 는 root 환경이라 그 파일을 못 본다. 그래서 기반 7단계를 끝낸 독자가 첫 명령부터 막힌다. 905건 가운데 게스트에서 돌아야 해서 sudo 가 맞는 것은 0건이었고 전부 잘못된 것이었다.
|
||||
|
||||
개별 명령은 전부 실제로 돌았던 것이라 명령만 읽어서는 틀려 보이지 않는다. 틀린 것은 명령이 아니라 그 명령이 놓인 위치다.
|
||||
|
||||
## 검증 환경
|
||||
|
||||
감사 대상 : 재현 가이드 26편
|
||||
가이드 위치 : ../source/docs/guides/experiments/
|
||||
kubeconfig 위치 : lab host 의 ~/.kube/config
|
||||
감사일 : 2026-09-11
|
||||
|
||||
## 재현 조건
|
||||
|
||||
1. 기반 가이드 7단계를 끝낸 상태에서 실험 가이드를 첫 편부터 순서대로 따라간다.
|
||||
|
||||
2. 각 편의 명령을 읽지 말고 실제로 쳐 보고, 막힌 곳과 막힌 이유를 적는다.
|
||||
|
||||
3. 26편에서 sudo kubectl 로 적힌 명령이 몇 건인지 세고, 그중 게스트에서 돌아야 하는 것이 몇 건인지 가른다.
|
||||
|
||||
4. 05·06 편의 kubectl apply -f deploy/... 가 가리키는 저장소가 lab host 에 있는지, 그 앞 단계에 클론이 있는지 확인한다.
|
||||
|
||||
5. 05 편의 -l app=bff 가 조회하는 리소스가 그 단계에 떠 있는지, 함께 실린 출력이 그 단계에서 나온 것인지 확인한다.
|
||||
|
||||
## 본문
|
||||
|
||||
<!-- body:start -->
|
||||
실험 26건의 결과를 적은 문서와 별개로, 같은 실험을 직접 쳐서 다시 만드는 가이드가 26편 있다. 실험 26건을 다 끝낸 뒤 2026-09-11 에 그 26편을 처음부터 순서대로 따라가며 감사했다. 따라가는 동안 실험 계획서에 미해결로 남아 있던 항목 하나가 풀렸고, 재현을 막는 결함도 그때 나왔다 — 기반 가이드 7단계를 끝낸 독자가 실험 가이드의 첫 명령에서 막혔다. 막힌 곳은 네 군데였고, 한 편에서만 나온 실수가 아니라 계열로 나왔다.
|
||||
|
||||
## 가이드 한 편은 다섯 단계로 적혀 있다
|
||||
|
||||
각 편이 따르는 순서는 같다.
|
||||
|
||||
```text label="가이드 한 편의 구조"
|
||||
기준선 → 주입 → 주입 검증 → 관찰 → 복구
|
||||
```
|
||||
|
||||
다섯 단계 가운데 주입 검증이 핵심인 편이 많다. 이 실험대에서 주입은 아홉 번 조용히 실패했는데, 실패한 주입은 아무 일도 없었던 것처럼 보여서 영향이 없는 주입과 구별되지 않는다. 그래서 주입한 다음 그것이 실제로 걸렸는지 확인하는 단계를 따로 둔다.
|
||||
|
||||
## sudo 가 kubeconfig 를 못 본다
|
||||
|
||||
실험 가이드는 26편 전체가 `sudo kubectl` 로 적혀 있고, 세어 보니 905건이었다. 그런데 기반 가이드는 kubeconfig 를 lab host 의 `~/.kube/config` 에 두고, `sudo` 는 root 환경이라 그 파일을 못 본다. 그래서 기반 7단계를 끝내고 실험 가이드로 넘어온 독자는 첫 명령에서 바로 막힌다.
|
||||
|
||||
905건 가운데 게스트(lab host 위에서 도는 VM)에서 돌아야 해서 `sudo` 가 맞는 것은 0건이었다. 905건 전부 잘못 적힌 명령이었다. 이 905건과 0건은 2026-09-11 에 26편을 한 번 따라가며 센 값이고, 그 뒤 다시 세지 않았다.
|
||||
|
||||
## 05·06 편은 아직 없는 것을 가리킨다
|
||||
|
||||
05 편과 06 편은 `kubectl apply -f deploy/...` 를 쓴다. 이 경로는 상대경로인데 기반 가이드에는 저장소를 lab host 에 클론하는 단계가 없어서, 독자의 lab host 에는 그 경로가 가리킬 파일이 없다.
|
||||
|
||||
05 편에는 `-l app=bff` 로 리소스를 조회하는 명령도 있다. BFF(Backend For Frontend) 는 B-0 실험에서 처음 뜨므로 05 편 시점에는 조회할 리소스가 없고, 가이드에 실린 출력도 나중 단계에서 복사해 온 것이었다.
|
||||
|
||||
## 왜 명령만 읽어서는 틀린 곳이 안 보이나
|
||||
|
||||
막힌 네 곳을 나란히 놓으면 이렇다.
|
||||
|
||||
| 무엇이 재현을 막나 | 몇 건인가 |
|
||||
|---|---|
|
||||
| 실험 가이드 전편이 `sudo kubectl` 을 쓴다 | 905건 |
|
||||
| 그중 게스트에서 도는 것 | 0건 |
|
||||
| 기반 가이드가 저장소를 lab host 에 클론하지 않는다 | 05·06 |
|
||||
| 그 단계에 아직 없는 리소스를 조회한다 | 05 |
|
||||
|
||||
네 결함의 원인은 하나로 모인다. 가이드를 쓰면서 순서대로 따라갔을 때 도는지를 검증하지 않고, 나중 시점의 환경에서 확인한 명령과 출력을 그 단계에 적었다.
|
||||
|
||||
개별 명령은 전부 실제로 돌았던 것이라 틀려 보이지 않는다. `-l app=bff` 의 출력도 지어낸 값이 아니라 나중 단계에서 실제로 나온 값을 옮겨 적은 것이었다. 틀린 것은 명령이 아니라 그 명령이 놓인 위치다.
|
||||
|
||||
## 이 감사가 확인하지 않은 것
|
||||
|
||||
고친 가이드를 처음부터 다시 따라가 끝까지 도는지는 확인하지 않았다. 905건과 0건도 2026-09-11 감사 한 번에서 나온 계수다.
|
||||
|
||||
이 감사의 결론을 「가이드를 쓴 뒤 처음부터 순서대로 한 번 따라간다」는 규칙으로 세우려면 어떤 가이드에 적용되고 어떤 가이드에는 적용되지 않는지를 대야 하는데, 그 적용 조건도 예외도 이 26편에서는 나오지 않았다.
|
||||
<!-- body:end -->
|
||||
Reference in New Issue
Block a user