Files
document-haness/docs/virtualization/tech-log-studio/lab-environment-build/decision/decision-edge-nginx-moved-into-a-guest-vm.md
T
DongHyeonkaandClaude Opus 5 2109f726fe 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>
2026-09-17 11:01:55 +09:00

6.8 KiB

kind, slug, title, topic, topicName, project, status, decisionStatus, sourceRevision, source
kind slug title topic topicName project status decisionStatus sourceRevision source
PROJECT_DECISION edge-nginx-moved-into-a-guest-vm 엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리 lab-environment-build 실험대 환경 구성 virtualization 게시 전 ADOPTED 9465582b5d1630eb4ae7c4e078021486919bf6b6
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 백엔드다.