16 KiB
03 — 엣지 nginx 라우팅 (kc-lab-edge)
이 단계가 끝나면
밖에서 보낸 요청이 호스트 DNAT → 엣지 nginx → Traefik → 파드로 닿는다. 아직 TLS 는 없다.
전제
02 가 끝나 두 노드가 Ready 이고, 01 에서
kc-lab-edge(192.168.122.10) 까지 세 대가 떠 있다.
왜 프록시가 두 겹인가
nginx 와 Traefik 이 하는 일이 다르다.
| 맡는 것 | |
|---|---|
엣지 nginx (kc-lab-edge) |
바깥세상과의 접점 — TLS 종단 · 인증서 · X-Forwarded-* |
| Traefik | 클러스터 안의 동적 라우팅 — Ingress 를 보고 서비스를 고른다 |
이 2홉이 운영 구조와 같다는 것이 이 배치의 핵심이고, 동시에 B-4 의 헤더 실험이 성립하는 이유다. 1홉을 가정하고 쓴 계약이 2홉에서도 유효한지를 재려면 두 겹이 있어야 한다.
왜 엣지가 물리 호스트가 아니라 VM 인가
L7 홉 수는 그대로 2홉이다. 늘어난 것은 커널이 하는 L4 전달 한 번뿐이다.
바뀐 것은 더러워지는 층이 어디냐다. nginx 설정 · 인증서 · certbot · deploy
훅은 자주 고치고 자주 갈아엎는 것들인데, 그것이 물리 호스트에 있으면
「깨끗하게 초기화하고 다시」가 불가능하다. 엣지가 VM 이면 초기화가
virsh undefine kc-lab-edge --remove-all-storage 한 줄이 된다.
물리 호스트에 남는 실험대 설정은 DNAT 규칙 하나와 DHCP 예약 세 줄뿐이고, 둘 다 한 번 쓰고 다시 안 건드린다.
덤으로 엣지 장애를 실험할 수 있게 된다. 엣지가 물리 호스트일 때는
systemctl stop nginx 가 진입 경로(SSH)까지 위험하게 만들어서 A층 실험 9건
어디에도 엣지 장애가 없었다. VM 이면 A-4 와 똑같이 virsh destroy 로 뽑는다.
1. 설정을 쓴다
원본은 deploy/lab/edge/nginx-keycloak-lab.conf.
하기 — [kc-lab-edge]. lab host 에서 원격 실행해도 된다.
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 에서 만든다. 그전까지는 443 블록을 주석 처리하고 80 만
proxy_pass로 두면 이 단계를 먼저 확인할 수 있다.
엣지가 Debian 이라 sites-available 관례가 기본으로 있다. 물리 호스트
(Arch) 였을 때는 디렉터리를 직접 만들고 nginx.conf 에 include 를 넣어야
했는데, 그 손질이 없어졌다. 운영도 Debian 계열이라 관례가 맞아떨어진다.
ssh kc-lab-edge '
sudo ln -sf /etc/nginx/sites-available/keycloak-lab /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default'
default 를 지우는 것을 빠뜨리지 않는다. Debian 기본 사이트가 :80 에
default_server 로 붙어 있어서, 우리 설정의 listen 80 default_server 와
충돌해 nginx -t 가 실패한다.
★
http2 on;을 쓰지 않는다. 그 지시어는 nginx 1.25.1 이상이다.엣지 (Debian 12): nginx version: nginx/1.22.1 물리 호스트 (Arch): nginx version: nginx/1.30.4물리 호스트에서 쓰던 설정을 그대로 옮기면 실측으로 이렇게 막힌다.
[emerg] 1339#1339: unknown directive "http2" in /etc/nginx/sites-enabled/keycloak-lab:29 nginx: configuration file /etc/nginx/nginx.conf test failed
listen 443 ssl http2;형태를 쓴다 — 1.22 와 1.30 양쪽에서 다 돈다.
2. 문법을 보고 적용한다
하기 — [kc-lab-edge]
sudo nginx -t && sudo systemctl reload nginx
실측 — 04 를 아직 안 했다면 여기서 이렇게 막히는 것이 정상이다. 설정은 맞는데 참조하는 파일이 아직 없는 것뿐이다.
[emerg] 1431#1431: cannot load certificate
"/etc/letsencrypt/live/auth.hyeonworks.com/fullchain.pem": BIO_new_file() failed
(SSL: ... No such file or directory ...)
nginx: configuration file /etc/nginx/nginx.conf test failed
어디를 봐야 하는가 — 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 에서 인증서 갱신에 그대로 쓴다.
systemctl status nginx --no-pager | head -20
어디를 봐야 하는가 — Active: 줄이 active (running) 인가, 그리고 그
아래 프로세스 트리에 nginx: master process 하나와 nginx: worker process
여럿이 붙어 있는가. 워커 줄의 PID 를 눈에 담아 둔다.
이 결과가 의미하는 것 — reload 는 마스터를 그대로 두고 워커만 갈아
끼운다. 그래서 reload 전후로 워커 PID 가 바뀌면 새 설정이 실제로 적용된
것이고, 안 바뀌었으면 -t 는 통과했는데 reload 가 안 간 것이다.
04 에서 인증서 갱신이 서빙까지 닿았는지를 정확히 이 방법으로 판정한다.
3. 호스트에서 엣지로 넘긴다 (DNAT)
여기까지 하면 엣지 nginx 는 살아 있는데 아무도 거기로 안 보낸다. tailnet
주소 100.83.212.4:443 을 받는 것은 여전히 물리 호스트다. 그 트래픽을
엣지로 넘기는 것이 이 단계고, 물리 호스트가 실험대를 위해 하는 일의
전부다.
하기 — [lab host]. 원본은
deploy/lab/edge/lab-edge-dnat.nft
sudo mkdir -p /etc/nftables.d
sudo cp deploy/lab/edge/lab-edge-dnat.nft /etc/nftables.d/
sudo cp deploy/lab/edge/lab-edge-dnat.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now lab-edge-dnat.service
규칙의 알맹이는 두 줄이다.
iifname "tailscale0" tcp dport { 80, 443 } dnat to 192.168.122.10
ip daddr 192.168.122.10 tcp dport { 80, 443 } ct state new accept
★ SNAT 을 걸지 않는다. 게스트의 기본 게이트웨이가 호스트라 응답은
어차피 여기로 돌아오고, conntrack 이 알아서 되돌린다. masquerade 를 붙이면
출발지가 덮여서 엣지가 모든 클라이언트를 192.168.122.1 로 본다 — 이
실험대가 재고 있는 X-Forwarded-For 계약이 통째로 무의미해진다.
두 번째 줄이 왜 필요한가 — libvirt 의 기본 네트워크 규칙은 게스트 대역으로
들어가는 RELATED,ESTABLISHED 만 허용한다. 밖에서 새로 들어오는 연결은
막는다. 그래서 명시적으로 열어 준다.
확인
sudo nft list table ip lab_edge
어디를 봐야 하는가 — dnat to 192.168.122.10 한 줄과 accept 한 줄이
다 있는가. 그리고 masquerade 나 snat 이 없는가.
이 결과가 의미하는 것 — PREROUTING nat 은 라우팅 결정보다 먼저 돌기
때문에 이 규칙이 호스트 자신의 :443 소켓보다 우선한다. 그래서 물리
호스트에 nginx 가 아직 떠 있어도 트래픽은 엣지로 간다 — 전환이 원자적이고,
되돌리기도 한 줄이다.
sudo nft delete table ip lab_edge # rollback
4. 층별로 확인한다 — 아래에서 위로
한 번에 밖에서 치지 말고, 가까운 층부터 본다. 어디서 끊겼는지가 바로 나온다.
curl을 두 형태로 쓴다. 처음 볼 때는-I(헤더까지 읽는 형태)로 응답을 눈으로 읽고, 같은 것을 여러 번 재거나 두 노드를 나란히 비교할 때만-w '%{http_code}'(값만 뽑는 형태)로 바꾼다. 값만 뽑는 형태는 골라 놓은 한 칸 말고는 전부 버리므로, 무엇이 잘못됐는지 모르는 상태 에서는 쓸 것이 못 된다.
확인 ① Traefik 이 듣고 있나 (nginx 를 건너뛴다)
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 로 돌아간다.
두 노드가 같은지 볼 때는 값만 뽑는 형태가 낫다. 두 줄을 나란히 놓고 눈으로 비교하는 것이 목적이기 때문이다.
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 가 직접 응답하나 (DNAT 을 건너뛴다)
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://192.168.122.10
어디를 봐야 하는가 — 301 과 Location 이 나오는가. 여기서 막히면
문제는 엣지 안이다(설정·기동). 통과하는데 아래 ③ 이 안 되면 문제는
DNAT 이다. 이 한 층을 끼워 두면 그 둘을 헷갈리지 않는다.
확인 ③ 밖에서, 즉 DNAT 을 거쳐 닿나
curl -I http://auth.hyeonworks.com
형태 (봐야 할 두 줄만)
HTTP/1.1 301 Moved Permanently
Location: https://auth.hyeonworks.com/
...
어디를 봐야 하는가 — 상태 줄과 Location: 헤더 한 줄. Location 이
https:// 로 시작하고 원래 호스트명을 그대로 들고 있는가. $host 대신
설정에 이름을 박아 두면 여기서 엉뚱한 호스트가 나온다.
이 결과가 의미하는 것 — 301 이 나왔다는 것은 바깥 요청이 DNAT 을 거쳐
엣지 nginx 까지 닿았다는 뜻이다(①은 Traefik 에 직접, ②는 엣지에 직접 친
것이라 DNAT 을 안 거쳤다). DNS·DNAT·80 리스너가 전부 살아 있다. 응답이 아예 없으면 nginx 가 안 떴거나
80 이 막힌 것이다. 리다이렉트를 따라가 끝까지 보려면 -L 을 붙인다.
값을 반복해서 잴 때의 형태는 이것이고, 아래가 이 실험대의 실측이다.
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://auth.hyeonworks.com
301 https://auth.hyeonworks.com/
확인 ④ 끝까지 닿나 (TLS 이후)
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 에서 그렇게 한다.
5. 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 · 아래 로그 |
로그를 볼 때 — 실무자가 치는 형태다.
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 로그를 본다 — 거기엔 제한이 없다.
grep oauth2/callback /var/log/nginx/access.log | tail -1
어디를 봐야 하는가 — 그 한 줄의 끝이다. 요청 URL 이 " 로 제대로
닫혀 있으면 온전한 줄이고, 단어 중간에서 멈춰 있으면 잘린 것이다. 길이가
궁금하면 세어 본다.
grep oauth2/callback /var/log/nginx/access.log | tail -1 | wc -c
이 결과가 의미하는 것 — error 로그와 access 로그의 같은 요청 길이가 다르면 error 쪽이 잘린 것이지 요청이 잘린 것이 아니다. 이 실험대에서 B-7 의 502 원인이 error 로그에 있었는데 잘려 있었고, access 로그에는 3492자로 온전히 남아 있었다. 잘린 문자열을 놓고 원인을 추측하면 없는 문제를 좇게 된다.