기록 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>
93 lines
10 KiB
Markdown
93 lines
10 KiB
Markdown
---
|
|
kind: REFERENCE
|
|
slug: check-the-nearest-layer-first
|
|
title: 가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다
|
|
topic: build-completion-judgment
|
|
topicName: 끝났다는 판정
|
|
project: virtualization
|
|
status: 게시 전
|
|
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
|
source:
|
|
- final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat
|
|
- final/document.md#185-가이드-묶음이-스스로-정한-규약
|
|
- final/document.md#191-단계-05-keycloak-2노드와-postgresql
|
|
---
|
|
|
|
# 가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다
|
|
|
|
가장 가까운 층부터 치고 한 칸씩 밖으로 나오며, 층마다 성공 신호를 미리 적는다. 200 이 아닌 층이 있다 — 03 단계 네 칸의 실측은 404 와 301 과 301 과 200 이었다. 밖에서 한 번 쳐서 받은 값 하나로는 여섯 층 가운데 어디서 끊겼는지가 나오지 않는다.
|
|
|
|
## 관계
|
|
|
|
- **빈 토큰이 조용히 흘러갔다 — 설치 출력은 성공이었고 agent 만 5초마다 다시 죽었다**
|
|
가장 안쪽 칸에서 연결 거부나 타임아웃이 나오면 02 단계로 돌아가는데, 그 기록이 거기서 노드가 안 붙는 이유 하나를 끝까지 따라간다.
|
|
- **갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초**
|
|
04 단계의 판정도 같은 모양으로 갈린다. 그 기록은 로그 문구가 아니라 워커 PID 로 판정하라는 결론을 낸다.
|
|
- **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다**
|
|
여기서는 어디서 끊겼는지까지만 좁힌다. 좁힌 층의 출력을 어떻게 읽고 무엇을 잘못 읽기 쉬운지는 그쪽에 적혀 있다.
|
|
- **packet 이 어디서 끊겼는지는 계층마다 capture 해서 가른다**
|
|
같은 「어디서 끊겼나」를 응답 코드가 아니라 네 지점의 capture 로 좁힌다. 응답 코드로는 가릴 수 없을 때 그쪽으로 넘어간다.
|
|
- **호스트 안에서는 404, 밖에서는 connection refused — 앞 체인의 accept 가 libvirt 의 reject 를 막지 못했다**
|
|
가장 안쪽 칸이 404 인데 밖에서만 막히는 상태를 그 기록이 다룬다. 층을 좁힌 뒤 남은 원인이 방화벽이었던 경우다.
|
|
|
|
## 목적
|
|
|
|
밖에서 한 번 친 값 하나로는 고칠 층을 고를 수 없다. §189 은 03 단계의 확인을 네 칸으로 나누고 칸마다 건너뛰는 층을 하나씩 늘렸다. 첫 칸은 엣지 nginx 를 건너뛰고 게스트의 80 을 직접 치고, 둘째 칸은 호스트 DNAT 을 건너뛰고 엣지를 직접 치고, 셋째 칸은 밖에서 도메인으로 치고, 넷째 칸은 TLS 가 붙은 뒤를 친다. 둘째 칸이 통과하는데 셋째 칸이 안 되면 막힌 곳은 DNAT 이고, 둘째 칸에서 막히면 막힌 곳은 엣지 안이다. 이 한 칸을 끼워 두면 그 둘이 섞이지 않는다.
|
|
|
|
이 순서를 쓰려면 층마다 무엇이 성공인지를 먼저 적어야 한다. 첫 칸의 404 는 게스트의 80 을 Traefik 이 듣고 있고 매칭되는 Ingress 규칙이 없다고 답한 것이라 성공이다. 502 면 Traefik 은 떴는데 뒤에 백엔드가 없는 것이고, 연결 거부나 타임아웃이면 02 단계의 노드 상태로 돌아간다. 미리 적어 두지 않으면 404 를 보고 nginx 설정부터 고치기 시작한다. 성공 신호를 적어 두지 않아 통과한 출력을 실패로 읽는 일은 이 실험대에서 이미 있었다. §189 은 nginx -t 가 Debian 12 에서 늘 같이 내놓는 경고 한 줄을 04 단계에서 실패로 오독하는 일이 실제로 벌어졌다고 적고, 그래서 경고와 오류를 구분하는 눈을 03 단계에서 들여 둔다.
|
|
|
|
05 단계도 같은 순서로 판정한다. 밖에서 200 이면 nginx 에서 Traefik, Ingress, Service 를 지나 파드까지 전부 이어졌다. 502 나 503 이면 Ingress 가 있는지, Service 뒤에 파드가 있는지, 파드가 Ready 인지를 뒤에서부터 되짚는다.
|
|
|
|
## 규칙
|
|
|
|
### 1. 가장 가까운 층에서 시작해 한 칸씩 밖으로 나오며 친다
|
|
|
|
밖에서 시작하면 응답 하나에 여섯 층이 전부 들어가 있어 어느 층이 답한 것인지 가릴 수 없다. 안쪽에서 시작해 한 칸씩 층을 더하면, 값이 처음 달라지는 칸에서 더한 층이 끊긴 층이다.
|
|
|
|
### 2. 층마다 성공 신호를 미리 적는다. 200 이 아닌 층이 있다
|
|
|
|
03 단계 네 칸의 실측은 404 와 301 과 301 과 200 이었다. 첫 칸의 404 와 둘째·셋째 칸의 301 은 그 층이 제 일을 했다는 뜻이라, 성공 신호를 적어 두지 않으면 이 셋이 전부 실패로 읽힌다.
|
|
|
|
### 3. 무엇이 잘못됐는지 모르는 동안에는 값만 뽑는 명령을 쓰지 않는다
|
|
|
|
§185 의 ① 이 확인 명령을 두 종류로 갈라 적는다. 실무자가 한 번 볼 때 치는 curl -I 는 헤더를 통째로 내놓고, 여러 번 재서 비교할 때 쓰는 curl -s -o /dev/null -w 는 골라 놓은 한 칸 말고 전부 버린다. 그래서 03 단계의 확인이 첫 칸에서 -I 로 시작해 넷째 칸에서 http_code 한 칸으로 줄어든다. 순서가 반대면 첫 칸에서 무엇이 잘못됐는지 알려 줄 헤더를 스스로 버리게 된다.
|
|
|
|
### 4. 층을 좁힌 뒤에는 그 층이 내는 문구와 errno 와 종료 코드를 읽는다
|
|
|
|
같은 「안 된다」가 층마다 다른 낱말로 나온다. nginx upstream 의 connect() failed (113: No route to host) 는 네트워크 쪽이고, (111: Connection refused) 는 프로세스 쪽이며, no live upstreams 는 둘 다 죽었다는 판단이다. 노드를 잃었을 때 이 세 줄이 1분 안에 순서대로 나왔다. k3s agent 노드의 dial tcp [::1]:8080: connect: connection refused 는 네트워크 문제가 아니라 kubeconfig 을 하나도 못 찾아 하드코딩된 기본값으로 넘어간 것이다. 파드의 Exit Code 도 그것만으로 말이 된다 — 137 은 OOM 이나 강제 종료, 1 은 애플리케이션이 스스로 끝낸 것, 127 은 명령을 못 찾은 것이다.
|
|
|
|
### 5. 그 층 안에 물어볼 도구가 없으면 밖에서 묻는다
|
|
|
|
Keycloak 컨테이너에는 curl 이 없다. 공식 이미지가 최소 구성이라 wget 도 nc 도 없고, 그때 나오는 것이 curl: command not found 와 command terminated with exit code 127 이다. 127 을 규칙 4 대로 읽으면 서버가 내려간 것이 아니라 명령이 없는 것이므로, 안에서 묻기를 그만두고 Prometheus 로 묻거나 curlimages/curl 임시 파드를 띄워 밖에서 묻는다.
|
|
|
|
## 적용 조건
|
|
|
|
- 프록시나 컨트롤러가 겹쳐 있어 밖에서 한 번 쳐서는 어디서 끊겼는지 알 수 없는 스택. 이 실험대의 요청 경로는 여섯 층이다
|
|
- 층마다 성공 신호를 미리 적을 수 있을 때. 모르면 그것부터 한 층씩 재서 적는다
|
|
- 각 칸을 어느 기계에서 치는지가 이미 정해져 있을 때
|
|
- 밖에서 200 이 나오면 전부 이어졌다고 말할 수 있는 스택. 05 단계가 그렇다
|
|
|
|
## 예외
|
|
|
|
치는 위치가 틀리면 층 판정이 통째로 무의미해진다. 04 단계의 확인을 엣지 게스트 안에서 치면 connect to 100.83.212.4 port 443 failed: Connection refused 가 돌아오는데, 이것은 어느 층의 답도 아니다. 엣지에서 나간 패킷은 호스트의 virbr0 으로 들어가고 DNAT 규칙은 tailscale0 으로 들어온 것만 매칭하므로 규칙에 안 걸리고, 호스트 443 에 리스너가 없어 거절된다. 그래서 이 절차를 쓰기 전에 각 칸을 어느 기계에서 치는지가 정해져 있어야 한다. 문서가 그것을 매번 적게 만드는 방법은 「단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다」에 있다.
|
|
|
|
네 칸이 다 통과하고도 나중에 터지는 것이 있다. DNAT 규칙의 443 을 433 으로 친 오타가 이 실험대에서 실제로 나왔는데, 433 도 유효한 포트라 nft 가 군말 없이 받고 80 은 멀쩡히 넘어가므로 03 단계의 확인은 다 통과하고 04 단계에서 443 쪽만 안 되는 형태로 뒤늦게 터진다.
|
|
|
|
이 절차는 어디서 끊겼는지를 좁힐 뿐 왜 끊겼는지를 말하지 않는다. 첫 칸에서 404 가 나와도 그 뒤의 값이 틀렸을 수 있고, 02 단계의 INTERNAL-IP 가 그렇다 — 두 노드가 Ready 인데 보고된 IP 가 우리가 준 값과 다르면 지금은 아무 증상이 없다가 03 단계의 upstream 에서 어긋난다. 로그를 읽어 좁히려다 잘린 문구를 붙들 수도 있다. nginx 에러 로그는 2048바이트에서 잘린다. 이 실험대에서도 502 원인이 error 로그에서는 잘린 채로 있었고, access 로그에는 3492자로 온전히 남아 있었다. 그쪽은 「도구가 낸 출력은 대상의 상태가 아니다」가 받는다.
|
|
|
|
제3부의 「packet 이 어디서 끊겼는지는 계층마다 capture 해서 가른다」와는 재는 것이 다르다. 그쪽은 네 지점에서 capture 를 떠 패킷이 사라진 구간을 좁히고, 이쪽은 층을 건너뛴 요청의 응답 코드로 좁힌다. 응답이 아예 안 돌아오고 패킷도 안 보이는 상태에서는 이 절차가 답을 못 내므로 그때 그쪽으로 넘어간다.
|
|
|
|
## 예시
|
|
|
|
- 첫 칸 : nginx 를 건너뛰고 curl -I http://192.168.122.11 을 쳐서 404. 이것이 성공 신호다
|
|
- 둘째 칸 : DNAT 을 건너뛰고 http://192.168.122.10 을 쳐서 301
|
|
- 셋째 칸 : 밖에서 http://auth.hyeonworks.com 을 쳐서 301 https://auth.hyeonworks.com/
|
|
- 넷째 칸 : TLS 이후 https://auth.hyeonworks.com/realms/master 를 쳐서 200
|
|
- 둘째 칸은 되는데 셋째 칸이 안 된다 : 막힌 곳이 DNAT 이다
|
|
- 둘째 칸에서 막힌다 : 막힌 곳이 엣지 안이다
|
|
- 첫 칸이 502 : Traefik 은 떴고 뒤에 백엔드가 없다
|
|
- 첫 칸이 연결 거부나 타임아웃 : 02 단계의 노드 상태로 돌아간다
|
|
- 노드를 잃었을 때 1분 안에 순서대로 나온 세 줄 : 113 은 네트워크, 111 은 프로세스, no live upstreams 는 둘 다 죽었다는 판단
|
|
- Keycloak 컨테이너 안에서 curl 이 exit code 127 : 서버가 아니라 명령이 없다. 밖에서 묻는다
|
|
- 04 단계 확인을 엣지 안에서 쳤을 때의 connection refused : 층의 답이 아니라 친 위치의 답이다
|