Files
keycloak-pattern/docs/guides/03-nginx/README.md
T
DongHyeonkaandClaude Opus 5 8062cc9a19 docs(guides): bring the seven setup guides in line with the practitioner skill
The guides were written before the skill existed and a scored audit found
three gaps. Fixed by a subagent running under the skill, with measured
values, versions, IPs and quoted output declared off limits.

The big one: every step showed a command and its output, and almost none
said which line to look at or what it meant. 64 interpretation pairs added
across the seven files, weighted where the reading is hardest — 20 in the
Keycloak stage, where a Secret existing and a pod having received it are
different facts.

Only the extracting form of curl appeared. Where the reader meets a response
for the first time the guides now open with curl -I or curl -v and name the
lines worth reading; -w '%{http_code}' survives only where the code is a
value being compared — two upstream nodes against each other, or the 900-run
control loop.

Listing Secret keys went from a three-stage pipe to kubectl describe secret,
which prints the key names and their byte counts in one native command
without exposing a value.

And a tool assumption: jq and yamllint are installed on neither the lab host
nor the guests. The guides now say so where JSON is read by eye, rather than
sending the reader to install something mid-diagnosis. cloud-init schema is
on the guests and is now the guest-side check.

Also removes a stray Playwright screenshot committed at the repository root
in 919547a; the evidence copy under docs/evidence/b7a-orphan-session/ is the
one the document references.

Four things the audit left standing are recorded in the agent's report rather
than papered over — notably that 04's reload measurements are stated without
a reproduction procedure, and that 05 and 06 reference each other as
prerequisites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 17:33:31 +09:00

283 lines
11 KiB
Markdown

# 03 — 호스트 nginx 라우팅
## 이 단계가 끝나면
밖에서 보낸 요청이 **nginx → Traefik → 파드**로 닿는다. 아직 TLS 는 없다.
## 전제
[02](../02-k3s/) 가 끝나 두 노드가 `Ready`.
## 왜 프록시가 두 겹인가
nginx 와 Traefik 이 하는 일이 다르다.
| | 맡는 것 |
|---|---|
| 호스트 nginx | 바깥세상과의 접점 — TLS 종단 · 인증서 · `X-Forwarded-*` |
| Traefik | 클러스터 안의 동적 라우팅 — Ingress 를 보고 서비스를 고른다 |
**이 2홉이 운영 구조와 같다는 것이 이 배치의 핵심**이고, 동시에 B-4 의 헤더
실험이 성립하는 이유다. 1홉을 가정하고 쓴 계약이 2홉에서도 유효한지를
재려면 두 겹이 있어야 한다.
---
## 1. 설정을 쓴다
원본은 [`deploy/lab/host/nginx-keycloak-lab.conf`](../../../deploy/lab/host/nginx-keycloak-lab.conf).
**하기**
```bash
sudo tee /etc/nginx/sites-available/keycloak-lab > /dev/null <<'EOF'
upstream k3s_traefik {
server 192.168.122.11:80;
server 192.168.122.12:80;
}
server {
listen 80 default_server;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl default_server;
http2 on;
server_name _;
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 / {
proxy_pass http://k3s_traefik;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
EOF
```
> **인증서 경로는 아직 없다.** [04](../04-tls/) 에서 만든다. 그전까지는 443
> 블록을 주석 처리하고 80 만 `proxy_pass` 로 두면 이 단계를 먼저 확인할 수 있다.
Arch 는 `sites-available` 관례가 없다. 직접 만들고 `nginx.conf``http` 블록
안에서 include 한다.
```bash
sudo mkdir -p /etc/nginx/sites-{available,enabled}
sudo ln -s /etc/nginx/sites-available/keycloak-lab /etc/nginx/sites-enabled/
# nginx.conf 의 http { } 안에: include /etc/nginx/sites-enabled/*;
```
## 2. 문법을 보고 적용한다
**하기**
```bash
sudo nginx -t && sudo systemctl reload nginx
```
**어디를 봐야 하는가**`nginx -t` 가 내놓는 **마지막 줄**이다. `syntax is
ok``test is successful` 두 마디가 다 나와야 통과다. 앞에 나오는
`[warn]` 줄(예: `types_hash_max_size`)은 통과를 막지 않는다 — 04 에서 이
경고를 실패로 오독하는 일이 실제로 벌어지므로, 여기서 **경고와 오류를
구분하는 눈**을 들여 둔다. 실패면 `[emerg]` 줄에 파일과 줄 번호가 찍힌다.
**이 결과가 의미하는 것** — 통과했으면 `&&` 뒤의 reload 가 이어서 돌고,
`systemctl reload` 는 아무 말 없이 끝난다(무소식이 좋은 소식이다). 실패했으면
`&&` 가 reload 를 **막아 준 것**이고, 지금 돌고 있는 nginx 는 옛 설정 그대로
멀쩡하다. 설정이 깨진 상태에서 reload 하면 새 워커를 못 띄운다 — `-t`
먼저 통과시키고 그때만 reload 하는 이유다.
reload 가 정말 반영됐는지는 워커가 갈렸는지로 본다. 같은 판정 방법을
04 에서 인증서 갱신에 그대로 쓴다.
```bash
systemctl status nginx --no-pager | head -20
```
**어디를 봐야 하는가**`Active:` 줄이 `active (running)` 인가, 그리고 그
아래 프로세스 트리에 `nginx: master process` 하나와 `nginx: worker process`
여럿이 붙어 있는가. 워커 줄의 **PID** 를 눈에 담아 둔다.
**이 결과가 의미하는 것** — reload 는 마스터를 그대로 두고 **워커만** 갈아
끼운다. 그래서 reload 전후로 워커 PID 가 바뀌면 새 설정이 실제로 적용된
것이고, 안 바뀌었으면 `-t` 는 통과했는데 reload 가 안 간 것이다.
04 에서 인증서 갱신이 서빙까지 닿았는지를 정확히 이 방법으로 판정한다.
## 3. 층별로 확인한다 — 아래에서 위로
한 번에 밖에서 치지 말고, **가까운 층부터** 본다. 어디서 끊겼는지가 바로 나온다.
> **`curl` 을 두 형태로 쓴다.** 처음 볼 때는 `-I`(헤더까지 읽는 형태)로
> **응답을 눈으로 읽고**, 같은 것을 여러 번 재거나 두 노드를 나란히 비교할
> 때만 `-w '%{http_code}'`(값만 뽑는 형태)로 바꾼다. 값만 뽑는 형태는
> 골라 놓은 한 칸 말고는 전부 버리므로, **무엇이 잘못됐는지 모르는 상태**
> 에서는 쓸 것이 못 된다.
**확인 ①** Traefik 이 듣고 있나 (nginx 를 건너뛴다)
```bash
curl -I http://192.168.122.11
```
**형태** (이 실험대에서 캡처해 두지 않았다 — 봐야 할 줄만)
```
HTTP/1.1 404 Not Found
...
```
**어디를 봐야 하는가** — 첫 줄의 상태 줄 하나. 그리고 그 앞에 **아무 오류도
없이** 헤더가 나왔다는 사실. `curl: (7) Failed to connect` 이면 응답 자체가
없는 것이라 상태 코드를 볼 일도 없다.
**이 결과가 의미하는 것****`404` 가 성공 신호다.** 게스트의 80 을 누가
듣고 있고(Traefik), 그가 요청을 받아 「매칭되는 Ingress 규칙이 없다」고
답한 것이다. 이 층은 통과. `502` 면 Traefik 은 떴는데 그 뒤 백엔드가 없는
것이고, 연결 거부·타임아웃이면 Traefik 이 안 떴거나 게스트가 죽은 것이라
**02 로 돌아간다.**
두 노드가 같은지 볼 때는 값만 뽑는 형태가 낫다. 두 줄을 나란히 놓고 눈으로
비교하는 것이 목적이기 때문이다.
```bash
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.122.11
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.122.12
```
**실측**`.11` 에서 잰 값이다.
```
404
```
**어디를 봐야 하는가** — 두 줄이 **같은 코드**인가.
**이 결과가 의미하는 것** — 둘이 같으면 nginx 가 어느 쪽으로 보내도 같은
결과가 나온다. 한쪽만 다르면 upstream 두 개 중 하나가 죽은 것이고, 그
상태에서는 **요청의 절반만 실패**해서 「가끔 안 된다」로 보인다.
**확인 ②** nginx 가 80 에서 리다이렉트하나
```bash
curl -I http://auth.hyeonworks.com
```
**형태** (봐야 할 두 줄만)
```
HTTP/1.1 301 Moved Permanently
Location: https://auth.hyeonworks.com/
...
```
**어디를 봐야 하는가** — 상태 줄과 **`Location:` 헤더 한 줄**. `Location`
`https://` 로 시작하고 원래 호스트명을 그대로 들고 있는가. `$host` 대신
설정에 이름을 박아 두면 여기서 엉뚱한 호스트가 나온다.
**이 결과가 의미하는 것** — 301 이 나왔다는 것은 **바깥 요청이 호스트 nginx
까지 닿았다**는 뜻이다(①은 게스트에 직접 친 것이므로 nginx 를 안 거쳤다).
DNS·방화벽·80 리스너가 전부 살아 있다. 응답이 아예 없으면 nginx 가 안 떴거나
80 이 막힌 것이다. 리다이렉트를 따라가 끝까지 보려면 `-L` 을 붙인다.
값을 반복해서 잴 때의 형태는 이것이고, 아래가 이 실험대의 실측이다.
```bash
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://auth.hyeonworks.com
```
```
301 https://auth.hyeonworks.com/
```
**확인 ③** 끝까지 닿나 (TLS 이후)
```bash
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
```
```
200
```
**어디를 봐야 하는가** — 코드 한 칸. 여기서만 값만 뽑는 형태를 바로 쓰는
이유는, 이 200 이 **04·05 에서 매번 같은 명령으로 다시 잴 기준값**이기
때문이다. 처음 한 번은 `curl -I https://auth.hyeonworks.com/realms/master`
로 헤더까지 보고, 그다음부터 이 형태로 줄인다.
**이 결과가 의미하는 것**`200` 이면 nginx → Traefik → 파드까지 2홉이 다
이어졌다. `502` 는 nginx 는 살아 있는데 upstream 을 못 잡은 것(4번의 로그를
본다), `curl: (60)` 같은 인증서 오류는 아직 04 를 안 한 것이다. TLS 단계에서
막히면 코드만 보지 말고 `curl -v` 로 협상 과정을 읽는다 — 04 에서 그렇게 한다.
## 4. upstream 이 둘인 이유
```
upstream k3s_traefik {
server 192.168.122.11:80;
server 192.168.122.12:80;
}
```
두 노드 모두 Traefik 이 뜨므로 어느 쪽으로 보내도 된다. nginx 는 기본
라운드로빈으로 번갈아 보내고, **한쪽이 죽으면 자동으로 뺀다.**
그 「빼는」 동작이 로그에 이렇게 남는다.
```
connect() failed (113: No route to host) ← 호스트에 못 닿는다
connect() failed (111: Connection refused) ← 포트에 아무도 없다
no live upstreams ← 둘 다 죽었다고 판단
```
**113 과 111 은 대응이 다르다.** 113 은 네트워크, 111 은 프로세스다.
A-4 에서 노드를 잃었을 때 이 세 줄이 1분 안에 순서대로 나왔다.
---
## 막히면
| 증상 | 어디서 끊겼나 | 확인 |
|---|---|---|
| ① 이 연결 거부 | Traefik 이 안 떴거나 게스트가 죽음 | `kubectl get pods -n kube-system` |
| ① 이 `502` | Traefik 은 떴는데 백엔드가 없음 | Ingress 확인 |
| ② 가 응답 없음 | nginx 가 안 떴거나 방화벽 | `systemctl status nginx` |
| ③ 이 `502` | 인증서 문제 또는 upstream 다운 | [04](../04-tls/) · 아래 로그 |
**로그를 볼 때** — 실무자가 치는 형태다.
```bash
journalctl -u nginx -p err -n 5 # 최근 에러만
journalctl -u nginx -f # 지금 벌어지는 것
```
**어디를 봐야 하는가** — 각 줄의 **괄호 안 errno**(`113`·`111`)와 그 뒤의
`upstream: "http://192.168.122.1x:80/..."` 부분. 어느 upstream 이 문제인지가
거기 적혀 있다. 그리고 **타임스탬프** — 방금 친 요청 시각과 안 맞으면 지금
보고 있는 것은 옛 사고다.
**이 결과가 의미하는 것**`-p err` 로 걸러도 아무것도 안 나오면 nginx 는
정상이고 문제는 더 위(Traefik·파드)에 있다. 두 번째 명령은 **띄워 놓은
채로 다른 창에서 요청을 치는** 용도다 — 요청과 로그 줄을 눈으로 짝지으면
「이 요청이 어느 upstream 으로 갔나」가 바로 보인다. 끝내려면 Ctrl+C.
**★ nginx 에러 로그는 2048바이트에서 잘린다.** 긴 URL 이 끝에서 단어 중간에
끊겨 보이면 그것이다. 되찾으려면 **access 로그를 본다** — 거기엔 제한이 없다.
```bash
grep oauth2/callback /var/log/nginx/access.log | tail -1
```
**어디를 봐야 하는가** — 그 한 줄의 **끝**이다. 요청 URL 이 `"` 로 제대로
닫혀 있으면 온전한 줄이고, 단어 중간에서 멈춰 있으면 잘린 것이다. 길이가
궁금하면 세어 본다.
```bash
grep oauth2/callback /var/log/nginx/access.log | tail -1 | wc -c
```
**이 결과가 의미하는 것** — error 로그와 access 로그의 같은 요청 길이가 다르면
**error 쪽이 잘린 것**이지 요청이 잘린 것이 아니다. 이 실험대에서 B-7 의
502 원인이 error 로그에 있었는데 잘려 있었고, access 로그에는 3492자로 온전히
남아 있었다. 잘린 문자열을 놓고 원인을 추측하면 없는 문제를 좇게 된다.