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
+72
@@ -0,0 +1,72 @@
|
||||
---
|
||||
kind: PROJECT_DECISION
|
||||
slug: dns-01-because-the-lab-is-not-on-the-public-internet
|
||||
title: 인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다
|
||||
topic: lab-environment-build
|
||||
topicName: 실험대 환경 구성
|
||||
project: virtualization
|
||||
status: 게시 전
|
||||
decisionStatus: ADOPTED
|
||||
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
source:
|
||||
- final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신
|
||||
- final/document.md#184-이-부의-출처와-범위
|
||||
---
|
||||
|
||||
# 인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다
|
||||
|
||||
인증서는 Let's Encrypt 의 DNS-01 로 받는다. HTTP-01 은 80 포트와 공개 A 레코드를 요구하는데 이 실험대의 주소는 공개 인터넷에 없고, certbot 이 DNS 공급자 API 로 나가는 DNS-01 만 성립한다. Cloudflare API 토큰이 엣지 VM 안 평문 파일에 놓이는 비용을 감수한다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초**
|
||||
이 결정으로 받은 인증서가 갱신 뒤에 실제로 서빙되는지는 이 결정 밖이고 그 기록이 받는다. 발급 방식을 정하는 것과 갱신된 것을 nginx 가 읽게 만드는 것은 다른 일이다.
|
||||
- **엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리**
|
||||
그 결정이 새로 요구한 일곱 가지 가운데 7번이 certbot 과 인증서와 갱신 훅을 게스트로 옮기는 것이었다. 이 결정이 세우는 것을 전부 엣지 게스트에 두는 이유가 거기서 왔다.
|
||||
- **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다**
|
||||
체인이 이어졌는지를 openssl s_client 의 단계 수로 판정하는 근거다. cert.pem 을 써서 체인이 끊겨도 브라우저는 캐시나 AIA 로 보완해 정상으로 보인다.
|
||||
- **가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다**
|
||||
04 단계의 확인을 어느 기계에서 치는지가 그 기준에 걸려 있다. 엣지 게스트 안에서 치면 층의 답이 아니라 친 위치의 답이 돌아온다.
|
||||
- **단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다**
|
||||
이 결정이 남긴 이름 규칙을 가이드가 틀리게 적었고, §182 가 그것을 결함 여섯 중 하나로 셌다. 결함 여섯을 나란히 적은 표는 그 기록에 있다.
|
||||
|
||||
## 결정문
|
||||
|
||||
인증서는 Let's Encrypt 의 DNS-01 로 받는다. 엣지 게스트에 certbot 과 python3-certbot-dns-cloudflare 를 깐다. Cloudflare API 토큰은 /etc/letsencrypt/cloudflare.ini 에 600 으로 두고, 발급 대상은 hyeonworks.com 과 그 와일드카드 둘을 -d 로 준다.
|
||||
|
||||
certbot 과 인증서와 갱신 타이머와 deploy 훅은 전부 엣지 게스트에 두고 물리 호스트에는 아무것도 두지 않는다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
두 방식은 검증이 오가는 방향이 반대다. HTTP-01 은 Let's Encrypt 가 우리 서버로 들어오는 인바운드 검증이라 80 포트와 공개 A 레코드가 있어야 한다. DNS-01 은 certbot 이 DNS 공급자 API 로 나가는 아웃바운드 검증이라 공개 인터넷에서 보일 필요가 없다. 이 실험대의 주소는 공개 인터넷에 없으므로 HTTP-01 은 성립하지 않는다.
|
||||
|
||||
§190 는 이 선택을 우열로 적지 않는다. 공개 서버라면 HTTP-01 이 맞고, 토큰도 DNS 연동도 없어 관리할 것이 적다고 대안 쪽을 먼저 적는다. 이 결정은 더 나은 방식을 고른 것이 아니라 하나만 성립하는 조건에서 그것을 쓴 것이다. 딸려 온 이득이 하나 있는데, DNS-01 은 와일드카드를 받을 수 있어 hyeonworks.com 아래의 이름을 인증서 한 장으로 덮는다. 그 이득은 뒤늦게 챙겼다. 이 실험대는 처음에 와일드카드를 안 쓰고 이름마다 따로 받았고, 네 번째 이름이 없어 다른 실험에서 app2 를 빌려 써야 했다.
|
||||
|
||||
정한 대로 이미 서 있다. 엣지 게스트에는 certbot 과 python3-certbot-dns-cloudflare 가 깔려 있다. certbot plugins 는 dns-cloudflare 와 standalone 과 webroot 세 줄을 내고, 자격증명 파일은 600 으로 놓여 있다. 밖에서 친 openssl s_client 는 체인 0 부터 3 까지 네 단계와 Verify return code: 0 (ok) 를 냈고, curl 은 404 tls=0 을 냈다. 설정 원본은 §184 가 가리키는 저장소의 deploy/lab/edge/ 에 있고 리비전은 frontmatter 에 적었다.
|
||||
|
||||
없는 것도 분명하다. HTTP-01 을 실제로 시도해 실패한 기록은 없다. 그 경로가 막혔다는 근거는 시도가 아니라 주소 대역이고, 100.64.0.0/10 이 CGNAT(Carrier-Grade NAT) 예약 대역이라는 것은 이 실험대가 잰 값이 아니라 규격이다.
|
||||
|
||||
## 영향
|
||||
|
||||
감수한 비용이 셋이다.
|
||||
|
||||
Cloudflare API 토큰 : 엣지 VM 안 평문 파일에 놓인다
|
||||
발급 검증 시간 : TXT 가 퍼질 때까지 기다리느라 수십 초 걸린다. 정상이므로 중간에 끊지 않는다
|
||||
DNS 공급자 의존 : 인증서를 받는 일이 Cloudflare 계정에 묶인다
|
||||
|
||||
토큰이 평문으로 놓이는 것을 줄이려고 가드레일을 넷 둔다.
|
||||
|
||||
권한 범위 : Edit zone DNS · Specific zone · hyeonworks.com 으로 좁힌다. All zones 로 두면 계정의 모든 도메인에서 DNS 를 고칠 권한이 그 파일에 놓이고, Global API Key 는 폐기하면 그 키를 쓰던 다른 것들이 전부 같이 못 쓰게 된다
|
||||
파일 권한 : install -m 600 /dev/null 로 비어 있을 때 먼저 600 을 만든다. 토큰을 쓰고 나서 권한을 고치면 그사이가 열려 있다
|
||||
확인 방법 : ls -l 이 -rw------- 을 내는지와 바이트 수가 0 이 아닌지만 보고 값은 찍지 않는다
|
||||
토큰 검증 : Cloudflare 의 user/tokens/verify 가 내는 status 가 active 이고 success 가 true 인지 본다. code 가 6003 이면 값이 틀렸거나 잘렸고 9109 면 권한 범위가 모자라다. 여기서 걸러 두면 뒤에서 실패했을 때 DNS 문제인지 토큰 문제인지가 섞이지 않는다
|
||||
|
||||
그래도 토큰이 엣지 안에만 있는 것은 아니다. 원본은 Cloudflare 쪽 서버가 들고 있고 엣지에 놓인 것은 사본이라, 엣지 게스트를 지우는 것으로는 계정 쪽 토큰이 없어지지 않는다. 폐기는 Cloudflare 에서 따로 해야 하는데 가이드는 그것을 적지 않았다.
|
||||
|
||||
그 배치가 노린 것이 하나 더 있다. virsh undefine kc-lab-edge 한 줄로 이 계층을 통째로 되돌린다는 것이 가이드가 적은 이유인데, 그것은 게스트를 지우면 같이 없어진다는 말이지 걷어내는 절차가 아니다. 원본 가이드 04 에는 걷어내는 순서도, 지운 뒤에 무엇을 확인하는지도 없고, 이 실험대가 그렇게 지워 본 적도 없다.
|
||||
|
||||
발급 자체에도 순서가 붙는다. --dry-run 을 먼저 돌리는 것은 Let's Encrypt 의 주당 중복 인증서 5장 한도를 dry-run 이 쓰지 않기 때문이다. dry-run 은 인증서를 저장하지 않으므로 그 직후 certbot certificates 가 No certificates found 를 내는 것이 정상이다.
|
||||
|
||||
이 결정이 남긴 이름 규칙이 하나 있다. live/hyeonworks.com/ 은 certbot 이 이 묶음을 관리하려고 첫 번째 -d 에서 따온 라벨이고 서빙과 무관하다. 브라우저가 보는 유효 호스트명은 -d 로 준 이름 전부이므로 auth.hyeonworks.com 으로 다시 받을 필요가 없다. 대신 nginx 설정에는 그 디렉터리 경로를 한 글자도 다르지 않게 적어야 한다. live/auth.hyeonworks.com/ 이라고 적으면 cannot load certificate 로 막히고, §182 가 이것을 가이드 결함 여섯 중 하나로 셌다. 와일드카드는 한 단계만 덮는다. a.b.hyeonworks.com 도, apex 인 hyeonworks.com 자신도 와일드카드에 들어가지 않아 -d 를 둘 준다.
|
||||
|
||||
이 결정이 끝내지 못한 것이 하나 있다. 받은 인증서가 갱신 뒤에 실제로 서빙되는지는 발급 방식과 별개이고, 근거로 건 첫 기록이 그것을 받는다.
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
---
|
||||
kind: PROJECT_DECISION
|
||||
slug: edge-nginx-moved-into-a-guest-vm
|
||||
title: 엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리
|
||||
topic: lab-environment-build
|
||||
topicName: 실험대 환경 구성
|
||||
project: virtualization
|
||||
status: 게시 전
|
||||
decisionStatus: ADOPTED
|
||||
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
source:
|
||||
- final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나
|
||||
- final/document.md#178-이-부의-출처와-범위
|
||||
- final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다
|
||||
---
|
||||
|
||||
# 엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리
|
||||
|
||||
호스트에서 돌던 엣지 nginx 를 게스트 한 대(.10) 안으로 옮기고, 호스트는 커널 DNAT 로 tailnet 의 443 포트를 그 게스트에 넘기기만 한다. 바꾼 이유는 성능이 아니라 자주 갈아엎는 층의 격리다. 밖에서 게스트로 들어오는 경로가 FORWARD 가 되면서 libvirt 방화벽에 구멍이 새로 필요해졌다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **호스트 안에서는 404, 밖에서는 connection refused — 앞 체인의 accept 가 libvirt 의 reject 를 막지 못했다**
|
||||
이 결정이 새로 요구한 것 가운데 3번을 실제로 치른 기록이다. 밖에서 게스트로 들어오는 경로가 FORWARD 가 되면서 libvirt 방화벽의 reject 에 막혔고, §180 은 이곳을 이 구축에서 가장 오래 막힌 곳으로 적었다.
|
||||
- **단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다**
|
||||
이 구성을 세운 기반 7단계 가이드를 순서대로 따라가면서 나온 결함 여섯이 그 기준의 근거다. 여섯 중 셋이 여기서 옮긴 nginx 와 인증서를 다룬다.
|
||||
- **libvirt 의 firewall_backend 가 iptables 일 때도 guest_input 에 구멍이 필요한가**
|
||||
§180 이 미확인으로 남긴 것을 받는 물음이다. 아래 3번은 이 호스트의 nftables 백엔드에서만 확인했다.
|
||||
- **Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge**
|
||||
커널 DNAT 뒤에 이어지는 게스트 쪽 경로를 그 글이 설명한다.
|
||||
- **이 호스트의 가상 머신 네트워크는 Bridge 인가 NAT 인가 Routing 인가**
|
||||
§178 이 적은 대로 이 호스트는 이더넷이 없어 libvirt NAT 를 택했다. 그 물음이 실제 구성을 이 호스트에서 확인한다.
|
||||
|
||||
## 결정문
|
||||
|
||||
엣지 nginx 를 물리 호스트에서 게스트 VM 으로 옮기고, 호스트는 커널 DNAT 로 tailnet 의 443 포트를 그 게스트에 넘기기만 한다.
|
||||
|
||||
§179 이 전후 경로를 나란히 적었다.
|
||||
|
||||
전 : tailnet 의 443 포트에서 호스트 nginx 를 거쳐 게스트 .11/.12 의 Traefik 으로
|
||||
후 : tailnet 의 443 포트에서 호스트 커널 DNAT 를 거쳐 엣지 nginx(.10) 으로, 거기서 .11/.12 의 Traefik 으로
|
||||
|
||||
## 판단 이유
|
||||
|
||||
바꾼 이유로 §179 이 든 것은 성능이 아니라 더러워지는 층의 격리다. nginx 설정과 인증서와 certbot 과 deploy 훅은 자주 갈아엎는 것들이라 호스트에 있으면 초기화가 불가능하다. 엣지에 장애를 일부러 넣어 보는 실험도 호스트에서 하면 SSH 까지 위험해진다.
|
||||
|
||||
성능은 근거가 아니다. L7 홉 수는 전후 모두 2홉이고(observed), 늘어난 것은 커널이 하는 L4 전달 한 번뿐이어서 X-Forwarded-* 계약은 그대로 성립한다. 전환 전후를 같은 부하로 잰 측정은 이 저장소에 없으므로, 느려지지 않았다는 말은 이 기록이 하지 않는다.
|
||||
|
||||
§178 이 적었듯 이 호스트에는 이더넷 없이 WiFi 만 있어 브리지를 못 쓰고 libvirt NAT(virbr0) 와 호스트 진입 구조를 택했다.
|
||||
|
||||
견준 것은 호스트에 두기와 게스트로 옮기기 둘이다. 브리지와 NAT 는 고른 것이 아니라 이더넷이 없어 하나만 남았다.
|
||||
|
||||
## 영향
|
||||
|
||||
감수한 비용을 §179 이 일곱 줄의 표로 적고, 그중 둘을 나머지 다섯과 갈라 놓았다(inferred). 경로가 OUTPUT 에서 FORWARD 로 바뀌면서 생긴 것이 그 둘이다.
|
||||
|
||||
DNAT(2번) : 전에는 호스트가 443 포트를 직접 들었으니 넘길 일이 없었다. 지금은 호스트에 리스너가 아예 없다
|
||||
libvirt 방화벽에 구멍(3번) : 호스트에서 게스트로 가는 것은 OUTPUT 경로라 필터를 안 탔다. 밖에서 게스트로 들어오는 것은 FORWARD 다
|
||||
|
||||
호스트가 게스트에 접속하는 것과 밖에서 게스트로 들어오는 것은 커널이 보기에 완전히 다른 일이다. 3번을 뚫는 데 이 구축에서 가장 오래 걸렸다. §180 에서 밖에서 친 curl 은 connection refused 로 돌아왔다. libvirt 의 guest_input 체인을 끝내는 reject 규칙의 카운터 4 패킷이 그때 친 curl 횟수와 정확히 일치했다. 구멍이 필요하다는 것을 몰라서 오래 걸린 것은 아니다. DNAT 파일에는 priority filter - 10 으로 먼저 도는 forward 체인과 거기 넣은 ct state new accept 가 이미 있었고, 그것이면 열린다고 보고 세웠다 — iptables 감각으로 쓰면 정확히 여기서 틀린다고 §180 이 적었다.
|
||||
|
||||
같은 계열의 가드레일이 하나 더 붙는다.
|
||||
|
||||
SNAT 금지를 명시(4번) : L4 를 한 번 더 타면서 masquerade 를 붙이고 싶어지는데, 붙이면 엣지가 모든 클라이언트를 192.168.122.1 로 본다
|
||||
|
||||
나머지 넷은 배포판이 달라서 생긴 잡무다.
|
||||
|
||||
nginx 설치(1번) : 호스트에는 이미 있었다. 새 게스트의 cloud-init 은 curl 과 nftables 만 깐다
|
||||
sites-available 관례(5번) : 호스트는 Arch 라 그 디렉터리가 없어 nginx.conf 에 include 를 직접 넣었다. 게스트는 Debian 이라 기본으로 있다
|
||||
nginx 버전 차이(6번) : Arch 1.30 vs Debian 12 의 1.22. http2 on; 지시어가 1.25.1 이상이다
|
||||
certbot 과 인증서와 갱신 훅이 게스트로(7번) : 인증서를 읽는 주체가 nginx 이기 때문이다
|
||||
|
||||
얻은 것은 격리다. 자주 갈아엎는 층이 게스트 한 대 안으로 들어가니 그 게스트를 통째로 다시 세울 수 있고, 엣지에 장애를 넣는 실험이 호스트 SSH 를 건드리지 않는다. 그러면서 L7 홉 수는 2홉 그대로라 뒤쪽 Traefik 이 받는 X-Forwarded-* 계약도 바뀌지 않았다.
|
||||
|
||||
아직 재지 않은 것이 셋 있다. 전환 전후를 같은 부하로 잰 측정은 이 저장소에 없다. 3번의 구멍은 휘발성이라 libvirt 가 네트워크를 다시 세우면 없어지고, 그래서 DNAT 유닛의 ExecStartPost 에 넣었다. 그 조치가 재기동을 견디는지 실제로 다시 세워 확인한 출력은 없다. libvirt 의 firewall_backend 가 iptables 일 때도 같은 구멍이 필요한지는 §180 이 미확인으로 남겼고, 이 호스트는 nftables 백엔드다.
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
---
|
||||
kind: PROJECT_DECISION
|
||||
slug: fix-guest-addresses-with-a-dhcp-reservation
|
||||
title: 게스트 주소는 libvirt DHCP 예약으로 고정한다 — 바꿀 수 없어서가 아니라 되돌리기가 가장 비싸서
|
||||
topic: lab-environment-build
|
||||
topicName: 실험대 환경 구성
|
||||
project: virtualization
|
||||
status: 게시 전
|
||||
decisionStatus: ADOPTED
|
||||
source:
|
||||
- final/document.md#247-dhcp-예약-ip-dhcp-host-과-mac-52-54-00
|
||||
- final/document.md#201-네트워크-dhcp-예약의-실제-동작
|
||||
- final/document.md#245-libvirt-default-네트워크와-virbr0
|
||||
- final/document.md#246-dnsmasq-libvirt-내장-dhcp-dns
|
||||
- final/document.md#248---live---config
|
||||
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
---
|
||||
|
||||
# 게스트 주소는 libvirt DHCP 예약으로 고정한다 — 바꿀 수 없어서가 아니라 되돌리기가 가장 비싸서
|
||||
|
||||
게스트 IP 를 libvirt 의 DHCP 예약으로 묶고 예약을 넣은 다음에 게스트를 만든다. 주소가 바뀌면 k3s 가 설정 파일과 인증서에 구워 둔 IP 부터 어긋나, 고치는 값이 한 곳이 아니라 재발급이나 재설치가 된다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다**
|
||||
이 예약을 실제로 넣는 명령이 그 절차에 있고, DHCP 예약이 `virt-install` 보다 먼저여야 한다는 순서도 거기서 지킨다.
|
||||
- **k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다**
|
||||
고정한 `192.168.122.11` 과 `192.168.122.12` 를 그대로 받아 쓰는 절차다. `--node-ip` 와 `--tls-san` 과 agent 의 `K3S_URL` 이 그 주소를 담는다.
|
||||
- **엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다**
|
||||
nginx 의 `upstream` 이 뒤쪽 게스트 주소를 적는 곳이다. 주소가 바뀌면 reload 전까지 502 가 이어진다.
|
||||
- **호스트에 직접 깔지 않고 게스트 VM 두 대로 간다 — 관측자가 실험 대상과 함께 죽으면 안 된다**
|
||||
이 결정이 서 있는 바닥이다. 게스트를 반복해 죽이는 실험이 전제라서 주소 고정이 필요해졌다.
|
||||
- **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다**
|
||||
`net-dumpxml` 의 예약과 `net-dhcp-leases` 의 리스가 서로 다른 것을 말한다는 것을 그 기준이 일반화한다.
|
||||
- **L7 이 두 겹인 이유와, 진입점 하나를 이중화하려 할 때 끝나지 않는 재귀**
|
||||
nginx 가 뒤쪽 노드를 주소로 가리키는 구조를 그 글이 설명한다. 여기서 고정한 주소가 그 upstream 에 적힌다.
|
||||
|
||||
## 결정문
|
||||
|
||||
게스트의 IP 는 libvirt `default` 네트워크의 DHCP 예약(`ip-dhcp-host`)으로 고정한다. 게스트 안에서 static IP 를 잡지 않고, MAC 은 QEMU/KVM 에 할당된 `52:54:00` 대역을 쓴다. 예약을 먼저 넣고 그다음에 `virt-install` 로 게스트를 만든다.
|
||||
|
||||
2026-09-10 에 실제로 돌린 결과가 그렇게 나왔다. `virsh net-update default add ip-dhcp-host ... --live --config` 이 `Updated network default persistent config and live state` 를 냈고, 예약을 먼저 넣고 만든 게스트가 첫 부팅에서 바로 `192.168.122.10` 을 받았다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
§247 이 인과 순서를 직접 못박아 두었다. upstream 에 IP 를 박으려고 예약을 거는 것이 아니다. 고정 주소가 필요한 이유가 여럿이고 그것을 충족하는 수단이 DHCP 예약이며, 그렇게 얻은 주소를 upstream 에도 적는 순서다.
|
||||
|
||||
고정이 필요한 이유를 §247 이 중요도 순으로 넷 든다.
|
||||
|
||||
k3s 가 IP 를 설정 파일과 인증서에 굽는다 : `--node-ip` 와 `--tls-san`, agent 의 `K3S_URL=https://192.168.122.11:6443`, kubeconfig 의 `server:` 필드가 전부 IP 를 담는다. server 노드의 IP 가 바뀌면 agent 가 클러스터에 합류하지 못한다. API 서버 인증서의 SAN 도 어긋나서 재발급이나 재설치가 필요해진다. 되돌리기가 가장 비싼 항목이다
|
||||
nginx 는 upstream 주소를 기동 시점에 한 번만 해석한다 : 오픈소스판은 `upstream` 블록의 이름을 설정 로드 때 풀고 런타임에 다시 조회하지 않는다. 그래서 뒤쪽 IP 가 바뀌면 reload 전까지 계속 502 다. 다시 조회하게 하려면 `resolver` 와 변수 조합을 쓰거나 상용판이 필요하다
|
||||
VM 을 반복해서 죽이는 것이 실험 그 자체다 : `virsh destroy` 로 노드 상실을 재현하는데 되살릴 때마다 주소가 달라지면 실험이 성립하지 않는다
|
||||
장애 주입 규칙이 주소 기반이다 : 「kc-lab-2 로 가는 7800 을 막아라」에서 IP 가 어긋나면 조용히 엉뚱한 것을 막는다. 실패가 드러나지 않아 특히 위험하다
|
||||
|
||||
수단은 셋을 견줬고 §247 이 표로 적었다. 게스트 안에서 static IP 를 설정하면 cloud-init 이 복잡해지고 libvirt 는 그 사실을 모르므로 주소 설정이 두 곳으로 흩어진다. upstream 에 호스트명을 쓰면 libvirt 의 dnsmasq 가 이름을 풀어 주기는 한다. 다만 호스트의 리졸버가 `virbr0` 를 바라봐야 하고, 기동 시 한 번만 해석한다는 둘째 문제는 이 방법으로 없어지지 않는다.
|
||||
|
||||
DHCP 예약을 고른 까닭은 주소 관리가 libvirt 한 곳에 모이기 때문이다. 그 한 곳이 libvirt 가 네트워크마다 하나씩 띄우는 dnsmasq 이고, 예약은 네트워크 정의 XML 의 `<ip><dhcp><host>` 요소에 들어간다. 게스트는 평범한 DHCP 클라이언트로 두면 되고, 게스트 쪽 설정 파일은 아무것도 손대지 않는다.
|
||||
|
||||
## 영향
|
||||
|
||||
값이 맞으면 조용히 되고 틀리면 더 조용히 틀린다. 그래서 치른 비용이 셋 다 「오류가 안 나는 실패」다.
|
||||
|
||||
순서가 결과를 바꾼다 : 예약을 넣고 나서 `virt-install` 해야 한다. 게스트를 먼저 만들면 동적 대역(`192.168.122.2`부터 `192.168.122.254`)에서 아무 주소나 받고, 예약이 먹으려면 리스 만료를 기다리거나 재부팅해야 한다
|
||||
MAC 이 한 글자만 달라도 조용히 무시된다 : 예약의 `mac` 과 `virt-install --network` 에 준 `mac=` 이 정확히 같아야 한다. 다르면 오류 메시지 없이 동적 범위에서 아무 주소나 받고, 증상은 「왜 IP 가 다르지」로만 나타난다
|
||||
플래그 둘을 다 줘야 한다 : `--live` 만 주면 재부팅에 사라지고 `--config` 만 주면 지금 반영이 안 된다. 성공 판정은 출력에 `persistent config` 와 `live state` 두 마디가 다 나오는 것이고, 한쪽만 나온 것을 성공으로 읽으면 「재부팅하니 IP 가 바뀐다」가 된다
|
||||
|
||||
예약을 조회하는 두 명령도 서로 다른 것을 말한다. `net-dumpxml` 이 보여 주는 예약은 dnsmasq 에게 준 의도이고, `net-dhcp-leases` 가 보여 주는 리스는 실제로 나간 기록이다. 둘이 다를 수 있으므로 예약을 넣었다는 것만으로 게스트가 그 주소를 받았다고 읽지 않는다.
|
||||
|
||||
동적 범위가 예약 주소를 품고 있는 것은 이대로 두었다. 현재 범위 `192.168.122.2`부터 `192.168.122.254` 안에 `192.168.122.11` 과 `192.168.122.12` 가 들어간다. 그래도 dnsmasq 는 정적으로 예약된 주소를 다른 클라이언트에게 내주지 않아 정상 동작한다. 더 방어적으로 가려면 범위를 `192.168.122.100`부터 `192.168.122.254` 로 좁혀 예약 대역과 나눈다.
|
||||
|
||||
네트워크가 비활성일 때는 `--live` 를 쓸 수 없다. 그때는 `--config` 만 주고 네트워크를 시작한다.
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
---
|
||||
kind: PROJECT_DECISION
|
||||
slug: no-docker-on-the-lab-host
|
||||
title: lab host 에는 Docker 를 깔지 않는다 — 이미지 저장소가 갈리고, 그 위에 네트워크 변수가 하나 더 붙는다
|
||||
topic: lab-environment-build
|
||||
topicName: 실험대 환경 구성
|
||||
project: virtualization
|
||||
status: 게시 전
|
||||
decisionStatus: ADOPTED
|
||||
source:
|
||||
- final/document.md#281-docker를-lab-host에-설치하면-안-되는-이유
|
||||
- final/document.md#282-그러면-이미지는-어떻게-넣는가
|
||||
- final/document.md#280-무엇을-어디에-설치하는가
|
||||
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
---
|
||||
|
||||
# lab host 에는 Docker 를 깔지 않는다 — 이미지 저장소가 갈리고, 그 위에 네트워크 변수가 하나 더 붙는다
|
||||
|
||||
Docker 는 워크스테이션에만 두고 lab host 와 게스트에서는 뺀다. 깔면 containerd 가 둘이 되어 `docker build` 한 이미지를 k3s 가 보지 못하고, 증상이 `docker images` 에는 보이는데 파드는 `ErrImageNeverPull` 인 모양으로 나온다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **호스트에 직접 깔지 않고 게스트 VM 두 대로 간다 — 관측자가 실험 대상과 함께 죽으면 안 된다**
|
||||
같은 축의 결정이다. 호스트를 진입점과 하이퍼바이저로만 남긴다는 그 판단이 컨테이너 런타임 쪽에서 한 번 더 쓰였다.
|
||||
- **k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다**
|
||||
k3s 가 자체 containerd 를 들고 들어오는 절차다. 그 절차가 끝난 뒤에 「이미지는 어떻게 넣지」가 처음 나온다.
|
||||
- **Keycloak 2노드와 PostgreSQL 을 k3s 에 올리고 클러스터가 묶였는지 확인한다**
|
||||
공개 이미지만 쓰는 배포라 반입이 필요 없는 쪽의 예다. 자체 빌드 이미지와 갈리는 지점을 그 절차가 보여 준다.
|
||||
- **Prometheus 와 Grafana 를 올려 클러스터 안을 밖에서 본다**
|
||||
같은 이유로 공개 이미지를 그대로 당겨 쓴다. 노드마다 따로 반입해야 하는 일이 여기서는 생기지 않는다.
|
||||
- **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다**
|
||||
`docker images` 가 보여 주는 목록이 k3s 의 이미지 목록이 아니라는 것을 그 기준이 일반화한다.
|
||||
|
||||
## 결정문
|
||||
|
||||
lab host 와 게스트에는 Docker 를 설치하지 않는다. 이미지 빌드는 워크스테이션에서만 하고, 자체 빌드 이미지는 `docker save` 로 내보내 lab host 를 경유해 게스트의 `k3s ctr images import` 로 밀어 넣는다.
|
||||
|
||||
§280 의 설치 위치 표가 그 상태를 적는다. libvirt 와 QEMU 와 nginx 와 certbot 과 `kubectl` 은 lab host 에, k3s 는 게스트에, Docker 는 워크스테이션에만 있다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
k3s 는 자체 containerd 를 번들한다. Docker 와 무관하게 이미 완결된 스택이고, 소켓도 이미지 저장 경로도 다르다.
|
||||
|
||||
k3s 의 소켓 : `/run/k3s/containerd/containerd.sock`
|
||||
Docker 의 소켓 : `/run/containerd/containerd.sock`
|
||||
k3s 의 이미지 저장 : `/var/lib/rancher/k3s/agent/containerd/`
|
||||
Docker 의 이미지 저장 : `/var/lib/docker/`
|
||||
|
||||
Docker 를 깔면 containerd 인스턴스가 두 개가 되고, 둘은 서로의 이미지를 알지 못한다. `docker build` 한 것은 `/var/lib/docker/` 로 들어가는데 k3s 는 거기를 보지 않아서, 파드를 만들면 이미지가 없다고 한다. 그래서 증상이 `docker images` 에는 보이는데 파드는 `ErrImageNeverPull` 인 모양으로 나오고, 원인이 눈에 보이지 않아 오래 헤맨다.
|
||||
|
||||
충돌은 저장소 말고도 §281 이 넷을 더 든다.
|
||||
|
||||
cgroup 드라이버 : dockerd 의 기본은 `cgroupfs` 이고 k3s 는 `systemd` 다. 한 노드에서 두 관리자가 cgroup 트리를 다툰다
|
||||
iptables 와 nftables : Docker 가 `DOCKER` 와 `DOCKER-USER` 체인과 MASQUERADE 규칙을 심는다. flannel 규칙과 순서가 엉키면 파드 트래픽이 Docker 규칙에 걸린다
|
||||
브리지 대역 : `docker0` 가 `172.17.0.0/16` 을 점유한다. 클러스터 서비스 대역이나 사내망과 겹치면 라우팅이 깨진다
|
||||
디스크 : 같은 이미지가 두 벌 저장된다
|
||||
|
||||
이 실험대에는 이유가 하나 더 붙는다. lab host 에서 libvirt 가 `virbr0` NAT 와 자체 방화벽 규칙을 이미 운영하고 있어서, Docker 의 iptables 규칙이 그 위에 얹히면 게스트 네트워크가 예측 불가능해진다. 네트워크 장애를 일부러 주입하는 실험대에서 원인 불명의 네트워크 변수를 늘리면 실험 결과인지 환경 문제인지 구분할 수 없게 된다.
|
||||
|
||||
대안 하나를 기각했다. `k3s server --docker` 로 Docker 를 런타임으로 지정하는 방법이 과거에 있었지만, 쿠버네티스 1.24 의 dockershim 제거 이후로는 별도 `cri-dockerd` 를 요구하며 권장되지 않는다. 얻는 것이 없다.
|
||||
|
||||
## 영향
|
||||
|
||||
이미지를 넣는 길 셋 가운데 둘째로 좁아졌다. 공개 레지스트리에서 당기는 쪽과 클러스터 안에 레지스트리를 두는 쪽이 나머지 둘이고, 지금은 `ctr images import` 로 반입한다.
|
||||
|
||||
Keycloak 과 PostgreSQL 과 Redis 처럼 공식 이미지를 쓰는 것은 아무 준비도 필요 없다. 값을 치르는 쪽은 자체 빌드 이미지인 BFF 와 token-mediator 와 echo 다. 워크스테이션에서 `docker save` 로 내보내 lab host 를 거쳐 게스트로 흘려 넣어야 하고, 그때 주의가 셋 붙는다.
|
||||
|
||||
노드마다 따로 반입한다 : 스케줄러가 어느 노드에 배치할지 모르기 때문에, 한쪽에만 있으면 반대편에 배치될 때 실패한다
|
||||
매니페스트에 `imagePullPolicy: Never` 를 준다 : 없으면 로컬에 이미지가 있어도 레지스트리에서 당기려 시도하다 실패한다
|
||||
`ctr` 이 아니라 `k3s ctr` 을 쓴다 : `k3s ctr` 은 k3s 의 containerd 소켓을 가리키는 래퍼다. 시스템에 별도 `ctr` 이 있으면 다른 소켓을 보게 되어 「성공했는데 파드는 이미지를 못 찾는」 상태가 된다
|
||||
|
||||
ssh 가 두 번 중첩되는 것도 값이다. 게스트가 lab host 의 libvirt NAT 뒤에 있어 워크스테이션에서 직접 붙지 못하고, lab host 의 `~/.ssh/config` 에 있는 `kc-lab-*` 별칭을 거쳐야 한다. 명령 한 줄이 `docker save` 와 바깥 ssh 와 안쪽 ssh 와 `k3s ctr images import` 를 한꺼번에 담게 된다.
|
||||
|
||||
되돌릴 조건은 정해 두었다. 빌드와 배포 반복이 잦아지면 셋째 길인 클러스터 내 레지스트리로 옮긴다. 지금은 자체 이미지가 셋뿐이라 반입 한 번이 레지스트리를 세우고 유지하는 것보다 싸다.
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
---
|
||||
kind: PROJECT_DECISION
|
||||
slug: two-guest-vms-instead-of-installing-k3s-on-the-host
|
||||
title: 호스트에 직접 깔지 않고 게스트 VM 두 대로 간다 — 관측자가 실험 대상과 함께 죽으면 안 된다
|
||||
topic: lab-environment-build
|
||||
topicName: 실험대 환경 구성
|
||||
project: virtualization
|
||||
status: 게시 전
|
||||
decisionStatus: ADOPTED
|
||||
source:
|
||||
- final/document.md#213-왜-호스트에-직접-깔지-않고-vm-2대인가
|
||||
- final/document.md#313-k3s-server와-agent-죽였을-때가-다르다
|
||||
- final/document.md#218-실험대-전체-배치-2026-09-03-구축-완료-실측값
|
||||
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
||||
---
|
||||
|
||||
# 호스트에 직접 깔지 않고 게스트 VM 두 대로 간다 — 관측자가 실험 대상과 함께 죽으면 안 된다
|
||||
|
||||
k3s 를 게스트 VM 두 대에 나눠 깔고 물리 호스트는 진입점과 하이퍼바이저로만 남긴다. 머신이 한 대뿐이라 같은 커널에 두 노드를 올리면 노드 상실 실험이 성립하지 않고, 호스트가 노드를 겸하면 SSH 와 libvirt 와 nginx 가 실험 대상과 같이 내려간다.
|
||||
|
||||
## 근거
|
||||
|
||||
- **엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리**
|
||||
같은 논리를 엣지 한 층에 다시 적용한 결정이다. 그쪽은 게스트 구조가 이미 있다고 놓고 엣지가 어디 사는지를 정했고, 이쪽은 게스트 구조를 쓸 것인지를 정했다.
|
||||
- **lab host 에 가상화 패키지를 깔고 virsh 가 sudo 없이 돌게 만든다**
|
||||
이 결정이 요구한 첫 절차다. 호스트에 k3s 대신 libvirt 와 QEMU 가 깔리는 까닭을 이 결정이 댄다.
|
||||
- **k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다**
|
||||
결정을 실제로 실행한 절차다. server 와 agent 가 서로 다른 게스트로 들어가고 `kubectl` 은 호스트에서 돈다.
|
||||
- **Prometheus 와 Grafana 를 올려 클러스터 안을 밖에서 본다**
|
||||
관측 스택을 server 노드에 못박는 절차가 거기 있다. 관측자를 살려 둔다는 이유가 클러스터 안에서 한 번 더 쓰인다.
|
||||
- **lab host 에는 Docker 를 깔지 않는다 — 이미지 저장소가 갈리고, 그 위에 네트워크 변수가 하나 더 붙는다**
|
||||
호스트를 깨끗이 남긴다는 같은 축의 결정을 컨테이너 런타임 쪽에 적용한 것이다.
|
||||
- **실험대를 껐다 켜고 게스트 메모리를 다시 나눈다**
|
||||
게스트로 갔기 때문에 가능해진 운영이다. `virsh setmaxmem` 으로 메모리를 다시 나누는 일은 호스트에 직접 깔았으면 생기지 않는다.
|
||||
|
||||
## 결정문
|
||||
|
||||
k3s server 를 게스트 `kc-lab-1` 에, agent 를 게스트 `kc-lab-2` 에 깐다. 물리 호스트 `test-server` 에는 nginx 와 libvirt/KVM 만 두고 k3s 를 설치하지 않는다.
|
||||
|
||||
2026-09-03 에 구축이 끝난 배치가 그 상태다. 게스트 둘이 `virbr0` 뒤에서 `192.168.122.11` 과 `192.168.122.12` 를 쓰고, 호스트에는 nginx 와 libvirt/KVM 만 있다. 나중에 엣지 게스트가 한 대 더 붙어 셋이 되는데, 그것은 엣지를 게스트로 옮긴 결정이 따로 받는다.
|
||||
|
||||
## 판단 이유
|
||||
|
||||
물리 머신이 한 대다. 그 한 대로 무엇을 재려 하는가가 이 판단을 갈랐다. §213 은 「나중에 반드시 다시 묻게 되는 판단이므로 근거를 남긴다」로 시작해 이유 여섯을 중요도 순으로 적는다.
|
||||
|
||||
독립 커널이 둘 필요하다 : 같은 커널에 k3s server 와 agent 를 올리면 노드가 이름뿐이라 노드 간 방화벽·파티션·노드 상실 실험이 성립하지 않는다
|
||||
파괴 실험 후 복원 : VM 은 qcow2 오버레이를 지우면 몇 초 만에 초기 상태로 돌아가고, 호스트는 재설치 말고 되돌릴 방법이 없다
|
||||
관측자를 살려 둔다 : 노드를 죽이는 실험인데 그 노드가 호스트면 SSH 와 libvirt 와 nginx 가 같이 죽는다
|
||||
호스트 오염 방지 : k3s 는 nftables 규칙과 CNI(Container Network Interface, 컨테이너 네트워크 규격) 인터페이스, 커널 모듈, systemd 유닛을 대량으로 심는다
|
||||
운영 배포판과 일치 : 호스트는 Arch 인데 운영 k3s 가 다른 배포판이면 커널과 systemd 차이가 잡음이 된다. 게스트를 운영과 같게 맞추면 그 잡음이 사라진다
|
||||
netem 격리 : 커널이 분리되어 지연 주입이 게스트 안에 갇힌다. 호스트에서 걸면 SSH 까지 느려진다
|
||||
|
||||
견준 쪽은 호스트에 단일 노드 k3s 를 직접 까는 것이다. §213 은 그것을 정직한 반대편으로 직접 적었다 — 계약 검증(2홉 헤더, 쿠키/origin)만 볼 거라면 호스트 직접 설치로 충분하고 그게 더 빠르다. VM 경로가 필요해지는 것은 클러스터와 장애 실험부터다.
|
||||
|
||||
절충안도 하나 검토하고 채택하지 않았다. 호스트를 노드 1로 쓰고 VM 을 노드 2로 두는 방법이다. 게스트 운영체제 하나(약 350MB)와 설치 수고를 아끼는 대신 관측자 분리와 호스트 오염 방지를 포기하게 된다. 7.4Gi 예산에서 그 350MB 보다 관측자 분리가 더 값지다고 판단했다.
|
||||
|
||||
## 영향
|
||||
|
||||
실험 범위를 넓히는 대가로 구축 시간과 게스트 운영체제 몫의 메모리를 치렀다. 호스트에 단일 노드로 깔았으면 하루 안에 끝났을 구축이 게스트 생성과 cloud-init 과 k3s 설치로 나뉘었고, 게스트마다 커널과 systemd 가 따로 돌아 그만큼의 메모리를 쓴다.
|
||||
|
||||
얻은 것은 되돌릴 수 있는 실험대다. 노드를 죽여도 `virsh start` 로 되살아나고, 게스트를 통째로 지워도 `base.qcow2` 위에 오버레이를 다시 얹으면 몇 초 만에 초기 상태가 된다. 호스트의 인증서와 DNS 와 진입 설정은 그동안 한 번도 건드려지지 않는다.
|
||||
|
||||
같은 구분이 클러스터 안에서 한 번 더 나온다. k3s server 와 agent 는 죽였을 때가 다르다.
|
||||
|
||||
kc-lab-1(server) 를 죽이면 : `kubectl` 이 안 되고 DNS 와 인그레스도 사라진다
|
||||
kc-lab-2(agent) 를 죽이면 : 그 노드의 워크로드만 사라지고 클러스터 제어는 살아 있다
|
||||
|
||||
그래서 노드 상실 실험은 agent 를 죽이는 것이고, server 를 죽이는 것은 노드 상실이 아니라 컨트롤 플레인 상실이다. §313 은 이 사실을 모르고 「keycloak 하나만 있는 노드를 죽이자」고 계획했다가 실제 배치를 조회한 뒤 정정했다고 적는다. 노드가 둘뿐이라 관측과 실험을 완전히 갈라놓을 수는 없으므로 §330 이 규칙으로 못박았다 — 관측 스택은 server 쪽에 두고 죽이지 않으며, 장애 주입은 agent 쪽에만 건다. `nodeSelector` 로 배치를 고정해야 그 규칙이 재현된다.
|
||||
|
||||
확인하지 않은 것이 하나 있다. 호스트 직접 설치와 게스트 두 대를 같은 실험으로 견준 측정은 이 저장소에 없다. 여섯 이유는 무엇이 성립하고 무엇이 성립하지 않는가에 대한 판단이고, 어느 쪽이 얼마나 느린지는 재지 않았다.
|
||||
Reference in New Issue
Block a user