refactor: 문서 개선 중

This commit is contained in:
donghyeon-ka
2026-09-21 14:30:55 +09:00
parent c93cdea150
commit 805a18f486
1497 changed files with 525837 additions and 59152 deletions
@@ -6,7 +6,11 @@ topic: build-completion-judgment
topicName: 끝났다는 판정
project: virtualization
status: 게시 전
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/15-k3s-precheck.txt
- ../../../final/evidence/raw/lab-state-before-rebuild/21-k3s-token.txt
- ../../../final/evidence/raw/lab-state-before-rebuild/22-k3s-agent-install.txt
source:
- final/document.md#185-가이드-묶음이-스스로-정한-규약
- final/document.md#188-단계-02-k3s-server-와-agent
@@ -156,7 +160,7 @@ ssh kc-lab-2 "curl -sfL https://get.k3s.io | sudo sh -s - agent \
## 확인하지 못한 것
설치 출력도 `journalctl` 출력도 `final/evidence/` 에 남기지 않았다. `level=fatal msg="Error: --token is required"``Host key verification failed.` 는 SSOT 본문에서 옮긴 것이고 명령 출력 파일이 근거가 아니다. 토큰 108자도 같다.
원래 빈 토큰 사건의 `level=fatal msg="Error: --token is required"` 와 당시 설치 출력은 여전히 직접 raw가 없다. 다만 2026-09-17 후속 원문은 현재 정상 경로를 보강한다. `15-k3s-precheck.txt` 에는 별도 precheck에서 `Host key verification failed.`가 남아 있고, `21-k3s-token.txt` 는 재구축 시점의 **비어 있지 않은 토큰**을 길이로 확인했으며, `22-k3s-agent-install.txt` 는 그 토큰으로 agent 설치가 성공한 출력을 담는다. 이 셋은 원래 빈 토큰 실패의 재현이 아니라 **follow-up corroboration**이다. 과거 사건의 토큰 108자와 후속 실행의 길이가 다르더라도 같은 측정으로 덮어쓰지 않는다.
이 실패를 일부러 다시 만들어 본 기록도 없어서, 빈 토큰으로 설치하면 설치 출력이 성공으로 끝난다는 것은 한 번의 관측이다. 재현이 쉽지 않은 까닭은 이 부의 검증 방식에 있다 — 만드는 명령은 다시 치면 돌고 있는 실험대가 없어지므로 구축할 때 쓴 것을 옮기고 결과 상태를 확인하는 것으로 대신했다. 재현 없이 남길 수 있는 것은 `journalctl` 쪽이고, 그 출력을 `final/evidence/raw/` 에 남기면 진단 절에 증거가 붙는다.
@@ -6,7 +6,10 @@ topic: build-completion-judgment
topicName: 끝났다는 판정
project: virtualization
status: 게시 전
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/14-cloudinit-schema-check.txt
- ../../../final/evidence/raw/lab-state-before-rebuild/15-k3s-precheck.txt
source:
- final/document.md#187-단계-01-게스트-세-대
- final/document.md#186-단계-00-lab-host-가상화-준비
@@ -178,18 +181,20 @@ python3 -c 'import yaml,sys; yaml.safe_load(open("kc-lab-1.yaml")); print("YAML
`0``2``YAML OK` 셋이 맞아야 시드를 만든다. 셋이 맞아도 cloud-config 로 유효하지는 않다 — 키 이름을 `users` 대신 `user` 로 친 오타는 이 세 줄을 그냥 통과한다. 그래서 게스트가 한 대라도 떠 있으면 cloud-init 자신의 스키마 검사기를 쓴다.
```bash
```bash label="[lab host] [reference] historical command"
ssh donghyeon@192.168.122.11 'umask 077; cat > ~/kc-lab-2.yaml' < kc-lab-2.yaml
ssh donghyeon@192.168.122.11 'cloud-init schema -c ~/kc-lab-2.yaml; rm -f ~/kc-lab-2.yaml'
```
이 두 줄은 **당시 문서에 남은 historical command**다. 둘째 줄은 스키마 검사가 실패해도 `; rm` 때문에 파일을 지운다. 따라 하는 절차에서는 검사와 삭제를 분리하고, 검사 성공 여부를 본 뒤 삭제한다.
이 파일에는 콘솔 비밀번호가 평문으로 들어 있어 `/tmp` 가 아니라 자기 홈에 `600` 으로 두고, 검사가 끝나면 바로 지운다.
## 확인하지 못한 것
넷 가운데 SSOT 가 관측으로 표시한 것은 시드를 SATA 로 붙였을 때 AHCI 장치가 보이지 않는 것과 스키마 검사기의 거부 문구뿐이다. YAML 파싱 실패와 `vol-upload` 누락과 네트워크 autostart 셋은 가이드가 막히면 표에 적어 둔 항목이라, 이 실험대에서 실제로 그 증상을 본 것인지 예상해 적은 것인지 SSOT 가 가르지 않았다. 이 글에서 그 셋은 그렇게 되는 구조까지이고 그렇게 됐다가 아니다.
명령 출력은 원문으로 남아 있지 않다. `Permission denied (publickey)``cloud-init status: done` 약 50초도 SSOT 본문이 근거다. `final/evidence/` 에 그 출력을 담은 파일이 없다. `virsh screenshot` 으로 뜬 화면도 저장해 두지 않았다.
후속 원문이 생겼다. `14-cloudinit-schema-check.txt` 는 cloud-init 22.4.2가 `sudo` 리스트 표기를 실제로 거부한 출력을 다시 담아 **스키마 거부 claim을 직접 보강한다**. `15-k3s-precheck.txt` 의 `Host key verification failed.` 는 2026-09-17 재구축 중 나온 후속 SSH 관측으로, 원래 사건의 `Permission denied (publickey)`를 재현한 것은 아니다. 원래 `Permission denied (publickey)`, `cloud-init status: done`, 약 50초, `virsh screenshot` 화면은 여전히 SSOT 본문만 근거이고 직접 raw는 없다.
만드는 명령은 재실행으로 검증되지 않았다. 게스트를 다시 만들면 돌고 있는 실험대가 없어지므로 시드와 관련된 셋은 다시 재현하기 어렵다. 네트워크 autostart 만은 호스트를 재부팅해 확인할 수 있는데 그 기록도 없다.
@@ -6,7 +6,7 @@ topic: build-completion-judgment
topicName: 끝났다는 판정
project: virtualization
status: 게시 전
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신
- final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat
@@ -63,7 +63,7 @@ post/ 가 아니라 deploy/ 인 까닭 : post/ 는 갱신이 없어도 매번
nginx : nginx/1.22.1
certbot 플러그인 : dns-cloudflare
발급 대상 : -d hyeonworks.com -d '*.hyeonworks.com'
lineage 디렉터리 : /etc/letsencrypt/live/hyeonworks.com/
lineage 디렉터리 : /etc/letsencrypt/live/auth.hyeonworks.com/ (2026-09-17 현재 관측)
nginx 가 읽는 파일 : fullchain.pem · privkey.pem
타이머 : certbot-renew.timer → certbot-renew.service
인증서 유효기간 : 오늘 + 90일
@@ -7,7 +7,7 @@ topicName: 끝났다는 판정
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#194-이-부에서-파생될-open-question
- final/document.md#184-이-부의-출처와-범위
@@ -6,7 +6,7 @@ topic: build-completion-judgment
topicName: 끝났다는 판정
project: virtualization
status: 게시 전
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#189-단계-03-엣지-nginx-라우팅과-호스트-dnat
- final/document.md#185-가이드-묶음이-스스로-정한-규약
@@ -6,7 +6,7 @@ topic: build-completion-judgment
topicName: 끝났다는 판정
project: virtualization
status: 게시 전
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#191-단계-05-keycloak-2노드와-postgresql
- final/document.md#192-단계-06-prometheus-와-grafana
@@ -7,7 +7,10 @@ topicName: 실험대의 진입 경로
project: virtualization
status: 게시 전
basisVersion: 이 실험대의 2026-09-03 배치 · 호스트 nginx 1.30.4 (Arch) · k3s v1.36.4 의 기본 ingress 인 Traefik
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
assets:
- key: two-l7-hops-entry-recursion
file: ../../../final/assets/diagrams/two-l7-hops-entry-recursion/two-l7-hops-entry-recursion.svg
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#261-진입점-자체가-죽으면-로드밸런서의-재귀-문제
- final/document.md#275-호스트-nginx와-traefik은-무엇이-다른가-둘-다-필요한-이유
@@ -38,6 +41,8 @@ source:
<!-- body:start -->
![브라우저에서 edge nginx와 Traefik을 거쳐 Pod로 가는 두 L7 홉과, edge nginx를 두 대로 늘릴 때 앞단 selector가 새 단일 장애점이 되는 재귀를 함께 보여 주는 흐름도.](../../../final/assets/diagrams/two-l7-hops-entry-recursion/two-l7-hops-entry-recursion.svg)
## 요청 하나가 L7 을 두 번 지난다
브라우저가 보낸 요청은 파드에 닿기까지 HTTP 를 읽는 서버를 두 번 지난다.
@@ -7,7 +7,7 @@ topicName: 실험대의 진입 경로
project: virtualization
status: 게시 전
decisionStatus: ADOPTED
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#305-tunnel-채택하지-않은-이유를-남긴-자산
- final/document.md#302-왜-적용하지-않는-것을-남겨두는가
@@ -32,7 +32,7 @@ Cloudflare named tunnel 을 앞에 붙이면 브라우저에서 파드까지가
- **엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리**
같은 2홉을 지킨 채 엣지의 위치만 바꾼 결정이라 홉 수를 건드리지 않는다.
- **이 실험대의 certbot 은 HTTP-01 로 받고 있는가 DNS-01 로 받고 있는가**
재구축 때 인증서 디렉터리를 지워도 되는지가 그 물음에 걸려 있다.
이 물음은 2026-09-17 `dns-cloudflare` 관측으로 닫혔다. 남은 것은 현재 Cloudflare 자격증명이 실제 재발급에 쓸 수 있는지다.
## 결정문
@@ -56,11 +56,11 @@ Cloudflare named tunnel 은 아웃바운드 연결만 쓰므로 포트포워딩
그 대가가 두 곳에서 청구됐다.
인증서 발급 : HTTP-01 로 받을 수 없어 DNS-01 로 갔다
재구축 : 인증서 디렉터리 `/etc/letsencrypt/`지워도 되는지가 미확정이
재구축 : DNS-01 등록은 확인됐지만 Cloudflare 자격증명 유효성과 dry-run 성공을 확인하기 전에는 `/etc/letsencrypt/`보존한
기각한 파일을 지우지 않는 방침에도 근거가 셋 있다.
① 저장소의 목적이 비교다 : 선택지를 나란히 두고 트레이드오프를 적는 것 자체가 산출물이라, 하나만 남기면 왜 이것을 골랐는지를 뒷받침할 근거가 없어진다
② 죽은 코드가 아니라 테스트되는 코드다 : `scripts/verify-public-tunnel-config.sh` 가 붙어 있어 실행되지 않을 뿐 깨지면 드러난다
③ 실험대 전용 설정은 `lab/` 아래로 분리했다 : 일반 배포 설정과 섞이지 않는다
재지 않은 것도 있다. 터널을 붙인 상태와 지금을 같은 방법으로 잰 비교는 이 저장소에 없다. 기각의 근거는 홉 수와 섞이는 헤더이지 두 구성을 재서 견준 값이 아니다. 인증서를 지금 어느 방식으로 받고 있는지도 이 실험대에서 아직 재지 않았다.
재지 않은 것도 있다. 터널을 붙인 상태와 지금을 같은 방법으로 잰 비교는 이 저장소에 없다. 기각의 근거는 홉 수와 섞이는 헤더이지 두 구성을 재서 견준 값이 아니다. 인증 방식은 2026-09-17 `dns-cloudflare` 로 확인됐다. 다만 같은 후속 원문에서 Cloudflare 토큰 길이는 0자로 측정돼 재발급 가능성까지 확인된 것은 아니다.
@@ -6,7 +6,7 @@ topic: lab-entry-path-and-measurement-integrity
topicName: 실험대의 진입 경로
project: virtualization
status: 게시 전
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#209-nginx-keycloak-lab-conf-스티키-스위치와-신뢰-경계
- final/document.md#259-x-forwarded--와-신뢰-경계
@@ -7,7 +7,7 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
lastVerifiedOn: 2026-09-10
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#198-자원-할당과-실사용은-다르다
- final/document.md#199-디스크-오버레이는-얼마나-쓰나
@@ -9,7 +9,7 @@ status: 게시 전
assets:
- key: nftables-forward-hook-chain-order
file: ../../../final/assets/diagrams/nftables-forward-hook-chain-order/nftables-forward-hook-chain-order.svg
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다
- final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나
@@ -7,7 +7,7 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
basisVersion: Debian 12 genericcloud 위의 cloud-init 22.4.2 · NoCloud 데이터소스 · 호스트는 QEMU 11.1.1 · libvirt 12.7.0
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#229-왜-os를-설치하지-않아도-vm이-뜨는가
- final/document.md#236-클라우드-이미지와-cloud-init
@@ -7,7 +7,10 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
basisVersion: QEMU 11.1.1 의 qcow2 v3 · cluster_size 65536(기본값) · 실측은 Debian 12 genericcloud base.qcow2 한 장 · 압축은 zlib
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
assets:
- key: qcow2-mapping-clusters
file: ../../../final/assets/diagrams/qcow2-mapping-clusters/qcow2-mapping-clusters.svg
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#231-qcow2-파일-내부는-어떻게-생겼나-매핑표가-전부다
- final/document.md#230-디스크-이미지를-"복사한다"는-것의-실제-원리
@@ -40,6 +43,8 @@ qcow2 는 raw 에 매핑표 하나를 더한 것이고, 표가 가리키는 값
<!-- body:start -->
![게스트 오프셋이 L1과 L2 표를 지나 현재 qcow2 데이터 클러스터로 가거나, L2 항목이 0일 때 backing file 또는 zero-fill로 갈라지는 qcow2 매핑도.](../../../final/assets/diagrams/qcow2-mapping-clusters/qcow2-mapping-clusters.svg)
## raw 는 배열을 그대로 담는다
디스크는 섹터가 0번부터 늘어선 1차원 배열로 보인다. 파티션 테이블도 파일시스템도 부트로더도 전부 그 배열 안의 특정 위치에 기록된 바이트이고, 디스크 바깥에 따로 보관되는 정보가 없다. 그래서 배열을 처음부터 끝까지 그대로 파일에 쓰면 raw 이미지가 되고, 되돌린 디스크는 원본과 바이트 단위로 같아 똑같이 부팅한다.
@@ -7,7 +7,7 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
basisVersion: QEMU 11.1.1 · libvirt 12.7.0 위의 qcow2 · 게스트는 Debian 12 genericcloud · 크기 계산은 클러스터 64KiB · L2 항목 8B 기준
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#181-qcow2-가-담는-것과-담지-않는-것
- final/document.md#178-이-부의-출처와-범위
@@ -7,7 +7,7 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
decisionStatus: ADOPTED
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신
- final/document.md#184-이-부의-출처와-범위
@@ -67,6 +67,6 @@ DNS 공급자 의존 : 인증서를 받는 일이 Cloudflare 계정에 묶인다
발급 자체에도 순서가 붙는다. --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 를 둘 준다.
이 결정 뒤에 lineage 이름을 추측하는 규칙을 두지 않는다. 2026-09-17 현재 실험대에서 `sudo ls /etc/letsencrypt/live/``certbot certificates` 로 확인한 실제 경로는 `live/auth.hyeonworks.com/` 이다(observed). 브라우저가 보는 유효 호스트명은 SAN 목록이고 lineage 디렉터리 이름과 별개다. 원본 가이드 04 에 남아 있던 `live/hyeonworks.com/` 을 nginx 에 쓰면 `cannot load certificate` 로 막고, §182 가 이것을 가이드 결함으로 셌다. 그래서 nginx 에는 이름을 추측해 적지 않고 `certbot certificates` 가 찍은 경로를 그대로 쓴다. 와일드카드는 한 단계만 덮으므로 `a.b.hyeonworks.com` apex `hyeonworks.com` 은 별도 조건으로 본다.
이 결정이 끝내지 못한 것이 하나 있다. 받은 인증서가 갱신 뒤에 실제로 서빙되는지는 발급 방식과 별개이고, 근거로 건 첫 기록이 그것을 받는다.
@@ -7,7 +7,7 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
decisionStatus: ADOPTED
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나
- final/document.md#178-이-부의-출처와-범위
@@ -13,7 +13,7 @@ source:
- final/document.md#245-libvirt-default-네트워크와-virbr0
- final/document.md#246-dnsmasq-libvirt-내장-dhcp-dns
- final/document.md#248---live---config
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# 게스트 주소는 libvirt DHCP 예약으로 고정한다 — 바꿀 수 없어서가 아니라 되돌리기가 가장 비싸서
@@ -11,7 +11,7 @@ source:
- final/document.md#281-docker를-lab-host에-설치하면-안-되는-이유
- final/document.md#282-그러면-이미지는-어떻게-넣는가
- final/document.md#280-무엇을-어디에-설치하는가
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# lab host 에는 Docker 를 깔지 않는다 — 이미지 저장소가 갈리고, 그 위에 네트워크 변수가 하나 더 붙는다
@@ -11,7 +11,7 @@ source:
- final/document.md#213-왜-호스트에-직접-깔지-않고-vm-2대인가
- final/document.md#313-k3s-server와-agent-죽였을-때가-다르다
- final/document.md#218-실험대-전체-배치-2026-09-03-구축-완료-실측값
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# 호스트에 직접 깔지 않고 게스트 VM 두 대로 간다 — 관측자가 실험 대상과 함께 죽으면 안 된다
@@ -7,7 +7,7 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#183-이-부에서-파생될-open-question-oq-1
- final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다
@@ -6,8 +6,10 @@ topic: lab-environment-build
topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
questionStatus: RESOLVED
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/58-authenticator-and-token.txt
source:
- final/document.md#204-재구축할-때-무엇이-남아-있나
- final/document.md#266-dns-01-은-언제-쓰는가-네-가지-경우
@@ -16,100 +18,75 @@ source:
# 이 실험대의 certbot 은 HTTP-01 로 받고 있는가 DNS-01 로 받고 있는가
어느 방식으로 받고 있는지 아직 읽지 못했다. §266 의 결론과 §190 은 DNS-01 을 가리키는데 원본 가이드 04 는 HTTP-01 로 적혀 있다. `authenticator` 한 값이면 갈리지만 호스트의 `sudo` 가 비밀번호를 요구해 비대화식으로 못 읽었다.
2026-09-17 renewal 설정을 직접 읽으면서 답이 나왔다. `authenticator = dns-cloudflare` 였다. 따라서 이 실험대의 certbot은 DNS-01로 등록되어 있다(observed). 이전에 HTTP-01과 DNS-01 중 어느 쪽인지 열어 둔 상태는 닫는다.
## 관계
- **인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다**
결정이 적은 것이 의도인지 이 호스트의 실제 설정인지를 이 물음이 가른다.
- **DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다**
그 절차가 `--dns-cloudflare` 로 받는 길을 적는다. 이 호스트가 그 길로 받았는지는 확인하지 않았다.
- **실험대를 철거하고 무엇이 남는지 확인한다**
그 절차의 「인증서를 지우지 않는다」가 이 물음의 답에 걸려 있다.
- **갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초**
갱신은 됐는데 서빙까지 안 간 사건이다. 이 물음이 캐는 것은 갱신 자체가 도는지다.
- **공개 터널을 쓰지 않고 tailnet 직결로 둔다 — 홉이 하나 늘면 재려던 계약이 오염된다**
공개 인터넷에서 이 호스트에 닿을 길을 두지 않기로 한 결정이고, HTTP-01 이 성립하지 않는 조건 결정이 만든다.
- **가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가**
그 물음이 재는 7단계 가운데 04 의 통과 조건이 이 답으로 확정된다.
- 인증서는 DNS-01 로 받는다 — 실험대 주소가 공개 인터넷에 없어 HTTP-01 이 성립하지 않는다
결정이 설계 의도만 적은 것이 아니라 현재 실험대의 등록 상태와도 일치한다는 것이 확인됐다.
- DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다
renewal 설정의 `authenticator`와 자격증명 경로를 실제 출력으로 확인한 Setup이다.
- 실험대를 철거하고 무엇이 남는지 확인한다
검증 방식은 닫혔지만 현재 Cloudflare 자격증명이 재발급에 쓸 수 있는지는 별도 확인이 필요해 인증서 보존 판단에는 그 조건이 남는다.
- 갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다 — 2305초와 1~2초
이 질문은 발급·갱신 방식만 닫는다. 갱신된 파일을 nginx가 다시 읽는 문제는 그 Case의 범위다.
- 공개 터널을 쓰지 않고 tailnet 직결로 둔다 — 홉이 하나 늘면 재려던 계약이 오염된다
공개 인터넷에서 HTTP-01이 성립하지 않는 조건대로다.
- 가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가
7단계 중 04의 인증 방식 판정은 닫혔다. 재발급 자격증명과 dry-run 성공 여부는 별도 검증으로 남긴다.
## 사실
§266 이 문서 둘이 어긋나 있다고 직접 적었다. 그 절의 결론과 §190 은 DNS-01 을 가리킨다.
2026-09-17 `sudo sh -c` 안에서 `/etc/letsencrypt/renewal/*.conf`를 읽었다. 후속 원문에는 다음 세 줄이 남아 있다.
원본 가이드 `docs/guides/04-tls/README.md``certbot certonly --webroot` 로 적혀 있다. 전제도 공개 DNS 에 이름 셋이 이 호스트를 가리켜야 한다는 것이다.
- `authenticator = dns-cloudflare`
- `dns_cloudflare_credentials = /etc/letsencrypt/cloudflare.ini`
- `server = https://acme-v02.api.letsencrypt.org/directory`
`dig +short auth.hyeonworks.com``100.83.212.4` 를 낸다.
일반 셸에서 `sudo grep ... /etc/letsencrypt/renewal/*.conf`로 친 첫 시도는 zsh가 글로브를 sudo보다 먼저 펼치면서 `no matches found`로 실패했다. 그래서 `sudo sh -c` 안에서 글로브를 펼쳤다.
`100.64.0.0/10` 은 CGNAT(Carrier-Grade NAT, 통신사 공용 주소 변환)용 예약 대역이라 공개 인터넷에서 라우팅 자체가 안 된다. 방화벽을 여는 문제가 아니라 그 주소가 인터넷에 존재하지 않는다. 이것은 규격이고 이 실험대가 잰 값이 아니다.
같은 후속 원문에서 Cloudflare 자격증명 파일의 토큰 길이는 `0자`로 측정됐고 토큰 검증은 `Invalid request headers`로 실패했다. 따라서 DNS-01 등록확인됐지만 현재 자격증명이 유효하다고 확인된 것은 아니다.
`certbot plugins` 를 돌린 실측이 `dns-cloudflare` · `standalone` · `webroot` 세 줄을 냈으니 플러그인은 깔려 있다.
`auth.hyeonworks.com`은 tailnet 주소 `100.83.212.4`로 풀리고, 이 주소는 공개 인터넷에서 라우팅되지 않는 `100.64.0.0/10` 안이다. 이 조건 때문에 HTTP-01을 현재 진입 경로에 그대로 적용할 수 없다는 판단도 바뀌지 않는다.
§190 이 적은 발급 대상은 `-d hyeonworks.com -d '*.hyeonworks.com'` 이고 lineage 디렉터리`/etc/letsencrypt/live/hyeonworks.com/` 이다. 자격증명 파일은 `/etc/letsencrypt/cloudflare.ini` 이고 권한이 `600`다.
ACME 명세가 와일드카드를 DNS-01 로만 허용한다. 호스트 한 대에 파일을 놓는 것은 그 이름 하나를 통제한다는 증명이고, DNS 존의 TXT 레코드를 고칠 수 있다는 것은 도메인 전체를 통제한다는 증명이라 증명의 급이 다르다.
§204 가 이 항목을 「미측정」으로 적었다. 호스트의 `sudo` 가 비밀번호를 요구해 비대화식으로 읽지 못했다.
2026-09-17 현재 lineage는 `/etc/letsencrypt/live/auth.hyeonworks.com/`다. `/etc/letsencrypt/live/hyeonworks.com/`은 원본 가이드에 남은 known-wrong 경로였고, 그대로 `nginx -t`를 실행하면 certificate file not found로 실패했다.
## 가정
지금 서빙되는 인증서가 와일드카드라면 발급은 DNS-01 로 이뤄졌을 수밖에 없다. 다만 그 인증서가 실제로 와일드카드인지는 이 저장소에 출력으로 남아 있지 않다.
플러그인이 보인다는 것과 그것으로 받았다는 것을 같게 읽지 않는다. `certbot plugins` 는 설치된 것을 세지 무엇으로 발급했는지를 세지 않는다.
갱신이 돌고 있다는 것도 아직 확인이 아니다. §218 의 구축 완료 판정 기준이 `systemctl is-active nginx certbot-renew.timer``active active` 를 요구하지만, §190 은 `systemctl list-timers certbot-renew.timer` 의 실제 출력이 남아 있지 않다고 적는다.
갱신 설정이 발급 시점의 방식을 그대로 물려받았다고 전제한다. 발급 뒤에 누가 `renewal/*.conf` 를 손으로 고쳤다면 그 전제가 깨진다.
renewal 설정이 2026-09-17 관측 뒤에 수동으로 변경되지 않았다고 전제한다. 이후 설정을 바꾸면 이 기록의 “현재” 범위도 그 시점까지만 유효하다.
## 미지수
`/etc/letsencrypt/renewal/*.conf` `authenticator` 가 무엇인가.
이 질문의 핵심 미지수였던 `authenticator`는 해소됐다.
그 값이 `webroot``standalone` 이면 지금 갱신이 실제로 돌고 있는가. 검증이 성립하지 않는 주소에 HTTP-01 로 설정돼 있다면 갱신은 조용히 실패한다.
남은 미지수는 다른 판단에 속한다.
그 값이 `dns-cloudflare` 라면 `/etc/letsencrypt/cloudflare.ini` 든 토큰의 권한이 `존 하나 + DNS:Edit` 으로 좁혀져 있는가. §266 이 그 범위를 값으로 치르는 것이라고 적었는데, 이 호스트의 토큰이 실제로 그 범위인지는 SSOT 에 없다.
- `/etc/letsencrypt/cloudflare.ini`유효한 토큰을 다시 넣었는가.
- 그 토큰의 권한이 필요한 zone과 DNS 편집 범위로 좁혀져 있는가.
- 현재 `certbot renew --dry-run`이 성공하는가.
지금 서빙되는 인증서가 와일드카드인가, 그리고 lineage 이름이 무엇인가.
이 셋은 DNS-01인지 HTTP-01인지를 다시 여는 근거가 아니다. 재발급·갱신 가능성을 확인하는 후속 조건이다.
## 제약
호스트의 `sudo` 가 비밀번호를 요구해서 비대화식으로는 읽을 수 없고, 콘솔에서 쳐야 한다. 이것이 §204 가 미측정으로 남긴 까닭이다.
비밀 값은 증거에 남기지 않는다. 자격증명은 길이·존재 여부와 API 검증 결과까지만 기록한다.
비밀 값을 옮기지 않는다. `cloudflare.ini` 의 토큰은 길이와 존재 여부까지만 적고 값을 찍는 명령을 남기지 않는다.
Let's Encrypt 를 tailnet 에 초대할 방법이 없다. 검증 방식을 바꿔 가며 돌려 보는 실험으로 답을 대신할 수 없다.
발급 한도가 같은 이름 조합에 주당 중복 5장이다. 시험은 `--dry-run` 으로 먼저 한다.
`sudo`가 필요한 디렉터리의 글로브는 일반 사용자 셸에서 먼저 펼치지 않는다. 이 실험대에서는 그 방식이 실제로 실패했다.
## 선택지
### 1. 갱신 설정 파일을 콘솔에서 직접 읽는다
### 1. DNS-01 — 현재 관측과 일치
`authenticator` 한 값이 이 물음을 통째로 닫는다. 같은 콘솔 세션에서 쓸 수 있는 방식과 갱신 예행연습과 이름이 가리키는 주소까지 한 번에 찍어 같은 시각의 출력으로 묶는다. 값을 읽을 수 없는 경우는 파일이 없을 때뿐이고, 그때는 이 호스트가 인증서를 받은 적이 없다는 다른 답이다.
renewal 설정이 `dns-cloudflare`를 직접 가리킨다. 이 질문의 답이다.
### 2. 지금 서빙되는 인증서가 와일드카드인지부터 본다
### 2. HTTP-01 — 현재 관측과 불일치
`sudo` 없이도 밖에서 TLS 핸드셰이크만으로 도메인 목록을 볼 수 있다. 와일드카드로 나오면 발급이 DNS-01 이었다는 쪽으로 정황이 좁혀진다. 다만 좁혀질 뿐 `authenticator` 를 대신하지는 못한다. 발급은 와일드카드로 받고 갱신 설정만 다른 경우를 이 방법으로는 가르지 못한다.
### 3. 인증서를 지우고 다시 받아 본다 — 제외
다시 받아 보면 어느 방식이 성립하는지 바로 드러나지만, §204 가 「어느 쪽인지 모르는 채로는 지우지 않는다」로 그 순서를 막아 두었다. HTTP-01 로 설정돼 있으면 재발급이 안 되고, 그러면 재구축을 시작하자마자 검증 방식부터 손봐야 한다. 답을 얻으려고 답이 필요한 상태를 만드는 순서다.
원본 가이드 04에는 `--webroot` 경로가 남아 있었지만, 현재 renewal 설정이 그것을 사용한다는 증거는 없다. historical document branch로만 남긴다.
## 다음 검증
§204 와 §266 이 적은 네 줄을 호스트 콘솔에서 그대로 친다.
이 Question은 RESOLVED다. `authenticator`를 다시 읽는 것은 질문을 닫기 위한 검증이 아니라 설정 변경 감시다.
1. `sudo grep -H authenticator /etc/letsencrypt/renewal/*.conf` 으로 검증 방식을 읽는다.
2. `certbot plugins` 를 돌려 앞에 `*` 가 붙은 줄을 적는다.
3. `sudo certbot renew --dry-run` 으로 갱신이 실제로 되는지 본다.
4. `dig +short auth.hyeonworks.com` 으로 Let's Encrypt 가 올 수 있는 주소인지를 같은 시각에 함께 남긴다.
5. 지금 서빙되는 인증서가 와일드카드인지와 lineage 이름이 무엇인지를 `certbot certificates` 의 도메인 목록으로 적는다.
6. 출력 원문을 `final/evidence/raw/` 에 남기고 `meta/` 에 명령과 cwd 와 실행 시각과 종료 코드를 적는다. `cloudflare.ini` 의 토큰 값은 찍지 않는다.
재발급 가능성을 확인하려면 유효한 Cloudflare 자격증명을 준비한 뒤 `sudo certbot renew --dry-run`을 별도 검증한다. 그 결과가 실패하더라도 이 Question을 다시 `OPEN`으로 돌리지 않는다. 실패 원인은 자격증명·DNS 권한·ACME 경로 쪽의 새 문제로 분리한다.
닫는 조건 : `authenticator` 한 값과 `--dry-run` 결과가 나오면 닫는다.
`dns-cloudflare` 면 「인증서는 DNS-01 로 받는다」가 이 호스트의 현재 상태를 적은 것으로 확인되고, 철거 절차의 「인증서를 지우지 않는다」가 정책에서 선택으로 바뀐다. §204 가 적은 대로 그때는 백업이 헛수고이므로 지워도 되고 재구축 절차가 한 단계 짧아진다.
`webroot``standalone` 이면 그 결정이 적은 것은 의도이고 실제 설정은 다른 것이므로, §190 과 가이드 04 가운데 어느 쪽이 실재인지를 먼저 고친 뒤 그 기록을 다시 판정한다.
`--dry-run` 이 실패하면 갱신이 이미 멈춰 있다는 뜻이라 그 자체가 새 Case 다. 「갱신은 매번 SUCCESS 였고 옛 인증서가 계속 나갔다」가 갱신은 되는데 서빙까지 안 간 것을 다뤘다면, 이번 것은 갱신 자체가 안 되는 쪽이고 증상이 또 조용하다.
닫는 조건 : 2026-09-17 raw evidence에서 `authenticator = dns-cloudflare`가 직접 관측되어 충족됐다.
@@ -7,7 +7,7 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#183-이-부에서-파생될-open-question-oq-3
- final/document.md#181-qcow2-가-담는-것과-담지-않는-것
@@ -7,7 +7,7 @@ topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
questionStatus: OPEN
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#183-이-부에서-파생될-open-question-oq-2
- final/document.md#181-qcow2-가-담는-것과-담지-않는-것
@@ -6,7 +6,7 @@ topic: lab-environment-build
topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#203-실측으로-드러난-함정-셋
- final/document.md#284-nginx-설정-구조-sites-available-은-nginx-기능이-아니다
@@ -6,7 +6,7 @@ topic: lab-environment-build
topicName: 실험대 환경 구성
project: virtualization
status: 게시 전
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#182-이-구축에서-드러난-문서-결함의-공통-원인
- final/document.md#178-이-부의-출처와-범위
@@ -64,7 +64,7 @@ source:
## 예외
이 기준이 잡는 것은 순서와 위치이고 명령의 정확성은 아니다. §182 이 적었듯 개별 명령은 전부 실제로 돌았던 것이라, 이 검사를 통과해도 오타나 잘못된 플래그나 낡은 옵션은 그대로 지나간다. 04 단계의 인증서 경로가 그런 경우다. 와일드카드로 받은 인증서 묶음의 이름(lineage)은 live/hyeonworks.com/ 인데 문서에는 live/auth.hyeonworks.com/ 이라 적혀 있었다. 단계 순서를 맞춰도 그 줄은 틀린 채다.
이 기준이 잡는 것은 순서와 위치이고 명령의 정확성은 아니다. §182 이 적었듯 개별 명령은 전부 실제로 돌았던 것이라, 이 검사를 통과해도 오타나 잘못된 플래그나 낡은 옵션은 그대로 지나간다. 04 단계의 인증서 경로가 그런 경우다. 2026-09-17 실제 lineage 는 `live/auth.hyeonworks.com/` 이었는데 원본 가이드에는 `live/hyeonworks.com/` 이라 적혀 있었다. 단계 순서를 맞춰도 그 known-wrong 경로는 그대로 실패한다.
배포판 차이도 이 두 검사로는 안 잡힌다. 03 단계의 설정 블록에 있던 http2 on; 은 Debian 12 의 nginx 1.22 에서 unknown directive 가 됐다. 순서를 맞춰도 같은 오류가 나므로 대상 배포판에서 실제로 돌려야 드러난다.
@@ -78,7 +78,7 @@ source:
- 03 단계 : nginx 설치 단계가 없어 /etc/nginx: No such file or directory
- 03 단계 : 설정 블록이 http2 on; 이라 Debian 12 의 nginx 1.22 에서 unknown directive
- 04 단계 : 인증서 경로가 lineage 이름과 다르다. 와일드카드는 live/hyeonworks.com/ 인데 live/auth.hyeonworks.com/ 이라 적혀 있었
- 04 단계 : 인증서 경로가 실제 lineage 다르다. 실제는 `live/auth.hyeonworks.com/` 인데 원본 가이드는 `live/hyeonworks.com/` 을 적어 `nginx -t` 가 실패했
- 00·03·05·06 단계 : 저장소가 lab host 에 있다고 가정해 cp: cannot stat 'deploy/...'
- 05 단계 : 그 단계에 없는 리소스를 -l app=bff 로 조회. BFF 는 한참 뒤에 뜬다
- 04 단계 : 확인 명령을 칠 위치가 틀렸다. 엣지 VM 안에서 tailnet 주소를 치면 connection refused
@@ -9,17 +9,17 @@ project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/f972ca27-7e18-41c1-9494-59cc6f676ae2/edit"
pinnedVersions:
- name: libvirt
- name: libvirt (observed; 이 Setup이 설치 버전을 고정하지 않음)
version: 12.7.0
- name: Debian GNU/Linux
version: 12 (bookworm)
- name: cloud-init
- name: cloud-init (observed; bookworm/latest 이미지라 exact version 미고정)
version: 22.4.2
source:
- final/document.md#187-단계-01-게스트-세-대
- final/document.md#185-가이드-묶음이-스스로-정한-규약
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다
@@ -227,7 +227,7 @@ grep -c 'ssh-ed25519\|ssh-rsa' kc-lab-1.yaml
**예상 결과** — `0``2`. 첫 줄이 `0` 이 아니면 `__LAB_HOST_KEY__` 같은 문자열이 남아 있고, 둘째 줄이 `2` 가 아니면 키 하나가 안 들어갔다. 두 숫자가 맞아야 시드를 만든다.
첫 파일에서는 여기까지다. cloud-config 스키마까지 보는 검증기는 `cloud-init schema` 인데, 그것은 게스트의 cloud-init `22.4.2` 이고 첫 파일을 쓰는 시점에는 게스트가 아직 없다. lab host 에 cloud-init 이 깔려 있는지는 원본 가이드에 적혀 있지 않고 재지도 않았으며(unknown), `yamllint` 은 이 실험대에 없다. 게스트가 한 대라도 뜬 뒤에는 둘째 파일부터 그 검증기로 본다. 파일을 보내고, 들어가고, 검사하고, 지운다.
첫 파일에서는 여기까지다. cloud-config 스키마까지 보는 검증기는 `cloud-init schema` 인데, 2026-09-17 이 실험대에서 관측한 게스트의 cloud-init `22.4.2` 였다. 하지만 base URL이 `bookworm/latest` 이므로 다음 다운로드도 같은 cloud-init 버전이라는 계약은 없다. 첫 파일을 쓰는 시점에는 게스트가 아직 없다. lab host 에 cloud-init 이 깔려 있는지는 원본 가이드에 적혀 있지 않고 재지도 않았으며(unknown), `yamllint` 은 이 실험대에 없다. 게스트가 한 대라도 뜬 뒤에는 둘째 파일부터 그 검증기로 본다. 파일을 보내고, 들어가고, 검사하고, 지운다.
```bash label="[lab host] ③ 검사할 파일을 게스트로 보낸다"
scp kc-lab-2.yaml donghyeon@192.168.122.11:~/
@@ -267,7 +267,7 @@ Valid cloud-config: /home/donghyeon/kc-lab-edge.yaml
이 실험대는 접속과 리다이렉션과 권한 설정을 두 줄에 몰아넣었다(observed).
```bash label="[lab host] 이 실험대가 실제로 친 형태"
```bash label="[lab host] [reference] 이 실험대가 실제로 친 형태"
ssh donghyeon@192.168.122.11 'umask 077; cat > ~/kc-lab-2.yaml' < kc-lab-2.yaml
ssh donghyeon@192.168.122.11 'cloud-init schema -c ~/kc-lab-2.yaml; rm -f ~/kc-lab-2.yaml'
```
@@ -469,7 +469,7 @@ Domain creation completed.
| 디스크 | base 위의 오버레이(`backing_store=`). 복사가 아니다 |
| 시드 | `seed-<이름>.iso``CIDATA` 라벨 · `user-data`·`meta-data` · `bus=virtio` |
| cloud-init 패키지 | `[curl, nftables]` |
| 스키마 검사기 | 게스트의 cloud-init `22.4.2` |
| 스키마 검사기 | 2026-09-17 관측: cloud-init `22.4.2` · `bookworm/latest` 사용으로 재다운로드 버전은 미고정 |
메모리는 처음 만들 때 세 대 다 3584MB 였고, 실험을 늘리며 5120 과 4096 으로 재배분했다(observed). 세 게스트의 배정 합은 5120+4096+1024 = 10,240MB 이고, 호스트 RAM 은 11,648MiB 다. 배정 합이 더 작아서 배정률은 10240/11648 = 87.9% 이고, 배정하지 않고 남은 것이 1,408MiB 다. 그래서 이 배치는 「Guest configured memory 총량이 Host physical RAM보다 크다」는 Memory Overcommit 에 해당하지 않는다.
@@ -9,7 +9,7 @@ project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/74a7bacf-e5d8-4129-926a-c8cf5cacb8c9/edit"
pinnedVersions:
- name: nginx (엣지 게스트)
- name: nginx (엣지 게스트, observed; apt 설치 버전 미고정)
version: 1.22.1
- name: Debian GNU/Linux
version: 12 (bookworm)
@@ -20,7 +20,7 @@ source:
- final/document.md#179-엣지를-물리-호스트에서-vm-으로-옮기면-무엇이-새로-필요해지나
- final/document.md#180-nftables-는-앞-체인의-accept-로-뒤-체인의-reject-를-막지-못한다
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# 엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다
@@ -17,7 +17,10 @@ source:
- final/document.md#188-단계-02-k3s-server-와-agent
- final/document.md#185-가이드-묶음이-스스로-정한-규약
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/22-k3s-agent-install.txt
- ../../../final/evidence/raw/lab-state-before-rebuild/23-k3s-cluster-verify.txt
---
# k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다
@@ -121,7 +124,7 @@ ssh donghyeon@192.168.122.11 'hostname; cat /etc/os-release | head -1'
**목적** — `kc-lab-1` 을 control-plane 으로 세우고, 그 노드가 자기 주소를 `192.168.122.11` 로 알게 한다.
```bash label="[lab host] ① server 설치 스크립트를 원격으로 돌린다"
ssh kc-lab-1 'curl -sfL https://get.k3s.io | sudo sh -s - server --node-ip 192.168.122.11'
ssh kc-lab-1 "curl -sfL https://get.k3s.io | sudo env INSTALL_K3S_VERSION=v1.36.4+k3s1 sh -s - server --node-ip 192.168.122.11"
```
```bash label="[lab host] ② 유닛이 떴고 자기 자신을 노드로 등록했는지 본다"
@@ -255,7 +258,7 @@ chmod 600 ~/node-token
```
```bash label="[kc-lab-2] ⑤ agent 를 깐다"
curl -sfL https://get.k3s.io | sudo sh -s - agent \
curl -sfL https://get.k3s.io | sudo env INSTALL_K3S_VERSION=v1.36.4+k3s1 sh -s - agent \
--server https://192.168.122.11:6443 \
--token-file ~/node-token \
--node-ip 192.168.122.12
@@ -315,9 +318,9 @@ exit
**왜 필요한가** — 설치 스크립트는 `sudo` 아래 root 로 도니 홈의 `600` 파일도 읽는다. `--token` 대신 `--token-file` 을 쓰면 토큰이 명령줄에 안 들어가므로 프로세스 목록과 셸 히스토리에 남지 않고, 토큰을 꺼낸 셸과 같은 셸에서 쳐야 한다는 제약도 없어진다.
이 실험대는 두 줄로 했다(observed).
이 실험대는 두 줄로 했다(observed). 아래는 당시 실행을 보존한 historical/reference block이고, 따라 하는 절차는 위 ①~⑪처럼 검증과 삭제를 분리한다.
```bash label="[lab host] 이 실험대가 실제로 친 형태"
```bash label="[lab host] [reference] 이 실험대가 실제로 친 형태"
ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token' \
| ssh kc-lab-2 'sudo tee /tmp/token >/dev/null'
ssh kc-lab-2 "curl -sfL https://get.k3s.io | sudo sh -s - agent \
@@ -528,7 +531,7 @@ subject=O = system:nodes, CN = system:node:kc-lab-2
## 무엇이 관측이고 무엇이 아닌가
- (observed) `v1.36.4+k3s1`, 두 노드 `Ready`, INTERNAL-IP 두 개, 토큰 108자, `ExecStart` 두 줄, agent 의 `localhost:8080` 오류와 인증서 subject.
- (observed) 2026-09-17 당시 stable installer가 선택한 `v1.36.4+k3s1`, 두 노드 `Ready`, INTERNAL-IP 두 개, 토큰 길이, `ExecStart` 두 줄, agent 의 `localhost:8080` 오류와 인증서 subject. 위 canonical 설치 명령은 재실행 때도 같은 k3s를 받도록 `INSTALL_K3S_VERSION=v1.36.4+k3s1`을 명시한다.
- (observed) `kube-system` 에 뜬 파드 여덟 줄과 `local-path (default)` StorageClass. `svclb-traefik-` 두 줄이 DaemonSet 이 두 노드에 다 떴다는 증거가 된다.
- (external) 쿠버네티스 1.20 이전의 평문 8080, Node authorizer 와 NodeRestriction 의 동작은 이 실험대에서 잰 값이 아니다.
- (unknown) 3번과 4번의 나눈 형태는 이 실험대에서 치지 않았다. 이 실험대가 친 것은 원격 셸 둘을 파이프로 이은 두 줄이고, 나눈 형태로 같은 클러스터가 서는지는 다시 재지 않았다.
@@ -13,7 +13,7 @@ pinnedVersions:
version: v1.36.4+k3s1
- name: curlimages/curl
version: 8.11.1
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#191-단계-05-keycloak-2노드와-postgresql
- final/document.md#184-이-부의-출처와-범위
@@ -24,7 +24,7 @@ source:
- final/document.md#333-안전한-종료-순서
- final/document.md#334-복구-순서-종료의-역순
- final/document.md#313-k3s-server와-agent-죽였을-때가-다르다
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# 실험대를 껐다 켜고 게스트 메모리를 다시 나눈다
@@ -9,15 +9,15 @@ project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/7c66a553-0008-4294-a27a-687bd1bda0c1/edit"
pinnedVersions:
- name: libvirt
- name: libvirt (observed; pacman 설치는 버전 미고정)
version: 12.7.0
- name: QEMU
- name: QEMU (observed; pacman 설치는 버전 미고정)
version: 11.1.1
source:
- final/document.md#186-단계-00-lab-host-가상화-준비
- final/document.md#185-가이드-묶음이-스스로-정한-규약
- final/document.md#184-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
---
# lab host 에 가상화 패키지를 깔고 virsh 가 sudo 없이 돌게 만든다
@@ -185,7 +185,7 @@ virsh --version
qemu-system-x86_64 --version
```
**예상 결과** — 판 번호 두 줄. 이 실험대는 libvirt `12.7.0``QEMU emulator version 11.1.1` 이었다(observed).
**예상 결과** — 판 번호 두 줄. 이 실험대는 libvirt `12.7.0``QEMU emulator version 11.1.1` 이었다(observed). 다만 위 `pacman -S --needed` 는 Arch 현재 저장소에서 받으므로 이 두 버전을 재설치에도 고정하는 명령이 아니다. frontmatter의 번호도 **tested/observed version**이지 package pin이 아니다.
**왜 필요한가** — 여기서 나오는 libvirt 판 번호가 뒤의 옵션 이름을 가른다. 훨씬 낮은 판이면 `virt-install --cloud-init` 같은 옵션의 동작이 달라질 수 있으니, 다음 단계에서 막힐 때 이 번호를 같이 본다.
@@ -13,7 +13,7 @@ pinnedVersions:
version: v1.36.4+k3s1
- name: Debian GNU/Linux
version: 12 (bookworm)
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
source:
- final/document.md#192-단계-06-prometheus-와-grafana
- final/document.md#184-이-부의-출처와-범위
@@ -23,7 +23,9 @@ source:
- final/document.md#202-철거-실제-출력-전문
- final/document.md#204-재구축할-때-무엇이-남아-있나
- final/document.md#195-이-부의-출처와-범위
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/58-authenticator-and-token.txt
---
# 실험대를 철거하고 무엇이 남는지 확인한다
@@ -39,7 +41,7 @@ sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
- **5120MB 를 줬는데 353MB 를 쓰고 있었다 — 선언한 양과 실제로 드는 양**
회수된 디스크 3.1GB 를 읽는 방법이 그 기록과 같다. 선언한 크기와 실제로 차지한 크기는 다른 값이다.
- **이 실험대의 certbot 은 HTTP-01 로 받고 있는가 DNS-01 로 받고 있는가**
인증서를 지우지 않고 남기는 까닭이 이 물음이 안 닫혔기 때문이다. 답이 나오면 정책이 바뀐다.
이 물음은 `dns-cloudflare` 관측으로 닫혔다. 여기서는 그 사실과 별개로 현재 자격증명이 재발급에 쓸 수 있는지를 철거 조건으로 본다.
- **가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가**
철거는 그 물음을 재기 위한 전제라, 지우고 다시 세워 봐야 답이 나온다.
- **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다**
@@ -159,9 +161,9 @@ error: XML error: Cannot use host name '' in network 'default'
zsh 에서 루프로 돌리면 이 오류를 만난다. zsh 는 따옴표 없는 변수를 단어 분리하지 않아서, bash 에서 되던 `set -- $entry``$1` 에 문자열 전체를 넣고 `$2``$3` 을 비워서 `name=""` 이 된다. 세 줄을 값 그대로 쓰는 편이 안전하다.
## 인증서는 건드리지 않는
## 인증서는 edge guest에서 백업한
**이 절의 전제가 03·04 뒤로 깨져 있다**(2026-09-17, observed). 아래 본문은 인증서가 랩 호스트`/etc/letsencrypt/` 에 있다고 보고 그 디렉터리를 정책으로 남긴다. 그런데 03 이 nginx `kc-lab-edge` 로 옮겼고 04 certbot 을 거기 깔았다. **지금 서빙하는 인증서는 그 게스트 안에 산다.** 그리고 이 절차의 1번은 게스트`--remove-all-storage` 로 지운다 — 인증서는 그 안에서 같이 사라진다.
2026-09-17 현재 서빙하는 인증서는 **`kc-lab-edge``/etc/letsencrypt/`** 에 있다(observed). 03에서 nginx를 edge guest로 옮겼고 04에서 certbot도 그 guest에 설치했다. 이 절차의 1번은 guest`--remove-all-storage`로 지우므로, 인증서도 백업하지 않으면 guest 디스크와 함께 사라진다.
두 기계를 나란히 열어 보면 이렇다.
@@ -175,32 +177,34 @@ ssh kc-lab-edge 'sudo ls -la /etc/letsencrypt/'
| 랩 호스트 | 있다 (읽으려면 `sudo`) | `/usr/bin/certbot` | 없다 | 안 한다 — 80·443 을 안 듣는다 |
| `kc-lab-edge` | 있다 — `cli.ini` · `renewal-hooks` | `/usr/bin/certbot` | `certbot.timer` | **한다** |
그래서 아래 백업 명령은 **틀린 기계를 묶는다.** 묶어야 할 것은 게스트 쪽이다.
따라서 **철거 전에 백업할 대상은 edge guest의 `/etc/letsencrypt/` 하나다.** 파일 이름은 host에서 한 번 만든 시각값으로 고정해, 원격 tar와 scp가 같은 파일을 가리키게 한다.
```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 ~/
```bash label="[lab host] 실제로 서빙하는 인증서를 edge guest에서 묶는다"
STAMP=$(date +%Y%m%d-%H%M%S)
ssh kc-lab-edge "sudo tar czf /tmp/letsencrypt-backup-$STAMP.tgz -C /etc letsencrypt"
scp "kc-lab-edge:/tmp/letsencrypt-backup-$STAMP.tgz" "$HOME/"
```
**「철거해도 남는 것」 표의 패키지 줄도 같은 이유로 틀렸다.** 그 줄은 `nginx``certbot` 을 남는 쪽에 적어 두었는데, 둘 다 게스트 안에 있으므로 게스트와 함께 사라진다. 랩 호스트에 남는 것은 `libvirt` · `qemu` · `kubectl` 과, 쓰이지 않는 호스트 쪽 `certbot` 이다.
**예상 결과** — host의 `$HOME/letsencrypt-backup-$STAMP.tgz`가 생긴다. 이 파일은 edge guest를 `--remove-all-storage`로 지우기 전에 host 쪽으로 빠져나온 복구본이다.
`/etc/letsencrypt/` 는 정책으로 남긴다. 한도 때문이 아니다 — Let's Encrypt 의 「같은 이름 조합에 주당 중복 5장」 제한은 가끔 재구축하는 정도로는 근처에도 못 간다.
패키지도 host와 guest를 나눠 본다. `kc-lab-edge` 안의 `nginx``certbot`은 guest와 함께 사라지고, host에는 `libvirt`·`qemu`·`kubectl`과 현재 서빙에는 쓰이지 않는 host `certbot`이 남는다.
남기는 까닭은 지금 재발급이 되는지를 모르기 때문이다. 이 실험대의 이름 셋은 tailnet 주소를 가리키고, `100.64.0.0/10` 은 CGNAT(Carrier-Grade NAT, 통신사 공용 주소 변환)용 예약 대역이라 공개 인터넷에서 라우팅되지 않는다. HTTP-01 검증은 Let's Encrypt 가 우리 서버로 들어오는 방식이므로 그 주소로는 검증이 성립하지 않는다. 지금 설정이 DNS-01 이면 지우고 다시 받으면 끝이고, HTTP-01 이면 검증 방식부터 손봐야 한다. 어느 쪽인지는 certbot 설정을 읽는 열린 물음이 한 줄로 닫는다.
정책으로 남기는 것은 guest 안의 디렉터리 자체가 아니라 **host로 복사한 인증서 backup tar**다. 한도 때문도 아니고 검증 방식이 미확정이어서도 아니다. 2026-09-17 renewal 설정에서 `authenticator = dns-cloudflare` 를 직접 읽어 **DNS-01 등록은 확인했다**.
§204 는 HTTP-01 인 경우를 「못 한다」가 아니라 「일이 하나 생긴다」로 적었다. 최악이라도 A 레코드를 공인 IP 로 잠깐 돌려 받거나 DNS-01 로 전환하면 되고, 다만 재구축을 시작하자마자 그 일부터 하게 된다. 그래서 이 정책은 「어느 쪽인지 모르는 채로는 지우지 않는다」가 전부다.
backup tar를 남기는 까닭은 **현재 자격증명으로 다시 받을 수 있는지가 확인되지 않았기 때문**이다. 같은 후속 원문에서 `/etc/letsencrypt/cloudflare.ini` 의 토큰 길이는 0자로 측정됐고 Cloudflare 검증은 `Invalid request headers` 로 실패했다. 따라서 DNS-01이라는 사실만으로 “guest를 지워도 다시 받으면 된다”고 결론내리지 않는다.
그래도 지워야 한다면 먼저 백업한다.
유효한 Cloudflare 토큰을 다시 준비하고 `certbot renew --dry-run` 이 성공한 원문까지 남기면 그때 보존 정책을 완화할 수 있다. 그 전에는 guest를 지우기 전에 실제 서빙 중인 `/etc/letsencrypt/` 를 host로 백업하고 그 tar를 보존한다.
```bash label="[lab host] 인증서를 통째로 묶어 둔다"
sudo tar czf ~/letsencrypt-backup-$(date +%Y%m%d-%H%M%S).tgz -C /etc letsencrypt
복원도 **새로 만든 `kc-lab-edge` 안으로** 되돌린다. host의 `/etc`에 풀지 않는다. `{{STAMP}}`에는 백업 파일 이름의 시각값을 넣는다.
```bash label="[lab host] 백업을 새 edge guest로 되돌린다"
BACKUP="$HOME/letsencrypt-backup-{{STAMP}}.tgz"
scp "$BACKUP" kc-lab-edge:/tmp/letsencrypt-backup.tgz
ssh kc-lab-edge 'sudo tar xzf /tmp/letsencrypt-backup.tgz -C /etc'
ssh kc-lab-edge 'sudo certbot certificates && sudo nginx -t'
```
복원은 반대로 한 줄이다. `{{STAMP}}` 는 위 명령이 만든 파일 이름에 찍힌 시각을 그대로 옮겨 넣는다.
```bash label="[lab host] 묶어 둔 인증서를 되돌린다"
sudo tar xzf ~/letsencrypt-backup-{{STAMP}}.tgz -C /etc
```
마지막 줄이 둘 다 성공해야 복원이 끝난 것이다. 인증서 파일이 돌아왔는지와 nginx가 그 경로를 실제로 읽을 수 있는지를 따로 확인한다.
## 구성 값
@@ -263,11 +267,13 @@ seed-kc-lab-{1,2,edge}.iso Capacity=370.00 KiB Allocation=372.00 KiB
| 무엇 | 어떻게 되나 | 왜 |
|---|---|---|
| `base.qcow2` (335MB) | 남는다 | 다음 오버레이의 바닥 |
| 패키지 (libvirt · qemu · nginx · certbot · kubectl) | 남는다 | 재설치가 무의미 |
| host 패키지 (`libvirt` · `qemu` · `kubectl`, 쓰이지 않는 host `certbot`) | 남는다 | guest 삭제와 무관한 host 설치 상태 |
| edge guest의 `nginx` · `certbot` | 사라진다 | `kc-lab-edge` 디스크와 함께 삭제된다 |
| `~/workspace/cloud/kc-lab-{1,2}.yaml` | 남는다 | 키와 비밀번호가 들어 있다 |
| `~/.ssh/config``kc-lab-*` 항목 | 남는다 | 재구축해도 IP 가 같다 |
| libvirt `default` 네트워크 정의 | 남는다 | 예약만 지웠다 |
| `/etc/letsencrypt/` | 남긴다 (정책) | 재발급이 되는지를 아직 모른다 |
| edge guest의 `/etc/letsencrypt/` | 사라진다 | guest 삭제 전에 host의 `$HOME/letsencrypt-backup-<stamp>.tgz`로 백업한다 |
| host에 복사한 인증서 백업 tar | 남긴다 (정책) | DNS-01 등록은 확인됐지만 credential 유효성과 `certbot renew --dry-run` 성공은 아직 미확인 |
| 게스트 디스크 · 시드 ISO | 사라진다 | `--remove-all-storage` |
| DHCP 예약 | 사라진다 | `net-update delete` |
| k3s · Keycloak · 모든 워크로드 | 사라진다 | 게스트와 함께 |
@@ -9,13 +9,16 @@ project: virtualization
status: 게시 전
studio: "https://hyeonworks.com/studio/documents/975a6d61-4e34-4034-a0d2-01fea3b498a3/edit"
pinnedVersions:
- name: nginx (엣지 게스트)
- name: nginx (엣지 게스트, observed; apt 설치 버전 미고정)
version: 1.22.1
- name: nginx (물리 호스트)
- name: nginx (물리 호스트, observed; 이 Setup이 설치·버전을 고정하지 않음)
version: 1.30.4
- name: Debian GNU/Linux
version: 12 (bookworm)
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
sourceRevision: import-head 9465582b5d1630eb4ae7c4e078021486919bf6b6 · uncommitted-working-tree snapshot
evidence:
- ../../../final/evidence/raw/lab-state-before-rebuild/58-authenticator-and-token.txt
- ../../../final/evidence/raw/lab-state-before-rebuild/60-doc04-step5-path-defect.txt
source:
- final/document.md#190-단계-04-let's-encrypt-와-인증서-갱신
- final/document.md#184-이-부의-출처와-범위
@@ -81,7 +84,7 @@ source:
| 패키지 | `certbot` · `python3-certbot-dns-cloudflare` |
| 자격증명 | `/etc/letsencrypt/cloudflare.ini``600`, root 만 읽기 |
| 발급 대상 | `-d hyeonworks.com -d '*.hyeonworks.com'` |
| lineage 디렉터리 | `/etc/letsencrypt/live/hyeonworks.com/`첫 번째 `-d` 에서 따온 라벨 |
| lineage 디렉터리 | `/etc/letsencrypt/live/auth.hyeonworks.com/`2026-09-17 현재 실험대에서 관측한 경로 |
| nginx 가 읽는 것 | `fullchain.pem` · `privkey.pem` |
| 유효기간 | 오늘 + 90일 |
| deploy 훅 | `/etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh` (`chmod +x`) |
@@ -98,7 +101,7 @@ source:
가이드는 이 선택을 우열로 적지 않는다. 공개 서버라면 HTTP-01 이 맞고, 토큰도 DNS 연동도 없어 관리할 것이 적다.
아래 절차는 DNS-01 로 받는 형태다. 다만 **지금 돌고 있는 이 실험대의 certbot 이 어느 쪽으로 등록되어 있는지는 아직 읽지 못했다**(unknown) — 호스트의 `sudo` 가 비밀번호를 요구해 `/etc/letsencrypt/renewal/*.conf` `authenticator` 를 비대화식으로 읽을 수 없었다. 이 실험대의 문서 두 개도 이 대목에서 서로 어긋나, 한쪽은 DNS-01 로 결론냈고 다른 한쪽은 같은 04 단계를 `certbot certonly --webroot` 로 적어 두었다. 어느 쪽이 실제로 등록되어 있는지는 그 한 줄을 읽어야 갈린다.
아래 절차는 DNS-01 로 받는 형태이고 **이 실험대의 현재 등록 방식도 DNS-01 로 확인됐다**(2026-09-17, observed). `sudo sh -c` 안에서 renewal 설정을 읽자 `authenticator = dns-cloudflare` `dns_cloudflare_credentials = /etc/letsencrypt/cloudflare.ini` 가 나왔다. 다만 같은 후속 원문에서 토큰 길이는 0자였고 Cloudflare 검증은 실패했다. 따라서 검증 방식은 확정됐지만 현재 자격증명이 재발급에 쓸 수 있는지는 별도 확인이 필요하다.
## 전제와 되돌리기
@@ -284,7 +287,7 @@ sudo certbot certificates
```
```bash label="[kc-lab-edge] ④ 인증서가 실제로 어떤 이름에 유효한지 본다"
sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/hyeonworks.com/fullchain.pem
sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem
```
**예상 결과** — ① 의 마지막 줄이 `The dry run was successful.` 이고, ② 는 `Successfully received certificate.` 와 저장 경로를 찍는다. ③ 에서 볼 것은 네 줄이다.
@@ -293,8 +296,8 @@ sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/hyeonwork
|---|---|
| `Domains:` | `hyeonworks.com *.hyeonworks.com` — 한 줄에 둘 다 |
| `Expiry Date:` | 오늘 + 90일, `VALID` |
| `Certificate Path:` | `/etc/letsencrypt/live/hyeonworks.com/fullchain.pem` |
| `Private Key Path:` | `/etc/letsencrypt/live/hyeonworks.com/privkey.pem` |
| `Certificate Path:` | `/etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem` — 2026-09-17 현재 관측 |
| `Private Key Path:` | `/etc/letsencrypt/live/auth.hyeonworks.com/privkey.pem` |
④ 는 `DNS:hyeonworks.com, DNS:*.hyeonworks.com` 두 개를 내놓고 `auth.hyeonworks.com` 은 두 번째에 걸린다.
@@ -304,8 +307,8 @@ sudo openssl x509 -noout -ext subjectAltName -in /etc/letsencrypt/live/hyeonwork
| 무엇 | 정해지는 방식 |
|---|---|
| 디렉터리 이름 `live/hyeonworks.com/` | certbot 이 이 묶음을 관리하려고 붙인 라벨. 첫 번째 `-d` 에서 따오고 서빙과 무관하다 |
| SAN 목록 | 브라우저가 보는 실제 유효 호스트명. `-d` 로 준 이름 전부 |
| 디렉터리 이름 `live/auth.hyeonworks.com/` | 2026-09-17 현재 실험대에서 실제로 존재한 lineage. `certbot certificates` 가 찍은 경로를 따른다 |
| SAN 목록 | 브라우저가 보는 실제 유효 호스트명. `-d` 로 준 이름 전부이며 lineage 이름과 별개 |
**문제가 생기면** — 최초 실행이면 계정 등록 대화가 먼저 뜬다. 이메일을 비우면 `Invalid email address: .` 로 되묻고, 약관은 `Y`, 뉴스레터는 발급과 무관하므로 `N` 이다. 프롬프트 없이 돌리려면 `-m <이메일> --agree-tos --no-eff-email` 을 붙인다. `--register-unsafely-without-email` 은 쓰지 않는다 — 갱신 실패를 알려 줄 통로가 사라지는데, 6번이 재는 것이 바로 그 갱신이다. DNS-01 은 TXT 레코드가 퍼질 때까지 기다리느라 수십 초 걸리므로 중간에 끊지 않는다.
@@ -334,8 +337,8 @@ server {
listen 443 ssl http2 default_server;
server_name _;
ssl_certificate /etc/letsencrypt/live/hyeonworks.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hyeonworks.com/privkey.pem;
ssl_certificate /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/auth.hyeonworks.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
@@ -369,7 +372,7 @@ sudo nginx -t && sudo systemctl reload nginx
`ssl_certificate` 에 적는 것은 `cert.pem` 이 아니라 `fullchain.pem` 이다. 서버 인증서만 보내면 중간 인증서가 빠져 체인이 끊기는데, 브라우저는 대개 캐시나 AIA 로 보완해서 정상으로 보이고 캐시가 없는 클라이언트에서만 깨지므로 발견이 늦다. 4번 ③ 에서 certbot 이 찍어 준 경로와 한 글자도 다르면 안 된다.
**★ 위 ② 의 인증서 경로가 틀렸다**(2026-09-17, observed). `live/hyeonworks.com/` 이라고 적혀 있는데 certbot 이 만드는 디렉터리는 **`live/auth.hyeonworks.com/`** 이다. 이름을 여럿 담은 인증서라도 디렉터리 이름은 `-d` 로 처음 준 이름 하나를 쓴다. 그대로 치면 3번에서 nginx 가 안 뜬다.
**★ 원본 가이드 04 의 5단계에는 known-wrong 경로가 있었다**(2026-09-17, observed). 가이드는 `live/hyeonworks.com/` 을 적었지만 실제 `live/` 아래에는 `auth.hyeonworks.com` 이 있었고, 그 잘못된 경로로 `nginx -t` 를 돌리면 아래처럼 실패했다. 위 canonical 절차는 이 관측을 반영해 처음부터 `live/auth.hyeonworks.com/` 을 쓴다.
```text label="문서 그대로 쳤을 때"
[emerg] cannot load certificate "/etc/letsencrypt/live/hyeonworks.com/fullchain.pem":
@@ -388,7 +391,7 @@ auth.hyeonworks.com
Certificate Path: /etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem
```
**아래 「문제가 생기면」의 진단은 방향이 거꾸로다.** `live/auth.hyeonworks.com/` 이 없는 경로가 아니라 **그쪽이 맞는 경로**이고, `live/hyeonworks.com/` 이 디스크에 없다. 4번 ③ 이 찍어 준 경로를 그대로 옮기라는 원칙은 옳고, 위 ② 가 그 원칙을 스스로 어겼다.
현재 truth는 하나다 — **이 실험대의 lineage는 `live/auth.hyeonworks.com/`이고, `live/hyeonworks.com/`은 원본 가이드에 남았던 historical wrong path**다. 다른 실험대에서 이름을 추측하지 말고 `certbot certificates` 가 찍은 `Certificate Path`를 그대로 nginx에 옮긴다.
**문제가 생기면** — `cannot load certificate` 로 막히면 경로를 본다. 자기 실험대의 디렉터리 이름은 `sudo ls /etc/letsencrypt/live/` 가 답한다. 확인 ② 에서 본 판 번호가 1.25.1 미만인데 `http2` 를 지시어로 썼다면 여기서 `unknown directive "http2"` 가 나온다.
@@ -9,6 +9,9 @@ project: virtualization
status: 초안
basisVersion: Linux memory reclaim · swap · OOM Killer · cgroup memory limit · QEMU 의 Guest RAM backing
studio: "https://hyeonworks.com/studio/documents/647d6d11-5bb2-4028-a530-b10c8925aa11/edit"
assets:
- key: memory-pressure-reclaim-swap-oom
file: ../../../final/assets/diagrams/memory-pressure-reclaim-swap-oom/memory-pressure-reclaim-swap-oom.svg
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#57-memory-overcommit
@@ -48,6 +51,8 @@ source:
<!-- body:start -->
![Host memory pressure가 reclaim에서 clean page 재읽기, anonymous page의 Host swap과 storage contention, 메모리 확보 실패에 따른 Host OOM으로 갈라지는 흐름도. QEMU가 OOM victim이 될 때 VM 전체가 중단될 수 있다는 결과는 본문에서 설명한다.](../../../final/assets/diagrams/memory-pressure-reclaim-swap-oom/memory-pressure-reclaim-swap-oom.svg)
## CPU 가 모자랄 때와 RAM 이 모자랄 때
가상 머신들에 설정한 메모리 총량이 호스트의 물리 RAM 보다 큰 구성을 메모리 초과 할당(memory overcommit)이라고 한다. 근거 문서는 이 구성을 예시 숫자로 설명한다.
@@ -90,7 +95,7 @@ QEMU 가 게스트 RAM 으로 잡아 둔 호스트 물리 페이지가 스왑으
메모리는 CPU 와 네트워크, 스토리지 실행을 모두 받치는 계층이라 압박이 메모리 안에서 끝나지 않는다. CPU 사용률이 낮은 구간에도 이 경로가 돌고 있을 수 있다.
절에는 그림을 넣지 않았다. 근거 절이 「Guest와 Host가 동시에 memory pressure를 겪으면」이라고 적어 네 갈래에 순서가 없는데, 그림 도구가 이 문맥에서 허용한 구성 문법은 순서를 요구하는 `sequence` 하나뿐이었다. 그리려면 없는 순서를 지어내야 한다.
흐름은 순서를 억지로 만드는 sequence가 아니라, **Host memory pressure가 reclaim 뒤 clean-page 회수·Host swap·storage contention·Host OOM으로 갈라지는 관계**를 component-flow로 그렸다. Guest OOM은 같은 원인선에 붙이면 계층을 섞게 되므로 그림에서는 합치지 않고 아래 절에서 따로 구분한다.
## 회수로도 모자라면 OOM
@@ -9,6 +9,9 @@ project: virtualization
status: 초안
basisVersion: QEMU block backend 세 형태 — qcow2 파일 · RAW 파일 · Host block device. libvirt 로 정의한 VM 기준이고 제4부가 QEMU 와 libvirt 판을 고정하지 않았다
studio: "https://hyeonworks.com/studio/documents/892b6087-8961-486c-b4d9-d3dd46bc2605/edit"
assets:
- key: qemu-block-backend-forms
file: ../../../final/assets/diagrams/qemu-block-backend-forms/qemu-block-backend-forms.svg
sourceRevision: no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다
source:
- final/document.md#140-vm-boundary를-넘으면-qemu가-등장
@@ -48,6 +51,8 @@ source:
<!-- body:start -->
![Guest의 /dev/vda가 virtio-blk와 virtqueue를 지나 QEMU block backend에 도착한 뒤 qcow2 파일, RAW 파일, Host block device 세 갈래로 나뉘는 구조도. 그 아래 Host block stack은 별도 Concept이 맡아 이 그림에서는 다시 합치지 않는다.](../../../final/assets/diagrams/qemu-block-backend-forms/qemu-block-backend-forms.svg)
## 가상 머신 경계를 넘으면 QEMU 가 받는다
게스트의 `virtio-blk``virtqueue` 에 게시한 요청은 가상 머신 경계를 넘어 호스트 사용자 공간의 QEMU 로 간다. QEMU 안에서는 `virtio-blk` 장치 모델과 블록 백엔드가 그 요청을 받는다. QEMU 는 게스트에게 가상 블록 장치를 노출하고, 게스트의 가상 I/O 를 호스트 백엔드에 연결한다.
File diff suppressed because one or more lines are too long