fix(setup): 실험대를 새로 세워 setup 35편을 밟고 어긋난 명령과 결과를 고친다
기반 가이드 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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab59130196
commit
024362d096
+50
-32
@@ -42,7 +42,11 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다 — 가이드가 「kubeconfig 를 사용자 홈에 복사해 뒀다면 `sudo` 는 빼도 된다」고 스스로 괄호를 달아 두었다. 마지막 한 단계만 호스트(`test-server`)로 넘어가고, 거기서는 사람이 비밀번호를 친다.
|
||||
명령은 `[lab host]` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다 — 가이드가 「kubeconfig 를 사용자 홈에 복사해 뒀다면 `sudo` 는 빼도 된다」고 스스로 괄호를 달아 두었다. 마지막 한 단계만 호스트(`test-server`)로 넘어가고, 거기서는 사람이 비밀번호를 친다.
|
||||
|
||||
**`[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]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -88,7 +92,7 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
되돌리기는 한 줄이고, 파괴하기 전에 읽어 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] 파괴하기 전에 읽어 두는 되돌리기 한 줄"
|
||||
```bash label="[lab host] 파괴하기 전에 읽어 두는 되돌리기 한 줄"
|
||||
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
< /tmp/keycloak-backup.sql
|
||||
```
|
||||
@@ -107,7 +111,7 @@ kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak
|
||||
|
||||
**무엇을 보는가** — 파드 넷의 상태와 배치.
|
||||
|
||||
```bash label="[kc-lab-1] 파드 배치를 본다"
|
||||
```bash label="[lab host] 파드 배치를 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
```
|
||||
|
||||
@@ -126,7 +130,7 @@ keycloak-1 1/1 Running 0 4m38s
|
||||
|
||||
**무엇을 보는가** — realm·client·user·세션의 개수. 처음 한 번은 읽는 형태로 친다. 값만 뽑는 형태부터 배우면 `psql` 이 무엇을 돌려주는지 모르게 된다.
|
||||
|
||||
```bash label="[kc-lab-1] ① psql 이 무엇을 돌려주는지 한 번 본다"
|
||||
```bash label="[lab host] ① psql 이 무엇을 돌려주는지 한 번 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from realm"
|
||||
```
|
||||
@@ -144,7 +148,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
이제 넷을 한 줄로 모은다. 비교할 값이 필요할 때만 이 형태를 쓴다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 대조할 한 줄을 뽑는다"
|
||||
```bash label="[lab host] ② 대조할 한 줄을 뽑는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select (select count(*) from realm), (select count(*) from client),
|
||||
(select count(*) from user_entity),
|
||||
@@ -165,7 +169,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
**무엇을 보는가** — 질문 ①의 재료. 세션 행이 실제로 테이블에 있어야 덤프에 들어갈 것이 있다.
|
||||
|
||||
```bash label="[kc-lab-1] 세션 행을 나열한다"
|
||||
```bash label="[lab host] 세션 행을 나열한다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select user_session_id, offline_flag, realm_id from offline_user_session"
|
||||
```
|
||||
@@ -189,13 +193,13 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 정문과 app1 의 응답. 처음 한 번은 응답을 읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 헤더를 통째로 본다"
|
||||
```bash label="[lab host] ① 헤더를 통째로 본다"
|
||||
curl -I https://auth.hyeonworks.com/realms/master
|
||||
```
|
||||
|
||||
헤더가 통째로 나온다. `HTTP/2 200`, `content-type: application/json` 을 본다. 같은 것을 반복해서 재고 비교할 때만 코드만 뽑는다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 코드만 뽑아 둘을 잰다"
|
||||
```bash label="[lab host] ② 코드만 뽑아 둘을 잰다"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
|
||||
```
|
||||
@@ -215,7 +219,7 @@ curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
|
||||
|
||||
① 시각을 남기고 덤프를 뜬다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각과 함께 덤프를 뜬다"
|
||||
```bash label="[lab host] ① 시각과 함께 덤프를 뜬다"
|
||||
date '+%H:%M:%S 백업 시작'
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
|
||||
--clean --if-exists > /tmp/keycloak-backup.sql
|
||||
@@ -243,7 +247,7 @@ date '+%H:%M:%S 백업 완료'
|
||||
|
||||
**문제가 생기면** — 이 단계는 읽기만 하므로 파일이 마음에 안 들면 지우고 다시 뜬다.
|
||||
|
||||
```bash label="[kc-lab-1] 덤프가 마음에 안 들면 지우고 다시 뜬다"
|
||||
```bash label="[lab host] 덤프가 마음에 안 들면 지우고 다시 뜬다"
|
||||
rm -f /tmp/keycloak-backup.sql
|
||||
```
|
||||
|
||||
@@ -253,7 +257,7 @@ rm -f /tmp/keycloak-backup.sql
|
||||
|
||||
① 크기와 줄 수.
|
||||
|
||||
```bash label="[kc-lab-1] ① 크기와 줄 수"
|
||||
```bash label="[lab host] ① 크기와 줄 수"
|
||||
ls -l /tmp/keycloak-backup.sql
|
||||
wc -l /tmp/keycloak-backup.sql
|
||||
```
|
||||
@@ -266,7 +270,7 @@ wc -l /tmp/keycloak-backup.sql
|
||||
|
||||
② 테이블 수.
|
||||
|
||||
```bash label="[kc-lab-1] ② 덤프 안의 테이블 수"
|
||||
```bash label="[lab host] ② 덤프 안의 테이블 수"
|
||||
grep -c '^CREATE TABLE' /tmp/keycloak-backup.sql
|
||||
```
|
||||
|
||||
@@ -280,7 +284,7 @@ grep -c '^CREATE TABLE' /tmp/keycloak-backup.sql
|
||||
|
||||
③ 끝까지 쓰였는가.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 마지막 세 줄"
|
||||
```bash label="[lab host] ③ 마지막 세 줄"
|
||||
tail -3 /tmp/keycloak-backup.sql
|
||||
```
|
||||
|
||||
@@ -296,7 +300,7 @@ tail -3 /tmp/keycloak-backup.sql
|
||||
|
||||
④ 세션이 들어갔는가. 이것이 질문 ① 자체다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ COPY 블록에 세션 행이 붙어 있는가"
|
||||
```bash label="[lab host] ④ COPY 블록에 세션 행이 붙어 있는가"
|
||||
grep -c 'offline_user_session' /tmp/keycloak-backup.sql
|
||||
grep -A3 'COPY public.offline_user_session' /tmp/keycloak-backup.sql | cut -c1-110
|
||||
```
|
||||
@@ -320,7 +324,7 @@ grep -A3 'COPY public.offline_user_session' /tmp/keycloak-backup.sql | cut -c1-1
|
||||
|
||||
**무엇을 보는가** — 파일의 경로와 그 파일이 올라앉은 디스크.
|
||||
|
||||
```bash label="[kc-lab-1] 덤프의 경로와 디스크를 본다"
|
||||
```bash label="[lab host] 덤프의 경로와 디스크를 본다"
|
||||
ls -l /tmp/keycloak-backup.sql
|
||||
df -h /tmp
|
||||
```
|
||||
@@ -337,7 +341,7 @@ df -h /tmp
|
||||
|
||||
① 시각을 남기고 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각을 남기고 스키마를 지운다"
|
||||
```bash label="[lab host] ① 시각을 남기고 스키마를 지운다"
|
||||
date '+%H:%M:%S 파괴'
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"
|
||||
@@ -362,12 +366,12 @@ CREATE SCHEMA
|
||||
|
||||
결과를 해석하기 전에, 의도한 것만 지워졌는지 먼저 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 남은 테이블을 센다"
|
||||
```bash label="[lab host] ① 남은 테이블을 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from pg_tables where schemaname='public'"
|
||||
```
|
||||
|
||||
이 실험대는 스크립트로 셌고(observed), 위 형태는 가이드가 미검증으로 표시한 줄이다(unknown). 결과는 이렇다(observed).
|
||||
이 실험대는 스크립트로 셌고(observed), **위 형태도 2026-09-17 에 쳐서 `0` 을 받았다**(observed). 결과는 이렇다(observed).
|
||||
|
||||
```text
|
||||
남은 테이블: 0
|
||||
@@ -377,7 +381,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
애플리케이션 테이블이 정말 없는지 직접 물어본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 테이블에 직접 물어본다"
|
||||
```bash label="[lab host] ② 테이블에 직접 물어본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from realm"
|
||||
```
|
||||
@@ -394,7 +398,7 @@ LINE 1: select count(*) from realm
|
||||
|
||||
**그런데 밖은 멀쩡하다.**
|
||||
|
||||
```bash label="[kc-lab-1] ③ 파드와 밖에서 본 상태를 다시 잰다"
|
||||
```bash label="[lab host] ③ 파드와 밖에서 본 상태를 다시 잰다"
|
||||
kubectl -n keycloak-lab get pods -o wide
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://app1.hyeonworks.com/
|
||||
@@ -413,9 +417,9 @@ keycloak-1 1/1 Running 0 4m38s
|
||||
|
||||
엉뚱한 것을 죽이지 않았는지도 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ Service 에서 빠진 파드가 있는가"
|
||||
```bash label="[lab host] ④ Service 에서 빠진 파드가 있는가"
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
```
|
||||
|
||||
ready 주소가 여전히 둘이다. 아무 파드도 Service 에서 빠지지 않았다. A-2 에서는 여기가 빈 목록이었다. `kubectl get endpoints` 는 v1.33+ 에서 deprecated 이고, 이 실험대에서 실제로 그 경고를 봤다.
|
||||
@@ -424,7 +428,7 @@ ready 주소가 여전히 둘이다. 아무 파드도 Service 에서 빠지지
|
||||
|
||||
여기까지의 주입 검증은 쉽게 통과한다. 어려운 확인은 반대편에 있다. 복구 명령에서 `-i` 를 빠뜨리면 파드 안의 `psql` 이 빈 입력을 받고 정상 종료하고, 셸은 오류를 내지 않고, 종료 코드도 0 이며, `date` 두 줄은 「1초 만에 끝났다」로 찍힌다. 복구된 것과 구별되지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] 치지 않는다 — -i 가 있는 줄과 없는 줄을 눈으로 견준다"
|
||||
```bash label="[lab host] 치지 않는다 — -i 가 있는 줄과 없는 줄을 눈으로 견준다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql ... < dump.sql # ✘
|
||||
kubectl -n keycloak-lab exec -i deploy/postgres -- psql ... < dump.sql # ✔
|
||||
```
|
||||
@@ -435,7 +439,7 @@ kubectl -n keycloak-lab exec -i deploy/postgres -- psql ... < dump.sql # ✔
|
||||
|
||||
**전부 깨지지는 않는다.** 세 경로를 나눠서 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 두 경로를 잰다"
|
||||
```bash label="[lab host] ① 두 경로를 잰다"
|
||||
curl -s -o /dev/null -w 'certs %{http_code}\n' \
|
||||
https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
|
||||
curl -s -o /dev/null -w 'well-known %{http_code}\n' \
|
||||
@@ -452,7 +456,7 @@ curl -s -o /dev/null -w 'well-known %{http_code}\n' \
|
||||
|
||||
토큰 발급은 값이 필요하므로 따로 친다. 이 실험대는 스크립트로 돌렸고(observed), 아래는 가이드가 미검증으로 표시한 형태다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 토큰 발급을 잰다"
|
||||
```bash label="[lab host] ② 토큰 발급을 잰다"
|
||||
curl -s -o /dev/null -w '토큰 %{http_code}\n' -X POST \
|
||||
https://auth.hyeonworks.com/realms/master/protocol/openid-connect/token \
|
||||
-d grant_type=password -d client_id=admin-cli -d username=admin \
|
||||
@@ -462,7 +466,7 @@ curl -s -o /dev/null -w '토큰 %{http_code}\n' -X POST \
|
||||
|
||||
**비밀번호를 화면에 찍지 않는다.** 명령 치환으로 넘기므로 값은 터미널에도 셸 히스토리에도 남지 않는다. 길이만 확인하려면 한 줄을 더 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 값이 아니라 길이만 잰다"
|
||||
```bash label="[lab host] ③ 값이 아니라 길이만 잰다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.KC_BOOTSTRAP_ADMIN_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -479,7 +483,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
|
||||
로그가 이유를 말한다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ Keycloak 로그를 읽는다"
|
||||
```bash label="[lab host] ④ Keycloak 로그를 읽는다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 --tail=50
|
||||
```
|
||||
|
||||
@@ -516,7 +520,7 @@ kubectl -n keycloak-lab logs keycloak-0 --tail=50
|
||||
|
||||
① 시각을 남기고 복구한다. `-i` 가 있는지 치기 전에 눈으로 확인한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각과 함께 덤프를 되돌린다"
|
||||
```bash label="[lab host] ① 시각과 함께 덤프를 되돌린다"
|
||||
date '+%H:%M:%S 복구 시작'
|
||||
kubectl -n keycloak-lab exec -i deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
< /tmp/keycloak-backup.sql > /tmp/restore.log 2>&1
|
||||
@@ -533,7 +537,7 @@ date '+%H:%M:%S 복구 완료'
|
||||
|
||||
② 로그의 오류를 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 복구 로그의 오류를 센다"
|
||||
```bash label="[lab host] ② 복구 로그의 오류를 센다"
|
||||
grep -ci '^ERROR' /tmp/restore.log
|
||||
tail -5 /tmp/restore.log
|
||||
```
|
||||
@@ -542,6 +546,20 @@ tail -5 /tmp/restore.log
|
||||
|
||||
**왜 필요한가** — 시각 두 줄과 오류 0건은 `-i` 를 빠뜨렸을 때도 똑같이 나온다. 그래서 이 둘로는 복구를 판정하지 않는다.
|
||||
|
||||
**2026-09-17 에 이 절차를 처음부터 끝까지 쳤다**(observed).
|
||||
|
||||
```text
|
||||
덤프 101 테이블 · 976873 bytes · 8569 줄
|
||||
파괴 15:31:02 DROP SCHEMA · CREATE SCHEMA → 남은 테이블 0 · select count(*) from realm 이 relation does not exist
|
||||
그동안 밖에서는 200 ← 스키마가 통째로 없는데 정문은 멀쩡했다
|
||||
복구 15:31:02 → 15:31:08 (6초) · 복구 로그 ERROR 0
|
||||
복구 뒤 101 테이블 · 세션 6건 — 파괴 전과 같다
|
||||
```
|
||||
|
||||
**크기와 줄 수는 실험대마다 다르다.** 위 실측의 `394945 bytes · 6956 줄` 은 그 실험대의 값이고, BFF 쪽 테이블이 있는 이 실험대에서는 `976873 bytes · 8569 줄` 이었다. **같아야 하는 것은 테이블 수 `101` 과 복구 전후의 행 수**다.
|
||||
|
||||
**스키마가 없는 동안에도 정문이 `200` 인 것이 이 편의 핵심이다.** Keycloak 은 이미 읽어 둔 것으로 답하므로, 밖에서 보는 코드만으로는 데이터베이스가 통째로 비었다는 것을 알 수 없다.
|
||||
|
||||
**문제가 생기면** — 1초 만에 끝났는데 다음 단계의 대조가 어긋나면 `-i` 를 의심한다.
|
||||
|
||||
### 2. 진짜 판정 — 주입 전과 문자 단위로 견준다
|
||||
@@ -550,7 +568,7 @@ tail -5 /tmp/restore.log
|
||||
|
||||
① 주입 전에 친 것과 똑같은 명령을 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 대조할 한 줄을 다시 뽑는다"
|
||||
```bash label="[lab host] ① 대조할 한 줄을 다시 뽑는다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select (select count(*) from realm), (select count(*) from client),
|
||||
(select count(*) from user_entity),
|
||||
@@ -574,7 +592,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
① 15초쯤 뒤에 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 15초 뒤에 다시 잰다"
|
||||
```bash label="[lab host] ① 15초 뒤에 다시 잰다"
|
||||
curl -s -o /dev/null -w 'well-known %{http_code}\n' \
|
||||
https://auth.hyeonworks.com/realms/keycloak-patterns/.well-known/openid-configuration
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
@@ -598,7 +616,7 @@ kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
|
||||
① 세션 행을 다시 나열한다. **주입 전 §3 에서 친 것과 열이 하나 다르다** — 거기는 `realm_id` 까지 셋을 뽑고 여기는 `user_session_id` 와 `offline_flag` 둘만 뽑는다. 가이드 원문이 그렇게 갈려 있어 그대로 싣는다. 열이 다르므로 행 수와 `user_session_id` 값으로 견준다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 세션 행을 다시 나열한다"
|
||||
```bash label="[lab host] ① 세션 행을 다시 나열한다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select user_session_id, offline_flag from offline_user_session"
|
||||
```
|
||||
|
||||
+67
-39
@@ -45,7 +45,9 @@ databasechangelog 행 수를 먼저 세고 이미지 태그를 정방향·롤백
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 호스트로 넘어가는 단계가 없어 전 구간을 게스트 안에서 끝낸다. 터미널을 두 개 열어 두면 편하다 — 하나는 가용성 폴링용, 하나는 관찰용이다.
|
||||
명령은 `[lab host]` 에서 친다. `kubectl` 에 `sudo` 를 붙이지 않는다. 호스트로 넘어가는 단계가 없어 전 구간을 게스트 안에서 끝낸다. 터미널을 두 개 열어 두면 편하다 — 하나는 가용성 폴링용, 하나는 관찰용이다.
|
||||
|
||||
**원 가이드는 이 명령들을 `kc-lab-1` 에서 치라고 적었다.** 기반 가이드가 세운 실험대에서는 그 기계에 kubeconfig 가 없어서 `sudo` 없는 `kubectl` 이 `permission denied` 로 막힌다 — kubeconfig 는 lab host 의 `~/.kube/config` 에만 있다(2026-09-17 에 양쪽에서 쳐서 확인했다, observed). 그래서 `kubectl` 블록의 기계 이름을 `[lab host]` 로 적었고, 노드 자체를 건드리는 명령에만 게스트 셸을 쓴다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -100,7 +102,7 @@ Keycloak 은 Liquibase 로 스키마를 관리한다. 적용한 변경 하나하
|
||||
|
||||
되돌리기는 전부 태그 한 줄이고, 각 단계 앞에서 먼저 읽는다.
|
||||
|
||||
```bash label="[kc-lab-1] 단계마다 먼저 읽어 두는 되돌리기 한 줄"
|
||||
```bash label="[lab host] 단계마다 먼저 읽어 두는 되돌리기 한 줄"
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||||
```
|
||||
@@ -121,7 +123,7 @@ kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
|
||||
① 덤프를 뜨고 끝까지 쓰였는지 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 덤프를 뜨고 마지막 세 줄을 본다"
|
||||
```bash label="[lab host] ① 덤프를 뜨고 마지막 세 줄을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- pg_dump -U keycloak -d keycloak \
|
||||
--clean --if-exists > /tmp/pre-upgrade.sql
|
||||
ls -l /tmp/pre-upgrade.sql
|
||||
@@ -145,7 +147,7 @@ tail -3 /tmp/pre-upgrade.sql
|
||||
|
||||
**무엇을 보는가** — StatefulSet 에 적힌 태그.
|
||||
|
||||
```bash label="[kc-lab-1] ① StatefulSet 의 태그를 본다"
|
||||
```bash label="[lab host] ① StatefulSet 의 태그를 본다"
|
||||
kubectl -n keycloak-lab get statefulset keycloak \
|
||||
-o jsonpath='{.spec.template.spec.containers[0].image}'; echo
|
||||
```
|
||||
@@ -158,19 +160,28 @@ quay.io/keycloak/keycloak:26.7.0
|
||||
|
||||
**이 값이 뜻하는 것** — `latest` 로 되어 있으면 무엇에서 무엇으로 가는지 말할 수 없기 때문에 이 실험이 성립하지 않는다. 그리고 StatefulSet 에 적힌 것과 파드가 실제로 돌리고 있는 것은 다를 수 있다 — 적용 중이거나 롤아웃이 멈춰 있으면 그렇다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드가 실제로 돌리는 이미지를 본다"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,\
|
||||
RESTARTS:.status.containerStatuses[0].restartCount | grep keycloak
|
||||
```bash label="[lab host] ② 파드가 실제로 돌리는 이미지를 본다"
|
||||
kubectl -n keycloak-lab get pods \
|
||||
-o "custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount" \
|
||||
| grep keycloak
|
||||
```
|
||||
|
||||
두 파드의 `IMAGE` 가 서로 같고 StatefulSet 과도 같은지, `READY` 가 둘 다 `true`, `RESTARTS` 가 `0` 인지를 본다.
|
||||
|
||||
**`grep keycloak` 은 BFF 파드도 잡는다.** B층을 먼저 밟아 `keycloak-pattern-bff` 가 떠 있으면 그 두 줄이 같이 나온다(2026-09-17, observed). 이 편이 보려는 것은 Keycloak 파드 둘이므로 라벨로 거르는 편이 낫다.
|
||||
|
||||
```bash label="[lab host] ②b 라벨로 걸러 Keycloak 파드만 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=keycloak \
|
||||
-o "custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount"
|
||||
```
|
||||
|
||||
**`-o` 값을 따옴표로 묶고 한 줄로 둔다.** 묶지 않으면 zsh 가 `[0]` 을 글로브로 읽어 명령이 아예 안 돌고, 중간에서 줄을 접으면 따옴표가 먼저 닫혀 뒤쪽 칸이 명령 인자로 떨어진다.
|
||||
|
||||
### 3. 마이그레이션 수 — 이 실험의 전부다
|
||||
|
||||
**무엇을 보는가** — `databasechangelog` 의 행 수. 처음 한 번은 읽는 형태로 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ① psql 이 무엇을 돌려주는지 한 번 본다"
|
||||
```bash label="[lab host] ① psql 이 무엇을 돌려주는지 한 번 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select count(*) from databasechangelog"
|
||||
```
|
||||
@@ -186,7 +197,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
비교용으로 값만 뽑는 형태도 익혀 둔다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 값만 뽑는 형태"
|
||||
```bash label="[lab host] ② 값만 뽑는 형태"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from databasechangelog"
|
||||
```
|
||||
@@ -199,7 +210,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
**이 값을 화면 밖에 적어 둔다.** 무엇이 마지막으로 적용됐는지도 한 번 본다. 나중에 「스키마가 언제 움직였나」를 물을 때 여기를 본다. 이 실험대는 개수만 셌고(observed), 아래는 가이드가 미검증으로 표시한 형태다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ③ 마지막 다섯 줄을 본다 (미검증)"
|
||||
```bash label="[lab host] ③ 마지막 다섯 줄을 본다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
-c "select id, author, orderexecuted, dateexecuted from databasechangelog
|
||||
order by orderexecuted desc limit 5"
|
||||
@@ -211,7 +222,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
|
||||
|
||||
**무엇을 보는가** — 판올림이 세션을 건드리는지 판정할 재료.
|
||||
|
||||
```bash label="[kc-lab-1] 세션 수를 센다"
|
||||
```bash label="[lab host] 세션 수를 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from offline_user_session"
|
||||
```
|
||||
@@ -229,7 +240,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
**무엇을 보는가** — 멤버 수와 괄호 안의 판 번호.
|
||||
|
||||
```bash label="[kc-lab-1] 클러스터 뷰 마지막 줄을 본다"
|
||||
```bash label="[lab host] 클러스터 뷰 마지막 줄을 본다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
||||
```
|
||||
|
||||
@@ -245,13 +256,13 @@ kubectl -n keycloak-lab logs keycloak-0 | grep ISPN000094 | tail -1
|
||||
|
||||
**무엇을 보는가** — 올라갈 곳이 실제로 있는가. 처음 한 번은 응답을 그대로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 응답을 통째로 본다"
|
||||
```bash label="[lab host] ① 응답을 통째로 본다"
|
||||
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true"
|
||||
```
|
||||
|
||||
한 줄짜리 JSON 이 통째로 나온다. 어떤 필드가 있는지 보고 나서 자른다. 이 실험대는 `jq` 가 없어 이렇게 읽었고(observed), 가이드가 그 줄을 미검증으로 표시했다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 이름만 잘라 본다 (미검증)"
|
||||
```bash label="[lab host] ② 이름만 잘라 본다 (미검증)"
|
||||
curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyActiveTags=true" \
|
||||
| tr ',' '\n' | grep '"name"'
|
||||
```
|
||||
@@ -264,7 +275,7 @@ curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyA
|
||||
|
||||
① 1초 간격으로 150회, 뒤에서 돌린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 폴링을 뒤에서 돌린다"
|
||||
```bash label="[lab host] ① 폴링을 뒤에서 돌린다"
|
||||
( for i in $(seq 1 150); do
|
||||
printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
https://auth.hyeonworks.com/realms/master)"
|
||||
@@ -274,7 +285,7 @@ curl -s "https://quay.io/api/v1/repository/keycloak/keycloak/tag/?limit=40&onlyA
|
||||
|
||||
그만 재려면 `kill %1` 이다. **`&` 로 붙인 작업은 그것을 띄운 창의 것이라 `kill %1` 도 그 창에서만 듣는다.** 그래서 이 한 줄은 폴링용 창에서 치고, 아래 ② 부터 관찰 절까지는 다른 창에서 친다. 두 창 다 `kc-lab-1` 이다. 30초쯤 두고 먼저 평시를 센다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 평시 응답 코드를 센다"
|
||||
```bash label="[lab host] ② 평시 응답 코드를 센다"
|
||||
tr ' ' '\n' < /tmp/d2-avail.txt | grep -c 200
|
||||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
```
|
||||
@@ -293,7 +304,7 @@ tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
|
||||
① 시각을 남기고 태그를 바꾼다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 시각을 남기고 태그를 바꾼다"
|
||||
```bash label="[lab host] ① 시각을 남기고 태그를 바꾼다"
|
||||
date '+%H:%M:%S 태그 변경'
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.7.3
|
||||
@@ -310,7 +321,7 @@ statefulset.apps/keycloak image updated
|
||||
|
||||
② 롤아웃을 기다린다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 롤아웃을 기다린다"
|
||||
```bash label="[lab host] ② 롤아웃을 기다린다"
|
||||
kubectl -n keycloak-lab rollout status statefulset/keycloak --timeout=600s
|
||||
date '+%H:%M:%S 롤아웃 완료'
|
||||
```
|
||||
@@ -332,10 +343,10 @@ partitioned roll out complete: 2 new pods have been updated...
|
||||
|
||||
결과를 해석하기 전에, 주입이 의도한 것을 정확히 했는지 먼저 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 파드가 새 이미지를 돌리는가"
|
||||
kubectl -n keycloak-lab get pods -o custom-columns=\
|
||||
NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,\
|
||||
RESTARTS:.status.containerStatuses[0].restartCount | grep keycloak
|
||||
```bash label="[lab host] ① 파드가 새 이미지를 돌리는가"
|
||||
kubectl -n keycloak-lab get pods \
|
||||
-o "custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount" \
|
||||
| grep keycloak
|
||||
```
|
||||
|
||||
실측은 이렇다(observed, `followup/01-d2-forward-upgrade.txt`).
|
||||
@@ -350,7 +361,7 @@ quay.io/keycloak/keycloak:26.7.3
|
||||
|
||||
실제로 새 파드인지는 나이로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 나이로 새 파드인지 본다"
|
||||
```bash label="[lab host] ② 나이로 새 파드인지 본다"
|
||||
kubectl -n keycloak-lab get pods -o wide | grep keycloak
|
||||
```
|
||||
|
||||
@@ -365,7 +376,7 @@ keycloak-1 1/1 Running 0 28s
|
||||
|
||||
버전은 파드가 자기 입으로 말하게 한다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ 로그가 말하는 판을 본다"
|
||||
```bash label="[lab host] ③ 로그가 말하는 판을 본다"
|
||||
kubectl -n keycloak-lab logs keycloak-0 | grep -i 'Keycloak 26' | tail -1
|
||||
```
|
||||
|
||||
@@ -379,9 +390,26 @@ kubectl -n keycloak-lab logs keycloak-0 | grep -i 'Keycloak 26' | tail -1
|
||||
|
||||
## 관찰
|
||||
|
||||
**2026-09-17 에 세 번을 이어서 쳤다**(observed). 네 값이 이 편의 판정 전부다.
|
||||
|
||||
| 언제 | 무엇 | 파드 | `databasechangelog` | 정문 |
|
||||
|---|---|---|---|---|
|
||||
| `15:32:15` → `15:33:40` (85초) | 26.7.0 → **26.7.3** | 둘 다 `true` · `RESTARTS 0` | 210 → **210** | 200 |
|
||||
| `15:33:48` → `15:34:55` (67초) | 26.7.3 → **26.7.0** (롤백) | 둘 다 `true` · `RESTARTS 0` | **210** | 200 |
|
||||
| `15:34:55` | 26.7.0 → **26.0** (역방향) | `keycloak-1` 만 `false` · `RESTARTS 2` | **210** | **200** |
|
||||
|
||||
**스키마가 한 번도 안 움직였다.** `orderexecuted` 210 의 `26.7.0-cluster-event` 이 끝까지 마지막 줄이었다. 패치 판올림이라 롤백이 막히지 않은 것이고, 역방향이 막힌 까닭은 판올림이 남긴 스키마가 아니라 **체크섬**이다.
|
||||
|
||||
```text
|
||||
liquibase.exception.ValidationFailedException: Validation Failed:
|
||||
1 changesets check sum
|
||||
```
|
||||
|
||||
**그런데 정문은 세 번 내내 `200` 이었다.** StatefulSet 이 파드를 하나씩 갈아 끼우고 `keycloak-0` 이 26.7.0 에 남아 있었기 때문이다. **한 파드가 `CrashLoopBackOff` 인데 밖에서는 아무 일도 없어 보인다** — 이 층이 「조용한 실패」라고 부르는 것이 이 모양이다. 실패한 기동은 스키마도 안 건드렸다(210 그대로).
|
||||
|
||||
### 1. 정방향은 무중단이었는가
|
||||
|
||||
```bash label="[kc-lab-1] 폴링 결과를 센다"
|
||||
```bash label="[lab host] 폴링 결과를 센다"
|
||||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
```
|
||||
|
||||
@@ -402,7 +430,7 @@ tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
|
||||
주입 전에 친 것과 똑같은 명령이다.
|
||||
|
||||
```bash label="[kc-lab-1] 마이그레이션 수를 다시 센다"
|
||||
```bash label="[lab host] 마이그레이션 수를 다시 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from databasechangelog"
|
||||
```
|
||||
@@ -431,7 +459,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
⓪ §1 에서 센 `200 응답: 87 회 / 비200: 0` 을 먼저 손으로 적어 둔다. 루프가 `>` 로 파일을 잘라 쓰기 때문에 다시 띄우면 그 값은 화면에서 사라진다. 폴링 창에서 `jobs` 를 쳐 아직 돌고 있으면 `kill %1` 로 멈춘 뒤, 같은 창에서 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ⓪ 폴링 창에서 다시 띄운다"
|
||||
```bash label="[lab host] ⓪ 폴링 창에서 다시 띄운다"
|
||||
( for i in $(seq 1 150); do
|
||||
printf '%s ' "$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 \
|
||||
https://auth.hyeonworks.com/realms/master)"
|
||||
@@ -441,7 +469,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
① 시각을 남기고 태그를 내린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 태그를 26.7.0 으로 되돌린다"
|
||||
```bash label="[lab host] ① 태그를 26.7.0 으로 되돌린다"
|
||||
date '+%H:%M:%S 롤백'
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||||
@@ -466,7 +494,7 @@ partitioned roll out complete: 2 new pods have been updated...
|
||||
|
||||
### 4. 전환 순간의 000 을 읽는다
|
||||
|
||||
```bash label="[kc-lab-1] 비200 이 어디에 있는지 본다"
|
||||
```bash label="[lab host] 비200 이 어디에 있는지 본다"
|
||||
tr ' ' '\n' < /tmp/d2-avail.txt | sort | uniq -c
|
||||
grep -n '000' /tmp/d2-avail.txt
|
||||
```
|
||||
@@ -497,7 +525,7 @@ grep -n '000' /tmp/d2-avail.txt
|
||||
|
||||
① 시각을 남기고 26.0 으로 내린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 26.0 으로 내린다"
|
||||
```bash label="[lab host] ① 26.0 으로 내린다"
|
||||
date '+%H:%M:%S 26.0 으로 내린다'
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.0
|
||||
@@ -505,7 +533,7 @@ kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
|
||||
② 이번에는 `rollout status` 로 기다리지 말고 눈으로 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드 상태를 눈으로 따라간다"
|
||||
```bash label="[lab host] ② 파드 상태를 눈으로 따라간다"
|
||||
kubectl -n keycloak-lab get pods -w
|
||||
```
|
||||
|
||||
@@ -532,7 +560,7 @@ statefulset.apps/keycloak image updated
|
||||
|
||||
### 6. 왜 실패했는지 물어본다
|
||||
|
||||
```bash label="[kc-lab-1] ① Liquibase 관련 줄만 뽑는다"
|
||||
```bash label="[lab host] ① Liquibase 관련 줄만 뽑는다"
|
||||
kubectl -n keycloak-lab logs keycloak-1 | grep -iE 'liquibase|changeset|validation'
|
||||
```
|
||||
|
||||
@@ -547,16 +575,16 @@ kubectl -n keycloak-lab logs keycloak-1 | grep -iE 'liquibase|changeset|validati
|
||||
|
||||
`1 changesets check sum` 이고 개수가 1이다. 26.7.0 이 적용한 changeset 하나를 26.0 도 알고 있는데 정의가 다르다. 같은 changeset 이 버전 사이에 수정됐고, Liquibase 는 스키마를 반쯤 아는 상태로 서비스하느니 기동 자체를 거부한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드가 이미 죽었으면 직전 로그를 본다"
|
||||
```bash label="[lab host] ② 파드가 이미 죽었으면 직전 로그를 본다"
|
||||
kubectl -n keycloak-lab logs keycloak-1 --previous
|
||||
```
|
||||
|
||||
### 7. 그런데 서비스는 살아 있다
|
||||
|
||||
```bash label="[kc-lab-1] 밖과 Service 와 StatefulSet 을 함께 본다"
|
||||
```bash label="[lab host] 밖과 Service 와 StatefulSet 을 함께 본다"
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://auth.hyeonworks.com/realms/master
|
||||
kubectl -n keycloak-lab get endpointslice -l kubernetes.io/service-name=keycloak \
|
||||
-o custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
|
||||
-o "custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready"
|
||||
kubectl -n keycloak-lab get statefulset keycloak
|
||||
```
|
||||
|
||||
@@ -587,7 +615,7 @@ A-8 에서 「무중단은 replica ≥ 2 와 readiness 의 조합」이라고
|
||||
|
||||
같은 명령을 세 번째로 친다.
|
||||
|
||||
```bash label="[kc-lab-1] 마이그레이션 수를 세 번째로 센다"
|
||||
```bash label="[lab host] 마이그레이션 수를 세 번째로 센다"
|
||||
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tAc \
|
||||
"select count(*) from databasechangelog"
|
||||
```
|
||||
@@ -632,7 +660,7 @@ kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak -tA
|
||||
|
||||
① 시각을 남기고 태그를 되돌린 뒤 롤아웃을 기다린다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 태그를 되돌리고 롤아웃을 기다린다"
|
||||
```bash label="[lab host] ① 태그를 되돌리고 롤아웃을 기다린다"
|
||||
date '+%H:%M:%S 복귀'
|
||||
kubectl -n keycloak-lab set image statefulset/keycloak \
|
||||
keycloak=quay.io/keycloak/keycloak:26.7.0
|
||||
@@ -686,7 +714,7 @@ keycloak-1 1/1 Running 0 28s
|
||||
|
||||
**업그레이드 전 행 수를 안 적었을 때**는 덤프 안에 그 테이블이 통째로 들어 있다. 가이드가 미검증으로 표시한 줄이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] 덤프에서 행 수를 되찾는다 (미검증)"
|
||||
```bash label="[lab host] 덤프에서 행 수를 되찾는다 (미검증)"
|
||||
sed -n '/^COPY public.databasechangelog /,/^\\\.$/p' /tmp/pre-upgrade.sql | wc -l
|
||||
```
|
||||
|
||||
|
||||
+63
-23
@@ -41,9 +41,24 @@ sourceRevision: cdac9b8178391311d8eca1ebc6cac15bb62d79af
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
명령은 `kc-lab-1` 에서 친다. k3s 서버의 저장 파일도 이 노드에 있어서 디스크를 보는 경로를 여기서 칠 수 있다. 게스트(`kc-lab-1`/`kc-lab-2`)의 `sudo` 는 무암호이고, 호스트와 다르다.
|
||||
**기계가 둘이다.** `kubectl` 은 `[lab host]` 에서 치고, k3s 서버의 저장 파일을 들여다보는 `k3s`·`ls`·`grep`·`strings` 는 그 파일이 있는 `[kc-lab-1]` 에서 친다. 게스트(`kc-lab-1`/`kc-lab-2`)의 `sudo` 는 무암호이고, 호스트와 다르다.
|
||||
|
||||
가이드의 전제는 「`kubectl` 은 `sudo` 로 쓴다」인데 본문의 `kubectl` 줄에는 `sudo` 가 없고 `k3s`·`ls`·`grep` 에만 붙어 있다. 아래는 본문의 형태를 그대로 옮긴다. **먼저 `sudo` 없이 치고, 권한 때문에 막히면 그때 앞에 `sudo` 를 붙인다.** 어느 쪽인지는 kubeconfig 를 어디에 뒀나가 가른다 — D-1 은 같은 전제를 옮기면서 「kubeconfig 를 사용자 홈에 복사해 뒀다면 `sudo` 는 빼도 된다」를 괄호로 달아 두었고, D-3 은 그 괄호가 없다.
|
||||
**`[kc-lab-1]` 라벨이 붙은 블록은 게스트 셸이다.** lab host 에서 `ssh kc-lab-1` 로 들어가서 치고, 끝나면 `exit` 로 나온다. `ssh kc-lab-2 '…'` 한 줄 형태는 lab host 에서 그대로 쳐도 되므로 `[lab host]` 로 두었다.
|
||||
|
||||
원 가이드는 둘을 한 기계로 묶어 전부 `kc-lab-1` 에서 치라고 적었고, 전제 한 줄은 「`kubectl` 은 `sudo` 로 쓴다」인데 본문의 `kubectl` 줄에는 `sudo` 가 없다. 그 조합은 이 실험대에서 안 돈다 — 2026-09-17 에 양쪽에서 쳐서 확인했다(observed).
|
||||
|
||||
| 어디서 | `kubectl …` | `sudo kubectl …` |
|
||||
|---|---|---|
|
||||
| lab host | 된다 | 안 된다 |
|
||||
| `kc-lab-1` | 안 된다 | 된다 |
|
||||
|
||||
기반 가이드가 kubeconfig 를 lab host 의 `~/.kube/config` 에만 두고 게스트의 사용자 홈에는 일부러 두지 않기 때문이다. `kc-lab-1` 에서 `sudo` 없이 치면 이렇게 끝난다.
|
||||
|
||||
```text
|
||||
error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied
|
||||
```
|
||||
|
||||
그래서 `kubectl` 은 `[lab host]` 에서 치고, 저장 파일을 보는 넷만 `[kc-lab-1]` 에서 `sudo` 로 친다. 굳이 `kc-lab-1` 한 기계에서 다 치겠다면 `kubectl` 에도 `sudo` 를 붙인다. D-1 은 같은 전제에 「kubeconfig 를 사용자 홈에 복사해 뒀다면 `sudo` 는 빼도 된다」를 괄호로 달아 두었는데, 그쪽은 반대 방향을 말한다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -95,7 +110,7 @@ Secret 이 base64 를 쓰는 까닭은 감추려는 것이 아니라 YAML 에
|
||||
|
||||
**파괴적인 단계가 없는 편이다.** 만드는 것은 카나리아 Secret 하나뿐이고 복구 절에서 지운다. 전 구간 약 15분. 되돌리기는 한 줄이다.
|
||||
|
||||
```bash label="[kc-lab-1] 되돌리기 한 줄"
|
||||
```bash label="[lab host] 되돌리기 한 줄"
|
||||
kubectl -n keycloak-lab delete secret d3-canary
|
||||
```
|
||||
|
||||
@@ -111,7 +126,7 @@ Secret 목록 → describe 가 감추는 화면 → 키 이름만 → 길이만
|
||||
|
||||
**무엇을 보는가** — 이름과 키 개수.
|
||||
|
||||
```bash label="[kc-lab-1] Secret 목록을 본다"
|
||||
```bash label="[lab host] Secret 목록을 본다"
|
||||
kubectl -n keycloak-lab get secret
|
||||
```
|
||||
|
||||
@@ -129,7 +144,7 @@ kubectl -n keycloak-lab get secret
|
||||
|
||||
**무엇을 보는가** — 키 이름과 바이트 수.
|
||||
|
||||
```bash label="[kc-lab-1] describe 화면을 본다"
|
||||
```bash label="[lab host] describe 화면을 본다"
|
||||
kubectl -n keycloak-lab describe secret bff-secrets
|
||||
```
|
||||
|
||||
@@ -149,7 +164,7 @@ kubectl -n keycloak-lab describe secret bff-secrets
|
||||
|
||||
**무엇을 보는가** — 어떤 키가 들어 있는가. 값은 보지 않는다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 키 이름만 뽑는다"
|
||||
```bash label="[lab host] ① 키 이름만 뽑는다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets -o jsonpath='{.data}' \
|
||||
| tr ',' '\n' | grep -o '"[A-Z_]*"'
|
||||
```
|
||||
@@ -163,7 +178,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets -o jsonpath='{.data}' \
|
||||
|
||||
`jq` 가 없어서 `tr` 과 `grep` 으로 자른다. D-2 가 레지스트리 태그 목록을 자를 때 쓴 것과 같은 수법이고, `jq` 가 없다는 전제가 여기서도 형태를 정한다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 값 대신 길이를 잰다"
|
||||
```bash label="[lab host] ② 값 대신 길이를 잰다"
|
||||
kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
-o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
|
||||
```
|
||||
@@ -190,7 +205,7 @@ kubectl -n keycloak-lab get secret keycloak-lab-secrets \
|
||||
|
||||
① 카나리아를 만든다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 카나리아 Secret 을 만든다"
|
||||
```bash label="[lab host] ① 카나리아 Secret 을 만든다"
|
||||
kubectl -n keycloak-lab create secret generic d3-canary \
|
||||
--from-literal=CANARY=d3-canary-zq7v-do-not-use
|
||||
```
|
||||
@@ -207,7 +222,7 @@ secret/d3-canary created
|
||||
|
||||
## 주입 검증
|
||||
|
||||
```bash label="[kc-lab-1] 카나리아가 심겼는지 본다"
|
||||
```bash label="[lab host] 카나리아가 심겼는지 본다"
|
||||
kubectl -n keycloak-lab get secret d3-canary
|
||||
kubectl -n keycloak-lab describe secret d3-canary
|
||||
```
|
||||
@@ -232,7 +247,7 @@ CANARY: 25 bytes
|
||||
|
||||
값을 아는 카나리아로 먼저 해 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 카나리아를 API 로 뽑아 본다"
|
||||
```bash label="[lab host] ① 카나리아를 API 로 뽑아 본다"
|
||||
kubectl -n keycloak-lab get secret d3-canary \
|
||||
-o jsonpath='{.data.CANARY}' | base64 -d; echo
|
||||
```
|
||||
@@ -306,7 +321,14 @@ sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
|
||||
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db-wal
|
||||
```
|
||||
|
||||
이 실험대는 카나리아 대신 실제 값으로 쟀다(observed). 위의 카나리아 형태는 가이드가 미검증으로 표시했다(unknown). 실제로 나온 결과가 이것이다(observed, `02-at-rest.txt`).
|
||||
이 실험대는 카나리아 대신 실제 값으로 쟀다(observed). **위의 카나리아 형태는 2026-09-17 에 쳐서 확인했다**(observed) — 그때까지 가이드가 미검증으로 표시해 둔 줄이다.
|
||||
|
||||
```text
|
||||
state.db 0
|
||||
state.db-wal 1
|
||||
```
|
||||
|
||||
**그 `0` 과 `1` 이 가이드가 예측만 하고 못 가른 것을 가른다.** 방금 만든 값은 아직 본체로 안 내려가고 `-wal` 에만 있다. 그러니 본체에서 `0` 이 나왔다고 「평문이 없다」가 아니라, **`-wal` 까지 봐야 답이 나온다.** 실제로 나온 결과가 이것이다(observed, `02-at-rest.txt`).
|
||||
|
||||
```text
|
||||
=== ★ 저장 파일에서 비밀번호가 그대로 보이는가 ===
|
||||
@@ -327,11 +349,19 @@ sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
|
||||
|
||||
**가이드는 이 두 줄의 인자에 클라이언트 비밀 평문을 적어 두었다. 값은 옮기지 않는다** — 그리고 가이드 자신이 주입 절에서 「진짜 비밀번호를 `grep` 인자로 쓰면 셸 히스토리와 `ps` 에 남는다」고 적었으므로, 아래에는 카나리아 문자열을 넣었다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ -wal 과 strings 로 한 번 더 본다 (미검증)"
|
||||
```bash label="[kc-lab-1] ④ -wal 과 strings 로 한 번 더 본다"
|
||||
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db-wal
|
||||
sudo strings /var/lib/rancher/k3s/server/db/state.db | grep -c 'd3-canary-zq7v-do-not-use'
|
||||
sudo sh -c 'strings /var/lib/rancher/k3s/server/db/state.db | grep -c d3-canary-zq7v-do-not-use'
|
||||
```
|
||||
|
||||
:::warning
|
||||
|
||||
**게스트에 `strings` 가 없다.** `binutils` 가 안 깔려 있어 그대로 치면 `sh: 1: strings: not found` 로 끝난다(2026-09-17, observed). `sudo apt install -y binutils` 로 깔거나, `strings` 없이 `grep -c` 만으로도 같은 답이 나온다 — 둘 다 쳐서 같은 수를 받았다(observed).
|
||||
|
||||
:::
|
||||
|
||||
**`sudo strings … | grep` 이 아니라 `sudo sh -c '…'` 인 까닭**은 파이프가 `sudo` 밖에서 이어지기 때문이다. 앞의 형태로 치면 `strings` 만 root 로 돌고 `grep` 은 일반 사용자로 도는데, 여기서는 읽는 쪽이 `strings` 라 결과는 같다. 다만 파일을 `grep` 이 직접 읽는 아래 형태에서는 `sudo` 가 `grep` 에 붙어야 한다.
|
||||
|
||||
| 왜 안 나올 수 있나 | 확인 |
|
||||
|---|---|
|
||||
| 아직 `-wal` 에만 있다 | `-wal` 을 같이 `grep` |
|
||||
@@ -357,7 +387,7 @@ k3s 는 `--secrets-encryption` 플래그로 켤 수 있다. 지금은 안 켜져
|
||||
|
||||
어느 파드를 볼지 먼저 정한다.
|
||||
|
||||
```bash label="[kc-lab-1] ① BFF 파드를 본다"
|
||||
```bash label="[lab host] ① BFF 파드를 본다"
|
||||
kubectl -n keycloak-lab get pods -l app=bff
|
||||
```
|
||||
|
||||
@@ -365,7 +395,7 @@ kubectl -n keycloak-lab get pods -l app=bff
|
||||
|
||||
**치기 전에 — 이 줄은 값을 화면에 찍는다.** 나오는 것은 카나리아가 아니라 `KEYCLOAK_CLIENT_SECRET` 과 `BFF_DB_PASSWORD` 의 평문이다. 바로 아래 실측을 `<평문 14자>` 로 가린 것은 이 문서이지 당신의 터미널이 아니다. 찍힌 값은 스크롤백과 셸 히스토리에 남고, 화면을 공유 중이면 보는 사람 모두에게 간다. 이 편이 「읽기 전에」에서 세운 「남의 진짜 비밀은 길이와 키 이름까지만 본다」를 이 줄 하나가 벗어나는데, 같은 결론을 값 없이 내는 명령은 가이드에 없다(unknown). 운영 클러스터에서는 치지 않는다. 실험대에서 쳤으면 복구 절의 `history` 확인까지 마치고, 운영 값을 찍었으면 회전(B-6·B-7)으로 이어 간다.
|
||||
|
||||
```bash label="[kc-lab-1] ② 파드 안의 환경변수를 본다 (미검증 · 평문이 화면에 찍힌다)"
|
||||
```bash label="[lab host] ② 파드 안의 환경변수를 본다 (미검증 · 평문이 화면에 찍힌다)"
|
||||
kubectl -n keycloak-lab exec deploy/bff -- sh -c 'env | grep -iE "secret|password"'
|
||||
```
|
||||
|
||||
@@ -380,7 +410,7 @@ kubectl -n keycloak-lab exec deploy/bff -- sh -c 'env | grep -iE "secret|passwor
|
||||
|
||||
`exec` 이 `deploy/bff` 로 안 되면(파드가 종료 중이거나 여럿이면) 이름을 골라 친다.
|
||||
|
||||
```bash label="[kc-lab-1] ③ Running 인 파드 이름을 고른다"
|
||||
```bash label="[lab host] ③ Running 인 파드 이름을 고른다"
|
||||
kubectl -n keycloak-lab get pod -l app=bff \
|
||||
--field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}'; echo
|
||||
```
|
||||
@@ -391,7 +421,7 @@ kubectl -n keycloak-lab get pod -l app=bff \
|
||||
|
||||
**이 줄도 값을 화면에 찍는다.** ②에서 본 것과 같은 평문이 같은 자국을 남긴다. 여기서 확인하려는 것은 「같은 값이 또 나오는가」뿐이므로, 화면을 공유 중이거나 운영 클러스터에 붙어 있으면 치지 않고 ②의 결과로 판정한다.
|
||||
|
||||
```bash label="[kc-lab-1] ④ 다른 프로세스의 환경변수를 읽는다 (미검증 · 평문이 화면에 찍힌다)"
|
||||
```bash label="[lab host] ④ 다른 프로세스의 환경변수를 읽는다 (미검증 · 평문이 화면에 찍힌다)"
|
||||
kubectl -n keycloak-lab exec deploy/bff -- \
|
||||
sh -c 'tr "\0" "\n" < /proc/1/environ | grep -i secret'
|
||||
```
|
||||
@@ -418,7 +448,7 @@ volumeMounts:
|
||||
|
||||
### 4. ④ RBAC — 유일하게 막는다
|
||||
|
||||
```bash label="[kc-lab-1] ① 기본 서비스계정이 Secret 을 읽을 수 있는지 묻는다"
|
||||
```bash label="[lab host] ① 기본 서비스계정이 Secret 을 읽을 수 있는지 묻는다"
|
||||
kubectl auth can-i get secrets -n keycloak-lab \
|
||||
--as=system:serviceaccount:keycloak-lab:default
|
||||
```
|
||||
@@ -433,7 +463,7 @@ kubectl auth can-i get secrets -n keycloak-lab \
|
||||
|
||||
어떤 권한이 있는지 통째로 보는 형태도 가이드에 있고, 미검증이다(unknown).
|
||||
|
||||
```bash label="[kc-lab-1] ② 권한 목록과 Role 을 본다 (미검증)"
|
||||
```bash label="[lab host] ② 권한 목록과 Role 을 본다 (미검증)"
|
||||
kubectl auth can-i --list -n keycloak-lab \
|
||||
--as=system:serviceaccount:keycloak-lab:default
|
||||
kubectl -n keycloak-lab get role,rolebinding
|
||||
@@ -473,7 +503,7 @@ kubectl -n keycloak-lab get role,rolebinding
|
||||
|
||||
① 지우고 목록을 다시 본다.
|
||||
|
||||
```bash label="[kc-lab-1] ① 카나리아를 지우고 목록을 본다"
|
||||
```bash label="[lab host] ① 카나리아를 지우고 목록을 본다"
|
||||
kubectl -n keycloak-lab delete secret d3-canary
|
||||
kubectl -n keycloak-lab get secret
|
||||
```
|
||||
@@ -492,13 +522,23 @@ secret "d3-canary" deleted
|
||||
|
||||
### 2. 지웠다고 파일에서 없어지지는 않는다
|
||||
|
||||
아래는 미검증이고, 이 실험은 삭제 후를 재지 않았다(unknown).
|
||||
**2026-09-17 에 삭제 후를 쟀다**(observed). 그때까지 이 실험이 안 밟고 넘어간 단계다.
|
||||
|
||||
```bash label="[kc-lab-1] 삭제 뒤에 저장 파일을 다시 본다 (미검증)"
|
||||
```bash label="[kc-lab-1] 삭제 뒤에 저장 파일을 다시 본다"
|
||||
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
|
||||
sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db-wal
|
||||
```
|
||||
|
||||
**지운 뒤에 오히려 늘었다.**
|
||||
|
||||
```text
|
||||
state.db state.db-wal
|
||||
삭제 전 0 1
|
||||
삭제 뒤 0 7
|
||||
```
|
||||
|
||||
`kubectl delete secret` 은 API 에서 그 객체를 없앨 뿐이고, **그 삭제 자체가 같은 값을 담은 레코드를 `-wal` 에 더 쓴다.** 그래서 「Secret 을 지웠다」와 「그 값이 디스크에서 사라졌다」는 다른 사건인 정도가 아니라, 지우는 동작이 흔적을 **늘린다.**
|
||||
|
||||
`0` 이 나오면 「이 시점에 이 형태로는 안 보인다」이고, `0` 이 아니면 지운 Secret 의 평문이 아직 파일에 있는 것이다. 어느 쪽이든 관찰 절의 결론은 안 바뀐다 — 판정은 이미 `2` 에서 났다.
|
||||
|
||||
데이터베이스 파일은 지운 행의 공간을 즉시 0으로 덮어쓰지 않는다. 「Secret 을 지웠다」와 「그 값이 디스크에서 사라졌다」는 다른 사건이고, 비밀이 유출됐을 때 실제로 해야 하는 일이 삭제가 아니라 회전(rotation)인 까닭이 여기 있다 — B-6·B-7 의 주제다.
|
||||
@@ -508,7 +548,7 @@ sudo grep -c 'd3-canary-zq7v-do-not-use' /var/lib/rancher/k3s/server/db/state.db
|
||||
| 항목 | 명령 | 돌아왔을 때 |
|
||||
|---|---|---|
|
||||
| 카나리아 | `kubectl -n keycloak-lab get secret d3-canary` | `NotFound` |
|
||||
| Secret 목록 | `kubectl -n keycloak-lab get secret` | 세 개 |
|
||||
| Secret 목록 | `kubectl -n keycloak-lab get secret` | 카나리아를 뺀 나머지. 2026-09-17 의 이 실험대에는 `keycloak-lab-secrets` 하나뿐이었다(observed) |
|
||||
| 파드 | `kubectl -n keycloak-lab get pods` | 전부 `Running` (아무것도 안 건드렸다) |
|
||||
| 터미널 | `history \| tail -40` | 비밀번호가 찍힌 줄이 어디까지 남았는지 본다 |
|
||||
|
||||
|
||||
+43
-5
@@ -212,6 +212,18 @@ Fri 2026-09-04 17:03:46 KST 1h 54min Fri 2026-09-04 03:19:39 KST 11h ago
|
||||
|
||||
`NEXT`/`LEFT` 가 채워져 있는가, `LAST`/`PASSED` 가 하루 안쪽인가를 본다. 표가 통째로 비면 타이머가 없는 것이고, 이름이 배포판마다 다르므로 `systemctl list-timers --all | grep -i certbot` 으로 찾는다.
|
||||
|
||||
**기계 이름과 유닛 이름이 둘 다 이 실험대와 다르다.** 2026-09-17 에 쳐서 확인했다(observed).
|
||||
|
||||
첫째, 이 편은 `ssh test-server` 로 간다고 적는데 **certbot 은 엣지 게스트에 있다.** 기반 가이드 03 이 엣지 nginx 를 호스트에서 `kc-lab-edge` 로 옮겼고 04 가 certbot 을 거기 깔았기 때문이다. `test-server` 에도 `certbot` 실행 파일은 있지만 타이머가 없다.
|
||||
|
||||
```text
|
||||
[test-server] systemctl list-timers certbot-renew.timer → 0 timers listed.
|
||||
[kc-lab-edge] systemctl list-timers --all | grep -i certbot
|
||||
Thu 2026-09-17 17:06:05 UTC 10h left certbot.timer certbot.service
|
||||
```
|
||||
|
||||
둘째, Debian 12 의 유닛 이름은 **`certbot.timer` · `certbot.service`** 이고 `certbot-renew.*` 가 아니다. 그래서 이 편의 `systemctl list-timers certbot-renew.timer` 와 `systemctl cat certbot-renew.service` 는 이 실험대에서 **빈 표와 오류**를 낸다. 아래부터는 `ssh kc-lab-edge` 로 가고 유닛 이름은 `--all | grep -i certbot` 이 찾아 준 것을 쓴다.
|
||||
|
||||
```bash label="[test-server] ② 서비스 상태와 오늘 journal 을 본다"
|
||||
ssh test-server 'systemctl status certbot-renew.service'
|
||||
```
|
||||
@@ -250,15 +262,33 @@ Let's Encrypt 는 90일 발급이고 certbot 은 30일 남았을 때 갱신한
|
||||
|
||||
**무엇을 보는가** — 갱신된 인증서를 서버에 읽히는 경로는 셋뿐이고, 셋을 하나씩 연다. 아직 아무것도 주입하지 않았는데 이 실험의 원인 진단이 여기서 이미 끝난다.
|
||||
|
||||
```bash label="[test-server] ① 유닛 본문을 본다"
|
||||
ssh test-server 'systemctl cat certbot-renew.service'
|
||||
```bash label="[kc-lab-edge] ① 유닛 본문을 본다"
|
||||
ssh kc-lab-edge 'systemctl cat certbot.service'
|
||||
```
|
||||
|
||||
```bash label="[test-server] ② 타이머 본문을 본다"
|
||||
ssh test-server 'systemctl cat certbot-renew.timer'
|
||||
```bash label="[kc-lab-edge] ② 타이머 본문을 본다"
|
||||
ssh kc-lab-edge 'systemctl cat certbot.timer'
|
||||
```
|
||||
|
||||
**어디를 보나** — 실측은 이렇다(observed, `07-renewal-hook-missing.txt`).
|
||||
```bash label="[kc-lab-edge] ③ 배포 훅 디렉터리가 비었는지 본다"
|
||||
ssh kc-lab-edge 'sudo ls -l /etc/letsencrypt/renewal-hooks/deploy/'
|
||||
```
|
||||
|
||||
**2026-09-17 실측**(observed) — 이름은 다르지만 **이 편이 찾는 것은 그대로 없다.**
|
||||
|
||||
```text
|
||||
# /lib/systemd/system/certbot.service
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/certbot -q renew --no-random-sleep-on-renew
|
||||
PrivateTmp=true
|
||||
|
||||
/etc/letsencrypt/renewal-hooks/deploy/ total 0
|
||||
```
|
||||
|
||||
`ExecStartPost` 도 `--deploy-hook` 도 없고 훅 디렉터리도 비어 있다. **갱신은 돌지만 받은 것을 누가 읽게 만드는 일은 아무도 하지 않는다** — 이 편의 결론이 유닛 이름과 무관하게 성립한다는 뜻이다.
|
||||
|
||||
**어디를 보나** — 원래 실행의 실측은 이렇다(observed, `07-renewal-hook-missing.txt`).
|
||||
|
||||
```text
|
||||
# /usr/lib/systemd/system/certbot-renew.service
|
||||
@@ -1072,6 +1102,14 @@ crt.sh 에 관한 곁다리도 실측이다. 발급 사실은 Certificate Transp
|
||||
|
||||
인증서에 SCT 가 박혀 있다는 것과 crt.sh 가 그것을 색인했다는 것은 다르다. 관측 도구가 진실의 부분집합만 본다는, A-2 의 `up` 지표와 같은 종류의 함정이다.
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 인증서 발급과 갱신·reload 판정 전부. Cloudflare API 토큰이 있어야 DNS-01 이 돈다. 지금까지 밟은 것 — 기계·유닛 이름 대조, 기본 유닛에 훅이 없다는 확인.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
- (observed) 주입 전 인증서의 `subject`·`issuer`·`notBefore=Sep 3 00:47:23 2026 GMT`·`notAfter=Dec 2 00:47:22 2026 GMT` 와 SAN 세 이름, SCT 두 줄, 체인 네 줄과 `Verify return code: 0 (ok)`, 타이머 표와 `status=0/SUCCESS`, `남은 일수: 88일`, 유닛 본문과 `ExecStart=/usr/bin/certbot -q renew`, 훅 디렉터리의 `Permission denied` 와 `sudo` 로 본 `total 8` 셋, `Discovered plugins: dns-cloudflare, manual, null, standalone, webroot` 와 `certbot 5.7.0`, 워커 두 줄(`585`·`586`·`80529`), 시계 측정 네 줄과 `+106.1` 세 번, 대조군 900건, 전송 중 대조군의 `845361`·`41.392198s`, `sudo -n -l` 두 줄, `Serial Number: 6c7cb6df1da8a6d7995d93c264bb9ecea1d`, `archive/` 여덟 줄과 `2026-09-04 17:22:13` mtime, 감시의 `serial=0520BB6416D569E26697B1691440F523B853` 과 161표본, `ssl_certificate` 두 줄, 일련번호가 바뀐 `08:58:52` 와 그 앞 구간의 `428회`, 폴링 `8856건` 과 구간 셋의 중앙·p95·최대, 전송 중 세 줄과 `845361`·`연결수=1`·`curl종료=0`, 아티팩트 76건과 `50µs`·`0/100`, reload 뒤의 워커 `28829`, crt.sh 의 `[]` 와 13건.
|
||||
|
||||
+40
-3
@@ -43,7 +43,17 @@ certbot 의 `deploy/` 훅에 두 줄짜리 파일 하나를 넣고, 갱신 뒤 n
|
||||
|
||||
## 읽기 전에 — 어디서 치는가
|
||||
|
||||
기계가 둘이다. 밖에서 보는 `openssl` 은 `[dev]` 에서 치고, 주입은 전부 `[test-server]` 쪽이라 사람이 비밀번호를 친다.
|
||||
기계가 둘이다. 밖에서 보는 `openssl` 은 `[dev]` 에서 치고, 주입은 전부 엣지 쪽이라 사람이 비밀번호를 친다.
|
||||
|
||||
**주입하는 기계는 `test-server` 가 아니라 `kc-lab-edge` 다.** 기반 가이드 03 이 엣지 nginx 를 호스트에서 게스트로 옮겼고 04 가 거기 certbot 을 깔았다. 2026-09-17 에 양쪽에서 확인했다(observed).
|
||||
|
||||
```text
|
||||
[test-server] ps -ef | grep '[n]ginx' → nginx 프로세스가 없다
|
||||
[kc-lab-edge] root 1065 nginx: master process /usr/sbin/nginx …
|
||||
www-data 1106 nginx: worker process
|
||||
```
|
||||
|
||||
그래서 아래 `[test-server]` 라벨이 붙은 블록은 전부 `ssh kc-lab-edge` 로 간다. 그 게스트의 `sudo` 는 무암호라 사람이 비밀번호를 칠 일도 없다.
|
||||
|
||||
| 무엇 | 값 |
|
||||
|---|---|
|
||||
@@ -154,7 +164,15 @@ echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonw
|
||||
ssh -t test-server 'sudo ls -la /etc/letsencrypt/renewal-hooks/deploy/'
|
||||
```
|
||||
|
||||
**어디를 보나** — 실측은 이렇다(observed, `d4-certificate-renewal/12-certbot-state.txt`).
|
||||
**어디를 보나** — 2026-09-17 에 엣지에서 친 것이다(observed). 디렉터리는 있고 안은 비었다.
|
||||
|
||||
```text
|
||||
total 8
|
||||
drwxr-xr-x 2 root root 4096 Sep 17 04:33 .
|
||||
drwxr-xr-x 5 root root 4096 Sep 17 04:33 ..
|
||||
```
|
||||
|
||||
원래 실측은 이렇다(observed, `d4-certificate-renewal/12-certbot-state.txt`).
|
||||
|
||||
```text
|
||||
/etc/letsencrypt/renewal-hooks/deploy/:
|
||||
@@ -173,11 +191,22 @@ drwxr-xr-x 5 root root 4096 2026-09-03 10:46:54.658520760 +0900 ..
|
||||
|
||||
```bash label="[dev] ① 세 번 잰다"
|
||||
for i in 1 2 3; do
|
||||
A=$(date -u +%s.%N); B=$(ssh test-server 'date -u +%s.%N'); C=$(date -u +%s.%N)
|
||||
A=$(date -u +%s.%N); B=$(ssh kc-lab-edge 'date -u +%s.%N'); C=$(date -u +%s.%N)
|
||||
echo "A=$A B=$B C=$C"
|
||||
done
|
||||
```
|
||||
|
||||
**재는 대상이 엣지 게스트다.** 이 절차의 판정은 엣지 nginx 가 언제 reload 됐나이므로, 어긋난 시계를 재야 할 짝은 「보는 기계 ↔ 엣지」다.
|
||||
|
||||
2026-09-17 에 lab host 와 엣지를 나란히 찍어 보니 **엣지가 94초 느렸다**(observed).
|
||||
|
||||
```text
|
||||
lab host 2026-09-17T06:51:28.799Z
|
||||
kc-lab-edge 2026-09-17T06:49:54.782Z
|
||||
```
|
||||
|
||||
문서가 적은 `106초` 는 그때 그 짝의 값이고, **수를 옮겨 쓰지 말고 지금 자기 실험대에서 다시 잰다.** 게스트는 호스트와 따로 시계를 맞추므로 새로 만들 때마다 값이 다르다.
|
||||
|
||||
세 줄 각각에서 `B` 와 `(A+C)/2` 의 차이를 눈으로 뺀다. 그리고 세 번의 값이 서로 비슷한가를 본다 — 흔들리면 네트워크 지연이 섞였고, 안정적이면 진짜 왜곡이다.
|
||||
|
||||
**2.** 어느 쪽이 맞는지는 외부 기준으로 가른다.
|
||||
@@ -617,6 +646,14 @@ echo | openssl s_client -connect auth.hyeonworks.com:443 -servername auth.hyeonw
|
||||
|
||||
이 편이 남기는 한 문장은 「처방을 적었으면 시험한다」이다. D-4 는 원인을 정확히 셋으로 특정하고 고치는 법까지 적었고, 그 처방이 듣는지 확인하는 데 든 비용은 파일 하나와 명령 두 줄이었다. 확인하지 않은 채로 문서에 남았다면 「고치는 법」 항목은 다음 갱신일까지 아무도 시험하지 않은 문장으로 남았을 텐데, 그날이 바로 시험할 수 없는 날이다.
|
||||
|
||||
## 이 실험대에서 아직 못 밟은 단계
|
||||
|
||||
2026-09-17 에 기반 가이드로 실험대를 새로 세우고 이 편을 어디까지 밟고 멈췄는지 적는다. **못 밟은 것을 밟은 것처럼 적지 않으려고 남긴다.**
|
||||
|
||||
- **남은 것** — 훅을 설치하고 강제 갱신으로 워커 PID 가 바뀌는지 보는 구간 전부. 인증서가 있어야 갱신할 것이 생긴다. 지금까지 밟은 것 — 훅 디렉터리가 빈 것, 두 기계 시계 차 94초.
|
||||
- **막는 것** — `https://auth.hyeonworks.com` 이 서지 않는다. 와일드카드 인증서(Cloudflare API 토큰이 필요한 DNS-01)와, 밖에서 실험대에 닿는 길(호스트의 libvirt `guest_input` 구멍 — A-4 에서 확인한 `ExecStartPost` 누락)이 둘 다 있어야 한다.
|
||||
- **그때까지 이 편의 실측 가운데 `(observed)` 로 적힌 2026-09-17 값은 위 「지금까지 밟은 것」 범위뿐이다.** 나머지는 원래 실행의 값이다.
|
||||
|
||||
## 무엇이 관측이고 무엇이 아닌가
|
||||
|
||||
- (observed) 주입 전 워커 두 줄(`585` 와 `28829`, `lstart Fri Sep 4 18:00:35`), 훅 디렉터리의 `total 8`, 시계 측정 네 줄과 `+106.1` 세 번, certbot 출력 전문(`ran with error output` · `types_hash` 경고 두 줄 · `test is successful` · `signal process started` · `Congratulations, all renewals succeeded` · `fullchain.pem (success)`), 주입 뒤 워커 두 줄(`585` · `37252` · `etimes 74` · `lstart Fri Sep 4 21:29:36 2026`), 새 인증서의 `serial=06F3E0EF4D1BB03DE58130EAAD1176101373` · `notBefore=Sep 4 11:29:18 2026 GMT` · `notAfter=Dec 3 11:29:17 2026 GMT` 와 SAN 세 이름, SCT 두 줄(`Sep 4 12:27:49.054` · `Sep 4 12:27:49.048`), `notBefore` ↔ SCT 표의 `약 89.8초` · `약 88.9초`.
|
||||
|
||||
Reference in New Issue
Block a user