기록 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>
135 lines
12 KiB
Markdown
135 lines
12 KiB
Markdown
---
|
|
kind: CONCEPT
|
|
slug: two-l7-hops-and-the-entry-point-recursion
|
|
title: L7 이 두 겹인 이유와, 진입점 하나를 이중화하려 할 때 끝나지 않는 재귀
|
|
topic: lab-entry-path-and-measurement-integrity
|
|
topicName: 실험대의 진입 경로
|
|
project: virtualization
|
|
status: 게시 전
|
|
basisVersion: 이 실험대의 2026-09-03 배치 · 호스트 nginx 1.30.4 (Arch) · k3s v1.36.4 의 기본 ingress 인 Traefik
|
|
sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6
|
|
source:
|
|
- final/document.md#261-진입점-자체가-죽으면-로드밸런서의-재귀-문제
|
|
- final/document.md#275-호스트-nginx와-traefik은-무엇이-다른가-둘-다-필요한-이유
|
|
- final/document.md#257-리버스-프록시와-upstream
|
|
- final/document.md#258-왜-tls를-끊어서-내용을-보는가
|
|
---
|
|
|
|
# L7 이 두 겹인 이유와, 진입점 하나를 이중화하려 할 때 끝나지 않는 재귀
|
|
|
|
이 실험대의 요청은 HTTP 를 읽는 서버를 두 번 지난다. 바깥의 nginx 가 TLS 를 끊고 어느 노드로 보낼지 정하고, 그 노드의 Traefik 이 어느 파드로 보낼지 정한다. 한쪽만으로는 안 되는 이유가 서로 다르고, 진입점을 이중화하려 하면 「어느 진입점으로 갈지 누가 정하는가」가 새로 생겨 재귀가 끝나지 않는다.
|
|
|
|
## 관계
|
|
|
|
- **엣지 nginx 를 호스트에서 게스트 VM 으로 옮긴다 — 성능이 아니라 더러워지는 층의 격리**
|
|
그 결정이 든 근거가 「L7 홉 수는 전후 모두 2홉」인데 왜 2홉인지는 그 기록에 없다. 이 글이 그 앞을 맡는다.
|
|
- **신뢰 경계에서는 forwarded 헤더를 덧붙이지 않고 덮어쓴다**
|
|
그 기준이 말하는 경계가 두 홉 가운데 어느 쪽인지를 이 글이 먼저 정한다.
|
|
- **공개 터널을 쓰지 않고 tailnet 직결로 둔다 — 홉이 하나 늘면 재려던 계약이 오염된다**
|
|
여기 적은 두 겹에 세 번째를 붙이지 않기로 한 결정이다.
|
|
- **엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다**
|
|
바깥 홉을 실제로 세우는 절차가 거기 있다.
|
|
- **Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge**
|
|
이 두 겹 아래에서 패킷이 게스트까지 가는 길을 그 글이 설명한다.
|
|
- **가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다**
|
|
진입 경로가 층으로 나뉘어 있어서 한 칸씩 건너뛰며 칠 수 있다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 요청 하나가 L7 을 두 번 지난다
|
|
|
|
브라우저가 보낸 요청은 파드에 닿기까지 HTTP 를 읽는 서버를 두 번 지난다.
|
|
|
|
```text
|
|
브라우저 ─▶ nginx (L7 · TLS 종료) ─▶ Traefik (L7 · Ingress 라우팅) ─▶ Pod
|
|
```
|
|
|
|
리버스 프록시는 클라이언트 요청을 대신 받아 뒤쪽 서버로 전달하는 서버를 말한다. nginx 의 `upstream` 블록은 뒤쪽 서버 여러 대를 하나의 논리 이름으로 묶고, `proxy_pass http://이름;` 으로 그 그룹을 가리키면 nginx 가 요청을 분배한다. 저장소의 단일 호스트용 설정은 `proxy_pass http://keycloak:8080` 으로 대상 하나를 가리키는데, 멀티노드로 재려면 `upstream` 형태로 바꿔야 한다. 바깥 홉이 이 형태이고 묶인 것은 게스트 두 대의 `80` 포트다.
|
|
|
|
엣지 nginx 는 물리 호스트에서 게스트 한 대로 옮겨졌는데 그때도 HTTP 를 읽는 홉의 수는 2 그대로였다. 늘어난 것은 호스트 커널이 하는 L4 전달 한 번이고, 커널은 HTTP 를 읽지 않는다.
|
|
|
|
## 바깥 홉이 TLS 를 끊는 이유
|
|
|
|
TLS 종료는 프록시가 암호를 풀어 평문 HTTP 를 읽는 것을 말한다. 굳이 푸는 첫째 이유는 내용을 안 보면 어디로 보낼지 정할 수 없기 때문이다. 여러 도메인이 하나의 IP 와 443 포트를 공유하고, 어느 서비스로 보낼지는 HTTP `Host` 헤더에 적혀 있는데 그 헤더가 TLS 안에 암호화돼 있다. TLS 핸드셰이크의 평문 부분에 도메인이 들어 있어서 도메인 단위 분기는 풀지 않고도 된다. 다만 경로 단위 분기는 그렇게 할 수 없고, 인증서도 백엔드마다 따로 관리해야 한다.
|
|
|
|
이 실험대에는 이유가 하나 더 있다. `X-Forwarded-Proto: https` 나 `X-Forwarded-Host` 같은 헤더는 평문 HTTP 를 편집할 수 있어야 넣을 수 있고, Keycloak 이 `iss` 클레임과 redirect URL 을 외부 주소로 만들려면 그 헤더가 필요하다. 그래서 이 구조에서 TLS 종료는 고를 수 있는 것이 아니라 전제다.
|
|
|
|
대가는 셋이다. 프록시 뒤 구간이 평문이 된다 — 운영은 `127.0.0.1`, lab 은 `virbr0` 로 둘 다 머신 밖으로 안 나간다. 신뢰 경계가 프록시까지 넓어져서 프록시 보안이 곧 전체 보안이 된다. 그리고 클라이언트 인증서가 사라지는데, 백엔드가 그것을 직접 검증해야 하면 TLS 를 끊으면 안 되고 그때는 TCP 를 그대로 흘리는 L4 통과 구성을 쓴다.
|
|
|
|
## 바깥과 안이 아는 것이 다르다
|
|
|
|
| 무엇을 견주나 | 바깥의 nginx | Traefik (k3s ingress) |
|
|
|---|---|---|
|
|
| 사는 곳 | 클러스터 밖의 프로세스 | 클러스터 안, 파드 |
|
|
| 아는 대상 | IP 와 포트 (고정) | 쿠버네티스 Service 와 Ingress (동적) |
|
|
| 설정하는 방법 | 파일 편집 뒤 `nginx -s reload` | `kubectl apply` 로 Ingress 리소스 |
|
|
| 대상이 바뀌면 | 사람이 고쳐야 한다 | 자동 반영 |
|
|
| 무엇을 정하나 | 어느 노드로 보낼까 | 어느 파드로 보낼까 |
|
|
| TLS | 여기서 종료 | 평문으로 받음 |
|
|
|
|
nginx 는 클러스터의 존재를 모르고 파드 IP 가 바뀌는 것도 모른다. Ingress 는 설정을 적어 둔 쿠버네티스 리소스여서 그 자체로는 아무 일도 하지 않고, 그 설정을 실제로 수행하는 프로그램이 Ingress Controller 다. 컨트롤러는 API 서버를 계속 감시하다가 Ingress 리소스가 생기거나 바뀌면 자기 라우팅 설정을 갱신한다.
|
|
|
|
Traefik 만 쓰면 어느 노드로 보낼지를 정할 것이 없다. k3s 에 딸린 servicelb 덕분에 Traefik 이 두 노드의 80 과 443 에 모두 바인딩되지만, 브라우저는 어느 노드로 가야 할지 모르고 그 노드가 내려가면 그 IP 로는 아무도 받지 못한다. Traefik 은 노드 안에서 파드로 나눠 주고, 노드들 사이에서 나눠 주는 일은 클러스터 밖의 무언가가 맡아야 한다.
|
|
|
|
nginx 만 쓰고 Traefik 을 끄면 Ingress 리소스를 못 쓰게 되고 서비스가 늘거나 파드 IP 가 바뀔 때마다 사람이 파일을 고친다. 그리고 두 경우 모두 운영 구조와 달라진다. 운영이 `host nginx → k3s(Traefik)` 이므로 실험대도 그 2홉을 복제해야 `X-Forwarded-*` 신뢰 경계 결론이 그대로 이전되고, 그것이 둘 다 두는 결정적인 이유다.
|
|
|
|
## ALB 와 NLB 는 같은 곳을 놓고 고르는 두 선택지다
|
|
|
|
ALB 와 NLB 는 AWS 의 두 제품이고 이 실험대에는 없다. 여기서는 진입점 배치를 견주는 이름으로만 쓴다. 둘 다 클러스터 밖의 로드밸런서여서 Ingress Controller 와 대응되는 짝이 아니다.
|
|
|
|
| 무엇이 다른가 | ALB (L7) | NLB (L4) |
|
|
|---|---|---|
|
|
| 이해하는 것 | HTTP/HTTPS | TCP/UDP |
|
|
| 라우팅 기준 | 호스트명·경로 | 포트 |
|
|
| TLS | 종료함 | 통과 또는 종료 |
|
|
| `X-Forwarded-*` | 추가함 | 추가 안 함 (PROXY protocol 사용) |
|
|
|
|
진입점은 한 곳이고 HTTP 를 읽는 처리는 어딘가에서 반드시 한 번 일어난다. 배치의 차이는 진입점과 L7 처리기가 같은 장비인가 다른 장비인가에 있다.
|
|
|
|
```text
|
|
[ALB 패턴]
|
|
브라우저 ─▶ ALB (L7 · TLS 종료 · 경로 라우팅) ─▶ Pod
|
|
└ 진입점이자 L7 처리기. 하나가 두 역할.
|
|
|
|
[NLB 패턴]
|
|
브라우저 ─▶ NLB (L4 · 통과) ─▶ ingress controller (L7 · TLS 종료) ─▶ Pod
|
|
└ 진입점만 └ L7 처리기. 역할이 둘로 나뉜다.
|
|
```
|
|
|
|
| 배치 | 진입점 | L7 처리 위치 |
|
|
|---|---|---|
|
|
| ALB 단독 | ALB (L7) | 진입점 한 곳 |
|
|
| NLB + ingress | NLB (L4) | 클러스터 안 한 곳 |
|
|
| L7 + ingress | nginx (L7) | 두 곳 모두 |
|
|
|
|
이 실험대는 두 패턴 중 어느 쪽도 아니다. 바깥의 nginx 가 TLS 를 끊고 `X-Forwarded-*` 를 넣으므로 ALB 에 가까운데, 그 뒤의 Traefik 이 또 HTTP 를 읽는다.
|
|
|
|
## 진입점을 이중화하려 하면 재귀가 끝나지 않는다
|
|
|
|
한 머신 안에서 nginx 를 여러 개 띄우는 것은 의미가 없다. nginx 는 이미 master 프로세스 1개와 worker N개 구조이고, worker 들이 리스닝 소켓을 공유하며 CPU 코어 수만큼 병렬로 처리한다. 같은 머신에 인스턴스를 늘려도 그 머신이 내려가면 전부 함께 내려가므로 가용성은 늘지 않는다.
|
|
|
|
진짜 이중화는 머신을 늘리는 것이고, 그러면 「어느 nginx 로 갈지는 누가 정하는가」가 새로 생긴다. 앞에 로드밸런서를 또 두면 이번에는 그것이 단일 장애점이 되어 같은 물음이 한 칸 앞으로 옮겨 갈 뿐이다. 실무는 이 재귀를 소프트웨어가 아니라 네트워크 계층의 장치로 끊는다.
|
|
|
|
| 방법 | 재귀를 끊는 원리 | 전환 시간 |
|
|
|---|---|---|
|
|
| VIP + VRRP (keepalived) | 선택자가 없다. IP 자체가 이동한다 | 1~3초 |
|
|
| DNS 다중 A 레코드 | 클라이언트가 고른다. 실패 시 다음 IP로 재시도 | TTL 의존, 느림 |
|
|
| 애니캐스트 + BGP/ECMP | 라우터가 가장 가까운 경로로 보낸다 | 즉시, 대규모 전용 |
|
|
| 클라우드 LB에 위임 | AWS가 내부적으로 다중 AZ로 이중화. 사용자는 DNS 이름만 받음 | 관리 불필요 |
|
|
|
|
VRRP 는 가상 IP 하나를 여러 장비가 번갈아 갖게 하는 방식으로 재귀를 끊는다. 가상 IP 는 한 번에 한 대만 갖고, MASTER 가 내려가면 BACKUP 이 그 IP 를 가져간 뒤 gratuitous ARP 를 브로드캐스트해 스위치의 MAC 테이블을 갱신한다. 클라이언트는 계속 같은 IP 로 접속하는데 그 IP 가 어느 장비에 붙어 있는지만 바뀐다. 고르는 주체가 없어서 「누가 정하는가」라는 물음 자체가 생기지 않는다.
|
|
|
|
클라우드 로드밸런서를 쓰면 AWS 가 여러 가용 영역에 걸쳐 이 재귀를 대신 풀어 주고 사용자는 DNS 이름 하나만 받는다.
|
|
|
|
## 이 실험대가 관측한 범위
|
|
|
|
바깥의 nginx 는 이 실험대의 단일 장애점이고 물리 머신도 한 대라 그것도 단일 장애점이다. 숨길 이유가 없으므로 알려진 한계로 적어 둔다.
|
|
|
|
이 실험대는 진입점을 이중화하지 않는다. 물리 머신이 한 대라 keepalived 를 구성해도 그 머신이 내려가면 끝이고, 검증 대상은 Keycloak 의 세션과 토큰이지 로드밸런서 가용성이 아니다. 다만 Traefik 은 이미 두 노드에 떠 있으므로, 노드 하나를 내리고 바깥 nginx 의 `upstream` 이 어떻게 반응하는지는 그대로 관찰할 수 있다.
|
|
|
|
ALB 와 NLB 의 대조는 AWS 의 두 제품을 기준으로 적은 것이고 이 실험대에서 관측한 것이 아니다. VRRP 도 keepalived 의 일반 동작이고 이 실험대에 구성하지 않았다. NLB 에 해당하는 것 역시 이 실험대에 아직 없고, 필요해지면 nginx 의 `stream {}` 블록이 그 일을 맡는다. k3s API 서버를 밖에서 접근하거나 PostgreSQL 과 Redis 를 게스트 밖에서 직접 관찰하거나 Keycloak 의 mTLS 를 실험할 때가 그런 경우다. nginx 는 한 프로세스에서 L7 과 L4 를 함께 수행할 수 있어서, AWS 에서 두 제품으로 나뉘어 있는 것과 다르다.
|
|
|
|
<!-- body:end -->
|