fix(setup): 실험대에서 35편을 끝까지 밟고 어긋난 명령·결과 31건을 고친다
test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.
막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다
검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다
certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754
독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다
관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건
남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4a457afde9
commit
32e39e20aa
+50
-10
@@ -127,24 +127,31 @@ qemu-img info /var/lib/libvirt/images/base.qcow2
|
||||
|
||||
**목적** — 다음 단계의 파일에 넣을 공개키 둘과 콘솔 비밀번호 하나를 손에 든다.
|
||||
|
||||
```bash label="[lab host] ① 자기 공개키를 만들거나 꺼낸다"
|
||||
[ -f ~/.ssh/id_ed25519.pub ] || ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
|
||||
```bash label="[lab host] ① 자기 공개키를 꺼내 본다"
|
||||
cat ~/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
```bash label="[워크스테이션] ② 워크스테이션 공개키를 꺼낸다"
|
||||
`ssh-ed25519 ...` 로 시작하는 줄이 나오면 키가 이미 있으니 ② 를 건너뛴다. `No such file or directory` 면 키가 없는 것이고, ② 로 만든 뒤 ① 을 다시 친다.
|
||||
|
||||
```bash label="[lab host] ② 키가 없을 때만 만든다"
|
||||
ssh-keygen -t ed25519 -N '' -f ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
```bash label="[워크스테이션] ③ 워크스테이션 공개키를 꺼낸다"
|
||||
cat ~/.ssh/id_ed25519.pub
|
||||
```
|
||||
|
||||
```bash label="[lab host] ③ 콘솔용 비밀번호를 만든다"
|
||||
```bash label="[lab host] ④ 콘솔용 비밀번호를 만든다"
|
||||
openssl rand -base64 18
|
||||
```
|
||||
|
||||
**예상 결과** — `ssh-ed25519 ...` 로 시작하는 줄 둘과 base64 한 줄이 화면에 나온다. 셋 다 다음 단계에서 붙여 넣으므로 창을 닫지 않는다.
|
||||
|
||||
**왜 필요한가** — 자리표시자를 문서에 남기지 않고 값을 찾는 명령을 함께 둔다. ③ 의 비밀번호는 cloud-init 의 `plain_text_passwd` 로 들어가고, 키가 안 들어갔을 때 게스트로 들어가는 유일한 통로가 된다. 이 문서에는 그 값을 싣지 않는다.
|
||||
**왜 필요한가** — 자리표시자를 문서에 남기지 않고 값을 찾는 명령을 함께 둔다. ④ 의 비밀번호는 cloud-init 의 `plain_text_passwd` 로 들어가고, 키가 안 들어갔을 때 게스트로 들어가는 유일한 통로가 된다. 이 문서에는 그 값을 싣지 않는다.
|
||||
|
||||
**문제가 생기면** — ② 만 워크스테이션에서 친다. lab host 에서 두 번 쳐서 같은 키를 두 줄 넣으면 워크스테이션에서는 게스트에 못 붙는다.
|
||||
**①②를 나눠 둔 까닭** — 이 실험대는 `[ -f ~/.ssh/id_ed25519.pub ] || ssh-keygen …` 한 줄로 쳤다(observed). 그 형태는 「있으면 두고 없으면 만든다」를 단축 평가에 접어 넣어서, 배울 것보다 `[ -f … ]` 와 `||` 를 먼저 읽게 만든다. 나눠 두면 키가 이미 있는지가 ① 의 출력에 그대로 보이고, 만드는 명령은 그때만 친다. 이 나눈 형태는 이 실험대에서 치지 않았다(unknown).
|
||||
|
||||
**문제가 생기면** — ③ 만 워크스테이션에서 친다. lab host 에서 두 번 쳐서 같은 키를 두 줄 넣으면 워크스테이션에서는 게스트에 못 붙는다.
|
||||
|
||||
### 3. 게스트마다 cloud-init 파일을 쓴다
|
||||
|
||||
@@ -156,7 +163,7 @@ openssl rand -base64 18
|
||||
nano kc-lab-1.yaml
|
||||
```
|
||||
|
||||
② `__` 로 둘러싼 세 곳에 2번의 ①②③ 출력을 넣는다.
|
||||
② `__` 로 둘러싼 세 곳에 2번의 ①③④ 출력을 넣는다.
|
||||
|
||||
```yaml label="kc-lab-1.yaml 에 쓸 내용"
|
||||
#cloud-config
|
||||
@@ -547,7 +554,7 @@ sudo cloud-init status --long
|
||||
sudo journalctl -u cloud-init -n 50
|
||||
```
|
||||
|
||||
콘솔 로그인에 쓰는 비밀번호가 2번 ③ 으로 만든 값이다. 이 한 장과 이 두 줄이 「SSH 가 안 되는 이유」를 절반으로 줄인다.
|
||||
콘솔 로그인에 쓰는 비밀번호가 2번 ④ 로 만든 값이다. 이 한 장과 이 두 줄이 「SSH 가 안 되는 이유」를 절반으로 줄인다.
|
||||
|
||||
## 통과 조건을 한 번에 다시 본다
|
||||
|
||||
@@ -579,8 +586,41 @@ SSH 가 안 붙는 증상 넷을 갈라 보는 방법은 이 표보다 자세한
|
||||
- (observed) 게스트 세 대의 IP·MAC·vCPU·메모리, `Debian GNU/Linux 12 (bookworm)`, `cloud-init status: done`, cloud-init 대기 약 50초, 스키마 검사기의 거부 문구, `Valid cloud-config` 한 줄, 예약을 넣을 때와 다시 넣을 때의 문구, `virt-install` 의 네 줄 출력.
|
||||
- (observed) 메모리는 처음 만들 때 3584MB 였고 실험을 늘리며 5120 과 4096 으로 재배분했다. 세 게스트의 배정 합 10,240MB 는 호스트 RAM 11,648MiB 보다 작고, 배정하지 않고 남은 것이 1,408MiB 다. 「Guest configured memory 총량이 Host physical RAM보다 크다」는 Memory Overcommit 에 해당하지 않는 배치다.
|
||||
- (unknown) `kc-lab-1.yaml` 을 템플릿에서 어떻게 만드는지가 원본에도 없다. `kc-lab.yaml.example` 이 반입되지 않아 대조하지 못했다.
|
||||
- (unknown) cloud-init 의 `packages` 에 certbot 이 들어 있었는지가 가이드 안에서 갈린다. 01 의 예시와 03 의 본문은 `curl` 과 `nftables` 뿐이라 적고, 04 는 템플릿의 `packages` 에 certbot 이 있다고 적는다.
|
||||
- (unknown) 파일을 옮겨 검사하는 다섯 줄 형태, `virsh vol-list` 와 `vol-info`, `net-dumpxml --inactive`, `cloud-init status --long`, `virsh console` 은 가이드가 적어 둔 명령이고 이 실험대가 캡처한 출력이 없다.
|
||||
- (observed) 2026-09-17 에 확인 ①·②·③ 을 그대로 쳤고 셋 다 위에 실은 대로 나왔다. 세 도메인이 `running`, `192.168.122.11` 의 `hostname` 이 `kc-lab-1` 이고 `PRETTY_NAME` 이 Debian 12, 엣지가 `192.168.122.10/24` 와 `status: done`, `virsh screenshot` 이 `.ppm` 이름으로 저장하면서 `with type of image/png` 라고 답하고 `file` 도 PNG 라고 읽는다. `virsh list` 의 Id 는 `13`·`15`·`16` 이고 줄 순서도 위와 다른데, 본문이 적은 대로 그 번호에는 뜻이 없다.
|
||||
- (observed) **cloud-init 의 `packages` 에 certbot 은 없다.** 2026-09-17 에 갈렸다. 뜬 게스트가 실제로 받은 user-data 를 열어 보니 `packages: [curl, nftables]` 한 줄이고, 저장소 템플릿에는 `nginx` · `certbot` · `python3-certbot-dns-cloudflare` 셋이 **주석으로 막혀** 있다. 넷 중 `curl` 과 `nftables` 만 산다. 04 의 표에 `kc-lab-edge` 가 `nginx · certbot` 으로 적힌 것은 그 게스트가 **끝나면 맡을 역할**이지 cloud-init 이 깔아 준 것이 아니다 — nginx 는 03 의 1번이, certbot 은 04 의 1번이 직접 깐다.
|
||||
|
||||
```bash label="[kc-lab-edge] 실제로 받은 user-data 를 연다"
|
||||
sudo grep -n "packages" /var/lib/cloud/instance/user-data.txt
|
||||
```
|
||||
|
||||
```text label="그 출력"
|
||||
19:packages: [curl, nftables]
|
||||
```
|
||||
- (observed) `virsh vol-info` · `net-dumpxml --inactive` · `cloud-init status --long` 은 2026-09-17 에 쳤다. 볼륨 일곱의 용량과 실제 할당은 이렇다 — **선언한 크기와 디스크가 실제로 먹는 양이 크게 다르다.** 오버레이라 쓴 만큼만 먹는다.
|
||||
|
||||
```text
|
||||
base.qcow2 Capacity=3.00 GiB Allocation=323.25 MiB
|
||||
kc-lab-1.qcow2 Capacity=20.00 GiB Allocation=5.40 GiB
|
||||
kc-lab-2.qcow2 Capacity=20.00 GiB Allocation=3.85 GiB
|
||||
kc-lab-edge.qcow2 Capacity=10.00 GiB Allocation=316.26 MiB
|
||||
seed-kc-lab-{1,2,edge}.iso Capacity=370.00 KiB Allocation=372.00 KiB
|
||||
```
|
||||
|
||||
`net-dumpxml default --inactive` 는 `virbr0` 과 동적 대역 한 줄과 예약 세 줄을 낸다 — `--inactive` 를 붙여도 예약이 그대로 보이는 것이 `--config` 로 넣었다는 증거다.
|
||||
|
||||
- (observed) `cloud-init status --long` 의 마지막 줄이 이 문서의 7번을 그대로 뒷받침한다.
|
||||
|
||||
```text
|
||||
status: done
|
||||
boot_status_code: enabled-by-generator
|
||||
last_update: Thu, 17 Sep 2026 04:24:22 +0000
|
||||
detail:
|
||||
DataSourceNoCloud [seed=/dev/vdb][dsmode=net]
|
||||
```
|
||||
|
||||
`seed=/dev/vdb` 가 핵심이다. 시드를 `--cloud-init` 으로 붙였다면 SATA CD-ROM(`sr0`)이 됐을 텐데, `bus=virtio` 로 디스크로 붙였기 때문에 `vdb` 로 잡혔고 cloud-init 이 거기서 데이터소스를 찾았다. 7번이 적은 이유가 이 한 줄로 확인된다.
|
||||
|
||||
- (unknown) 파일을 옮겨 검사하는 다섯 줄 형태와 `virsh console` 은 가이드가 적어 둔 명령이고 이 실험대가 캡처한 출력이 없다. `virsh console` 은 대화형이라 비대화식으로 밟지 않았다.
|
||||
- (unknown) 편집기로 `kc-lab-1.yaml` 과 `meta-kc-lab-1` 을 여는 형태도 이 실험대가 치지 않았다. 이 실험대는 치환 명령과 서식 출력으로 만들었고, 같은 상태에 닿는지는 다시 재지 않았다.
|
||||
- (unknown) 원본 가이드에 되돌리는 절차가 없다. 단계 03 이 지나가며 적은 한 줄 말고는 게스트와 시드 볼륨과 DHCP 예약을 걷어내는 순서가 어디에도 없다.
|
||||
- (inferred) `virt-install` 세 줄은 구축할 때 친 것을 옮겼고 재실행으로 검증되지 않았다.
|
||||
|
||||
+225
-9
@@ -62,7 +62,7 @@ sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
|
||||
**4번과 5번만 호스트에서 치는 까닭이 둘이다.** DNAT 규칙의 첫 줄이 `iifname "tailscale0"` 인데 VM 에는 Tailscale 을 넣지 않기로 했으므로 엣지에는 그 인터페이스 자체가 없고, 넘기는 대상이 `192.168.122.10` 으로 가는 트래픽이라 넘기는 주체는 그 앞에 있는 호스트다. 엣지에 들어가서 치면 `tailscale0` 이 없어 규칙이 의미가 없다.
|
||||
|
||||
여는 파일은 셋이다 — nginx 라우팅 설정, nftables DNAT 파일, systemd 유닛. 셋 다 사람이 내용을 읽고 고쳐야 하는 파일이라 편집기로 연다. `nano` 는 저장이 `Ctrl+O` 다음 `Enter`, 나가기가 `Ctrl+X` 다. 설치와 링크와 문법 검사와 reload 는 운영자가 그대로 치는 명령을 쓴다.
|
||||
여는 파일은 넷이다 — nginx 라우팅 설정, nftables DNAT 파일, systemd 유닛, CoreDNS 매니페스트. 셋 다 사람이 내용을 읽고 고쳐야 하는 파일이라 편집기로 연다. `nano` 는 저장이 `Ctrl+O` 다음 `Enter`, 나가기가 `Ctrl+X` 다. 설치와 링크와 문법 검사와 reload 는 운영자가 그대로 치는 명령을 쓴다.
|
||||
|
||||
## 이 단계가 세우는 것
|
||||
|
||||
@@ -108,8 +108,9 @@ sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
| 규칙만 즉시 걷어낸다 | `[lab host]` | `sudo nft delete table ip lab_edge` |
|
||||
| libvirt 에 뚫은 구멍을 막는다 | `[lab host]` | `sudo nft -a list chain ip libvirt_network guest_input` 로 handle 을 보고 `sudo nft delete rule ip libvirt_network guest_input handle <번호>` |
|
||||
| 엣지 라우팅을 끈다 | `[kc-lab-edge]` | `sudo rm /etc/nginx/sites-enabled/keycloak-lab && sudo nginx -t && sudo systemctl reload nginx` |
|
||||
| 클러스터 안에 더한 이름을 걷어낸다 | `[lab host]` | `kubectl -n kube-system delete configmap coredns-custom` 하고 `kubectl -n kube-system delete pod -l k8s-app=kube-dns` |
|
||||
|
||||
`sites-available` 의 원본은 남으므로 `ln -sf` 로 다시 걸면 복구된다. 네 줄이 원문 그대로이고, 그 네 줄을 실제로 쳐서 걷어내 본 기록은 없다(unknown).
|
||||
`sites-available` 의 원본은 남으므로 `ln -sf` 로 다시 걸면 복구된다. **앞 네 줄이 가이드 원문 그대로이고, 마지막 줄은 이 기록이 더한 것이다** — CoreDNS 매니페스트를 이 기록이 처음 넣었는데 걷어내는 명령이 빠져 있었다. 다섯 줄 어느 것도 실제로 쳐서 걷어내 본 기록은 없다(unknown). 마지막 줄을 치면 클러스터 안에서 `auth.hyeonworks.com` 이 다시 안 풀리므로, 그 이름에 의존하는 실험을 끝낸 뒤에 친다.
|
||||
|
||||
**`.nft` 파일 맨 위의 `delete` 는 이것과 다르다.** 그 두 줄은 파일을 적용할 때마다 자동으로 도는 재적용 안전장치이고, 위 표의 삭제 명령은 사람이 끄는 버튼이다.
|
||||
|
||||
@@ -491,15 +492,34 @@ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://auth.hyeonworks.
|
||||
301 https://auth.hyeonworks.com/
|
||||
```
|
||||
|
||||
**그 `301` 도 다음 단계 이후의 값이다.** 여기까지만 했다면 ② 와 같은 `404` 가 정상이고, 이 층이 묻는 것은 코드값이 아니라 밖에서 친 것이 엣지까지 닿았는가다.
|
||||
**그 `301` 은 다음 단계 이후의 값이다.** 여기까지만 했을 때 나오는 것은 `302` 이고 보내는 곳도 다르다. 2026-09-17 에 밖에서 재 보니 이랬다(observed).
|
||||
|
||||
2026-09-17 에 밖에서 재 보니 닿지 않았다(observed).
|
||||
```text
|
||||
302 https://auth.hyeonworks.com/admin/
|
||||
```
|
||||
|
||||
**리다이렉트를 내는 것이 둘이고, 어느 쪽이 답하느냐가 단계에 따라 바뀐다.** 04 를 끝내면 엣지 nginx 의 `return 301 https://$host$request_uri` 가 먼저 답해 `301 https://auth.hyeonworks.com/` 이 되고, 03 까지만 했으면 그 블록이 없으므로 요청이 Keycloak 까지 가서 **Keycloak 이** `/` 를 `/admin/` 으로 보낸다. 코드도 `301` 이 아니라 `302` 다.
|
||||
|
||||
`Location` 이 `https://` 인 것도 엣지가 아니라 Keycloak 이 정한다 — `KC_HOSTNAME=https://auth.hyeonworks.com` 과 `KC_HOSTNAME_STRICT=true` 가 그렇게 시킨다. **요청이 `http` 로 들어와도 Keycloak 은 자기가 아는 주소로 답한다.**
|
||||
|
||||
**같은 엣지에 어떻게 치느냐가 코드를 가른다**(2026-09-17, observed). 이 표를 먼저 보고 층 ②③ 을 읽는다.
|
||||
|
||||
| 어떻게 치나 | 코드 |
|
||||
|---|---|
|
||||
| Host 없이 엣지 IP 로 (층 ②) | `404` — Traefik 에 매칭되는 Ingress 가 없다 |
|
||||
| `Host: auth.hyeonworks.com` 으로 엣지 IP 에 | `302 https://auth.hyeonworks.com/admin/` |
|
||||
| 밖에서 도메인으로 (층 ③) | `302 https://auth.hyeonworks.com/admin/` |
|
||||
| 밖에서 도메인 + `/realms/master` | **`200`** |
|
||||
|
||||
마지막 줄이 중요하다 — **TLS 를 얹기 전에도 `/realms/master` 는 `200` 이다.** 층 ④ 의 `200` 은 「TLS 가 섰다」가 아니라 「끝까지 이어졌다」를 재는 값이고, `https` 로 그 값을 받는 것이 04 의 몫이다.
|
||||
|
||||
밖에서 닿지 않을 때의 모양도 적어 둔다. 2026-09-17 에 구멍을 뚫기 전에는 이랬다(observed).
|
||||
|
||||
```text
|
||||
curl: (7) Failed to connect to auth.hyeonworks.com port 80 after 54 ms
|
||||
```
|
||||
|
||||
같은 시각에 호스트 안에서 친 `http://192.168.122.10` 은 `404` 로 답했다. 아래 「막히면」 표의 마지막 줄이 가리키는 상태 그대로였고, 원인도 거기 적힌 대로였다 — **그 호스트에 깔려 있는 유닛에 `ExecStartPost` 줄이 없다.** 아래 「배포된 것과 적어 둔 것이 다르다」를 본다.
|
||||
같은 시각에 호스트 안에서 친 `http://192.168.122.10` 은 `404` 로 답했다. 아래 「막히면」 표의 마지막 줄이 가리키는 상태 그대로였고, 원인도 거기 적힌 대로였다 — **그 호스트에 깔려 있던 유닛에 `ExecStartPost` 줄이 없었다.** 아래 「배포된 것과 적어 둔 것이 다르다」를 본다. 그 줄을 넣자 같은 `curl` 이 `302` 를 받았다(observed).
|
||||
|
||||
### 확인 ④ 끝까지 닿나
|
||||
|
||||
@@ -566,9 +586,10 @@ grep oauth2/callback /var/log/nginx/access.log | tail -1 | wc -c
|
||||
| 우리 테이블 | `sudo nft list table ip lab_edge` | `dnat to 192.168.122.10` · 포트 `80, 443` · SNAT 없음 |
|
||||
| 층 ① | `curl -I http://192.168.122.11` | `404` |
|
||||
| 층 ② | `curl -sS -D - -o /dev/null http://192.168.122.10` | `404` · `Server: nginx/1.22.1` |
|
||||
| 층 ③ | `curl -I http://auth.hyeonworks.com` | ② 와 같은 응답이 밖에서도 온다 |
|
||||
| 층 ③ | `curl -I http://auth.hyeonworks.com` | `302` · `Location` 이 `https://auth.hyeonworks.com/admin/` |
|
||||
| 층 ④ 를 `http` 로 미리 | `curl -s -o /dev/null -w '%{http_code}\n' http://auth.hyeonworks.com/realms/master` | **`200`** |
|
||||
|
||||
층 ④ 는 다음 단계가 인증서를 얹은 뒤에 통과한다. **층 ②③ 의 `301` 도 그때 나온다** — 여기서는 `404` 가 통과다.
|
||||
층 ④ 를 `https` 로 받는 것은 다음 단계가 인증서를 얹은 뒤다. **층 ③ 의 `301` 도 그때 나온다** — 여기서는 `302` 가 통과이고, 그 둘을 내는 주체가 다르다(위 표).
|
||||
|
||||
## 막히면
|
||||
|
||||
@@ -609,14 +630,209 @@ grep -c ExecStartPost /etc/systemd/system/lab-edge-dnat.service
|
||||
|
||||
`0` 과 `1` 이 나와야 맞다. 반대로 나오면 고치기 전 판이 깔려 있는 것이고, 다시 써서 `sudo systemctl daemon-reload && sudo systemctl restart lab-edge-dnat.service` 로 올린다.
|
||||
|
||||
**고쳤는지는 확인하지 못했다**(unknown). 그 호스트의 `sudo` 는 비밀번호를 묻고 이번 작업에는 그 비밀번호가 없었다. `nft list table ip lab_edge` 도 `guest_input` 체인 조회도 못 돌렸고 두 파일을 다시 배포하지도 않았다. 위 표는 **파일 내용과 타임스탬프만으로** 적은 것이고 `reject` 줄의 카운터가 실제로 올라갔는지는 안 봤다.
|
||||
**2026-09-17 늦게 그 `sudo` 가 열려 셋을 마저 봤다**(observed). 결론은 **절반만 고쳐져 있다**이다.
|
||||
|
||||
```text label="위 대조 두 줄의 실제 값"
|
||||
grep -c 'chain forward' /etc/nftables.d/lab-edge-dnat.nft → 1 (0 이어야 한다)
|
||||
grep -c ExecStartPost /etc/systemd/system/lab-edge-dnat.service → 1 (맞다)
|
||||
```
|
||||
|
||||
유닛에는 `ExecStartPost` 가 들어갔는데 `.nft` 는 아직 옛 판이다 — **효과 없는 `forward` 체인을 아무도 걷어내지 않았다.** 위 표의 두 줄 가운데 아래만 고쳐졌으므로, `.nft` 를 이 문서의 4번 내용으로 다시 쓰고 `sudo systemctl restart lab-edge-dnat.service` 로 올려야 이 문서가 싣는 판이 된다.
|
||||
|
||||
```bash label="[lab host] guest_input 체인을 연다"
|
||||
sudo nft list chain ip libvirt_network guest_input
|
||||
```
|
||||
|
||||
```text label="그 출력 — 같은 규칙이 셋이다"
|
||||
oif "virbr0" ip daddr 192.168.122.10 tcp dport { 80, 443 } ct state new counter packets 44 bytes 2640 accept
|
||||
oif "virbr0" ip daddr 192.168.122.10 tcp dport { 80, 443 } ct state new counter packets 0 bytes 0 accept
|
||||
oif "virbr0" ip daddr 192.168.122.0/24 ct state established,related counter packets 141705 bytes 2375116597 accept
|
||||
oif "virbr0" counter packets 5 bytes 300 reject
|
||||
oif "virbr0" ip daddr 192.168.122.10 tcp dport { 80, 443 } ct state new counter packets 10 bytes 600 accept
|
||||
```
|
||||
|
||||
**구멍이 하나가 아니라 셋이다.** `ExecStartPost` 가 `nft insert` 로 맨 앞에 끼워 넣기 때문에, 유닛을 다시 시작할 때마다 같은 규칙이 한 줄씩 늘어난다. **이 명령은 여러 번 쳐도 안전한 형태가 아니다.** 기능은 멀쩡하다 — 맨 앞 하나가 통과시키고 나머지는 카운터가 안 오르거나(둘째 줄 `0`) 아예 닿지 않는다(다섯째 줄은 `reject` 뒤라 새 패킷이 거기까지 못 간다).
|
||||
|
||||
`reject` 줄의 카운터는 `5` 다. 구멍이 들어가기 전에 막힌 것들이고, 들어간 뒤로는 안 오른다. 맨 앞 줄이 `44` 를 세고 있는 것이 지금 밖에서 오는 요청이 그 규칙으로 통과한다는 뜻이다.
|
||||
|
||||
## 클러스터 안에서도 공개 이름에 못 닿는다 — 이 문서에 없던 또 한 단계
|
||||
|
||||
랩 호스트만의 일이 아니다. **파드 안에서도 같은 이름이 같은 이유로 막힌다**(2026-09-17, observed). 이름은 호스트의 tailnet 주소로 풀리는데 그 포트를 듣는 것이 없다.
|
||||
|
||||
```bash label="[lab host] ① 파드 안에서 이름을 풀고 포트를 두드린다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- sh -c "getent hosts auth.hyeonworks.com"
|
||||
```
|
||||
|
||||
```text label="① 의 출력 — 고치기 전"
|
||||
100.83.212.4 auth.hyeonworks.com
|
||||
```
|
||||
|
||||
같은 파드에서 `100.83.212.4` 의 80 과 443 을 두드리면 둘 다 닫혀 있고, 엣지 게스트의 `192.168.122.10:80` 은 열려 있다. 길은 있는데 **이름이 그 길을 안 가리킨다.** 클러스터 DNS 어디에도 그 매핑이 없다 — CoreDNS 의 `NodeHosts` 에는 노드 둘뿐이고 노드의 `/etc/hosts` 에도 없다(observed).
|
||||
|
||||
여기서 걸리는 것은 토큰을 검증하는 앱이다. 토큰을 받는 쪽은 클러스터 안 주소를 따로 갖고 있어서 안 걸리는데, 검증하는 쪽은 공개 이름 하나만 갖고 있어서 걸린다.
|
||||
|
||||
**고치는 한 단계는 CoreDNS 에 서버 블록을 하나 더 주는 것이다.**
|
||||
|
||||
```bash label="[lab host] ② 매니페스트 파일을 연다"
|
||||
nano coredns-custom.yaml
|
||||
```
|
||||
|
||||
```yaml label="③ coredns-custom.yaml 에 쓸 내용"
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: coredns-custom
|
||||
namespace: kube-system
|
||||
data:
|
||||
hyeonworks.server: |
|
||||
hyeonworks.com:53 {
|
||||
hosts {
|
||||
192.168.122.10 auth.hyeonworks.com
|
||||
192.168.122.10 app1.hyeonworks.com
|
||||
192.168.122.10 app2.hyeonworks.com
|
||||
fallthrough
|
||||
}
|
||||
forward . /etc/resolv.conf
|
||||
}
|
||||
```
|
||||
|
||||
```bash label="[lab host] ④ 적용하고 CoreDNS 파드를 다시 띄운다"
|
||||
kubectl apply -f coredns-custom.yaml
|
||||
kubectl -n kube-system delete pod -l k8s-app=kube-dns --wait=false
|
||||
```
|
||||
|
||||
이 실험대는 같은 매니페스트를 `kubectl apply -f - <<'YAML'` 힙독으로 흘려 넣었다(observed). 힙독은 ConfigMap 을 명령 안에 묻어 버려서, 서버 블록을 한 줄 고치려면 같은 힙독을 처음부터 다시 친다. 파일로 두면 열어 보고 고쳐서 ④ 를 다시 치면 된다. 이 나눈 형태는 이 실험대에서 치지 않았다(unknown).
|
||||
|
||||
**파일 이름이 `.server` 인 것이 핵심이다.** k3s 는 `*.override` 를 기본 서버 블록 안에 끼워 넣는데, 그 블록은 이미 `hosts /etc/coredns/NodeHosts` 로 `hosts` 를 한 번 쓰고 있다. `.override` 에 `hosts` 를 또 쓰면 CoreDNS 가 안 뜬다.
|
||||
|
||||
```text label="`.override` 로 넣었을 때 — CrashLoopBackOff"
|
||||
plugin/hosts: this plugin can only be used once per Server Block
|
||||
```
|
||||
|
||||
`.server` 는 **별개의 서버 블록**으로 들어가므로 그 안에서 `hosts` 를 처음 쓰는 것이 된다. 뜨고 나면 로그 머리에 듣는 영역이 둘로 찍힌다.
|
||||
|
||||
```text label="제대로 들어갔을 때"
|
||||
.:53
|
||||
hyeonworks.com.:53
|
||||
```
|
||||
|
||||
```bash label="[lab host] ⑤ 파드에서 다시 푼다"
|
||||
kubectl -n keycloak-lab exec keycloak-0 -- sh -c "getent hosts auth.hyeonworks.com"
|
||||
```
|
||||
|
||||
```text label="⑤ 의 출력 — 고친 뒤"
|
||||
192.168.122.10 auth.hyeonworks.com
|
||||
```
|
||||
|
||||
이제 `192.168.122.10:80` 이 파드에서 열린다. **443 은 여전히 닫혀 있다** — 그것은 다음 단계가 인증서를 얹어야 열린다.
|
||||
|
||||
## 이 단계를 밟고 나면 랩 호스트에서 공개 URL 을 못 친다
|
||||
|
||||
nginx 가 호스트에 있던 동안에는 호스트에서 `curl https://auth.hyeonworks.com` 이 그냥 됐다. 이 단계가 nginx 를 엣지 게스트로 옮기면서 그것이 끊긴다. 2026-09-17 에 재 보니 80 도 443 도 안 열린다(observed).
|
||||
|
||||
```bash label="[lab host] ① 문서에 적힌 대로 쳐 본다"
|
||||
curl -s -m 10 -o /dev/null -w "%{http_code}\n" https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
|
||||
```
|
||||
|
||||
```text label="① 의 출력"
|
||||
000
|
||||
Failed to connect to auth.hyeonworks.com:443 after 22 ms: Could not connect to server
|
||||
```
|
||||
|
||||
이유는 두 줄이다. `auth.hyeonworks.com` 은 호스트 자신의 tailnet 주소로 풀리고, 호스트에는 그 포트를 듣는 것이 없다.
|
||||
|
||||
```bash label="[lab host] ② 왜 그런지 두 줄로 본다"
|
||||
getent hosts auth.hyeonworks.com
|
||||
ss -lnt | grep -E ":(80|443) "
|
||||
```
|
||||
|
||||
```text label="② 의 출력"
|
||||
100.83.212.4 auth.hyeonworks.com
|
||||
```
|
||||
|
||||
두 번째 줄은 아무것도 안 낸다. 4번의 DNAT 은 `iifname "tailscale0"` 만 매칭하므로 호스트가 스스로 낸 패킷은 그 규칙을 안 탄다. 그래서 **밖에서 치는 확인은 tailnet 에 붙은 다른 기계에서 친다** — 04 가 자기 4번 확인을 엣지가 아닌 다른 머신으로 보낸 것과 같은 이유다.
|
||||
|
||||
랩 호스트에서 꼭 쳐야 하면 이름은 그대로 두고 주소만 엣지로 못박는다.
|
||||
|
||||
```bash label="[lab host] ③ 이름은 그대로, 주소만 엣지로"
|
||||
curl -s -m 10 --resolve auth.hyeonworks.com:80:192.168.122.10 \
|
||||
http://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
|
||||
```
|
||||
|
||||
이 형태는 응답을 받는다(observed). 다만 DNAT 을 안 거치므로 **층 ③ 을 재는 것이 아니다.** 층 ② 를 도메인 이름으로 재는 것이고, 4번과 5번이 제대로 들어갔는지는 여전히 밖에서 쳐야 갈린다.
|
||||
|
||||
## `X-Forwarded-For` 계약은 지금 성립하지 않는다
|
||||
|
||||
4번에서 SNAT 을 안 거는 이유로 「masquerade 를 붙이면 엣지가 모든 클라이언트를 `192.168.122.1` 로 보게 된다」고 적었다. 2026-09-17 에 밖에서 한 번 치고 엣지의 로그를 열었더니 **이미 그 상태였다**(observed).
|
||||
|
||||
```bash label="[dev] ① 밖에서 친다"
|
||||
curl -s -o /dev/null http://app1.hyeonworks.com/api/echo
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-edge] ② 엣지가 누구를 클라이언트로 적었나"
|
||||
sudo tail -3 /var/log/nginx/access.log
|
||||
```
|
||||
|
||||
```text label="② 의 출력"
|
||||
192.168.122.1 - - [17/Sep/2026:07:10:45 +0000] "GET /api/echo HTTP/1.1" 200 617 "-" "curl/8.5.0"
|
||||
```
|
||||
|
||||
`lab_edge` 테이블에는 `masquerade` 도 `snat` 도 없다. 4번이 싣는 파일 그대로다. 그런데도 출발지가 덮여 있으므로 덮는 것은 이 테이블이 아니다. **덮는 것은 Tailscale 이다**(2026-09-17, observed). 호스트의 규칙을 전부 읽어서 갈랐다.
|
||||
|
||||
```bash label="[lab host] 누가 덮는지 규칙에서 찾는다"
|
||||
sudo nft list ruleset | grep -nE 'masquerade|snat'
|
||||
sudo iptables -t nat -S | grep ts-postrouting
|
||||
sudo tailscale debug prefs | grep -i snat
|
||||
```
|
||||
|
||||
```text label="세 줄이 이어진다"
|
||||
chain ts-forward { iifname "tailscale0" counter ... xt target "MARK" }
|
||||
-A ts-postrouting -m mark --mark 0x40000/0xff0000 -j MASQUERADE
|
||||
"NoSNAT": false
|
||||
```
|
||||
|
||||
읽는 순서는 이렇다. `tailscale0` 으로 들어와 전달되는 패킷에 Tailscale 이 표식 `0x40000` 을 찍고, `ts-postrouting` 이 그 표식이 붙은 것을 전부 `MASQUERADE` 한다. 마스커레이드는 나가는 인터페이스의 주소로 출발지를 바꾸는데, 이 패킷이 나가는 곳이 `virbr0` 이라 출발지가 `192.168.122.1` 이 된다. `NoSNAT` 이 `false` 인 것이 그 동작이 켜져 있다는 뜻이고, 그것이 Tailscale 의 기본값이다.
|
||||
|
||||
**그래서 4번의 경고는 옳았는데 엉뚱한 테이블을 지켰다.** 「masquerade 를 붙이면 엣지가 모든 클라이언트를 `192.168.122.1` 로 보게 된다」는 그대로 일어났고, 다만 그 `masquerade` 를 붙인 것이 우리 테이블이 아니라 Tailscale 이다. `lab_edge` 만 깨끗하게 지켜도 소용이 없다.
|
||||
|
||||
libvirt 의 마스커레이드는 이 일과 무관하다. 그 규칙은 `ip saddr 192.168.122.0/24 ip daddr != 192.168.122.0/24` 이라 **게스트에서 밖으로 나가는** 것만 고르고, 밖에서 게스트로 들어오는 이 패킷은 목적지가 그 대역 안이라 안 걸린다.
|
||||
|
||||
범인보다 결과가 먼저다. `proxy_set_header X-Forwarded-For $remote_addr` 는 시키는 대로 동작하고 있고, 다만 `$remote_addr` 가 이미 호스트의 브리지 주소다. 그래서 **이 실험대의 `X-Forwarded-For` 는 진짜 클라이언트를 담지 않는다.** 통과 조건에 「엣지 로그의 출발지가 밖에서 친 기계다」를 넣으면 늘 실패한다.
|
||||
|
||||
## Traefik 이 그 헤더를 다시 덮는다 — 이 문서에 없던 한 단계
|
||||
|
||||
위 2번이 세운 헤더가 앱까지 가는지를 처음으로 재 봤다. **안 간다.** Traefik 은 자기가 신뢰하지 않는 곳에서 온 `X-Forwarded-*` 를 자기 연결 기준으로 다시 쓰고, 기본값에는 신뢰 목록이 비어 있다.
|
||||
|
||||
```bash label="[dev] ① 앱이 무엇을 받았나"
|
||||
curl -s http://app1.hyeonworks.com/api/echo
|
||||
```
|
||||
|
||||
| 헤더 | 이 한 단계를 안 밟았을 때 | 밟은 뒤 |
|
||||
|---|---|---|
|
||||
| `x-real-ip` | `10.42.0.1` — flannel 게이트웨이. 엣지가 보낸 값이 사라졌다 | `192.168.122.1` — 엣지가 보낸 값 그대로 |
|
||||
| `remoteAddr` | `10.42.0.1` | `192.168.122.1` |
|
||||
| `x-forwarded-for` | 없다 | 없다 |
|
||||
|
||||
빠진 한 단계는 아래 두 줄이다. 저장소에 파일이 있는데 **이 실험대의 setup 아홉 편 어디에도 이 명령이 없었다**(observed).
|
||||
|
||||
```bash label="[lab host] ② Traefik 에 신뢰 목록을 준다"
|
||||
kubectl apply -f deploy/lab/k8s/traefik-forwarded-headers.yaml
|
||||
kubectl -n kube-system rollout status deploy/traefik --timeout=180s
|
||||
```
|
||||
|
||||
목록에는 파드 대역 `10.42.0.0/16` 하나만 담는다. Traefik 의 Service 가 `externalTrafficPolicy: Cluster` 라 svclb 가 출발지를 덮고, 그래서 Traefik 에 닿는 주소는 엣지가 아니라 파드 대역에서 온다. 노드·호스트 대역을 같이 넣어 봤지만 위 표의 값이 하나도 안 바뀌었다 — 나타날 수 없는 대역이라 넣어도 소용이 없다(observed).
|
||||
|
||||
`x-forwarded-for` 는 이 단계를 밟아도 앱까지 안 온다(observed). **왜 사라지는지는 안 가렸다**(unknown). 앞 절과 합치면 이 실험대에서는 그 헤더로 클라이언트를 가릴 수 없다. 두 번 재서 두 번 다 없었다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
- (observed) 2026-09-11 새로 만든 엣지에서 `/etc/nginx` 가 없던 것, `nginx -t` 출력 세 줄, 다 끝난 실험대에서 잰 층별 확인 ①~④ 의 코드, `301 https://auth.hyeonworks.com/`, upstream 실패 errno 세 줄, access 로그 3492자.
|
||||
- (observed) 2026-09-17 에 이 절차를 따라 엣지를 다시 세우고 잰 것 — `nginx -t` 가 두 줄인 것, `which nginx` 가 깔린 상태에서도 빈손인 것, 층 ① 이 `.11`·`.12` 둘 다 `404` 인 것, 층 ② 가 `404` · `Server: nginx/1.22.1` 인 것, 층 ③ 이 `curl: (7)` 인 것, 호스트의 두 파일이 위 본문과 다른 것.
|
||||
- (observed) 2026-09-17 에 밖에서 친 요청이 엣지 access 로그에 `192.168.122.1` 로 적힌 것, 이 한 단계를 밟기 전 앱이 받은 `x-real-ip` 가 `10.42.0.1` 이고 밟은 뒤 `192.168.122.1` 인 것, 두 상태 모두 `x-forwarded-for` 가 없는 것, setup 아홉 편에 `traefik-forwarded-headers.yaml` 을 거는 명령이 없는 것.
|
||||
- (observed) 엣지에 닿기 전에 출발지를 덮는 것은 **Tailscale** 이다. `ts-forward` 가 표식 `0x40000` 을 찍고 `ts-postrouting` 이 그 표식에 `MASQUERADE` 를 걸며 `NoSNAT` 이 `false` 다. 호스트에 깔린 두 파일의 실제 상태(`.nft` 는 옛 판, 유닛은 새 판)와 `guest_input` 의 중복 규칙 셋도 같이 봤다.
|
||||
- (unknown) `x-forwarded-for` 가 어디서 사라지는지는 아직 안 가렸다. Traefik 에 신뢰 목록을 주기 전과 뒤 둘 다 앱까지 안 오는 것만 쟀다.
|
||||
- (observed) 위에 실은 `.nft` 와 유닛의 내용은 저장소 원본과 같다. 다만 **호스트에 실제로 깔려 있는 두 파일은 그것이 아니다** — 「배포된 것과 적어 둔 것이 다르다」를 본다.
|
||||
- (inferred) 이 이동으로 L7 홉 수가 2홉 그대로라는 판정이 이 배치의 전제다. 늘어난 것은 커널이 하는 L4 전달 한 번뿐이라 `X-Forwarded-*` 계약이 그대로 성립한다.
|
||||
- (inferred) 이 이동으로 L7 홉 수가 2홉 그대로라는 판정이 이 배치의 전제다. 늘어난 것은 커널이 하는 L4 전달 한 번뿐이라 홉 수 자체는 맞다. 다만 **홉 수가 그대로여도 `X-Forwarded-*` 계약은 성립하지 않았다** — 위 두 절에서 실제로 재 보니 클라이언트 주소가 엣지에 닿기 전에 덮이고, Traefik 이 신뢰 목록 없이 한 번 더 덮었다.
|
||||
- (unknown) `curl -I http://192.168.122.11` 의 전체 출력은 캡처해 두지 않았다. 가이드도 봐야 할 줄만 적었다.
|
||||
- (unknown) `systemctl status nginx` 두 번과 `ls -l /etc/nginx/sites-enabled/`, 파일 찾기, 체인 조회, `journalctl -u nginx`, access 로그 두 줄은 가이드가 적어 둔 명령이고 출력이 남아 있지 않다.
|
||||
- (unknown) libvirt 의 `firewall_backend` 가 iptables 일 때도 `guest_input` 구멍이 필요한지는 재지 않았다. 이 호스트는 nftables 백엔드다.
|
||||
|
||||
+38
-8
@@ -58,7 +58,7 @@ sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
| 4 | agent 설치 | 토큰 파일을 옮긴 뒤 `[kc-lab-2]` 안에서 |
|
||||
| 5 | 워크스테이션에서 쓰기 | `[워크스테이션]` |
|
||||
|
||||
편집기를 여는 곳은 한 군데다. 2번에서 kubeconfig 의 `server:` 줄 하나를 고친다.
|
||||
편집기를 여는 곳은 둘이다. 2번에서 kubeconfig 의 `server:` 줄 하나를 고치고, 4번에서 agent 유닛의 토큰 경로 한 줄을 고친다.
|
||||
|
||||
## 이 단계가 세우는 것
|
||||
|
||||
@@ -266,19 +266,29 @@ sudo install -m 600 -o root -g root ~/node-token /etc/rancher/node-token
|
||||
sudo ls -l /etc/rancher/node-token
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-2] ⑦ 유닛이 그 자리를 보게 고치고 다시 읽는다"
|
||||
sudo sed -i "s|$HOME/node-token|/etc/rancher/node-token|" /etc/systemd/system/k3s-agent.service
|
||||
```bash label="[kc-lab-2] ⑦ agent 유닛을 편집기로 연다"
|
||||
sudo nano /etc/systemd/system/k3s-agent.service
|
||||
```
|
||||
|
||||
⑧ `ExecStart=` 아래에서 `'--token-file'` 다음 줄의 경로를 `/etc/rancher/node-token` 으로 고친다. 고치고 나면 그 두 줄이 이렇게 보인다.
|
||||
|
||||
```text label="⑧ 고치고 난 뒤의 k3s-agent.service 두 줄"
|
||||
'--token-file' \
|
||||
'/etc/rancher/node-token' \
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-2] ⑨ 유닛을 다시 읽히고 agent 를 재시작한다"
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart k3s-agent
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-2] ⑧ 이제 홈의 사본을 지운다"
|
||||
```bash label="[kc-lab-2] ⑩ 이제 홈의 사본을 지운다"
|
||||
rm ~/node-token
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
**⑥⑦ 을 건너뛰고 `rm ~/node-token` 만 치면 이 노드는 다시 못 뜬다.** 설치 스크립트가 `--token-file` 에 준 경로를 유닛의 `ExecStart` 에 그대로 굽기 때문에, 파일이 사라지면 agent 가 영원히 그 파일을 기다린다. 2026-09-17 에 A-4 를 돌려 노드 전원을 뽑았다 다시 켜면서 그대로 겪었다(observed) — VM 은 떴는데 노드가 `NotReady` 에서 안 돌아왔다.
|
||||
**⑥부터 ⑨ 까지를 건너뛰고 `rm ~/node-token` 만 치면 이 노드는 다시 못 뜬다.** 설치 스크립트가 `--token-file` 에 준 경로를 유닛의 `ExecStart` 에 그대로 굽기 때문에, 파일이 사라지면 agent 가 영원히 그 파일을 기다린다. 2026-09-17 에 A-4 를 돌려 노드 전원을 뽑았다 다시 켜면서 그대로 겪었다(observed) — VM 은 떴는데 노드가 `NotReady` 에서 안 돌아왔다.
|
||||
|
||||
:::
|
||||
|
||||
@@ -295,12 +305,14 @@ k3s-agent.service: Scheduled restart job, restart counter is at 1.
|
||||
'/etc/rancher/node-token' \
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-2] ⑨ lab host 로 나온다"
|
||||
```bash label="[kc-lab-2] ⑪ lab host 로 나온다"
|
||||
exit
|
||||
```
|
||||
|
||||
**예상 결과** — 설치가 끝나면 `k3s-agent.service` 가 그 노드에 서고, lab host 의 `kubectl get nodes` 에 `kc-lab-2` 가 한 줄 더 붙는다.
|
||||
|
||||
**⑦ 을 편집기로 여는 까닭** — 이 실험대는 `sudo sed -i "s|$HOME/node-token|/etc/rancher/node-token|" /etc/systemd/system/k3s-agent.service` 로 고쳤다(observed). `sed -i` 는 패턴이 안 맞아도 조용히 성공하고, 이 패턴에는 `$HOME` 이 들어 있어 설치할 때와 다른 계정으로 치면 아무 줄도 안 바뀐 채 ⑨ 가 이어서 돈다. 그러면 지금 떠 있는 노드는 멀쩡해 보이고 다음 재부팅에서야 `Waiting for file` 로 멎는다. 유닛을 열면 고칠 줄이 눈에 보이고, 고쳤는지도 저장하기 전에 눈으로 본다. 이 편집기 형태는 이 실험대에서 치지 않았다(unknown).
|
||||
|
||||
**왜 필요한가** — 설치 스크립트는 `sudo` 아래 root 로 도니 홈의 `600` 파일도 읽는다. `--token` 대신 `--token-file` 을 쓰면 토큰이 명령줄에 안 들어가므로 프로세스 목록과 셸 히스토리에 남지 않고, 토큰을 꺼낸 셸과 같은 셸에서 쳐야 한다는 제약도 없어진다.
|
||||
|
||||
이 실험대는 두 줄로 했다(observed).
|
||||
@@ -313,7 +325,7 @@ ssh kc-lab-2 "curl -sfL https://get.k3s.io | sudo sh -s - agent \
|
||||
--node-ip 192.168.122.12; rm -f /tmp/token"
|
||||
```
|
||||
|
||||
두 줄 안에 원격 셸 둘과 `sudo` 둘, 파이프 하나, 설치 스크립트 하나, 마지막 삭제 하나가 겹쳐 있다. 실패했을 때 어느 쪽이 실패했는지 갈리지 않아 위에서는 ① 부터 ⑦ 까지로 나눴다. 토큰이 `/tmp/token` 대신 자기 홈에 놓이고 `sudo tee` 대신 `scp` 와 `chmod 600` 이 그 파일을 만드는 것도 그래서 달라진다. 이 나눈 형태는 이 실험대에서 치지 않았다(unknown).
|
||||
두 줄 안에 원격 셸 둘과 `sudo` 둘, 파이프 하나, 설치 스크립트 하나, 마지막 삭제 하나가 겹쳐 있다. 실패했을 때 어느 쪽이 실패했는지 갈리지 않아 위에서는 ① 부터 ⑪ 까지로 나눴다. 토큰이 `/tmp/token` 대신 자기 홈에 놓이고 `sudo tee` 대신 `scp` 와 `chmod 600` 이 그 파일을 만드는 것도 그래서 달라진다. 이 나눈 형태는 이 실험대에서 치지 않았다(unknown).
|
||||
|
||||
**문제가 생기면** — agent 설치는 성공했는데 노드가 안 보이면 로그에 `--token is required` 가 있는지 본다. 토큰이 빈 값이었으면 설치 스크립트는 내려받기와 유닛 생성과 활성화까지 다 성공으로 찍고 끝나고, 유닛은 `Restart=always` 라 5초마다 조용히 재시도한다.
|
||||
|
||||
@@ -522,7 +534,25 @@ subject=O = system:nodes, CN = system:node:kc-lab-2
|
||||
- (unknown) 3번과 4번의 나눈 형태는 이 실험대에서 치지 않았다. 이 실험대가 친 것은 원격 셸 둘을 파이프로 이은 두 줄이고, 나눈 형태로 같은 클러스터가 서는지는 다시 재지 않았다.
|
||||
- (unknown) 2번의 ②④ 도 이 실험대에서 치지 않았다. 이 실험대가 친 것은 치환 한 줄이고, 받아 놓고 편집기로 고친 kubeconfig 로 같은 클러스터가 보이는지는 다시 재지 않았다.
|
||||
- (unknown) 5번의 ②③도 이 실험대에서 치지 않았다. 이 실험대가 친 것은 원격 셸을 두 겹으로 겹친 한 줄이다.
|
||||
- (unknown) 인증서 SAN 조회와 두 `journalctl` 은 가이드가 적어 둔 명령이고 이 실험대가 캡처한 출력이 없다. SAN 에 두 주소가 들어 있다는 것은 주소 치환과 터널이 둘 다 통한다는 사실로 뒷받침된다(inferred).
|
||||
- (observed) 인증서 SAN 조회와 두 `journalctl` 을 2026-09-17 에 쳤다. **SAN 에 두 주소가 다 들어 있다** — `127.0.0.1` 과 `192.168.122.11` 이라, kubeconfig 의 주소를 어느 쪽으로 고쳐도 검증이 통과한다.
|
||||
|
||||
```text label="SAN 실측"
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc,
|
||||
DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:kc-lab-1,
|
||||
IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1,
|
||||
IP Address:192.168.122.11, IP Address:10.43.0.1, IP Address:192.168.122.11
|
||||
```
|
||||
|
||||
`192.168.122.11` 이 두 번 나오는데 중복이고 동작에는 영향이 없다. `10.43.0.1` 은 클러스터 안에서 API 서버를 부르는 Service 주소다.
|
||||
|
||||
- (observed) **`journalctl -u k3s-agent` 는 정상일 때도 `E` 로 시작하는 줄이 가득하다.** 이것을 모르면 「막혔을 때 여기를 보라」는 안내대로 열었다가 멀쩡한 노드를 고장으로 읽는다. 같은 순간 `kubectl get nodes` 는 둘 다 `Ready` 였다.
|
||||
|
||||
```text label="Ready 인 agent 의 로그"
|
||||
E0917 07:42:16.919786 conn.go:353] "Error on socket receive" err="read tcp 127.0.0.1:10250->127.0.0.1:55428: use of closed network connection"
|
||||
```
|
||||
|
||||
`kubectl exec` 나 `logs` 로 연결했다가 끊을 때마다 한 줄씩 남는다. **봐야 할 것은 `E` 인지가 아니라 문구다** — 토큰이나 주소가 틀렸으면 `failed to get CA certs` 나 `401 Unauthorized` 가 나온다.
|
||||
- (unknown) 원본 가이드에 되돌리는 절차가 없다. `k3s-uninstall.sh` 라는 이름이 가이드에 한 번도 안 나오고 이 실험대도 부른 적이 없다.
|
||||
- (unknown) 설치 명령 두 줄은 재실행으로 검증되지 않았다. 토큰 108자도 k3s 판올림에 따라 달라진다.
|
||||
|
||||
|
||||
+7
-3
@@ -55,6 +55,8 @@ source:
|
||||
|
||||
이 단계에는 손으로 쓰는 파일이 없다. 세우는 것이 전부 매니페스트 한 장에 들어 있고, 그 원문은 저장소의 `deploy/lab/k8s/keycloak-cluster.yaml` 이다.
|
||||
|
||||
**공개 이름은 랩 안에서 안 풀린다.** `auth.hyeonworks.com` 같은 공개 이름이 랩 호스트에서도 게스트에서도 호스트 자신의 tailnet 주소 `100.83.212.4` 로 풀리는데 그 주소에는 443 을 듣는 것이 없다. 이름만 치면 `curl` 이 `000` 을 낸다(2026-09-17, 랩 호스트와 `kc-lab-1` 양쪽에서 쳐서 확인했다, observed). 그래서 랩 안에서 치는 `curl` 에는 `--resolve <이름>:443:192.168.122.10` 을 붙여 엣지 게스트를 짚었고, 그 형태로는 정문이 `200` 이다(observed). tailnet 에 붙은 다른 기계에서 치면 이름 그대로 닿으므로 `--resolve` 가 필요 없다.
|
||||
|
||||
## 이 단계가 세우는 것
|
||||
|
||||
가이드 05 의 「이 단계가 끝나면」은 두 줄이다.
|
||||
@@ -109,7 +111,8 @@ ls deploy/lab/k8s/
|
||||
### 확인 ② 이 이름이 아직 Keycloak 이 아니다
|
||||
|
||||
```bash label="[lab host] 앞 단계에서 잰 값을 그대로 다시 잰다"
|
||||
curl -s -o /dev/null -w '%{http_code} tls=%{ssl_verify_result}\n' https://auth.hyeonworks.com/
|
||||
curl -s -o /dev/null -w '%{http_code} tls=%{ssl_verify_result}\n' --resolve auth.hyeonworks.com:443:192.168.122.10 \
|
||||
https://auth.hyeonworks.com/
|
||||
```
|
||||
|
||||
**어디를 봐야 하는가** — 앞 단계에서 잰 `404 tls=0` 이 그대로인가. **앞의 코드를 같이 본다.**
|
||||
@@ -465,7 +468,8 @@ vendor_cluster_size{cache_manager="keycloak",node="keycloak-0-46674"} 2.0
|
||||
**무엇을 확인하는가** — nginx 에서 Traefik, Ingress, Service 를 지나 파드까지 전부 이어졌는지.
|
||||
|
||||
```bash label="[lab host] 앞 두 단계와 같은 명령"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
curl -s -o /dev/null -w '%{http_code}\n' --resolve auth.hyeonworks.com:443:192.168.122.10 \
|
||||
https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
```text
|
||||
@@ -477,7 +481,7 @@ curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/mast
|
||||
**이 결과가 의미하는 것** — `200` 이면 2홉이 다 이어졌다. `502` 나 `503` 이면 뒤에서부터 되짚는다 — Ingress 가 있는지(확인 ①), Service 뒤에 파드가 있는지(확인 ④), 파드가 Ready 인지(확인 ②) 순서다. 처음 보는 오류라 헤더가 필요하면 읽는 형태로 바꾼다.
|
||||
|
||||
```bash label="[lab host] 처음 보는 오류는 헤더까지 읽는다"
|
||||
curl -I https://auth.hyeonworks.com/realms/master
|
||||
curl -I --resolve auth.hyeonworks.com:443:192.168.122.10 https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
**이 `200` 은 앞 단계를 끝냈을 때의 값이다.** 인증서 단계를 건너뛰고 이 단계만 했다면 443 을 듣는 것이 없으므로 여기는 `000` 이다. 2026-09-17 에 그 상태에서 재 보니 이렇게 나왔다(observed).
|
||||
|
||||
+16
-9
@@ -124,8 +124,9 @@ kubectl -n keycloak-lab scale deployment/postgres --replicas=0
|
||||
kubectl -n keycloak-lab wait --for=delete pod -l app=postgres --timeout=120s
|
||||
```
|
||||
|
||||
```bash label="[lab host] ③ 게스트를 ACPI 정상 종료한다"
|
||||
virsh shutdown kc-lab-1 && virsh shutdown kc-lab-2
|
||||
```bash label="[lab host] ③ 게스트를 한 대씩 ACPI 정상 종료한다"
|
||||
virsh shutdown kc-lab-1
|
||||
virsh shutdown kc-lab-2
|
||||
```
|
||||
|
||||
```bash label="[lab host] ④ 호스트를 끈다"
|
||||
@@ -136,6 +137,8 @@ sudo systemctl poweroff
|
||||
|
||||
**왜 필요한가** — ③ 의 `virsh shutdown` 은 게스트 systemd 가 k3s 를 멈추고 k3s 가 컨테이너에 SIGTERM 을 보내는 연쇄다. 유예 시간이 짧으면 PostgreSQL 이 강제 종료되어 다음 기동에 crash recovery 가 돈다. ① 과 ② 로 미리 내려 두면 그 연쇄가 데이터베이스까지 닿지 않는다. 순서를 뒤집어 PostgreSQL 을 먼저 내리면 Keycloak 이 데이터베이스 없이 남아 기동 실패와 재시작을 반복한다.
|
||||
|
||||
**③ 의 두 줄을 `&&` 로 잇지 않는다.** 두 게스트는 서로 앞뒤가 없고, `A && B` 는 A 가 성공했을 때만 B 를 실행한다. `kc-lab-1` 이 이미 `shut off` 면 `virsh shutdown` 이 `domain is not running` 으로 실패해 `kc-lab-2` 는 켜진 채로 남고, 바로 다음 ④ 가 그 게스트를 강제로 끈다 — 이 단계가 막으려던 바로 그 상태다. 이 실험대는 `virsh shutdown kc-lab-1 && virsh shutdown kc-lab-2` 로 쳤고(observed) 나눈 형태는 치지 않았다(unknown).
|
||||
|
||||
**문제가 생기면** — `wait` 가 `120s` 안에 안 끝나면 파드가 종료 중에 걸린 것이다. `kubectl -n keycloak-lab get pods -o wide` 로 어느 파드가 어느 노드에서 `Terminating` 인지 보고, 그 상태로 ③ 을 치지 않는다.
|
||||
|
||||
### 3. 종료의 역순으로 되살린다
|
||||
@@ -144,25 +147,29 @@ sudo systemctl poweroff
|
||||
|
||||
게스트를 띄워 노드가 `Ready` 가 되기를 기다린 뒤, PostgreSQL 을 먼저 올리고 Keycloak 을 나중에 올린다.
|
||||
|
||||
```bash label="[lab host] ① 게스트를 띄우고 노드가 Ready 가 되기를 기다린다"
|
||||
virsh start kc-lab-1 && virsh start kc-lab-2
|
||||
```bash label="[lab host] ① 게스트를 한 대씩 띄운다"
|
||||
virsh start kc-lab-1
|
||||
virsh start kc-lab-2
|
||||
```
|
||||
|
||||
```bash label="[lab host] ② 노드가 Ready 가 되기를 기다린다"
|
||||
kubectl get nodes
|
||||
```
|
||||
|
||||
```bash label="[lab host] ② PostgreSQL 을 먼저 올리고 기동을 확인한다"
|
||||
```bash label="[lab host] ③ PostgreSQL 을 먼저 올리고 기동을 확인한다"
|
||||
kubectl -n keycloak-lab scale deployment/postgres --replicas=1
|
||||
kubectl -n keycloak-lab rollout status deployment/postgres
|
||||
```
|
||||
|
||||
```bash label="[lab host] ③ Keycloak 을 두 벌로 올린다"
|
||||
```bash label="[lab host] ④ Keycloak 을 두 벌로 올린다"
|
||||
kubectl -n keycloak-lab scale statefulset/keycloak --replicas=2
|
||||
```
|
||||
|
||||
**예상 결과** — ① 의 `kubectl get nodes` 에 두 노드가 `Ready` 로 나온다. ② 의 `rollout status` 가 배포 완료로 돌아온 뒤에 ③ 을 친다.
|
||||
**예상 결과** — ② 의 `kubectl get nodes` 에 두 노드가 `Ready` 로 나온다. ③ 의 `rollout status` 가 배포 완료로 돌아온 뒤에 ④ 를 친다.
|
||||
|
||||
**왜 필요한가** — PostgreSQL 이 먼저다. Keycloak 이 데이터베이스 없이 뜨면 기동에 실패하기 때문이다. 그리고 스케일을 0 으로 내려 둔 것은 자동으로 복구되지 않는다. 게스트를 켜고 노드가 `Ready` 가 돼도 파드 수는 0 그대로이므로 ② 와 ③ 을 명시적으로 쳐야 한다.
|
||||
**왜 필요한가** — PostgreSQL 이 먼저다. Keycloak 이 데이터베이스 없이 뜨면 기동에 실패하기 때문이다. 그리고 스케일을 0 으로 내려 둔 것은 자동으로 복구되지 않는다. 게스트를 켜고 노드가 `Ready` 가 돼도 파드 수는 0 그대로이므로 ③ 과 ④ 를 명시적으로 쳐야 한다. ① 을 두 줄로 나눈 까닭은 2번의 ③ 과 같다 — `kc-lab-1` 이 이미 `running` 이면 `virsh start` 가 `domain is already active` 로 실패해 `kc-lab-2` 가 안 뜨고, 그러면 ② 에 노드가 하나만 나와 게스트를 안 띄운 것인지 노드가 안 붙은 것인지 갈리지 않는다.
|
||||
|
||||
**문제가 생기면** — ① 에서 노드가 `Ready` 로 안 올라오면 게스트 안의 k3s 유닛부터 본다. ② 의 `rollout status` 가 멈춰 있으면 PostgreSQL 이 crash recovery 중일 수 있으니 아래 확인 방법의 `postmaster.pid` 를 함께 본다.
|
||||
**문제가 생기면** — ② 에서 노드가 `Ready` 로 안 올라오면 게스트 안의 k3s 유닛부터 본다. ③ 의 `rollout status` 가 멈춰 있으면 PostgreSQL 이 crash recovery 중일 수 있으니 아래 확인 방법의 `postmaster.pid` 를 함께 본다.
|
||||
|
||||
## 구성 값
|
||||
|
||||
|
||||
+46
-9
@@ -51,7 +51,7 @@ sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
|---|---|---|
|
||||
| `[lab host]` | `test-server`. `virsh` 가 도는 곳 | `ssh test-server` |
|
||||
|
||||
편집기를 여는 곳은 한 군데다. 4번에서 `~/.bashrc` 를 연다. 나머지는 전부 조회·설치·유닛 조작이라 운영자가 평소에 치는 CLI 를 그대로 쓴다.
|
||||
편집기를 여는 곳은 4번 한 단계다. 거기서 `~/.bashrc` 와 `~/.config/libvirt/libvirt.conf` 를 차례로 연다. 나머지는 전부 조회·설치·유닛 조작이라 운영자가 평소에 치는 CLI 를 그대로 쓴다.
|
||||
|
||||
## 이 단계가 세우는 것
|
||||
|
||||
@@ -85,7 +85,7 @@ sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
|
||||
전제는 가이드가 두 줄로 적었다 — 물리 기계 한 대가 있고, 배포판은 상관없다. 이 실험대는 Arch Linux 로 세웠고 배포판이 다르면 패키지 이름만 달라진다. 앞 단계가 없으므로 이 절차는 다른 무엇도 전제하지 않는다.
|
||||
|
||||
**되돌리는 절차는 원본 가이드 00 에 없다**(unknown). 가이드 7편 가운데 되돌리기를 적은 편은 단계 03 하나다. 이 단계가 호스트에 남기는 것은 다섯이다.
|
||||
**되돌리는 절차는 원본 가이드 00 에 없다**(unknown). 가이드 7편 가운데 되돌리기를 적은 편은 단계 03 하나다. 이 단계가 호스트에 남기는 것은 여섯이다.
|
||||
|
||||
| 남는 것 | 어디에 |
|
||||
|---|---|
|
||||
@@ -93,6 +93,7 @@ sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
| `libvirt` 보조 그룹 | 사용자 계정 |
|
||||
| `libvirtd.socket` 활성화 | systemd |
|
||||
| `export` 한 줄 | `~/.bashrc` |
|
||||
| `uri_default` 한 줄 | `~/.config/libvirt/libvirt.conf` |
|
||||
| `default` 네트워크의 autostart | libvirt 설정 |
|
||||
|
||||
무엇을 어떤 순서로 걷어내는지는 가이드에 적혀 있지 않고 이 실험대도 걷어내 본 적이 없다. 여기에 되돌리는 명령을 적으려면 지어내야 하므로 적지 않는다.
|
||||
@@ -281,11 +282,21 @@ ssh 로 한 줄 LIBVIRT_DEFAULT_URI=[(비어 있음)]
|
||||
대화형 셸 LIBVIRT_DEFAULT_URI=qemu:///system
|
||||
```
|
||||
|
||||
**셸과 무관하게 고정하려면 libvirt 자기 설정에 넣는다.** 이 실험대에는 그 파일도 있었다(observed).
|
||||
**셸과 무관하게 고정하려면 libvirt 자기 설정에 넣는다.** 이 실험대에는 그 파일도 있었고, 같은 줄을 `echo 'uri_default = "qemu:///system"' >> ~/.config/libvirt/libvirt.conf` 로 넣었다(observed). 여기서도 디렉터리는 명령으로 만들고 파일은 편집기로 연다.
|
||||
|
||||
```bash label="[lab host] 셸을 안 타는 자리에 고정한다"
|
||||
```bash label="[lab host] ④ 셸을 안 타는 자리를 만든다"
|
||||
mkdir -p ~/.config/libvirt
|
||||
echo 'uri_default = "qemu:///system"' >> ~/.config/libvirt/libvirt.conf
|
||||
```
|
||||
|
||||
```bash label="[lab host] ⑤ libvirt 자기 설정을 편집기로 연다"
|
||||
nano ~/.config/libvirt/libvirt.conf
|
||||
```
|
||||
|
||||
```text label="⑥ libvirt.conf 에 더할 줄"
|
||||
uri_default = "qemu:///system"
|
||||
```
|
||||
|
||||
```bash label="[lab host] ⑦ 어느 하이퍼바이저를 보는지 다시 본다"
|
||||
virsh uri
|
||||
```
|
||||
|
||||
@@ -294,6 +305,8 @@ virsh uri
|
||||
qemu:///system
|
||||
```
|
||||
|
||||
첫 줄은 `virsh` 가 그 파일을 읽었다고 알리는 것이고 둘째 줄이 답이다. 이 나눈 형태는 이 실험대에서 치지 않았다(unknown).
|
||||
|
||||
**둘 중 하나만 있으면 어디선가 어긋난다.** rc 만 있으면 `ssh` 한 줄에서 `qemu:///session` 을 보게 되고 — 그쪽에는 VM 이 없으므로 **목록이 빈 채로 나와 「VM 이 죽었다」로 읽힌다** — libvirt 설정만 있으면 대화형 셸의 `echo $LIBVIRT_DEFAULT_URI` 가 비어서 「설정이 안 됐다」로 읽힌다. 둘 다 넣어 두는 편이 낫다.
|
||||
|
||||
따라 하는 사람에게는 편집기 쪽이 맞다. `echo >>` 는 같은 가이드를 두 번 따라 하면 같은 줄을 한 번 더 붙이고, 파일에 이미 무엇이 들어 있는지도 보여 주지 않는다. 파일을 열면 둘 다 해결된다.
|
||||
@@ -404,11 +417,35 @@ groups
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
- (observed) libvirt `12.7.0` 과 `QEMU emulator version 11.1.1`, `groups` 의 세 이름, `default` 네트워크가 `active` 이고 autostart 가 `yes` 인 것, `virsh uri` 가 내놓은 `qemu:///system`.
|
||||
- (unknown) `lsmod | grep kvm` 의 출력은 캡처해 두지 않았다. 가이드도 줄 모양만 적고 값을 싣지 않았다.
|
||||
- (unknown) 가이드의 실측 줄은 이 호스트가 16 코어 전부에서 지원한다고 적었는데 대상 환경 쪽은 논리 코어 8(i5-1135G7)로 적혀 있다. 두 값이 어긋나고 어느 쪽이 이 호스트의 값인지는 재지 않았다. 세 게스트의 vCPU 합이 5 라 8 에서도 16 에서도 CPU overcommit 이 아니므로 이 단계의 판정은 어느 쪽이어도 바뀌지 않는다.
|
||||
- (observed) `lsmod | grep kvm` 의 출력을 2026-09-17 에 받았다. 세 줄이고, 마지막 칸이 그 모듈을 쓰는 수다.
|
||||
|
||||
```text
|
||||
kvm_intel 524288 11
|
||||
kvm 1490944 6 kvm_intel
|
||||
irqbypass 16384 1 kvm
|
||||
```
|
||||
|
||||
- (observed) **이 호스트는 논리 코어 8 이다** — 가이드가 적은 「16 코어 전부」가 아니다. 2026-09-17 에 `nproc` 이 `8`, `lscpu` 가 `11th Gen Intel(R) Core(TM) i5-1135G7`, 물리 4 코어에 코어당 스레드 2 라고 답했다. 가이드의 그 줄은 다른 기계의 값이다.
|
||||
|
||||
```bash label="[lab host] 코어 수를 가른다"
|
||||
nproc
|
||||
lscpu | grep -E "^Model name|^CPU\(s\):|^Core\(s\)|^Thread\(s\)"
|
||||
```
|
||||
|
||||
- (observed) `systemctl status libvirtd.socket` 도 같은 날 쳤다. 보는 줄은 `Active` 와 `Listen` 과 `Triggers` 셋이다.
|
||||
|
||||
```text
|
||||
● libvirtd.socket - libvirt legacy monolithic daemon socket
|
||||
Loaded: loaded (/usr/lib/systemd/system/libvirtd.socket; enabled; preset: disabled)
|
||||
Active: active (running) since Thu 2026-09-03 19:00:35 KST; 1 week 6 days ago
|
||||
Triggers: ● libvirtd.service
|
||||
Listen: /run/libvirt/libvirt-sock (Stream)
|
||||
```
|
||||
|
||||
`Triggers` 가 `libvirtd.service` 를 가리키는 것이 소켓 활성화가 걸렸다는 뜻이다. 소켓이 먼저 뜨고 첫 접속이 올 때 서비스가 깨어난다 — 그래서 `.service` 가 아니라 `.socket` 을 `enable` 한다.
|
||||
- (unknown) 원본 가이드에 되돌리는 절차가 없다. 패키지와 그룹과 유닛과 `~/.bashrc` 와 네트워크 autostart 를 걷어내 본 적이 없다.
|
||||
- (unknown) `sudo modprobe kvm_intel` 과 `systemctl status libvirtd.socket` 은 막혔을 때 치라고 가이드가 적어 둔 명령이고, 이 실험대에서는 막히지 않아 치지 않았다.
|
||||
- (unknown) `nano ~/.bashrc` 로 여는 형태는 이 실험대가 치지 않았다. 이 실험대는 `echo >>` 로 넣었고, 편집기 쪽은 같은 상태에 닿는 형태로 적었다.
|
||||
- (unknown) `sudo modprobe kvm_intel` 은 막혔을 때 치라고 가이드가 적어 둔 명령이고, 이 실험대에서는 막힌 적이 없어 치지 않았다.
|
||||
- (unknown) `nano ~/.bashrc` 와 `nano ~/.config/libvirt/libvirt.conf` 로 여는 형태는 이 실험대가 치지 않았다. 이 실험대는 두 파일 다 `echo >>` 로 넣었고, 편집기 쪽은 같은 상태에 닿는 형태로 적었다.
|
||||
- (external) Debian 과 Ubuntu 의 패키지 이름은 가이드가 참고로 적어 둔 것이고 이 실험대는 Arch 로 세웠다.
|
||||
- (external) 보조 그룹이 로그인 시점에 고정된다는 것, `virsh net-start` 가 이미 `active` 면 실패한다는 것, `sudo` 가 환경 변수를 물려주지 않는다는 것은 리눅스와 libvirt 의 동작이다. 이 실험대가 그 세 가지를 따로 재 보지는 않았다.
|
||||
- (unknown) 만드는 명령은 구축할 때 친 것을 옮긴 것이라, 지금 다시 쳐도 같은 상태가 되는지는 확인되지 않았다.
|
||||
|
||||
+4
-1
@@ -53,6 +53,8 @@ source:
|
||||
|
||||
이 단계에도 손으로 쓰는 파일이 없다. 세우는 것이 전부 매니페스트 한 장에 들어 있고, 그 원문은 저장소의 `deploy/lab/k8s/observability.yaml` 이다.
|
||||
|
||||
**공개 이름은 랩 안에서 안 풀린다.** `auth.hyeonworks.com` 같은 공개 이름이 랩 호스트에서도 게스트에서도 호스트 자신의 tailnet 주소 `100.83.212.4` 로 풀리는데 그 주소에는 443 을 듣는 것이 없다. 이름만 치면 `curl` 이 `000` 을 낸다(2026-09-17, 랩 호스트와 `kc-lab-1` 양쪽에서 쳐서 확인했다, observed). 그래서 랩 안에서 치는 `curl` 에는 `--resolve <이름>:443:192.168.122.10` 을 붙여 엣지 게스트를 짚었고, 그 형태로는 정문이 `200` 이다(observed). tailnet 에 붙은 다른 기계에서 치면 이름 그대로 닿으므로 `--resolve` 가 필요 없다.
|
||||
|
||||
## 이 단계가 세우는 것
|
||||
|
||||
가이드 06 의 「이 단계가 끝나면」은 두 줄이다.
|
||||
@@ -273,7 +275,8 @@ kubectl -n observability exec deploy/prometheus -- \
|
||||
**이 결과가 의미하는 것** — 503 이 나는 동안에도 `up` 은 1 이었다(observed). 프로세스가 살아 있고 `/metrics` 가 응답하기만 하면 1 이 되므로, 살아 있지만 쓸모없는 상태를 이 지표로는 보지 못한다. 경보를 `up == 0` 하나로 걸면 그 상태를 통째로 놓친다. 그래서 기능 지표를 함께 본다. 밖에서 실제 응답을 받아 보는 것이 가장 짧다.
|
||||
|
||||
```bash label="[lab host] up 과 나란히 놓고 비교한다"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
curl -s -o /dev/null -w '%{http_code}\n' --resolve auth.hyeonworks.com:443:192.168.122.10 \
|
||||
https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
코드 한 칸을 `up` 의 1 과 0 옆에 놓는다. `up=1` 인데 이쪽이 200 이 아니면 그 조합이 곧 「살아 있지만 쓸모없는」 상태의 증거가 된다. 처음 보는 오류를 파고들 때는 값만 뽑는 형태를 버리고 헤더까지 읽는 형태로 바꾼다.
|
||||
|
||||
+49
@@ -74,6 +74,15 @@ df -h /
|
||||
|
||||
**이 결과가 의미하는 것** — 이 네 값이 철거의 성공 판정 기준이 된다. 여기를 건너뛰면 지운 뒤에 「원래 몇 개였지」를 되짚을 방법이 없다.
|
||||
|
||||
2026-09-17 에 다시 세운 실험대에서 같은 네 줄을 쳤다(observed). 도메인 3 대에 볼륨 7 개, 예약 3 줄까지는 2026-09-10 과 같고 **디스크만 11G 가 아니라 18G 다.** 그사이에 k3s 와 컨테이너 이미지가 쌓였다. 그래서 이 네 값은 「이 호스트의 정답」이 아니라 **지금 이 실행에서 받아 두는 대조군**이다 — 위에 실은 숫자와 다르다고 잘못된 것이 아니다.
|
||||
|
||||
```text label="2026-09-17 의 네 값"
|
||||
도메인 kc-lab-1 · kc-lab-edge · kc-lab-2 (셋 다 running)
|
||||
볼륨 base.qcow2 + kc-lab-{1,2,edge}.qcow2 + seed-kc-lab-{1,2,edge}.iso = 7
|
||||
예약 52:54:00:aa:bb:{10,11,12} = 3 줄
|
||||
디스크 /dev/nvme0n1p3 226G 18G 197G 9% /
|
||||
```
|
||||
|
||||
## 실행 절차
|
||||
|
||||
### 1. 게스트 세 대를 지운다
|
||||
@@ -150,6 +159,30 @@ zsh 에서 루프로 돌리면 이 오류를 만난다. zsh 는 따옴표 없는
|
||||
|
||||
## 인증서는 건드리지 않는다
|
||||
|
||||
**이 절의 전제가 03·04 뒤로 깨져 있다**(2026-09-17, observed). 아래 본문은 인증서가 랩 호스트의 `/etc/letsencrypt/` 에 있다고 보고 그 디렉터리를 정책으로 남긴다. 그런데 03 이 nginx 를 `kc-lab-edge` 로 옮겼고 04 가 certbot 을 거기 깔았다. **지금 서빙하는 인증서는 그 게스트 안에 산다.** 그리고 이 절차의 1번은 게스트를 `--remove-all-storage` 로 지운다 — 인증서는 그 안에서 같이 사라진다.
|
||||
|
||||
두 기계를 나란히 열어 보면 이렇다.
|
||||
|
||||
```bash label="[lab host] 어느 쪽에 무엇이 있나"
|
||||
ls -la /etc/letsencrypt/
|
||||
ssh kc-lab-edge 'sudo ls -la /etc/letsencrypt/'
|
||||
```
|
||||
|
||||
| 기계 | `/etc/letsencrypt/` | certbot | 타이머 | 서빙 |
|
||||
|---|---|---|---|---|
|
||||
| 랩 호스트 | 있다 (읽으려면 `sudo`) | `/usr/bin/certbot` | 없다 | 안 한다 — 80·443 을 안 듣는다 |
|
||||
| `kc-lab-edge` | 있다 — `cli.ini` · `renewal-hooks` | `/usr/bin/certbot` | `certbot.timer` | **한다** |
|
||||
|
||||
그래서 아래 백업 명령은 **틀린 기계를 묶는다.** 묶어야 할 것은 게스트 쪽이다.
|
||||
|
||||
```bash label="[lab host] 실제로 서빙하는 인증서를 묶는다"
|
||||
ssh kc-lab-edge "sudo tar czf /tmp/letsencrypt-backup-$(date +%Y%m%d-%H%M%S).tgz -C /etc letsencrypt"
|
||||
scp kc-lab-edge:/tmp/letsencrypt-backup-*.tgz ~/
|
||||
```
|
||||
|
||||
**「철거해도 남는 것」 표의 패키지 줄도 같은 이유로 틀렸다.** 그 줄은 `nginx` 와 `certbot` 을 남는 쪽에 적어 두었는데, 둘 다 게스트 안에 있으므로 게스트와 함께 사라진다. 랩 호스트에 남는 것은 `libvirt` · `qemu` · `kubectl` 과, 쓰이지 않는 호스트 쪽 `certbot` 이다.
|
||||
|
||||
|
||||
`/etc/letsencrypt/` 는 정책으로 남긴다. 한도 때문이 아니다 — Let's Encrypt 의 「같은 이름 조합에 주당 중복 5장」 제한은 가끔 재구축하는 정도로는 근처에도 못 간다.
|
||||
|
||||
남기는 까닭은 지금 재발급이 되는지를 모르기 때문이다. 이 실험대의 이름 셋은 tailnet 주소를 가리키고, `100.64.0.0/10` 은 CGNAT(Carrier-Grade NAT, 통신사 공용 주소 변환)용 예약 대역이라 공개 인터넷에서 라우팅되지 않는다. HTTP-01 검증은 Let's Encrypt 가 우리 서버로 들어오는 방식이므로 그 주소로는 검증이 성립하지 않는다. 지금 설정이 DNS-01 이면 지우고 다시 받으면 끝이고, HTTP-01 이면 검증 방식부터 손봐야 한다. 어느 쪽인지는 certbot 설정을 읽는 열린 물음이 한 줄로 닫는다.
|
||||
@@ -204,6 +237,22 @@ ip -br addr show virbr0
|
||||
|
||||
**이 결과가 의미하는 것** — 디스크에서 3.1GB 가 회수됐다. 잰 내역은 `kc-lab-1` 1.4GB 와 `kc-lab-2` 665MB 와 시드 ISO 세 개(각 370KB)이고, `kc-lab-edge` 의 디스크 크기는 재 두지 않았다. 합계에서 빼면 1GB 안팎인데 그것은 잰 값이 아니라 역산한 값이다.
|
||||
|
||||
**그 역산은 아마 너무 컸다**(2026-09-17, observed). 같은 호스트에 다시 세운 실험대의 볼륨 일곱을 `virsh vol-info` 로 재 보니 엣지가 가장 작다. 선언한 크기와 실제로 먹는 양이 크게 다르고, 오버레이라 쓴 만큼만 먹는다.
|
||||
|
||||
```bash label="[lab host] 볼륨이 실제로 먹는 양을 본다"
|
||||
virsh vol-info --pool default kc-lab-edge.qcow2
|
||||
```
|
||||
|
||||
```text
|
||||
base.qcow2 Capacity=3.00 GiB Allocation=323.25 MiB
|
||||
kc-lab-1.qcow2 Capacity=20.00 GiB Allocation=5.40 GiB
|
||||
kc-lab-2.qcow2 Capacity=20.00 GiB Allocation=3.85 GiB
|
||||
kc-lab-edge.qcow2 Capacity=10.00 GiB Allocation=316.26 MiB
|
||||
seed-kc-lab-{1,2,edge}.iso Capacity=370.00 KiB Allocation=372.00 KiB
|
||||
```
|
||||
|
||||
**이 값을 2026-09-10 의 역산 대신 그대로 끼워 넣지는 않는다** — 다른 날 다른 실험대이고 그 위에 올린 것도 다르다. 다만 엣지가 nginx 하나만 돌리는 게스트라 300MB 대에 머문다는 것은 두 실행에서 같을 가능성이 높고, 그렇다면 그때의 「1GB 안팎」은 과대 추정이다. **다시 철거할 때는 역산하지 말고 지우기 전에 이 명령으로 세 볼륨을 직접 잰다.**
|
||||
|
||||
`virbr0` 가 `DOWN` 인 것은 고장이 아니다. 브리지에 붙은 tap 인터페이스가 하나도 없어서 캐리어가 없는 것으로 표시될 뿐이고, 주소 `192.168.122.1/24` 는 그대로 있다. VM 을 다시 띄우면 그 VM 의 `vnetN` 인터페이스가 브리지에 붙으면서 `UP` 이 된다. `virsh net-start` 를 찾아 헤매지 않는다.
|
||||
|
||||
## 철거해도 남는 것과 사라지는 것
|
||||
|
||||
+49
-2
@@ -369,7 +369,28 @@ sudo nginx -t && sudo systemctl reload nginx
|
||||
|
||||
`ssl_certificate` 에 적는 것은 `cert.pem` 이 아니라 `fullchain.pem` 이다. 서버 인증서만 보내면 중간 인증서가 빠져 체인이 끊기는데, 브라우저는 대개 캐시나 AIA 로 보완해서 정상으로 보이고 캐시가 없는 클라이언트에서만 깨지므로 발견이 늦다. 4번 ③ 에서 certbot 이 찍어 준 경로와 한 글자도 다르면 안 된다.
|
||||
|
||||
**문제가 생기면** — `cannot load certificate` 로 막히면 경로를 본다. `live/auth.hyeonworks.com/` 이라고 적으면 그 디렉터리가 없다. 확인 ② 에서 본 판 번호가 1.25.1 미만인데 `http2` 를 지시어로 썼다면 여기서 `unknown directive "http2"` 가 나온다.
|
||||
**★ 위 ② 의 인증서 경로가 틀렸다**(2026-09-17, observed). `live/hyeonworks.com/` 이라고 적혀 있는데 certbot 이 만드는 디렉터리는 **`live/auth.hyeonworks.com/`** 이다. 이름을 여럿 담은 인증서라도 디렉터리 이름은 `-d` 로 처음 준 이름 하나를 쓴다. 그대로 치면 3번에서 nginx 가 안 뜬다.
|
||||
|
||||
```text label="문서 그대로 쳤을 때"
|
||||
[emerg] cannot load certificate "/etc/letsencrypt/live/hyeonworks.com/fullchain.pem":
|
||||
BIO_new_file() failed (SSL: error:80000002:system library::No such file or directory
|
||||
nginx: configuration file /etc/nginx/nginx.conf test failed
|
||||
```
|
||||
|
||||
```bash label="[kc-lab-edge] 실제 디렉터리 이름을 보고 쓴다"
|
||||
sudo ls /etc/letsencrypt/live/
|
||||
sudo certbot certificates | grep 'Certificate Path'
|
||||
```
|
||||
|
||||
```text label="그 출력"
|
||||
README
|
||||
auth.hyeonworks.com
|
||||
Certificate Path: /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem
|
||||
```
|
||||
|
||||
**아래 「문제가 생기면」의 진단은 방향이 거꾸로다.** `live/auth.hyeonworks.com/` 이 없는 경로가 아니라 **그쪽이 맞는 경로**이고, `live/hyeonworks.com/` 이 디스크에 없다. 4번 ③ 이 찍어 준 경로를 그대로 옮기라는 원칙은 옳고, 위 ② 가 그 원칙을 스스로 어겼다.
|
||||
|
||||
**문제가 생기면** — `cannot load certificate` 로 막히면 경로를 본다. 자기 실험대의 디렉터리 이름은 `sudo ls /etc/letsencrypt/live/` 가 답한다. 확인 ② 에서 본 판 번호가 1.25.1 미만인데 `http2` 를 지시어로 썼다면 여기서 `unknown directive "http2"` 가 나온다.
|
||||
|
||||
### 6. 갱신이 서빙까지 닿게 한다
|
||||
|
||||
@@ -458,6 +479,24 @@ curl -v https://auth.hyeonworks.com/ -o /dev/null
|
||||
|
||||
**어디를 봐야 하는가** — 별표로 시작하는 줄 넷이다. 어떤 TLS 판으로 협상했는가, `subject` 의 CN 이 무엇인가, `issuer` 가 Let's Encrypt 인가, 그리고 `SSL certificate verify ok.` 가 있는가. 그 아래 `<` 로 시작하는 첫 줄이 응답 상태다. `subject` 가 `hyeonworks.com` 인 것이 맞다 — 와일드카드 인증서라 CN 은 apex 이름이고 `auth.hyeonworks.com` 은 SAN 의 `*.hyeonworks.com` 에 걸린다.
|
||||
|
||||
**★ 그런데 이 실험대에서는 CN 이 `auth.hyeonworks.com` 이다**(2026-09-17, observed). 가이드대로 와일드카드를 받으면 위 두 문단처럼 보인다. 이 실험대는 와일드카드를 안 받았고(§「가이드는 여기에 값을 하나 더 적어 두었다」), 계보도 `auth.hyeonworks.com` 하나만 있다.
|
||||
|
||||
```bash label="[lab host] 엣지가 내놓는 인증서가 어느 이름을 덮는가"
|
||||
echo | openssl s_client -connect 192.168.122.10:443 -servername auth.hyeonworks.com 2>/dev/null | openssl x509 -noout -subject -ext subjectAltName -dates
|
||||
```
|
||||
|
||||
```text label="이 실험대의 값"
|
||||
subject=CN=auth.hyeonworks.com
|
||||
X509v3 Subject Alternative Name:
|
||||
DNS:app1.hyeonworks.com, DNS:app2.hyeonworks.com, DNS:auth.hyeonworks.com
|
||||
notBefore=Sep 4 11:29:18 2026 GMT
|
||||
notAfter=Dec 3 11:29:17 2026 GMT
|
||||
```
|
||||
|
||||
**CN 은 합격 조건이 아니다.** 와일드카드를 받았으면 `hyeonworks.com`, 이름을 나열해 받았으면 첫 번째 `-d` 의 이름이 CN 이 된다. 보는 것은 `SSL certificate verify ok.` 한 줄과, 지금 친 이름이 SAN 에 있는가다.
|
||||
|
||||
**이름 셋 밖은 TLS 단계에서 끝난다.** `curl` 은 종료 코드 `60` 을 내고 `%{http_code}` 는 `000` 이 되는데, 화면에서는 「서버가 죽었다」와 같아 보인다. A-4 의 Grafana 탐침이 그렇게 쓸모없어졌다.
|
||||
|
||||
**이 결과가 의미하는 것** — 네 줄이 다 나오면 인증서가 붙었고 체인이 클라이언트 기준으로 검증됐다. 인증서를 처음 붙인 직후에는 코드 한 칸이 아니라 이 화면을 본다. `verify` 줄 대신 `unable to get local issuer certificate` 가 나오면 중간 인증서가 빠진 것이고, 원인은 5번의 인증서 파일 이름이라 확인 ② 로 간다.
|
||||
|
||||
같은 것을 반복해서 재거나 앞뒤 단계의 값과 나란히 비교할 때만 값만 뽑는 형태로 줄인다.
|
||||
@@ -578,7 +617,15 @@ ps -eo pid,lstart,args | grep 'nginx: worker' | grep -v grep
|
||||
|
||||
- (observed) 이름 풀이가 낸 `100.83.212.4`, `certbot plugins` 세 줄, `nginx -v` 두 줄, 체인 4단계와 `Verify return code: 0 (ok)`, `404 tls=0`, 배포판 기본 유닛에 훅이 없다는 것, 갱신에서 서빙까지 2305초 대 1~2초, reload 무중단 측정값.
|
||||
- (observed) 2026-09-17 에 새로 만든 엣지에서 잰 것 — `dig` 가 없다는 것, `getent hosts` 와 `dnsutils` 설치 뒤의 `dig` 가 같은 주소를 낸다는 것, `nginx -v` 가 `sudo` 없이는 `command not found` 라는 것, `certbot plugins` 세 줄이 문서에 적힌 것과 같다는 것.
|
||||
- (unknown) 2번부터 4번까지(토큰 발급 · 자격증명 파일 · 시험 발급 · 실제 발급)는 2026-09-17 에 다시 밟지 않았다. Cloudflare 토큰이 필요한 구간이라 토큰 없이는 칠 수 없고, 값을 옮겨 오지도 않았다. 그래서 **5번의 443 블록과 6번의 훅도 재확인하지 못했다** — 인증서 파일이 없으면 그 설정은 `nginx -t` 를 못 지난다.
|
||||
- (observed) **이 실험대의 certbot 은 DNS-01 로 등록되어 있다.** 위에서 「아직 읽지 못했다」고 적은 그 한 줄을 2026-09-17 에 읽었다 — `authenticator = dns-cloudflare` 이고 `dns_cloudflare_credentials = /etc/letsencrypt/cloudflare.ini` 이며 `server` 는 Let's Encrypt 운영 디렉터리다. 두 문서가 갈렸던 대목이 DNS-01 쪽으로 닫혔다.
|
||||
|
||||
```bash label="[lab host] 등록된 인증 방식을 읽는다"
|
||||
sudo sh -c 'grep -H "authenticator\|dns_cloudflare_credentials\|server =" /etc/letsencrypt/renewal/*.conf'
|
||||
```
|
||||
|
||||
**`sudo grep ... /etc/letsencrypt/renewal/*.conf` 로 치면 안 된다**(observed). 글로브를 일반 사용자 셸이 먼저 펼치는데 그 디렉터리를 못 읽어서 `zsh: no matches found` 로 끝나고 `grep` 은 시작도 안 한다. `sudo sh -c '...'` 로 sudo 안에서 펼쳐야 한다.
|
||||
|
||||
- (unknown) 2번부터 4번까지(토큰 발급 · 시험 발급 · 실제 발급)는 2026-09-17 에 다시 밟지 않았다. 5번의 443 블록은 이미 있던 인증서를 엣지로 옮겨서 밟았고, 그때 위 ② 의 경로 결함이 드러났다.
|
||||
- (external) `100.64.0.0/10` 이 CGNAT 예약 대역이라는 것과 Let's Encrypt 의 발급 한도와 유효기간 90일은 규격이고 이 실험대가 잰 값이 아니다.
|
||||
- (external) Cloudflare 계정에 남는 토큰은 엣지를 지운다고 없어지지 않는다. 가이드는 폐기를 적지 않았다.
|
||||
- (inferred) 2305초 측정은 훅이 물리 호스트에만 있던 시절의 기록이라, 엣지 게스트에서 다시 잰 값이 아니다. 지금 배치에서 같은 수가 나오는지는 재지 않았다.
|
||||
|
||||
Reference in New Issue
Block a user