기록 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>
13 KiB
kind, slug, title, topic, topicName, project, status, sourceRevision, source
| kind | slug | title | topic | topicName | project | status | sourceRevision | source | |||
|---|---|---|---|---|---|---|---|---|---|---|---|
| REFERENCE | tool-output-is-not-the-subject-state | 도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다 | build-completion-judgment | 끝났다는 판정 | virtualization | 게시 전 | 9465582b5d1630eb4ae7c4e078021486919bf6b6 |
|
도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다
상태를 묻는 명령의 출력을 대상의 상태로 바로 읽지 않고, 그 명령이 무엇을 세고 무엇을 안 세는지 먼저 적는다. 7800 포트를 끊었을 때 외부 응답은 전부 200 이었고, 503 이 나는 동안에도 up 은 1 이었다.
관계
- 빈 토큰이 조용히 흘러갔다 — 설치 출력은 성공이었고 agent 만 5초마다 다시 죽었다 설치 명령의 출력과 설치된 상태가 어긋났다. 출력은 끝까지 성공이었고 실패는 journalctl 안에만 있었다.
- cloud-init 이 안 도는 원인은 넷인데 증상은 「SSH 가 안 붙는다」 하나였다 검사가 통과해도 안 도는 쪽과 검사에 걸려도 도는 쪽이 한 사건 안에 같이 있다. 검사 결과를 상태로 읽으면 어느 방향이든 틀린다는 것이 그 기록의 결론이다.
- 갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초 로그 문구가 SUCCESS 인데 서빙되는 인증서는 옛것이었다. 판정을 문구가 아니라 워커 PID 로 옮긴 기록이다.
- 가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다 어디서 끊겼는지를 그 기준이 좁히고, 좁힌 층의 출력을 어떻게 읽는지를 이쪽이 받는다.
- 가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가 단계마다의 통과 조건 일곱이 전부 이런 출력이라, 다시 세운 실험대가 같은 상태인지를 무엇으로 판정할지가 그 물음에 걸려 있다.
목적
판정을 밖에서만 하면 놓친다. §192 는 그래서 클러스터 안을 보는 관측대(Prometheus 와 Grafana)를 따로 세웠다. 7800 포트를 끊었을 때 외부 응답이 전부 200 이었고, 분단된 노드가 스스로 로드밸런서에서 빠져 밖에서는 아무 일도 없어 보였다.
안쪽에 세운 관측대도 지표 하나로는 같은 실패를 되풀이한다. 503 이 나는 동안에도 up 은 1 이었다. 프로세스가 살아 있고 metrics 경로가 응답하기만 하면 1 이 되므로, 살아 있지만 쓸모없는 상태를 up 은 보지 못한다. 경보를 up 이 0 인지 하나로 걸면 그 상태를 통째로 놓친다.
같은 일이 구축 7단계 전체에서 되풀이된다. 목록이 비어 있어서 없다고 읽은 것, 검사기가 통과해서 동작한다고 읽은 것, 아무것도 안 찍혀서 멈췄다고 읽은 것이 전부 같은 오독이다. 이 기준은 그 셋을 하나로 묶고, 판정하기 전에 그 명령이 무엇을 세는지 적게 한다.
규칙
1. 그 명령이 무엇을 세는지 먼저 적는다
어느 연결에 붙어 있나. virsh 는 기본으로 qemu:///session 에 붙는데 VM 은 qemu:///system 에 만들므로, 어긋나면 VM 은 만들어졌는데 virsh list 에 안 나온다. 빈 목록이 VM 의 부재가 아니라 다른 연결을 보고 있다는 뜻이다.
꺼진 것도 세나. net-list 는 --all 을 빼면 inactive 인 네트워크가 아예 안 나와 「없음」과 「꺼짐」이 구분되지 않는다. 이 실험대에서 default 네트워크의 autostart 가 no 면 지금은 되고 호스트를 재부팅한 다음 01 단계의 SSH 가 전부 실패하는데, 그때는 원인을 게스트에서 찾게 된다.
이름대로 다 내놓나. kubectl get all 은 이름과 달리 Secret 과 ConfigMap 과 PVC 와 Ingress 를 내놓지 않으므로, 그 넷이 빠진 줄 모르고 다 만들어졌다고 판정하게 된다. -l app=postgres 에 Deployment 줄이 없는 것도 라벨을 파드 템플릿에만 달았기 때문이지 Deployment 가 없는 것이 아니다.
어디까지 남기나. nginx 에러 로그는 2048바이트에서 잘리고 쿠버네티스 이벤트는 기본 한 시간만 남는다. 이 실험대에서 502 원인이 잘린 채로 error 로그에 있었고 access 로그에는 3492자로 온전히 남아 있었다.
2. 빈 출력을 낼 때 「없다」와 「못 봤다」를 갈라 적는다
Prometheus 질의가 빈 배열을 내면 그 값이 0 이라는 뜻이 아니라 그런 지표가 없다는 뜻이다. 스크레이프 대상 목록에서는 거기 없는 이름이 답을 준다 — 이 실험대는 Redis 와 BFF 와 PostgreSQL 을 긁지 않으므로 그 지표가 안 나오는 것이 측정 실패가 아니라 측정된 공백이다. §192 는 그것을 스크린샷 누락이 아니라 측정된 공백으로 적었다. grep 이 아무것도 안 내놓을 때도 같다. ip-dhcp-host 로 grep 하면 DHCP 예약이 멀쩡히 들어가 있어도 아무것도 안 나오는데, 그것은 net-update 의 섹션 이름이라 XML 안에 그 문자열이 없기 때문이다. 이벤트가 하나도 없는 것도 무사하다는 뜻이 아니라 한 시간이 지났다는 뜻일 수 있다.
3. 한 근거로 판정하지 않고 시제나 층이 다른 것을 함께 본다
§191 이 클러스터가 섰는지를 근거 셋으로 보고, 그 셋이 서로 다른 것을 본다고 적었다. 로그 ISPN000094 는 「그때 그렇게 보였다」이고, 테이블 jgroups_ping 은 「지금 등록되어 있다」이며, 지표 vendor_cluster_size 는 「지금 그 노드가 그렇게 안다」다. 테이블에는 둘 다 있는데 로그가 (1) 이면 서로를 찾기는 했는데 7800 포트로 메시지가 안 가는 것이고, 이 실험대에서 실제로 그 일이 벌어졌다. 각 노드가 자기가 아는 멤버 수를 보고하므로 한 노드만 보면 분단을 놓친다.
저장과 주입도 다른 층이다. describe 가 보여 주는 19 bytes 와 22 bytes 는 Secret 에 저장된 값이고, 파드 안에서 잰 길이 19 는 그 파드가 받은 값이다. 두 수가 같아야 Secret 에서 파드 환경변수까지 이어진 것이고, 길이가 0 이면 Secret 에는 있는데 이 파드가 그것을 안 받았다.
보고된 값과 준 값도 다르다. kubectl get nodes -o wide 의 INTERNAL-IP 는 k3s 가 보고한 값이고 systemctl cat 의 ExecStart 줄은 우리가 준 값이라 둘을 견준다. 값을 안 찍고 길이만으로 확인하는 §185 의 ② 도 같은 갈래다. 비밀은 값을 보지 않고 0 이 아니라는 것만 확인한다.
4. 검사기가 통과한 것을 동작하는 상태로 읽지 않는다
sites-available 을 site-available 로 잘못 치면 빈 새 파일이 열리고, 저장해도 nginx 는 그 파일을 읽지 않는데 nginx -t 는 멀쩡히 통과한다. 아무 에러 없이 아무 일도 안 일어나므로, 설정을 썼는데 변화가 없으면 경로 오타부터 의심한다. nft 도 같다. .nft 의 포트를 433 으로 쳐도 433 이 유효한 포트라 군말 없이 받고 80 은 멀쩡히 넘어가므로, 03 단계는 다 통과한 뒤 04 단계에서 HTTPS 만 안 되는 형태로 드러난다. 이 실험대에서 실제로 나왔던 오타다.
노드도 그렇다. kubectl get nodes 두 줄이 Ready 여도 -o wide 의 INTERNAL-IP 는 --node-ip 로 준 값과 다를 수 있다. 그때는 지금 아무 증상이 없다가 03 단계의 upstream 과 노드 상실 실험에서 어긋난다. 유닛 이름이 노드마다 달라 agent 노드에서 systemctl stop k3s 를 치면 아무 일도 일어나지 않고, 그것이 「주입했는데 증상이 없다」로 읽힌다.
파드 둘이 Running 이어도 Endpoints 가 하나면 트래픽은 이미 한쪽으로만 가고 있고, 그 상태에서 이중화 실험을 하면 그것을 이중화 실패로 오독하게 된다. node-exporter 로 시작하는 줄이 하나뿐일 때도 같다. 그 노드의 CPU 와 메모리와 디스크 지표가 통째로 없는 채로 실험을 하게 된다.
인증서도 같다. cert.pem 을 쓰면 중간 인증서가 빠져 체인이 끊긴다. 그런데 브라우저는 대개 캐시나 AIA(Authority Information Access) 로 보완해서 정상으로 보이고, 캐시가 없는 클라이언트에서만 깨진다. 그래서 체인이 이어졌는지는 openssl s_client 의 단계 수로 판정한다. 이 실험대의 실측은 0 부터 3 까지 네 단계와 Verify return code: 0 (ok) 였고, 단계가 1개면 cert.pem 을 쓴 것이다.
5. 침묵과 경고도 상태가 아니다
kubectl rollout status 는 끝날 때까지 아무것도 안 찍고 그 침묵이 정상이다. StatefulSet 은 파드를 하나씩 순서대로 띄우므로 중간에 0/2 로 한참 멈춰 있는 것도 정상이고, 300초를 다 쓰고 타임아웃으로 끝나면 그것도 답이다 — 안 떴다가 확정된다. 반대쪽에서는 경고가 실패로 읽힌다. nginx -t 의 [warn] could not build optimal types_hash 줄은 통과를 막지 않고, 실패는 [emerg] 줄에 파일과 줄 번호로 나온다. 04 단계에서 이 경고를 실패로 오독하는 일이 실제로 벌어졌다.
적용 조건
- 상태를 묻는 명령의 출력으로 구축 단계의 통과를 판정할 때
- 같은 대상을 보는 명령이 여럿이고 서로 다른 시제나 층을 볼 때. 로그와 테이블과 지표가 그런 셋이다
- 검사기나 문법 검사가 앞에 있는 단계. nginx -t 와 nft 와 cloud-init 스키마 검사기가 그렇다
- 목록이나 질의 결과가 비어 있을 때. 판정하기 전에 그 명령이 무엇을 세는지 먼저 적는다
예외
이 기준은 출력을 상태로 읽는 오독을 잡고, 출력 자체가 정확한지는 보지 않는다. 세 근거가 다 통과로 나와도 그 셋이 다 같은 층에서 나왔으면 여전히 한 근거다. 밖에서 친 200 이 분단을 가린 것이 그런 경우이고, 그래서 관측대를 클러스터 안쪽에 따로 세웠다. 그 관측대도 up 하나로는 같은 실패를 되풀이하므로 기능 지표를 함께 본다.
근거를 늘리는 데는 비용이 든다. 명령이 늘고 손으로 치는 선을 넘으면 파서를 짜게 되는데, §192 는 그 선을 grep -o 와 tr 로 쉼표마다 줄을 나누는 데까지로 그었다. 그 이상 가공해야 하면 파서를 짜지 않고 화면에 나온 JSON 을 그대로 읽는다.
「없다」와 「못 봤다」를 가르는 일도 도구가 대신해 주지 않는다. 스크레이프 대상 목록에서 없는 이름을 알아보려면 사람이 그 이름을 미리 알고 있어야 한다. Redis 와 BFF 와 PostgreSQL 이 빠진 것을 그 목록만 보고 알아낼 방법은 없다.
이 기준의 근거는 대부분 이 실험대 한 대에서 한 번씩 본 것이다. 다른 판 번호나 다른 배포판에서 같은 명령이 같은 것을 세는지는 재지 않았다.
가이드에 실린 출력이 이 호스트의 것인지도 한 군데에서 어긋난다. §186 의 실측 줄은 이 호스트가 16 코어 전부에서 지원한다고 적었는데 §178 의 대상 환경은 논리 코어 8(i5-1135G7)이다. 어느 쪽이 이 호스트의 값인지는 재지 않았다.
예시
- virsh list 가 비었다 : VM 이 없는 것인가 qemu:///session 에 붙은 것인가. virsh uri 로 가른다
- net-list 에 그 네트워크가 없다 : --all 을 줬는가. 없음과 꺼짐이 구분되나
- kubectl get all 이 다 나왔다 : Secret · ConfigMap · PVC · Ingress 는 거기 없다. 따로 한 번 더 친다
- Prometheus 질의가 빈 배열 : 0 이 아니라 그런 지표가 없다
- 스크레이프 대상에 Redis · BFF · PostgreSQL 이 없다 : 측정 실패가 아니라 측정된 공백이다
- ip-dhcp-host 로 grep 해서 아무것도 안 나온다 : 섹션 이름이라 XML 에 그 문자열이 없다. host mac 으로 찾는다
- 이벤트가 하나도 없다 : 기본 한 시간만 남는다. 무사하다는 뜻이 아니다
- 로그는 (1) 인데 jgroups_ping 에는 둘 다 있다 : 서로를 찾았고 7800 으로 메시지가 안 간다
- describe 가 19 bytes 인데 파드 안 길이가 0 : 저장은 됐고 주입이 안 됐다
- 두 노드가 Ready 인데 INTERNAL-IP 가 --node-ip 와 다르다 : 지금 증상 없음. 03 과 노드 상실 실험에서 터진다
- nginx -t 통과 : site-available 로 잘못 쳐도 통과한다. 설정을 썼는데 변화가 없으면 경로 오타다
- nft 가 433 을 받았다 : 80 은 되고 04 에서 HTTPS 만 안 된다
- 파드 둘이 Running 인데 Endpoints 가 하나 : 이미 한쪽으로만 가고 있다
- node-exporter 줄이 하나 : 그 노드의 지표가 통째로 없다
- up 이 1 : 503 중에도 1 이었다. 기능 지표를 함께 본다
- rollout status 가 아무것도 안 찍는다 : 정상이다. 타임아웃으로 끝나는 것도 답이다