기반 가이드 7단계로 실험대를 철거하고 다시 세운 뒤 virtualization setup 9편과 keycloak-session-store 26편을 순서대로 밟았다. 24편은 끝까지, 11편은 되는 데까지 밟았고 밟은 범위를 편마다 적었다. 명령이 못 도는 것을 고쳤다. - kubectl 을 `kc-lab-1` 에서 치라고 적었는데 그 기계에 kubeconfig 가 없다. 라벨 639개와 각 편의 「어디서 치는가」를 `[lab host]` 로 옮겼다 - `-o custom-columns=…[0]…` 이 zsh 에서 글로브로 읽혀 안 돈다. 28곳에 따옴표 - busybox `sed` 가 끝 개행을 안 붙여 A-3 의 측정이 언제나 0 이었다 - `--token-file ~/node-token` 뒤에 그 파일을 지우면 k3s agent 가 재부팅을 못 견딘다. `/etc/rancher/node-token` 으로 옮기는 처방을 재서 넣었다 - 게스트에 없는 도구를 전제로 한 명령 넷 — `conntrack`·`dig`·`strings`·`nginx -v` - `echo` 와 JWT 헤더가 `"이름" : [ 값 ]` 으로 찍는데 문서는 공백 없이 옮겨 적어 그 실측으로 만든 grep·sed 가 한 줄도 못 잡는다 - B-0 이 `directAccessGrantsEnabled` 와 계정 완성을 빠뜨려 B-3 이 못 돈다 - D-4·D-4a 가 `test-server` 와 `certbot-renew.*` 를 가리키는데 실제로는 `kc-lab-edge` 의 `certbot.service` 다 - `virsh setmaxmem --config` 를 `dominfo` 로 판정하면 틀린다. `--inactive` 로 - `LIBVIRT_DEFAULT_URI` 를 rc 에만 넣으면 `ssh host '명령'` 에서 안 먹는다 결과가 조건부인 것을 갈랐다. - readiness 는 즉시 안 뒤집힌다. A-1·A-2 의 60초 창을 적었다 - 03 의 층 ②③ `301` 은 04 이후의 값이고 그 단계에서는 `404` 다 - A-0 의 로그 필터를 요청 직후에 치면 정반대 결론이 나온다 - A-5 의 한 방향 차단은 잠깐 `1` 이었다 `2` 로 돌아온다 증거는 두 프로젝트의 `evidence/raw/` 에 99벌을 README 와 함께 남겼다. 비밀은 길이만 적었고 화면에 찍힌 토큰은 가렸다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
49 KiB
id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, source, sourceRevision
| id | kind | slug | title | topic | topicName | project | status | studio | pinnedVersions | source | sourceRevision | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| b90d719f-39fb-4bab-a263-0e32eedb2b36 | SETUP | reproduce-a5-asymmetric-partition | 한 방향만 끊어 보고 raw PREROUTING 까지 내려간다 | losing-a-node-or-the-store | PostgreSQL 을 내리고 노드 전원을 뽑았을 때 | keycloak-session-store | 게시 전 | https://hyeonworks.com/studio/documents/b90d719f-39fb-4bab-a263-0e32eedb2b36/edit |
|
|
cdac9b8178391311d8eca1ebc6cac15bb62d79af |
한 방향만 끊어 보고 raw PREROUTING 까지 내려간다
iptables 로 Keycloak 두 노드 사이의 JGroups 채널을 끊는 절차다. 주입 넷 중 앞의 둘은 일부러 실패시킨다 — 실패한 주입은 화면에 「아무 일도 없었다」로 보이므로 절차의 절반이 패킷 카운터를 읽는 일이다. 전 구간 약 30분.
관계
- 노드를 잃는 두 가지 — 저장소가 같이 죽는 것과 들어갈 길이 없는 것 기계를 끄지 않고 네트워크만 끊었을 때 무엇이 다른지를 그 기록과 견주면 갈린다.
- 주입이 아홉 번 조용히 실패했고 전부 아무 일도 없는 것처럼 보였다 여기서 일부러 밟는 실패 둘이 그 아홉 건에 들어 있다.
- 예측을 먼저 적고, 주입이 걸렸는지 결과와 따로 확인하고, 대조군 없이 귀속하지 않는다 카운터로 주입을 먼저 판정하고 그다음에 클러스터를 보는 이 순서가 그 기준을 따른다.
- readiness 가 깨진 노드를 시야에서 먼저 치운다
양방향 차단에서
keycloak-1만0/1로 내려가고 정문이 계속200을 내는 상태를 이 절차가 만든다. - 7800 을 막고 디스커버리와 트랜스포트를 갈라 끊는다 앞 편이고, 거기서 NetworkPolicy 가 기존 연결을 못 끊은 것이 이 편의 출발 조건이다.
본문
읽기 전에 — 어디서 치는가
kubectl 은 [lab host] 에서 친다. iptables 는 노드 자체를 건드리는 명령이라 게스트 셸이 필요하고, 두 노드에 각각 넣어야 하며 어느 노드에 넣느냐가 결과를 가른다. 이쪽 노드의 규칙은 [kc-lab-1] 에서 그대로 치고, 반대 노드의 규칙은 ssh kc-lab-2 로 붙어서 친다. 코드블록마다 어느 셸인지 붙여 두었다.
[kc-lab-1] 라벨이 붙은 블록은 게스트 셸이다. lab host 에서 ssh kc-lab-1 로 들어가서 치고, 끝나면 exit 로 나온다. ssh kc-lab-2 '…' 한 줄 형태는 lab host 에서 그대로 쳐도 되므로 [lab host] 로 두었다.
원 가이드는 이 명령들을 kc-lab-1 에서 치라고 적었다. 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 sudo 없는 kubectl 이 permission denied 로 막힌다 — kubeconfig 는 lab host 의 ~/.kube/config 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 kubectl 블록의 기계 이름을 [lab host] 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
가이드는 터미널 둘을 권한다 — 하나는 상주 탐침 파드용, 하나는 관찰용이다. 다만 이 절차에는 탐침 파드의 셸 안에서 치는 명령이 하나도 없다. a5-probe 는 kubectl exec 으로만 쓰므로 명령은 전부 [lab host] 에서 치고, $K0 와 $K1 도 그 셸의 변수다. 다른 터미널에서 관찰 §7 의 헬스체크를 치면 두 변수가 빈 문자열이라 양쪽 다 안 닿고, 그 화면을 「둘 다 DOWN」으로 읽게 된다. 실제로는 한쪽만 DOWN 이다.
| 무엇 | 값 |
|---|---|
| 네임스페이스 | keycloak-lab · 관측 스택은 observability |
| 막는 포트 | 7800(트랜스포트) 과 57800(FD_SOCK2 = bind_port + 50000) |
| 주입 지점 | raw PREROUTING. filter FORWARD 는 kube-router 와 경쟁한다 |
| 탐침 파드 | a5-probe — curlimages/curl:8.11.1, sleep 1800, --restart=Never |
| 로그 시각 | 컨테이너는 UTC. KST 에서 9시간을 뺀다 |
| 걸리는 시간 | 전 구간 약 30분 |
| 도구 | jq 가 이 실험대에 없다. Prometheus 출력은 tr 과 grep 으로 자른다 |
이 실험이 가르는 것
A-1 이 답하지 못하고 넘긴 물음에서 출발한다. 가이드는 그 물음을 그대로 인용한다.
keycloak-1은 멤버가 하나 줄어든 정상적인 사건이라 Ready 를 유지했고,keycloak-0은 합류 자체를 못 해DOWN이 됐다. 양쪽이 동시에DOWN이 되는 경로가 있다면 전면 장애다.
A-1 은 도구도 하나 남겼다. NetworkPolicy 는 기존 연결을 못 끊는다 — conntrack 의 ESTABLISHED 가 먼저 통과시킨다. 그래서 이번에는 iptables 로 직접 간다.
그런데 iptables 에도 벽이 셋 있다. 이 절차는 그 셋을 일부러 다시 밟는다.
실패 ① filter FORWARD 최상단에 넣었는데 → CNI 가 밀어낸다
실패 ② raw 로 옮겼는데도 0 패킷 → 연결 방향을 잘못 짚었다
성공 수신측 노드의 raw PREROUTING → 19 패킷
그런데 그래도 안 갈라진다 → 반대 방향으로 재연결한다
끝나면 이것들을 자기 화면에서 본다 — 규칙을 넣었는데 0 패킷인 상태를 iptables -L -n -v 의 카운터에서, kube-router 가 내 규칙을 아래로 밀어내는 것을 FORWARD 체인의 줄 번호에서, JGroups 연결 방향이 A-1 때와 반대인 것을 conntrack -L 에서, 단방향 차단이 스스로 낫는 것을 뒤집힌 연결에서, coord = t 가 둘인 split brain 을 PostgreSQL JGROUPS_PING 에서, 그런데도 한쪽만 DOWN 이고 외부는 200 인 것을 health/ready 와 endpointslice 에서, MergeView 로 50초 만에 합쳐지는 것을 Keycloak 로그에서.
전제와 되돌리기
05-keycloak과06-observability가 끝나 있다.- A-1 을 먼저 해 두면 훨씬 이해가 빠르다. 이 절차는 A-1 이 실패한 곳에서 시작한다.
kc-lab-2에는ssh kc-lab-2로 붙는다. 앞 절의 표가 어느 명령을 어느 셸에서 치는지 적는다.
:::warning
이 절차는 Keycloak 클러스터를 실제로 분단시킨다. 실험대에서만 한다.
:::
1. 중단하는 방법을 먼저 확인한다
목적 — 어느 단계에서든 두 노드의 규칙을 한꺼번에 걷어낼 수 있게 해 둔다.
이 실험대는 두 줄로 쳤다. 둘째 줄에 SSH(Secure Shell, 원격 셸) 접속과 원격 셸의 인용과 세미콜론으로 이은 명령 둘이 한꺼번에 들어 있다.
sudo iptables -t raw -F PREROUTING ; sudo iptables -F FORWARD
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING ; sudo iptables -F FORWARD'
따라 하는 사람은 반대 노드 쪽을 나눈다. 먼저 붙고, 붙은 다음에 두 줄을 따로 친다. 행동 하나가 명령 하나가 된다. 이 나눈 형태는 이 실험대에서 치지 않았다.
이쪽 노드의 규칙을 먼저 걷어낸다.
ssh kc-lab-1 'sudo iptables -S FORWARD'
ssh kc-lab-1 'sudo iptables -t raw -F PREROUTING'
ssh kc-lab-1 'sudo iptables -F FORWARD'
그다음 반대 노드에 붙는다.
ssh kc-lab-2
같은 두 줄을 원격 셸에서 친다.
sudo iptables -t raw -F PREROUTING
sudo iptables -F FORWARD
exit
예상 결과 — -F 는 아무것도 찍지 않는다. ① 에 무엇이 있었는지가 유일한 기록이므로 건너뛰지 않는다.
왜 필요한가 — -F FORWARD 는 그 체인 전체를 비운다. 이 실험대의 FORWARD 정책은 ACCEPT 이고 실제 규칙은 kube-router 와 kube-proxy 가 자기 체인에 두므로 잠시 뒤 스스로 복구된다. 그래도 무엇을 지웠는지 모르면 나중에 클러스터가 이상할 때 이 명령 탓인지 가릴 수 없다.
문제가 생기면 — 해제했는데 2~3분째 클러스터가 안 붙으면 반대 노드 규칙이 남아 있는 것이므로 두 노드 모두에서 -t raw -S PREROUTING 을 본다.
주입 전에 같은 명령으로 먼저 본다
주입이 걸리기 전과 후가 화면상 똑같이 보이는 절차다. 먼저 본 것이 없으면 실패를 성공으로 읽는다. 순서는 이렇다.
파드 IP·노드 → 디스커버리(DB) → 클러스터 뷰(로그) → 지표 → 연결 방향 → 밖
1. 파드 IP 와 노드 배치를 지금 다시 뽑는다
무엇을 확인하는가 — 어느 파드가 어느 노드에 있고 IP(Internet Protocol 주소)가 무엇인지.
kubectl -n keycloak-lab get pods -o wide
K0=$(kubectl -n keycloak-lab get pod keycloak-0 -o jsonpath='{.status.podIP}')
K1=$(kubectl -n keycloak-lab get pod keycloak-1 -o jsonpath='{.status.podIP}')
echo "K0=$K0 K1=$K1"
출력에서 답이 되는 것 — NODE 열과 파드 번호의 짝이다. 실측은 이렇다.
keycloak-0=10.42.1.77 (kc-lab-2) keycloak-1=10.42.0.42 (kc-lab-1)
keycloak-0 1/1 Running 0 11m
keycloak-1 1/1 Running 1 (2m48s ago) 155m
postgres-7b474b88c8-9cmsv 1/1 Running 0 14m
이 결과가 뜻하는 것 — NODE 와 파드 번호가 어긋나 있다. keycloak-0 이 kc-lab-2 에 있다. iptables 를 어느 노드에 넣을지 정할 때 이걸 헷갈리면 규칙은 걸리는데 패킷은 안 걸린다. IP 도 A-1 때와 다르다(10.42.1.43 에서 10.42.1.77 로). 파드가 재시작되면 바뀌므로 여기 적힌 값을 쓰지 말고 ② 로 지금 뽑는다. keycloak-1 의 RESTARTS 가 1 인 것은 A-4 에서 노드를 껐다 켠 흔적이다.
2. 디스커버리와 클러스터 뷰
무엇을 확인하는가 — 지금 코디네이터가 하나인지, 그리고 뷰 ID 가 몇인지.
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
kubectl -n keycloak-lab logs keycloak-1 | grep ISPN000094 | tail -1
출력에서 답이 되는 것 — ① 의 coord 열과 ② 의 뷰 ID 다. 모양은 이렇고 숫자는 환경마다 다르다.
name | ip | coord
------------------+-----------------+-------
keycloak-0-24309 | 10.42.1.77:7800 | f
keycloak-1-45480 | 10.42.0.42:7800 | t
(2 rows)
ISPN000094: [keycloak-0-24309(v=16.0.12)|13] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(v=16.0.12)]
이 결과가 뜻하는 것 — coord 열에 t 가 정확히 하나여야 한다. 둘이면 이미 갈라져 있고, 그 상태에서 주입해 봐야 아무것도 판정하지 못한다. 뷰 ID(|13)를 적어 둔다. 이 절차의 판정 기준이 그 숫자의 변화다.
3. 지표는 밖에서 Prometheus 에 묻는다
무엇을 확인하는가 — 두 노드가 보는 클러스터 크기.
Keycloak 컨테이너에는 curl 도 wget 도 없다(exit 127).
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size'
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_cluster_size' \
| tr ',' '\n' | grep -E '"pod":|^"[0-9]'
출력에서 답이 되는 것 — value 배열의 두 번째 값이다. 모양은 이렇고 값은 환경마다 다르다.
{"status":"success","data":{"resultType":"vector","result":[
{"metric":{"__name__":"vendor_cluster_size","pod":"keycloak-1","node":"kc-lab-1"},"value":[1757037600.123,"2"]},
{"metric":{"__name__":"vendor_cluster_size","pod":"keycloak-0","node":"kc-lab-2"},"value":[1757037600.123,"2"]}]}}
이 결과가 뜻하는 것 — 두 줄이고 값이 둘 다 2 다. 원 실행에서는 이 값이 안 남았다. 값을 뽑으려고 붙인 파이썬 한 줄이 죽으면서 원본까지 같이 사라졌고, 화면에 남은 것은 스택트레이스뿐이다.
Traceback (most recent call last):
File "<string>", line 3, in <module>
for r in json.load(sys.stdin)["data"]["result"]: print(f" cluster_size {r["metric"].get("pod"):12} = {r["value"][1]}")
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
wget 원문을 먼저 보고 나중에 자르면 같은 일이 안 생긴다.
4. 연결 방향 — 이 절차에서 가장 중요한 사전 관측이다
무엇을 확인하는가 — 두 파드 중 어느 쪽이 클라이언트이고 어느 쪽이 서버인지.
이 실험대는 두 줄로 쳤다.
sudo conntrack -L 2>/dev/null | grep 7800
ssh kc-lab-2 'sudo conntrack -L 2>/dev/null | grep 7800'
:::warning
게스트에 conntrack 이 깔려 있지 않다. cloud-init 이 까는 것은 curl 과 nftables 뿐이라 이 명령은 sudo: conntrack: command not found 로 끝나는데, 2>/dev/null 이 그 한 줄을 지우고 종료 코드도 0 이다. 빈 화면이 「7800 연결이 없다」로 읽힌다. 두 노드에 먼저 sudo apt install -y conntrack 을 치거나, 패키지 없이 sudo grep 7800 /proc/net/nf_conntrack 으로 커널 표를 직접 읽는다. A-1 에서 재서 확인했다(observed).
:::
따라 하는 사람은 둘째 줄을 나눈다. 붙고 나서 원격 셸에서 친다. 이 나눈 형태는 이 실험대에서 치지 않았다.
ssh kc-lab-2
sudo conntrack -L 2>/dev/null | grep 7800
exit
출력에서 답이 되는 것 — dport=7800 인 쪽이 서버이고 src 가 클라이언트다.
ESTABLISHED src=10.42.1.77 dst=10.42.0.42 sport=60485 dport=7800
──────────── ────────────────────
keycloak-0 가 클라이언트 keycloak-1 이 서버
이 결과가 뜻하는 것 — A-1 때와 방향이 반대다. A-1 에서는 10.42.0.35:40023 → 10.42.1.43:7800, 즉 keycloak-1 이 걸었다. 지금은 keycloak-0 이 건다. JGroups 의 TCP 연결 방향은 고정이 아니라 먼저 뜬 쪽과 먼저 JOIN 을 건 쪽에 따라 달라지고, 파드가 재시작될 때마다 바뀔 수 있다. 가정하지 말고 매번 conntrack -L 로 본다. 2>/dev/null 은 conntrack 이 stderr 로 찍는 「N flow entries have been shown」 요약을 지우는 것이므로, 처음에는 빼고 쳐서 그 줄도 한 번 본다.
5. 밖에서 보이는 상태
무엇을 확인하는가 — 정문이 지금 무엇을 답하는지.
curl -I --max-time 8 https://auth.hyeonworks.com/realms/master
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
출력에서 답이 되는 것 — 200 이어야 한다.
이 결과가 뜻하는 것 — 주입 뒤에도 이 값이 200 으로 남는 것이 이 절차의 결론 하나다. 지금 값을 안 잡아 두면 나중에 그것을 말할 수 없다.
주입
주입은 네 번이고 앞의 둘은 일부러 실패한다. 건너뛰지 않는다 — 이 실패의 모양을 봐 둬야 다음에 자기 규칙을 의심할 수 있다.
넷을 연달아 걸지 않는다. 하나를 걸고 같은 번호의 주입 검증을 친 뒤 그 규칙을 걷어내고 다음으로 간다. 넷이 같은 두 포트를 막으므로 걷어내지 않고 쌓으면 어느 규칙이 잡았는지 가릴 수 없다. 치는 순서는 이렇다.
주입 ① ─▶ 검증 §1 ─▶ 관찰 §1 ─▶ 주입 ① 의 ④⑤ 로 철거 ─▶ 주입 ② ─▶ 검증 §2 ─▶ 주입 ② 의 ④⑤ 로 철거
│
┌────────────────────────────────────────────────────────────────────────────────┘
▼
주입 ③ ─▶ 검증 §3 ─▶ 관찰 §2~§4 ─▶ 주입 ④ (③ 의 규칙은 그대로 둔다) ─▶ 검증 §4 ─▶ 관찰 §5~§7 ─▶ 복구
주입 ③ 의 규칙만 예외다. 네 번째 주입이 「kc-lab-1 의 규칙은 그대로 두고」 반대 방향을 더하는 것이라, 셋째만 걷어내지 않고 이어 간다.
1. 시도 ① — filter 테이블 최상단
목적 — FORWARD 최상단에 넣으면 conntrack 승인보다 먼저 평가되리라는 가설을 실제로 확인한다.
ssh kc-lab-2 "sudo iptables -I FORWARD 1 -p tcp -d $K0 --dport 7800 -j DROP"
ssh kc-lab-2 "sudo iptables -I FORWARD 1 -p tcp -d $K0 --dport 57800 -j DROP"
date '+%H:%M:%S 주입'
예상 결과 — iptables 는 아무것도 찍지 않는다. 주입: 12:28:23 만 남는다.
이 줄들은 「전제와 되돌리기」의 중단 절차처럼 나눠 치면 안 된다. 거기는 ssh kc-lab-2 로 붙고 원격 셸에서 쳤지만, 여기는 $K0 가 들어간다. $K0 는 [lab host] 셸의 변수라 원격 셸에는 없고, 나눠 치면 빈 문자열이 들어가 -d 없는 규칙이 걸린다. 큰따옴표가 그 값을 [lab host] 에서 펴서 보내므로 한 줄 형태 그대로 친다.
왜 필요한가 — 57800 을 같이 막는 것은 FD_SOCK2(장애 감지 채널)가 bind_port + 50000 을 쓰기 때문이다. 7800 만 막으면 장애 감지는 계속 통해서 분단이 어정쩡해진다.
결과를 본 다음 반드시 걷어낸다. 주입 검증 §1 을 치고 여기로 와서 ④ 와 ⑤ 를 친다. 시도 ② 는 같은 두 포트를 같은 노드에서 다시 막으므로, FORWARD 에 남은 DROP 둘을 그대로 두면 어느 테이블이 패킷을 잡았는지 가릴 수 없다. 치우는 명령은 넣을 때와 인자가 같아야 한다.
ssh kc-lab-2 "sudo iptables -D FORWARD -p tcp -d $K0 --dport 7800 -j DROP"
ssh kc-lab-2 "sudo iptables -D FORWARD -p tcp -d $K0 --dport 57800 -j DROP"
ssh kc-lab-2 'sudo iptables -L FORWARD -n --line-numbers | head -5'
문제가 생기면 — ⑤ 에 DROP 이 남아 있으면 줄 번호로 지운다 — sudo iptables -D FORWARD 3.
2. 시도 ② — raw 테이블로 옮긴다
목적 — conntrack 조회보다 먼저 평가되는 체인에 같은 규칙을 넣는다.
netfilter 의 처리 순서가 그 근거다.
패킷 도착
│
├─▶ raw PREROUTING ← conntrack 보다 먼저. NOTRACK·DROP 용
│
├─▶ conntrack 조회/생성 ← 여기서 ESTABLISHED 가 결정된다
│
├─▶ mangle PREROUTING
├─▶ nat PREROUTING
├─▶ filter FORWARD ← NetworkPolicy·kube-router 가 여기 있다
└─▶ 목적지 파드
| 어디에 넣는가 | 기존 연결을 끊는가 | CNI 와 경쟁하는가 |
|---|---|---|
| NetworkPolicy (filter) | 못 끊는다 — conntrack 이 먼저 통과시킨다 (A-1) | 없음 |
| filter FORWARD 직접 | 순서에 따라 | 경쟁한다 (kube-router 가 밀어낸다) |
| raw PREROUTING | 끊는다 | 없다 — CNI 가 안 쓰는 테이블 |
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 7800 -j DROP"
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 57800 -j DROP"
date '+%H:%M:%S 주입'
예상 결과 — 규칙이 목록에 보이고, 카운터는 0 으로 남는다. 왜 0 인지는 다음 절이 답한다.
왜 필요한가 — 목적지와 노드는 그대로 두고 테이블만 바꿔야 무엇 때문에 결과가 달라졌는지 하나씩 가릴 수 있다.
이 규칙도 반드시 걷어낸다. 주입 검증 §2 를 치고 여기로 와서 ④ 와 ⑤ 를 친다. 네 번째 주입이 kc-lab-2 의 raw PREROUTING 에 글자까지 같은 두 줄을 다시 넣는다. 걷어내지 않으면 같은 규칙이 두 벌 쌓여 카운터가 네 줄로 갈리고, 검증 §4 의 예상 결과는 그 모양을 적어 두지 않았다. 지우기 전에 -L 로 무엇이 있는지 본다. -F 는 체인 전체를 비운다.
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n --line-numbers'
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
문제가 생기면 — ④ 에 kube-router 규칙이 섞여 있으면 -F 대신 줄 번호로 내가 넣은 둘만 지운다.
3. 성공한 주입 — 수신측 노드의 raw PREROUTING
목적 — 7800 으로 실제로 들어가는 패킷을 잡는다. 목적지 파드가 있는 노드에서 잡아야 한다.
ssh kc-lab-1 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 7800 -j DROP"
ssh kc-lab-1 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K1 --dport 57800 -j DROP"
ssh kc-lab-1 "…" 한 줄 형태인 까닭이 반대 노드 때와 같다. $K1 은 [lab host] 셸의 변수다. ssh kc-lab-1 로 먼저 들어가서 치면 그 셸에는 그 변수가 없어 빈 문자열이 들어가고, -d 없는 규칙이 걸려 7800 으로 가는 모든 패킷이 끊긴다. 2026-09-17 에 게스트 셸에서 확인했다(observed).
게스트 셸의 K1=[]
큰따옴표가 값을 [lab host] 에서 펴서 보내므로 이 두 줄은 한 줄 형태 그대로 친다.
date '+%H:%M:%S 주입'
예상 결과
=== keycloak-1(수신측)으로 들어가는 7800/57800 만 DROP — kc-lab-1 에 넣는다 ===
주입: 12:33:58
왜 필요한가 — 시도 ② 는 10.42.1.77(keycloak-0)을 목적지로 잡았는데 그것은 이 연결의 출발지다. 7800 으로 들어가는 패킷은 10.42.0.42(keycloak-1) 쪽으로 간다.
내가 막은 것: → 10.42.1.77:7800 (그런 패킷이 없다)
실제 흐름: → 10.42.0.42:7800 (여기를 막아야 한다)
문제가 생기면 — 앞의 conntrack -L 출력을 다시 본다. 방향이 바뀌었으면 목적지도 바뀐다.
4. 네 번째 주입 — 양방향
목적 — kc-lab-1 의 규칙은 그대로 두고 kc-lab-2 에 반대 방향을 더해 실제 분단을 만든다.
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 7800 -j DROP"
ssh kc-lab-2 "sudo iptables -t raw -I PREROUTING 1 -p tcp -d $K0 --dport 57800 -j DROP"
date '+%H:%M:%S 주입'
예상 결과
=== 양방향 차단 — 두 노드 모두에 raw DROP ===
주입: 12:40:25
왜 필요한가 — 한 방향만 막으면 JGroups 가 열린 방향으로 다시 붙는다. 양쪽을 막아야 coord = t 가 둘이 된다.
2026-09-17 에 그 「다시 붙는」 과정을 초 단위로 봤다(observed). 한 방향만 막은 채로 두면 잠깐 1 로 떨어졌다가 2분 안에 2 로 돌아온다.
주입 +60초 keycloak-1=1 keycloak-0=1 ← 여기서 멈추면 「분단됐다」로 읽는다
주입 +2분 keycloak-1=2 keycloak-0=2
주입 +3분 keycloak-1=2 keycloak-0=2
그 첫 값을 결론으로 삼지 않는다. 한 방향 차단은 한 번 재서 판정하지 않는다. 2~3분 두고 값이 돌아오는지를 본다.
양방향으로 막은 뒤의 실측은 이렇다(observed).
kc-lab-1 의 raw PREROUTING 7800: 21건 57800: 21건
kc-lab-2 의 raw PREROUTING 7800: 19건 57800: 0건
멤버 수 keycloak-1=1 keycloak-0=1
jgroups_ping keycloak-0-13476 t · keycloak-1-8002 t
분단을 가장 짧게 증명하는 것은 coord 가 둘 다 t 로 찍히는 순간이다. kc-lab-2 쪽 57800 카운터만 0 인 것도 정상이다 — FD_SOCK2 가 그 방향에서 안 쓰였을 뿐이고, 7800 쪽이 올라갔으면 규칙은 걸렸다. 두 체인을 비우자 2분 안에 2 와 coord 하나로 돌아왔다(observed).
문제가 생기면 — 양쪽 카운터를 다 본다. 한쪽만 걸리면 그것은 여전히 단방향이다.
주입 검증
카운터가 유일한 판정 기준이다. 규칙이 목록에 보이는 것은 검증이 아니다.
1. 시도 ① — 넣은 직후에는 맞게 보인다
ssh kc-lab-2 'sudo iptables -L FORWARD -n -v --line-numbers'
넣은 직후의 실측은 이렇다.
Chain FORWARD (policy ACCEPT)
num target prot opt source destination
1 DROP 6 -- 0.0.0.0/0 10.42.1.77 tcp dpt:57800
2 DROP 6 -- 0.0.0.0/0 10.42.1.77 tcp dpt:7800
주입 시각: 12:28:23
여기서 만족하고 넘어가면 속는다. 1~2분 뒤 같은 명령을 다시 친다.
num pkts bytes target
1 232 377K KUBE-ROUTER-FORWARD /* kube-router netpol */ ← 다시 1번이 되었다
2 0 0 DROP tcp dpt:57800
3 0 0 DROP tcp dpt:7800 ← 0 패킷
두 열을 동시에 본다.
| 열 | 무엇을 말하는가 |
|---|---|
num |
내 규칙이 1번이 아니다. kube-router 체인이 위로 돌아왔다 |
pkts |
0. 이 규칙에는 패킷이 단 한 개도 도달하지 않았다 |
kube-router 가 주기적으로 자기 체인을 FORWARD 최상단에 다시 삽입한다. 1번에 넣어도 곧 2번과 3번으로 밀려나고 kube-router 체인이 패킷을 먼저 처리한다. 직접 넣은 iptables 규칙은 CNI(Container Network Interface, 컨테이너 네트워크 플러그인 규격) 가 관리하는 체인과 경쟁하므로, 넣는 것으로 끝나지 않고 패킷 카운터로 확인해야 한다.
이 확인을 담았어야 할 02-injection-verify.txt 는 원 실험 시점에 0바이트로 저장됐다. 리다이렉션이 stdout 만 받았는데 출력이 stderr 로 갔던 것으로 보인다. 지금 그 파일에 들어 있는 것은 사후에 다시 수집한 것이고, 원 시점의 DROP 규칙은 이미 없어서 재현되지 않는다. 남아 있는 사실은 kube-router 체인이 FORWARD 1번을 차지하고 있다는 것 하나이고, 따라 하는 사람은 실제 카운터를 볼 수 있다.
2. 시도 ② — CNI 와 경쟁하지도 않는데 0 이다
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'
=== [검증] 이번엔 패킷이 걸렸는가 ===
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.77 tcp dpt:57800
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.77 tcp dpt:7800
앞에서 본 conntrack 이 답이다. -d 10.42.1.77 --dport 7800 은 존재하지 않는 패킷을 노린 규칙이었다. 규칙을 넣은 노드도 틀렸다.
3. 성공한 주입 — 처음으로 숫자가 올라간다
ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v'
pkts bytes target prot opt in out source destination
0 0 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:57800
19 2938 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:7800
7800 규칙의 pkts 가 19 다. 57800 이 아직 0 인 것도 정보다 — FD_SOCK2 는 이미 붙어 있는 연결을 쓰고 있어서 새 연결을 시도하지 않았다. 조금 지나면 이쪽에도 숫자가 올라간다.
=== 차단 규칙 누적 카운터 ===
19 1096 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:57800
21 3058 DROP 6 -- * * 0.0.0.0/0 10.42.0.42 tcp dpt:7800
pkts |
뜻 | 할 일 |
|---|---|---|
0 |
아무것도 측정하지 않았다 | 해석 금지. 방향과 테이블을 다시 본다 |
| 조금씩 는다 | 재연결 시도가 막히고 있다 | 관찰로 넘어간다 |
| 폭증한다 | 대상이 너무 넓다 | -d 와 --dport 를 좁힌다 |
4. 양방향 주입 — 양쪽 카운터를 다 본다
ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v'
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v'
한쪽만 걸리면 그것은 여전히 단방향이고, 그 상태에서 클러스터를 봐도 앞 단계와 같은 답만 나온다.
관찰
1. 시도 ① 뒤 — 아무 일도 없다
시도 ① 의 규칙이 FORWARD 에 걸려 있는 동안 친다. 주입 §1 의 ④⑤ 로 걷어낸 뒤에 치면 규칙이 없는 상태를 재게 되는데, 화면은 규칙이 있든 없든 외부 200 이라 틀렸다는 신호가 안 나온다.
kubectl -n keycloak-lab get pods | grep keycloak
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
+25초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+50초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
...
+200초 | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
여기서 「비대칭 차단은 클러스터를 안 가른다」고 결론 내리면 틀린다. 결론이 우연히 맞더라도 근거가 없다 — 규칙에 패킷이 0 개 왔으니 이 관찰은 아무것도 측정하지 않았다.
2. 성공한 단방향 주입 뒤 — 흔들렸다가 스스로 낫는다
같은 두 줄이 다른 답을 낸다.
+25초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+50초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+75초 - | keycloak-0:1/1 keycloak-1:0/1 | 외부 200
+100초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+125초 - | keycloak-0:1/1 keycloak-1:1/1 | 외부 200
+75초 에 keycloak-1 이 한 번 0/1 로 흔들렸다가 +100초 에 돌아온다. 주입이 닿기는 했고(시도 ① 의 아무 일 없음과 다르다) 스스로 나았다.
3. 로그 시각은 UTC 다
kubectl -n keycloak-lab logs keycloak-0 --since=20m | grep ISPN000094
kubectl -n keycloak-lab logs keycloak-1 --since=20m | grep ISPN000094
시각을 비교하려다 대부분 한 번은 틀린다.
당신 셸의 date 12:33:58 KST
컨테이너 로그의 시각 03:33:58 ← 같은 순간이다. UTC 다
Keycloak 컨테이너는 UTC(Coordinated Universal Time, 협정 세계시)로 찍는다. KST(Korea Standard Time, 한국 표준시)는 UTC+9 이므로 9시간을 빼서 맞춰 본다. 이걸 모르면 주입 전 로그와 주입 후 로그를 정반대로 가른다.
2026-09-04 03:33:49 | MergeView::[keycloak-0-24309(v=16.0.12)|13] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(v=16.0.12)],
뷰 13, 멤버 (2), 그리고 MergeView 다. 해설 문서는 처음에 「주입 이후 뷰 변화가 하나도 없었다」고 썼고 그 자체는 맞는데, 「주입 전부터 그대로」의 「전」이 9초였다.
03:33:49 MergeView 로 뷰 13 이 만들어짐 ← 그 직전에는 |12] (1), 즉 분단 상태였다
03:33:58 내 주입 ← 9초 뒤
앞선 실패한 주입 시도들이 만든 흔들림이 막 봉합된 직후였다. 로그 한 줄만 보고 「변화 없음」이라고 말하지 않고, 그 줄이 언제 생겼는지를 함께 본다. 결론 자체는 유지되지만 먼저 본 상태가 9초짜리였다는 사실을 같이 적어야 정직하다.
4. 왜 안 갈라졌나 — 연결이 뒤집혔다
앞에서 친 것과 똑같은 명령을 다시 친다. 그것이 대조하는 방법이다.
ssh kc-lab-1 'sudo conntrack -L 2>/dev/null | grep 7800'
tcp 6 299 ESTABLISHED src=10.42.0.42 dst=10.42.1.77 sport=48473 dport=7800 src=10.42.1.77 dst=10.42.0.42 sport=7800 dport=48473
tcp 6 86232 ESTABLISHED src=10.42.0.42 dst=10.42.1.77 sport=44205 dport=57800 src=10.42.1.77 dst=10.42.0.42 sport=57800 dport=44205
src 와 dst 를 앞의 관측과 나란히 놓는다.
차단 전: src=10.42.1.77 → dst=10.42.0.42:7800 ← 내가 막은 방향
차단 후: src=10.42.0.42 → dst=10.42.1.77:7800 ← 열린 방향으로 다시 붙었다
JGroups 는 막힌 연결이 죽자 반대 방향으로 새로 연결했다. FD_SOCK2 가 상대를 의심하기 전에 복구가 끝났고, 의심 카운터가 그것을 뒷받침한다.
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_fd_sock2_get_num_suspected_members'
kubectl -n observability exec deploy/prometheus -- \
wget -qO- 'localhost:9090/api/v1/query?query=vendor_jgroups_merge3_get_num_merge_events'
keycloak-0 merge_events=1.0 suspected=0.0
keycloak-1 merge_events=1.0 suspected=0.0
suspected = 0 이므로 아무도 상대를 의심하지 않았다. 끊긴 적이 없는 것과 같다. merge_events = 1 은 9초 전 병합의 값이다. 한 방향만 막는 것으로는 JGroups 를 가를 수 없다 — 두 노드는 서로에게 연결을 걸 수 있으므로 한쪽 길이 막히면 다른 길로 간다. 운영에서는 단방향 방화벽 오설정이 자가 치유된다는 뜻이고, 분단을 재현하려는 실험자에게는 함정이다.
5. 양방향으로 막으면 갈라진다
kubectl -n keycloak-lab get pods | grep keycloak
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
-o "custom-columns=ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
curl -s -o /dev/null -w '%{http_code}\n' --max-time 8 https://auth.hyeonworks.com/realms/master
+75초 keycloak-0:1/1 keycloak-1:1/1 | ready=[10.42.0.42 10.42.1.77] 외부 200
+100초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
+125초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
...
+225초 keycloak-0:1/1 keycloak-1:0/1 | ready=[10.42.1.77] 외부 200
keycloak-1 이 0/1 로 내려가서 안 돌아오고(단방향 때와 다르다), ready 주소가 둘에서 하나로 줄었으며, 외부는 계속 200 이다. kubectl get endpoints 는 v1.33 부터 deprecated 라 경고가 뜨므로 endpointslice 를 본다.
뷰도 갈린다.
keycloak-0 [keycloak-0-24309(v=16.0.12)|14] (1) [keycloak-0-24309(v=16.0.12)]
keycloak-1 [keycloak-1-45480(v=16.0.12)|14] (1) [keycloak-1-45480(v=16.0.12)]
뷰 ID 는 둘 다 14 인데 멤버는 각자 1 명이다. 같은 번호의 다른 세계다.
6. split brain 은 DB 한 줄로 확인한다
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select name, ip, coord from jgroups_ping order by name"
name | ip | coord
------------------+-----------------+-------
keycloak-0-24309 | 10.42.1.77:7800 | t
keycloak-1-45480 | 10.42.0.42:7800 | t
(2 rows)
coord = t 가 둘이다. 앞에서 하나였던 것과 대조한다. 분단을 확인하는 가장 짧은 명령이 이 한 줄이고, 로그를 두 번 긁는 것보다 빠르며 지표보다 정확하다.
7. 그런데 한쪽만 DOWN 이다
Keycloak 컨테이너에 curl 이 없으므로 상주 파드를 띄운다.
kubectl -n keycloak-lab run a5-probe --image=curlimages/curl:8.11.1 \
--restart=Never --command -- sleep 1800
kubectl -n keycloak-lab wait --for=condition=Ready pod/a5-probe --timeout=120s
kubectl -n keycloak-lab exec a5-probe -- curl -s "http://$K0:9000/health/ready"
kubectl -n keycloak-lab exec a5-probe -- curl -s "http://$K1:9000/health/ready"
일회용 파드를 안 쓰는 까닭은 원 실행이 --rm -it 로 했다가 붙지 못했기 때문이다.
warning: couldn't attach to pod/a5-h, falling back to streaming logs: Internal error occurred: error attaching to container: container is in CONTAINER_EXITED state
파드가 뜬 뒤 명령이 끝나 버리기 전에 붙어야 하는 경주가 된다. 관찰을 여러 번 반복할 것이라면 상주 파드가 항상 낫다.
상주 파드라서 --rm 이 없다. 그래서 이 절차를 두 번째 칠 때는 앞선 실행의 a5-probe 가 같은 이름으로 이미 있어 ① 이 AlreadyExists 로 거절된다. 아래 원상복구 확인표의 삭제 명령을 먼저 치고 ① 로 돌아온다.
--- keycloak-0 ---
{"status":"UP","checks":[{"name":"GracefulShutdown","status":"UP"}
{"name":"KeycloakInitialized","status":"UP"}
{"name":"Keycloakclusterhealthcheck","status":"UP"}
--- keycloak-1 ---
{"status":"DOWN","checks":[{"name":"GracefulShutdown","status":"UP"}
{"name":"Keycloakdatabaseconnectionsasynchealthcheck","status":"UP"}
{"name":"KeycloakInitialized","status":"UP"}
맨 앞의 "status" 를 본다. keycloak-0 은 UP, keycloak-1 은 DOWN 이고, keycloak-1 쪽에서 DB 체크는 UP 이다. DB 때문이 아니라 클러스터 때문이다.
A-1 의 열린 질문에 대한 답이 여기서 나온다.
| 무엇 | keycloak-0 | keycloak-1 |
|---|---|---|
| 분단 전 역할 | 코디네이터 (뷰 13 의 발행자) | 일반 멤버 |
| 분단 후 자기 인식 | 「멤버가 하나 나갔다」 — 정상 사건 | 「코디네이터를 잃었다」 — 비정상 |
| 헬스체크 | UP | DOWN |
| Service 엔드포인트 | 남는다 | 빠진다 |
Keycloak 의 클러스터 헬스체크는 비대칭이다. 코디네이터였던 쪽은 자기가 정상이라고 보고 잃은 쪽만 DOWN 이 되므로, 완전 분단조차 용량 저하로 끝나고 전면 장애가 되지 않는다. A-2(DB 상실)에서는 양쪽이 동시에 DOWN 이었다. 차이는 DB 가 모두 의존하는 하나인 데 비해 클러스터 멤버십은 서로 상대적이라는 것이다.
복구와 원상복구 확인표
1. 두 노드의 규칙을 걷어낸다
목적 — 양쪽 raw PREROUTING 을 비워 JGroups 가 다시 붙게 한다.
지우기 전에 무엇이 있는지 본다.
ssh kc-lab-1 'sudo iptables -t raw -L PREROUTING -n -v --line-numbers'
ssh kc-lab-2 'sudo iptables -t raw -L PREROUTING -n -v --line-numbers'
그다음 해제 시각을 찍고 양쪽을 비운다.
date '+%H:%M:%S 해제'
ssh kc-lab-1 'sudo iptables -t raw -F PREROUTING'
ssh kc-lab-2 'sudo iptables -t raw -F PREROUTING'
예상 결과 — 해제: 12:44:37 이 남고, 25초 간격으로 파드를 보면 이렇다.
+25초 keycloak-0:1/1 keycloak-1:0/1
+50초 keycloak-0:1/1 keycloak-1:1/1
→ 복구 완료
50초, 사람 개입 없음. conntrack 은 건드리지 않아도 된다 — 차단이 풀리면 새 연결이 스스로 성립한다.
��� 필요한가 — 두 노드 중 한쪽만 비우면 여전히 단방향 차단이 걸려 있는 것이고, 클러스터는 열린 방향으로 붙어 겉보기에 복구된 것처럼 보인다.
여기서 비우는 것은 raw 뿐이다. 시도 ① 은 kc-lab-2 의 filter FORWARD 에 DROP 둘을 넣었는데 여기의 ④⑤ 는 그 체인을 안 본다. 주입 §1 의 ④ 를 그때 쳤으면 이미 없고, 건너뛰었으면 지금 남아 있다. 아래 원상복구 확인표의 filter 규칙 줄도 kc-lab-1 만 보므로 잡히지 않는다. 남아 있다면 「전제와 되돌리기」 §1 의 중단 절차를 친다 — 그쪽이 두 노드의 두 체인을 다 비운다.
문제가 생기면 — 2~3분째 안 붙으면 반대 노드 규칙이 남아 있다. 두 노드 모두에서 -t raw -S PREROUTING 을 본다.
2. 누가 붙였는지는 MergeView 가 말한다
kubectl -n keycloak-lab logs keycloak-0 | grep MergeView | tail -1
kubectl -n keycloak-lab logs keycloak-1 | grep MergeView | tail -1
keycloak-0 MergeView::[keycloak-0-24309(v=16.0.12)|15] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(
keycloak-1 MergeView::[keycloak-0-24309(v=16.0.12)|15] (2) [keycloak-0-24309(v=16.0.12), keycloak-1-45480(
뷰 ID 가 15, 멤버 (2), 양쪽이 같은 줄이다.
[keycloak-0-24309|13] (2) ← 정상
[keycloak-0-24309|14] (1) ← 분단. 양쪽이 각자 14 를 발행
MergeView::[...|15] (2) ← 병합. 뷰 ID 는 계속 증가한다
뷰 ID 는 단조 증가하므로 언제 몇 번 갈라졌는지를 로그만으로 셀 수 있다.
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN100007 | tail -6
[Context=work] ISPN100007: After merge (or coordinator change) ...
[Context=clientSessions] ISPN100007: After merge ...
[Context=offlineSessions] ISPN100007: After merge ...
[Context=loginFailures] ISPN100007: After merge ...
[Context=actionTokens] ISPN100007: After merge ...
증거 파일의 원문은 한 줄이 길어 잘려 있다. 그 모양도 한 번 본다.
2026-09-04 03:32:59,874 INFO [org.infinispan.CLUSTER] (non-blocking-thread--p2-t2) [Context=work] ISPN100007: After merge (or coo
ISPN100007 은 병합 또는 코디네이터 변경 후의 캐시별 토폴로지 재계산이다. 캐시가 여럿이므로 로그도 캐시 수만큼 나오고, 한 줄만 보고 한 번 재분배됐다고 세면 틀린다.
3. 원상복구 확인표
filter 규칙 줄은 kc-lab-1 만 본다. 시도 ① 이 kc-lab-2 의 FORWARD 에 넣은 DROP 둘은 이 표로 안 잡히므로, 그쪽이 의심되면 「전제와 되돌리기」 §1 의 중단 절차를 친다. 반대 노드의 FORWARD 를 조회하는 명령은 원 가이드에 없다(unknown).
| 항목 | 명령 | 돌아왔을 때 |
|---|---|---|
| raw 규칙 | sudo iptables -t raw -S PREROUTING |
-P PREROUTING ACCEPT 만 |
| filter 규칙 | sudo iptables -S FORWARD | head -5 |
내가 넣은 DROP 이 없음 |
| (반대 노드) | ssh kc-lab-2 'sudo iptables -t raw -S PREROUTING' |
같음 |
| 파드 | kubectl -n keycloak-lab get pods |
keycloak 둘 다 1/1 Running |
| 디스커버리 | psql -c "select name, ip, coord from jgroups_ping order by name" |
coord = t 가 하나 |
| 뷰 | logs keycloak-0 | grep ISPN000094 | tail -1 |
멤버 (2), 양쪽 동일 |
| 지표 | vendor_cluster_size |
양쪽 2 |
| Service | get endpointslice -l kubernetes.io/service-name=keycloak |
ready 주소 둘 |
| 탐침 파드 | kubectl -n keycloak-lab get pod a5-probe |
지웠으면 NotFound |
| 밖 | curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master |
200 |
kubectl -n keycloak-lab delete pod a5-probe --ignore-not-found
막히면
아래는 이 실험대가 실제로 겪은 증상이다. 마지막 줄만 A-2·A-3 에서 겪은 것을 옮겼다 — 탐침 파드를 같은 방식으로 띄우므로 여기서도 그대로 걸린다.
| 증상 | 원인 | 확인 |
|---|---|---|
| 규칙을 넣었는데 아무 일도 없다 | 카운터가 0 이면 아무것도 측정 안 된 것 | iptables -L -n -v 의 pkts |
| 내 규칙이 1번이 아니다 | kube-router 가 자기 체인을 재삽입한다 | --line-numbers 로 순서 |
raw 인데도 0 패킷 |
연결 방향을 잘못 짚었다 | conntrack -L | grep 7800 |
| conntrack 에 아무것도 안 보인다 | 반대 노드에서 봤다 | 두 노드 모두에서 본다 |
| 단방향인데 안 갈라진다 | 정상이다. 열린 방향으로 재연결한다 | conntrack 의 src/dst 뒤집힘 |
| 로그에 변화가 없어 보인다 | 컨테이너 로그는 UTC. KST 와 9시간 차 | logs 의 시각에서 9를 뺀다 |
| 「주입 전부터 그대로」인데 미심쩍다 | 그 「전」이 9초일 수 있다 | 앞 뷰가 언제 생겼는지 본다 |
-D 로 규칙이 안 지워진다 |
넣을 때와 인자가 다르다 | 줄 번호로 지운다: -D FORWARD 3 |
| 임시 파드에 attach 실패 | --rm -it 는 경주가 된다 |
상주 파드를 쓴다 |
kubectl exec keycloak-0 -- curl 이 exit 127 |
이미지에 curl 도 wget 도 없다 |
탐침 파드나 Prometheus |
| 지표를 파이썬으로 자르다 죽었다 | 원본까지 같이 사라진다 | wget 원문을 먼저 본다 |
get endpoints 가 경고를 찍는다 |
v1.33 부터 deprecated | get endpointslice -l kubernetes.io/service-name=... |
| 57800 카운터만 0 이다 | FD_SOCK2 가 아직 재연결을 안 했다 | 조금 기다렸다 다시 본다 |
| 해제했는데 2~3분째 안 붙는다 | 반대 노드 규칙이 남아 있다 | 두 노드 모두 -t raw -S PREROUTING |
a5-probe 를 다시 못 만든다 |
앞선 실행의 파드가 그 이름으로 남아 있다 | delete pod a5-probe --ignore-not-found |
무엇이 관측이고 무엇이 아닌가
- (observed) 파드 IP
10.42.1.77과10.42.0.42와 노드 배치, 뷰 ID13→14→15, 시도 ① 의pkts 0과 kube-router 가 되찾은num 1, 시도 ② 의pkts 0, 성공한 주입의19 2938과 이어서19 1096과21 3058, 주입12:28:23과12:33:58과12:40:25와 해제12:44:37, 단방향에서+75초의0/1과+100초의 복귀, 양방향에서+100초이후0/1고정과 ready 주소 하나,coord = t둘, 양쪽 헬스체크의UP과DOWN,suspected=0.0과merge_events=1.0, 복구 50초,MergeView뷰15,ISPN100007다섯 캐시,MergeView가03:33:49에 생기고 주입이03:33:58인 9초 간격. - (unknown)
tr ',' '\n' | grep -E로 자른 Prometheus 출력. 가이드가 미검증으로 표시했다.ssh kc-lab-2로 들어가 원격 셸에서conntrack과iptables -F를 따로 치는 두 단계 형태도 이 실험대에서 치지 않았다. 반대 노드의filter FORWARD를 조회하는 명령은 원 가이드에 아예 없어서 확인표에 넣지 못했다. - 증거가 비어 있는 곳 — 시도 ① 의 카운터를 담았어야 할
02-injection-verify.txt가 원 시점에 0바이트로 저장됐다. 지금 그 파일에 있는 것은 사후 수집이고 원 시점의DROP규칙은 재현되지 않는다. - 원 실행에 안 남은 것 — 주입 전
vendor_cluster_size값. 파이썬 한 줄이 죽으면서 Prometheus 원본까지 함께 사라졌다. - 이 절차가 재지 않은 것 — 분단 중에 세션이 어떻게 되는지는 재지 않았다(그것은 A-1 의 주제다). 여기서는 누가 살아남는가만 봤다.