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:
DongHyeonka
2026-09-17 15:59:42 +09:00
co-authored by Claude Opus 5
parent ab59130196
commit 024362d096
197 changed files with 8336 additions and 960 deletions
@@ -61,11 +61,18 @@ aside[class*="studio-document-status"] ← 이것이 안 보이면 인증 실
### 1. 기록을 읽고 무엇을 넣을지 정한다
`kind` 로 칸 목록이 정해진다. 어떤 `##` 제목이 Studio 의 어느 칸인지는
[references/studio-form-map.md](references/studio-form-map.md).
[references/studio-form-map.md](references/studio-form-map.md), 그 칸이 서버에서 어떤 이름과
자료형인지는 [references/studio-api.md](references/studio-api.md).
**frontmatter 는 메타데이터고, 본문의 `##` 가 Studio 의 칸이며, 제목 아래 첫 문단이 `요약`
이다.** 계약에 없는 `##` 는 화면에 자리가 없어 통째로 사라진다.
**본문은 이제 화면으로 넣지 않는다.** 편집 화면의 본문이 블록 편집기로 바뀌어 블록 하나가
`<textarea>` 하나다 — 기록 한 편이 블록 171개인 것을 실제로 봤다. 그런데 저장이 서버로 보내는
것은 여전히 `bodyMarkdown` 이라는 마크다운 문자열 하나다. 그래서 **옮기는 일은 화면이 아니라
`PUT /api/v1/studio/documents/{id}` 로 한다** — 로그인된 그 페이지 안에서 `fetch` 로 부르므로
세션과 CSRF 가 그대로 실린다. 화면은 읽고 대조하는 데 쓴다.
### 2. 그림이 있으면 Asset 을 먼저 올린다
frontmatter `assets:``file:` 이 올릴 파일이고 `key:` 는 저장소 쪽 이름이다. 올리면 서버가
@@ -101,6 +108,12 @@ python3 scripts/studio-body.py <기록.md> --key <저장소 key>=<서버가 준
### 4. 저장한다
API 로 넣었으면 `PUT` 이 200 을 내는 것이 저장이다. **`expectedVersion` 은 저장 직전에 `GET`
으로 읽은 `document.version` 이다** — 지어내지 않는다. 보내지 않은 칸은 비워지므로 그 종류의
칸을 전부 담는다.
화면으로 고쳤으면 버튼을 누른다.
```
aside[class*="studio-document-status"] 안의 `저장` 버튼
→ 같은 aside 의 글자가 `저장됨` 으로 바뀔 때까지 기다린다 (최대 30초)
@@ -141,12 +154,12 @@ python3 scripts/verify-tech-log-tree.py <프로젝트>
| 종류 | 경로 | 본문 |
|---|---|---|
| Case | `DELETE /v1/studio/cases/{id}` (`:72`) | `{"expectedVersion": <저장 버전>}` |
| Reference | `DELETE /v1/studio/references/{id}` (`:81`) | `{"expectedVersion": <저장 버전>}` |
| Concept | `DELETE /v1/studio/concepts/{id}` (`:95`) | `{"expectedVersion": <저장 버전>}` |
| Case | `DELETE /api/v1/studio/cases/{id}` (`:72`) | `{"expectedVersion": <저장 버전>}` |
| Reference | `DELETE /api/v1/studio/references/{id}` (`:81`) | `{"expectedVersion": <저장 버전>}` |
| Concept | `DELETE /api/v1/studio/concepts/{id}` (`:95`) | `{"expectedVersion": <저장 버전>}` |
| Setup | `DELETE /api/v1/studio/setups/{id}` | `{"expectedVersion": <저장 버전>}` |
| Question | `DELETE /v1/studio/questions/{id}` (`:114`) | `{"expectedVersion": <저장 버전>}` |
| Decision | `DELETE /v1/studio/projects/{id}/decisions/{decisionId}` (`:123`) | `{"expectedVersion": <저장 버전>}` |
| Question | `DELETE /api/v1/studio/questions/{id}` (`:114`) | `{"expectedVersion": <저장 버전>}` |
| Decision | `DELETE /api/v1/studio/projects/{id}/decisions/{decisionId}` (`:123`) | `{"expectedVersion": <저장 버전>}` |
**본문 없이 부르면 204 가 아니라 422 다.** `ExpectedVersionRequest` 가 없으면
`REQUEST_VALIDATION_FAILED` / `Request body is malformed` 로 거절된다. 헤더에는
@@ -114,31 +114,49 @@ while (cur < want) { // 모자라면 더한다
const labels = await fs.locator('label span').allInnerTexts();
```
## 칸 DOM 이 두 가지다
## 칸은 `aria-label` 로 찾는다 (2026-09-17 에 다시 잼)
위 xpath 는 절반만 통한다. 실제 편집 화면에는 모양이 둘이다.
```html
<label class="studio-field"><span>문제</span><textarea></textarea></label> <!-- A -->
<div class="studio-field"><label for="studio-field-summary">요약</label>
<textarea id="studio-field-summary"></textarea></div> <!-- B -->
```
`요약` · `본문 Markdown` · `관계 N 이유` 가 B형이다. A형만 찾으면 이 칸들이 조용히 안 채워진다.
**둘 다 본다.**
**위의 `label` 두 모양 이야기는 낡았다.** 지금 `label.studio-field` 가 감싸는 것은 제목과 요약
둘뿐이고, 나머지 칸은 전부 `aria-label` 을 갖는다. xpath 로 `span` 을 뒤질 일이 없다.
```js
async function field(page, name) {
const a = page.locator(`xpath=//label[./span[normalize-space(.)="${name}"]]`)
.locator('textarea, input').first();
if (await a.count()) return a;
const id = await page.locator(`xpath=//label[normalize-space(.)="${name}"]`)
.first().getAttribute('for');
return id ? page.locator('#' + id) : null;
}
document.querySelector('input[aria-label="제목"]')
document.querySelector('textarea[aria-label="요약"]')
document.querySelector('input[aria-label="slug"]')
document.querySelector('input[aria-label="판단 기준 1 제목"]')
document.querySelector('textarea[aria-label="적용할 때 3"]')
```
**본문 칸의 이름은 `본문` 이 아니라 `본문 Markdown` 이다.**
되풀이 칸을 늘리는 버튼은 `+ 판단 기준` · `+ 적용할 때` · `+ 예외와 주의` · `+ 예시` ·
`+ 버전 추가` 다. 줄 수를 먼저 맞추고 값을 넣는 규칙은 그대로다.
## 본문 칸이 사라졌다 — 블록 편집기다
**`본문 Markdown` 이라는 textarea 는 더 없다.** 본문은 `.studio-block-editor` 이고 블록 하나가
`<textarea>` 하나다. 기록 한 편이 블록 171개인 것을 실제로 봤다.
```
.studio-block-editor
├ textarea.studio-markdown-source 본문 전체의 거울. display:none · aria-hidden · readonly
├ .studio-block-editor__blocks
│ └ .studio-authoring-block[data-kind] heading · paragraph · code · bullet · raw
└ button + 블록 추가
```
**코드 블록의 `input.studio-code-language` 에 울타리 뒤 정보 문자열 전체가 들어간다**
`bash label="[kc-lab-1] ① 기동 로그에서 쿠키 설정을 찾는다"` 가 그 입력의 값이었다. 라벨을
넣을 별도 칸은 없다.
합성한 `ClipboardEvent` 로 붙여넣는 것은 **안 먹는다.** 그래서 쓰인 기록을 화면으로 옮기지
않는다 — [studio-api.md](studio-api.md) 의 `PUT` 으로 `bodyMarkdown` 을 통째로 보낸다.
`.studio-markdown-source``readonly` 라 입력 자리가 아니지만, 화면이 지금 무엇을 직렬화할지를
그대로 보여 주므로 **저장 전에 눈으로 대조하기에 좋다.**
## 저장 다음은 게시가 아니라 검증이다
편집 화면의 `aside` 에는 `저장``게시` 둘뿐인데, 저장만 된 기록은 서버가 `nextAction`
**`VALIDATE`** 로 준다. 검증 화면은 `/studio/documents/{id}/validation` 이다. 이 스킬은 저장까지라
거기까지 가지 않는다 — 다만 「저장했는데 왜 게시가 안 되나」의 답이 이것이다.
## Asset 을 올릴 때
@@ -0,0 +1,175 @@
# Studio 가 실제로 주고받는 것
여기 적은 것은 **2026-09-17 에 로그인한 브라우저에서 직접 불러 확인한 것**이다. 문서를 보고
적은 것이 아니라 요청을 보내고 돌아온 것을 옮겼다. 확인한 판이 바뀌면 이 파일도 낡는다.
## 왜 이 파일이 필요해졌나
**편집 화면의 본문이 블록 편집기로 바뀌었다.** 전에는 본문이 `본문 Markdown` 이라는 textarea
하나였고 거기에 통째로 붙여넣으면 됐다. 지금은 블록 하나가 `<textarea>` 하나다 — 기록 한 편이
블록 171개인 것을 실제로 봤다. 화면으로 그것을 채우는 것은 사람이 한 편을 쓸 때의 방식이지
쓰인 기록을 옮기는 방식이 아니다.
그런데 **저장이 서버로 보내는 것은 블록이 아니라 마크다운 문자열 하나**다. 화면은 그 문자열
위의 편집기이고, 저장 요청은 `bodyMarkdown` 한 칸을 보낸다. 그래서 옮기는 일은 화면이 아니라
이 API 로 한다.
## 인증
**자격증명을 이 저장소에 두지 않는다.** 이미 로그인된 브라우저 세션을 쓰고, 요청은 그 페이지
안에서(`fetch`) 보낸다. 쿠키가 그대로 실린다.
CSRF 토큰은 `XSRF-TOKEN` 쿠키에 있고 `x-csrf-token` 헤더로 되돌려 준다.
```js
document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/)?.[1]
```
로그인이 안 돼 있으면 `/studio` 가 「기록을 쓰려면 로그인이 필요합니다」를 낸다.
**로그인 화면을 자동으로 통과하려 들지 않는다** — 사람에게 로그인해 달라고 말하고 멈춘다.
## 응답 껍데기
성공이든 실패든 같은 모양이다.
```json
{ "success": true, "data": { }, "error": null,
"meta": { "requestId": "…", "traceId": "…", "correlationId": "…", "page": null } }
```
실패하면 `data``null` 이고 `error``{code, category, message, retryable, details}` 가 온다.
`details.supportedMethods` 처럼 고칠 방법을 담아 주는 경우가 있다.
## 경로
| 무엇 | 요청 |
|---|---|
| 목록 | `GET /api/v1/studio/documents?limit=100` |
| 한 편 | `GET /api/v1/studio/documents/{id}` |
| 새로 만들기 | `POST /api/v1/studio/documents` → 201 |
| **저장** | `PUT /api/v1/studio/documents/{id}` → 200 |
| 지우기 | `DELETE /api/v1/studio/{종류}/{id}` → 204 |
**`limit` 은 100 이 상한이다.** 200 을 주면 422 `REQUEST_VALIDATION_FAILED` 다. 더 받으려면
`data.nextCursor``cursor` 로 넘긴다.
**`/documents/{id}``PUT``GET` 만 받는다.** 거기에 `DELETE` 를 보내면 405 이고
`details.supportedMethods``["PUT","GET"]` 을 준다. 삭제는 종류별 경로다 —
`cases` · `references` · `concepts` · `setups` · `questions`, 그리고 Decision 은
`projects/{projectId}/decisions/{decisionId}`.
## 한 편 읽기
```
GET /api/v1/studio/documents/{id}
→ data: { document, currentValidation, latestPreview, currentPublication,
dependencyRevision, nextAction }
```
`nextAction` 이 다음에 할 일을 말한다 — 저장만 된 기록은 `VALIDATE` 다. 검증 화면은
`/studio/documents/{id}/validation` 이고 편집 화면의 `aside` 에는 **`저장``게시` 둘뿐**이라
검증은 그 화면에서 따로 한다.
## 저장
```
PUT /api/v1/studio/documents/{id}
headers: content-type: application/json
accept: application/json
x-csrf-token: <XSRF-TOKEN 쿠키>
idempotency-key: <요청마다 새로>
body: { "expectedVersion": <읽어 온 document.version>, "document": { … } }
```
**`expectedVersion` 은 방금 읽은 `document.version` 이다.** 저장이 끝나면 1 올라간다. 지어내지
말고 저장 직전에 `GET` 으로 읽는다.
`document`**그 종류의 칸을 전부** 담는다. 빠뜨린 칸은 비는 것으로 저장된다 — 부분 갱신이
아니다.
## 종류마다의 칸
여섯이 함께 갖는 것: `kind` · `title` · `slug` · `summary` · `relations` · `projectId` ·
`topicId` · `variantIds`. 서버가 주지만 보내지 않는 것: `id` · `version` · `updatedAt`.
| kind | 그 종류만의 칸 |
|---|---|
| `CASE` | `problem` · `conclusion` · `environment` · `reproduction` · `bodyMarkdown` · `lastVerifiedOn` |
| `CONCEPT` | `bodyMarkdown` · `basisVersion` |
| `SETUP` | `bodyMarkdown` · `pinnedVersions` |
| `REFERENCE` | `purpose` · `rules` · `applyWhen` · `exceptions` · `examples` · `verifiedOn` |
| `QUESTION` | `facts` · `assumptions` · `unknowns` · `constraints` · `options` · `nextValidation` · `resolution` · `questionStatus` |
되풀이되는 칸의 모양이다. `id` 는 서버가 붙이므로 새로 넣을 때는 빼고 `order` 만 0 부터 센다.
| 칸 | 항목 |
|---|---|
| `relations` | `{ targetId, reason, order }` |
| `facts` · `assumptions` · `unknowns` · `constraints` · `applyWhen` · `exceptions` · `examples` | `{ text, order }` |
| `options` | `{ title, description, order }` |
| `rules` | `{ title, body, order }`**제목과 본문이 따로다** |
| `pinnedVersions` | `{ name, version }``order` 가 없다 |
## `bodyMarkdown` 은 바이트 그대로 돌아온다
넣은 것과 읽어 온 것이 같은지 확인했다. **코드 울타리의 정보 문자열이 그대로 살아남는다.**
```
```bash label="[host] ① 친다"
```
넣고 다시 읽었을 때 `label="…"` 까지 한 글자도 안 바뀌었다. 그래서 `studio-body.py` 가 만든
본문을 그대로 실어 보내면 된다.
## 화면에서 블록이 어떻게 생겼나
옮기는 일은 API 로 하지만, 사람이 화면을 읽을 때와 무엇이 어긋났는지 볼 때 필요하다.
```
.studio-block-editor
├ textarea.studio-markdown-source 본문 전체의 거울. display:none · aria-hidden · readonly
├ .studio-block-editor__blocks
│ └ .studio-authoring-block[data-kind] 블록 하나
└ button + 블록 추가
```
`data-kind``heading` · `paragraph` · `code` · `bullet` · `raw` 다. `raw`(화면 이름
**고급 블록**)가 표처럼 블록으로 안 갈리는 것을 그대로 담는다.
**코드 블록의 `input.studio-code-language` 는 언어 이름만 담지 않는다.** 울타리 뒤의 정보
문자열 **전체**가 거기 들어간다 — `bash label="[kc-lab-1] ① 기동 로그에서 쿠키 설정을 찾는다"`
가 그 입력의 값이었다. 라벨을 넣을 별도 칸은 없다.
**`.studio-markdown-source` 는 읽는 데만 쓴다.** `readonly` 이고 `display:none` 이라 입력
자리가 아니다. 다만 화면이 지금 무엇을 직렬화할지를 그대로 보여 주므로, 저장 전에 눈으로
대조하기에 좋다.
합성한 `ClipboardEvent` 로 붙여넣는 것은 **안 먹는다.** 화면으로 본문을 채우려면 블록을 하나씩
만들어야 한다.
## 화면의 칸 셀렉터
칸은 이제 `aria-label` 로 찾는다. `label.studio-field` 는 제목과 요약 둘만 감싼다.
```js
document.querySelector('input[aria-label="제목"]')
document.querySelector('textarea[aria-label="요약"]')
document.querySelector('input[aria-label="slug"]')
document.querySelector('input[aria-label="판단 기준 1 제목"]')
document.querySelector('textarea[aria-label="적용할 때 3"]')
```
되풀이 칸을 늘리는 버튼은 `+ 판단 기준` · `+ 적용할 때` · `+ 예외와 주의` · `+ 예시` ·
`+ 버전 추가` 다.
상태 레일은 그대로다 — `aside[class*="studio-document-status"]`(실제 class 는
`studio-document-status-bar`). 그 안에 **`저장``게시` 둘뿐**이고, 저장된 상태에서는
`저장``disabled` 이며 글자가 `저장됨` 이다.
## 하지 않는 것
- **게시하지 않는다.** 이 스킬은 저장까지다. 한 번이라도 게시한 문서는 게시를 취소해도 삭제가
409 로 거절된다
- **`expectedVersion` 을 지어내지 않는다.** 저장 직전에 읽는다
- **부분 갱신이라고 생각하지 않는다.** 보내지 않은 칸은 비워진다
- **로그인 화면을 자동으로 넘기지 않는다**
@@ -100,8 +100,20 @@ Decision 의 도메인 `ACCEPTED` 가 화면에서는 `ADOPTED` 로 보인다.
`Reference`·`Question`·`Decision` 의 칸은 **평문으로 렌더링된다.**
- 백틱과 파이프가 글자 그대로 보인다. 코드·표를 넣지 않는다
- 줄바꿈은 `<br>` 로만 살아난다
**「평문」이 「전부 글자 그대로」라는 뜻은 아니다.** 렌더러
(`tech-log-frontend``presentation/shared/public-render/prose-text.tsx`)가 셋을 해석한다.
| 무엇 | 평문 칸에서 |
|---|---|
| 백틱 쌍 | **인라인 `<code>` 로 산다.** 빼지 않는다 — 빼면 식별자가 민무늬로 나온다 |
| 빈 줄 | 문단이 갈린다 |
| 한 줄 바꿈 | `<br>` |
| 별표 · 파이프 · `#` · 코드펜스 · 인용 표지 `>` | **글자 그대로 나온다.** 넣지 않는다 |
**전에 이 자리에 「백틱과 파이프가 글자 그대로 보인다」고 적혀 있었고 백틱 쪽은 틀렸다.**
백틱이 글자로 나오던 것은 고쳐진 옛 버그이고 그 파일 주석에 그렇게 적혀 있다. 칸이 어떻게
보이는지는 **렌더러가 정본이다** — 이 문서가 아니다.
- 나열은 쉼표로 잇지 말고 `이름 : 값` 으로 줄을 나눈다
코드·표·그림이 필요하면 짝이 되는 Case·Concept·Setup 에 담고 `관계` 로 가리킨다.
@@ -367,6 +367,163 @@ Not `${NAME}` — guides use `$TOKEN`, `$SID` and friends as real shell
variables, and a reader who reads a placeholder as one will paste it unchanged.
`{{ }}` is not shell syntax, so pasting it fails where the reader can see it.
## The reader cannot see what is not on screen
Everything below came out of reading 35 finished guides in one pass. Each rule
names the accident that produced it. None of them is a style preference — in
every case the guide passed every other rule in this file and still handed the
reader a wrong answer that looked right.
### A value copied into a container is a snapshot, not a reference
`kubectl run --env="K0=$K0"`, `docker run -e`, cloud-init user-data: each bakes
the string as it was at that moment. From then on two spellings mean different
things, and they differ by two characters:
```bash
exec pod -- sh -c '...$K0...' # the value baked into the pod
exec pod -- sh -c '...'"$K0"'...' # the value in the shell typing this
```
**They agree until something restarts.** In one guide a `rollout restart` changed
the pod IP, and a later step still read the baked copy; the request went to an
address that no longer existed, and the failure it produced was a `500` — which
was also that step's expected result. The screen looked correct.
So: when a step replaces the thing a baked value points at — a pod IP, a node
address, a lease — say in that step which spelling the next commands use.
Prefer recreating the container. If state inside it forbids that, show the
live-read form and add one line saying why it differs from the block above.
**Distance is not the test.** A variable defined 700 lines up can be safe, and
one defined nine lines up can be stale. Ask what happened in between.
### A step that only works inside a window says when, not just what
The four-part shape above — what / command / where to look / what it means —
has no *when*. Several steps read correctly only during a restart, only while a
rule is installed, only within sixty seconds. Read late, they do not error;
they print a different, plausible number.
Name the command that opens the window and the one that closes it, and say
**what the reader sees if they type it after it closed** — that is what most
readers will actually get.
### A check must be able to fail
For every line that says "이렇게 나오면 통과", state what would print the same
thing while the condition is false. Three real cases:
- `virsh list --all` used to prove a group membership took effect — it connects
to the per-user URI, which works with no group at all
- a `postmaster.pid` path checked on a fixed node, when the volume may be on the
other one — "no such file" passes either way
- `source ~/.bashrc` on a host whose login shell is zsh — true in that shell,
gone at next login, and the symptom surfaces one guide later
If the check passes when the step was skipped — because it reads a different
scope, host or shell than the step wrote to — it is not a check. Put the scope
in the command, or write one line naming the failure this check cannot see.
### Never ship a command the guide knows is wrong
Three guides printed a command and told the reader, in prose underneath, to
edit it before typing. Prose is not a guard. The worst of them was a **valid**
assignment:
```bash
OLDID=980ee9b7-... # ← copy from the output above
```
Paste it and `OLDID` holds the literal string, and the `delete` two lines later
runs. Every other hardcoded value in those guides failed loudly — `(0 rows)`,
`(nil)`. This one succeeded at the wrong thing.
If prose under a block says "replace X first", the block already says it — or
says `{{X}}`, which is not shell syntax and fails where the reader can see it.
### One block, one machine
A code block is the reader's copy unit. When the machine changes, the block
ends, even when the commands form one logical step. Splitting the explanation
in prose does not help: they copy the block.
And a machine label tells the reader *where* a block runs — it does not put
them there. When consecutive blocks carry different labels, the transition is
its own step with its own command (`ssh host`, `exit`, "open a second
terminal"). Count them per guide: entries and exits must balance. A
verification section that runs somewhere else needs both.
### A shell function defined mid-guide is worse than all three tiers
It leaves no file, so the reader cannot re-read it. It dies with the shell, so
a new window silently breaks every later step. `unset` does not reach it.
One guide defined `R()` on line 188 and used it through line 617 — a one-letter
name for `kubectl -n … exec deploy/redis -- redis-cli`. At the point of use
the reader cannot see what command they are running.
If something is repeated often enough to want a name, make it a script file
with that name. If it must be a function, define it in the same block as its
first use, name it for what it does (`redis`, not `R`), and restate the
definition at the top of any later section more than a screen away.
### Names the reader must already have
A host alias, a directory, a volume, a file that no earlier step created is not
a placeholder — it is an assumption, and the placeholder rule above does not
catch it. Four guides in one set ran entirely on `ssh kc-lab-1` with no stanza
anywhere that creates it; the second guide hits it on its first command.
Show the command that creates it, or name the guide that owns it, in the prose
above the first block that uses it.
### Walk the guide twice
`echo >>` is one case of a larger rule. Anything created under a fixed name — a
pod, a volume, a DHCP reservation, a namespace — says what the second run
prints and how to clear it first. `--rm` only removes on clean exit; Ctrl+C
leaves the pod and the next run dies on `AlreadyExists`.
And anything that destroys and recreates a host says what identity changes with
it — SSH host keys, MAC-bound leases, certificates — and what that breaks the
next time the reader connects.
### The output block belongs to the command above it
If it came from a wider `grep`, an extra `uniq -c`, or a `sed` that rewrote
names, show that command too or say so on the line before. Readers compare
their screen to yours character by character; a silent edit makes them hunt for
a fault that is not there.
### Context selectors are read as a set
`-n`, `--context`, `-h`, `-U`. One guide's `kubectl exec` was missing
`-n keycloak-lab` while the eight commands around it had it. On its own the
line looks fine; next to its neighbours it is obviously running somewhere else.
Read them as a column, not line by line.
### When the same action has several forms, one of them is the step
One guide gave three ways to kill a backend and marked none of them. Show the
step; put the rest under a heading that says they are alternatives. Leaving the
reader to choose asks them to weigh a trade-off the guide has not explained yet.
### A fixed output path makes a block single-use
"Change the variable and run it again" is incomplete when the block writes to a
fixed file: the second run overwrites the first run's baseline, silently, and
the comparison two sections later has nothing to compare against. Name the
output path among the things to change.
### Run the remedy you prescribe
A guide diagnosed lexical sorting and prescribed `sort -g`. Every line in that
file began with the same `200`, so `sort -g` compared equal and fell through to
byte order — its output was identical to plain `sort`. The fix the guide
offered did not fix anything, and the sentence "miss this and you misread the
maximum" stayed true after following it.
## Rationalization table
| Excuse | Reality |
@@ -383,6 +540,11 @@ variables, and a reader who reads a placeholder as one will paste it unchanged.
| "The file ends up the same either way" | Only on the first run. `echo >>` appends again every time someone repeats the guide |
| "So I should use nano for everything" | No. The trigger is authoring a file the reader must understand. `grep`, `virsh`, a long `virt-install` stay as they are |
| "`python3 -c` only checks syntax" | Then it misses the schema. If the format has a validator — `cloud-init schema`, `nginx -t`, `--dry-run` — that is the step |
| "The prose right under it says to change that value" | The reader copies the block. Prose is not a guard — put `{{NAME}}` in the block |
| "The variable is defined earlier in the same shell" | Ask what restarted in between. A pod IP baked at line 877 was stale by line 764 |
| "The label says which machine it runs on" | A label says where, not how to get there. The transition is its own step |
| "It's the same command, just shorter" | A one-letter function hides the command at the exact moment the reader needs to read it |
| "The check passed" | Ask what it would print if the step had been skipped. Three checks in one set passed either way |
## Red flags — stop and rewrite
@@ -398,6 +560,14 @@ variables, and a reader who reads a placeholder as one will paste it unchanged.
- one line stacking connect + redirect + file creation (`ssh host 'cat > f' < f`)
- `python3 -c` checking syntax when the format has its own validator
- a step that appends, so walking the guide twice appends the line twice
- prose under a block telling the reader to edit the command before typing it
- a block whose machine label differs from the one above, with no command between
- a one- or two-letter shell function, or any function defined far from its use
- a host alias, directory or volume that no step in any guide creates
- `--rm` with no line saying what a Ctrl+C leaves behind
- a "통과" line that would print the same thing if the step had been skipped
- a step that reads correctly only during a window, with no word about the window
- a remedy you have not run against the data that made you prescribe it
## References
+124
View File
@@ -0,0 +1,124 @@
// 저장 계획을 브라우저 문맥 안에서 보낸다.
//
// **왜 브라우저 안인가.** 세션 쿠키가 httpOnly 라 밖으로 못 읽고, 읽어서 옮기는 것은
// 「비밀은 길이·존재 여부만」에 걸린다. `page.request` 는 그 브라우저 문맥의 쿠키를
// 그대로 쓰므로 값이 이 프로세스 밖으로 나가지 않는다.
//
// **왜 계획을 그대로 안 보내나.** `studio-save.py` 의 계획은 `projectId`·`topicId` 를
// `null` 로, `relations` 를 빈 배열로 낸다. 서버 저장은 부분 갱신이 아니라서 그대로
// 보내면 **그 세 칸이 지워진다.** 그래서 저장 직전에 GET 으로 읽어 그 셋과
// `variantIds` 는 서버 값을 쓰고, 기록이 갖는 칸만 계획 값으로 덮는다.
//
// **게시하지 않는다.** publish/unpublish 경로가 섞이면 그 편을 건너뛴다.
async (page) => {
// 이 문맥은 샌드박스라 require 도 동적 import 도 안 된다. 그래서 계획은
// 127.0.0.1 의 정적 서버에서 받아 온다 — page.request 는 CORS 를 안 탄다.
const SUB = 'plans';
const LOCAL = 'http://127.0.0.1:8731/' + SUB + '/';
const getLocal = async (n) => {
const r = await page.request.fetch(LOCAL + n);
if (!r.ok()) throw new Error('계획을 못 받았다: ' + n + ' ' + r.status());
return await r.json();
};
const files = await getLocal('_index.json');
// 기록이 소유하는 칸. 이것만 계획 값으로 덮는다.
const OWNED = new Set([
'kind', 'title', 'slug', 'summary',
'problem', 'conclusion', 'environment', 'reproduction', 'bodyMarkdown', 'lastVerifiedOn',
'basisVersion', 'pinnedVersions',
'purpose', 'rules', 'applyWhen', 'exceptions', 'examples', 'verifiedOn',
'facts', 'assumptions', 'unknowns', 'constraints', 'options', 'nextValidation',
'resolution', 'questionStatus',
]);
// 서버가 정본인 칸. 계획이 뭐라 하든 서버 값을 쓴다.
const SERVER_OWNED = ['projectId', 'topicId', 'relations', 'variantIds'];
const csrf = await page.evaluate(
() => document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/)?.[1] || null);
if (!csrf) return { fatal: 'XSRF-TOKEN 쿠키가 없다 — 로그인 상태가 아니다' };
const api = async (method, url, body, headers) => {
const r = await page.request.fetch('https://hyeonworks.com' + url, {
method,
headers: Object.assign({ accept: 'application/json' },
body ? { 'content-type': 'application/json' } : {},
headers || {}),
data: body ? JSON.stringify(body) : undefined,
});
let j = null; try { j = await r.json(); } catch (e) { /* 204 는 본문이 없다 */ }
return { status: r.status(), body: j };
};
const results = [];
for (const f of files) {
const plan = await getLocal(f);
const name = f.replace(/\.json$/, '');
const put = plan.requests.find(r => r.method === 'PUT');
if (!put) { results.push({ name, skipped: 'PUT 요청이 없다' }); continue; }
if (plan.requests.some(r => /\/(publish|unpublish)(\/|$)/.test(r.path))) {
results.push({ name, skipped: '게시 경로가 섞여 있다' }); continue;
}
const id = put.path.split('/').pop();
// ① 지금 상태를 읽는다
const cur = await api('GET', put.path);
if (cur.status !== 200) { results.push({ name, id, step: 'GET', status: cur.status }); continue; }
const server = cur.body.data.document;
const pub = cur.body.data.currentPublication;
if (pub && pub.status === 'PUBLISHED') {
results.push({ name, id, skipped: '게시된 문서다' }); continue;
}
// ② 합친다 — 서버 값을 살리고 기록이 갖는 칸만 덮는다
const planned = put.body.document;
const doc = {};
for (const [k, v] of Object.entries(planned)) if (OWNED.has(k)) doc[k] = v;
for (const k of SERVER_OWNED) doc[k] = server[k];
// ③ 보낸다
const res = await api('PUT', put.path,
{ expectedVersion: server.version, document: doc },
{ 'x-csrf-token': csrf,
'idempotency-key': (put.headers && put.headers['Idempotency-Key']) || ('send-' + id + '-' + Date.now()) });
if (res.status !== 200) {
results.push({ name, id, step: 'PUT', status: res.status,
code: res.body && res.body.error && res.body.error.code });
continue;
}
// ④ 되읽어 대조한다
const back = await api('GET', put.path);
const b = back.body.data.document;
results.push({
name, id, kind: server.kind,
status: 200,
versionBefore: server.version, versionAfter: b.version,
bodyMatches: (b.bodyMarkdown || '') === (doc.bodyMarkdown || ''),
titleMatches: b.title === doc.title,
summaryMatches: b.summary === doc.summary,
keptProject: b.projectId === server.projectId,
keptTopic: b.topicId === server.topicId,
keptRelations: (b.relations || []).length === (server.relations || []).length,
stillUnpublished: !back.body.data.currentPublication,
});
}
const ok = results.filter(r => r.status === 200);
return {
total: files.length,
saved: ok.length,
skipped: results.filter(r => r.skipped).length,
failed: results.filter(r => r.status && r.status !== 200).length,
bodyMismatch: ok.filter(r => !r.bodyMatches).length,
lostProject: ok.filter(r => !r.keptProject).length,
lostTopic: ok.filter(r => !r.keptTopic).length,
lostRelations: ok.filter(r => !r.keptRelations).length,
becamePublished: ok.filter(r => !r.stillUnpublished).length,
problems: results.filter(r => r.skipped || (r.status && r.status !== 200)
|| (r.status === 200 && (!r.bodyMatches || !r.keptProject
|| !r.keptTopic || !r.keptRelations || !r.stillUnpublished))),
};
}
+695
View File
@@ -0,0 +1,695 @@
{
"count": 138,
"byId": {
"8d6b8a7f-e08e-4c9f-9772-a14a9d569260": {
"kind": "SETUP",
"title": "TTL 로 고아 세션을 골라내 지운다",
"version": null
},
"d53181b9-bb28-4703-97ed-98adfb5b18dc": {
"kind": "SETUP",
"title": "cookie secret 을 갈아치우고 로그인해 있던 세션이 어떻게 되는지 본다",
"version": null
},
"9bb0b051-c158-497f-a744-b24769c93783": {
"kind": "SETUP",
"title": "신원 헤더를 위조해 보내고 어디까지 도착하는지 본다",
"version": null
},
"cf2e783c-424b-4167-aa27-3bd6b5f46ee2": {
"kind": "SETUP",
"title": "flannel.1 에 200ms 를 넣고 커넥션 풀이 고갈되는 것을 본다",
"version": null
},
"b90d719f-39fb-4bab-a263-0e32eedb2b36": {
"kind": "SETUP",
"title": "한 방향만 끊어 보고 raw PREROUTING 까지 내려간다",
"version": null
},
"d845adc8-be2c-4471-aa1d-7e4db864c471": {
"kind": "SETUP",
"title": "기계 전원을 뽑고 쿠버네티스가 알아채는 시각을 잰다",
"version": null
},
"91ce17ad-758d-4a63-be5b-489786609557": {
"kind": "SETUP",
"title": "PostgreSQL 을 진짜로 크래시시키고 잃은 로그인을 센다",
"version": null
},
"bf169fef-f900-4247-88f8-427742ae3fe9": {
"kind": "SETUP",
"title": "PostgreSQL 을 정상 종료시키고 네 경로를 잰다",
"version": null
},
"4f32b469-185a-4dea-8eba-599864a3b476": {
"kind": "SETUP",
"title": "deploy 훅 파일 하나를 넣고 nginx 워커가 저절로 갈리는지 확인한다",
"version": null
},
"9349a3fe-5234-48ae-af9f-029ffc0d2296": {
"kind": "SETUP",
"title": "인증서를 강제로 갱신하고 밖에서 보이는 일련번호가 언제 바뀌는지 잰다",
"version": null
},
"186443e8-a32a-4a94-8609-845a4247d120": {
"kind": "SETUP",
"title": "카나리아 Secret 을 심고 네 경로에서 평문이 어디까지 나오는지 본다",
"version": null
},
"e53c5947-e1df-400a-ad79-e9d55b1da452": {
"kind": "SETUP",
"title": "이미지 태그를 올렸다 내리며 롤백이 언제 막히는지 가른다",
"version": null
},
"7dc48b91-e31b-455c-9a9d-c766f95ff491": {
"kind": "SETUP",
"title": "스키마를 통째로 지우고 덤프 하나로 되살아나는지 본다",
"version": null
},
"5296a106-4c42-437d-b721-33a5e53a045c": {
"kind": "SETUP",
"title": "IdP 쪽에만 로그아웃 주소를 넣고 한쪽만 고치면 안 퍼지는 것을 확인한다",
"version": null
},
"3421185f-5f3c-4263-9455-6243306e9fc9": {
"kind": "SETUP",
"title": "두 앱을 한 로그인으로 묶고 IdP 세션만 끊어 앱 세션이 남는지 본다",
"version": null
},
"7447ccbf-1800-43a4-a9d2-8ac774965c4b": {
"kind": "SETUP",
"title": "서명 키를 더한 뒤 옛 키를 지우고 옛 토큰이 언제 끊기는지 본다",
"version": null
},
"af9645a0-8ca8-481d-9624-69fce6449b7c": {
"kind": "SETUP",
"title": "Redis 를 0대로 내리고 파드가 Ready 를 유지하는지 본다",
"version": null
},
"405c4206-9b59-491f-aed1-8b97cfd9f584": {
"kind": "SETUP",
"title": "같은 refresh token 다섯 개를 동시에 던지고 client session 을 센다",
"version": null
},
"df2ee798-7f31-4570-a355-11f96c0eea84": {
"kind": "SETUP",
"title": "토큰을 PostgreSQL 로 옮기고 기본키와 로그아웃 정리를 확인한다",
"version": null
},
"1e5d05fa-88f4-4ef5-a402-4a520ae4a52d": {
"kind": "SETUP",
"title": "Redis 를 붙이고 무엇이 옮겨졌는지 빈 목록으로 견준다",
"version": null
},
"4ef91d43-1f1d-481a-8b6f-5e015d068578": {
"kind": "SETUP",
"title": "아무 저장소도 주지 않고 Spring 이 무엇을 고르는지 찍어서 확인한다",
"version": null
},
"975a6d61-4e34-4034-a0d2-01fea3b498a3": {
"kind": "SETUP",
"title": "DNS-01 으로 와일드카드 인증서를 받고 갱신이 서빙까지 닿게 한다",
"version": null
},
"0cb0f195-b06b-4f8e-b52e-675eb0918805": {
"kind": "SETUP",
"title": "Prometheus 와 Grafana 를 올려 클러스터 안을 밖에서 본다",
"version": null
},
"7c66a553-0008-4294-a27a-687bd1bda0c1": {
"kind": "SETUP",
"title": "lab host 에 가상화 패키지를 깔고 virsh 가 sudo 없이 돌게 만든다",
"version": null
},
"7b113a04-180a-40ec-9270-033531c22221": {
"kind": "SETUP",
"title": "Keycloak 2노드와 PostgreSQL 을 k3s 에 올리고 클러스터가 묶였는지 확인한다",
"version": null
},
"5e629c2f-e653-4dd9-b1c9-1fe6b2bb181d": {
"kind": "SETUP",
"title": "k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다",
"version": null
},
"74a7bacf-e5d8-4129-926a-c8cf5cacb8c9": {
"kind": "SETUP",
"title": "엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다",
"version": null
},
"f972ca27-7e18-41c1-9494-59cc6f676ae2": {
"kind": "SETUP",
"title": "cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다",
"version": null
},
"0b64d23a-f82a-44b4-ad54-e079578977c4": {
"kind": "SETUP",
"title": "롤링 재시작을 걸고 재시작 전 토큰이 통하는지 본다",
"version": null
},
"21dce25a-a165-47dc-bb40-2ed9f6f9efea": {
"kind": "SETUP",
"title": "문장 로깅으로 그 500 을 낸 SQL 을 확정하고 캐시 온도 셋을 재현한다",
"version": null
},
"b8d7db33-afdc-4e49-9eab-ed4edbbe398e": {
"kind": "SETUP",
"title": "persistent-user-sessions 를 끄고 A층 결론 넷을 다시 잰다",
"version": null
},
"95e29500-b535-4246-86db-d569ed814904": {
"kind": "SETUP",
"title": "7800 을 막고 디스커버리와 트랜스포트를 갈라 끊는다",
"version": null
},
"4f56ed58-fc82-4ed1-ac87-c356b30c34f7": {
"kind": "SETUP",
"title": "세션을 공유하는 것이 Infinispan 인지 PostgreSQL 인지 손으로 가른다",
"version": null
},
"3b05c1f0-13e9-414a-97a6-2078fb4bab26": {
"kind": "CONCEPT",
"title": "Guest 의 write() 요청부터 virtqueue까지",
"version": null
},
"5b1de31c-0495-4226-8607-73ed7284b314": {
"kind": "CONCEPT",
"title": "Guest 의 packet에서 Host Physical NIC까지",
"version": null
},
"5bcae89b-0873-4a2b-af4a-10e5234c085b": {
"kind": "CONCEPT",
"title": "KVM 에서 vCPU 가 물리 CPU 에서 실행되기까지",
"version": null
},
"ecaaa37d-7b33-4cbf-9bc5-4c6caad14ee5": {
"kind": "CONCEPT",
"title": "VM 아래에 한 번 더 있는 Host block stack — blk-mq · I/O Scheduler · NVMe",
"version": null
},
"8fafaeea-8746-4537-87a4-679a3a91aa4c": {
"kind": "CONCEPT",
"title": "완료라는 말의 네 가지 뜻 — write · writeback · flush · 전원 장애 생존",
"version": null
},
"eead2379-94fe-463a-9e5e-06a01bf2105d": {
"kind": "QUESTION",
"title": "disk image 는 최종적으로 어느 Host block device 위에 있는가",
"version": null
},
"480f86ac-86fb-4290-9bf5-584763aa1363": {
"kind": "QUESTION",
"title": "이 VM 들의 QEMU disk cache mode 는 무엇인가",
"version": null
},
"decbab8d-9c98-45c6-b5b8-e6162e407299": {
"kind": "REFERENCE",
"title": "빨라진 구성이 durability contract 를 지운 것은 아닌지 먼저 가른다",
"version": null
},
"b1cda8c4-d31d-4ae4-a4d9-4103a0ab3109": {
"kind": "REFERENCE",
"title": "지연 원인을 CPU 로 읽기 전에 storage 계층을 따로 잰다",
"version": null
},
"ed64d4ff-aa01-48cc-ae68-6a683b025cde": {
"kind": "QUESTION",
"title": "VM1 의 storage 부하가 VM2 의 지연을 실제로 밀어 올리는가",
"version": null
},
"77aa0188-5c2b-4f47-bda7-7c66b084968d": {
"kind": "QUESTION",
"title": "이 호스트의 I/O Scheduler 는 무엇인가",
"version": null
},
"be3c2c58-1ed4-4eae-876a-3114faa61c94": {
"kind": "QUESTION",
"title": "Guest 의 fsync() 지연과 Host storage 지연은 같이 오르는가",
"version": null
},
"892b6087-8961-486c-b4d9-d3dd46bc2605": {
"kind": "CONCEPT",
"title": "/dev/vda 뒤에 있는 것 — qcow2 · RAW · Host block device",
"version": null
},
"ae69e2e1-1a3b-4ae3-ae2a-572af07b95d7": {
"kind": "QUESTION",
"title": "이 disk image 는 qcow2 인가 RAW 인가, 그리고 Guest 가 보는 크기와 Host 가 실제로 쓰는 크기는 얼마나 다른가",
"version": null
},
"1f76d54b-bb17-4174-8128-0c2d71d4256f": {
"kind": "QUESTION",
"title": "이 호스트의 /dev/vda 는 어떤 backend 에 붙어 있는가",
"version": null
},
"274c5636-33f6-4c19-a54b-d2a96af31289": {
"kind": "REFERENCE",
"title": "Guest 안에서 본 disk 로 backend 를 단정하지 않는다",
"version": null
},
"484fd832-c7d3-413e-897a-97892e4e10ac": {
"kind": "QUESTION",
"title": "이 호스트에서 vhost-net 이 실제로 packet datapath 를 맡고 있는가",
"version": null
},
"397c4789-0272-449b-86d6-2c4a3a122401": {
"kind": "QUESTION",
"title": "VM1 과 VM2 의 TAP/vnet interface 는 무엇이고 어디에 붙어 있는가",
"version": null
},
"38716d7f-1aa1-48d8-ab4c-3fc505332128": {
"kind": "QUESTION",
"title": "이 호스트의 가상 머신 네트워크는 Bridge 인가 NAT 인가 Routing 인가",
"version": null
},
"ca36f0db-9028-4761-91a8-afb7eb30f2b9": {
"kind": "REFERENCE",
"title": "Keycloak 실험 결과를 애플리케이션 원인으로 읽기 전에 network 계층을 따로 검증한다",
"version": null
},
"68a848c7-eaff-4421-ad25-de21673ca40c": {
"kind": "REFERENCE",
"title": "packet 이 어디서 끊겼는지는 계층마다 capture 해서 가른다",
"version": null
},
"79ac61d6-cbe6-483b-9961-02ca8c02b5c2": {
"kind": "QUESTION",
"title": "부하 중 network 가상화가 지연을 바꿀 만큼 호스트 CPU 를 쓰는가",
"version": null
},
"600a2621-f6d8-42df-9629-7db65011545d": {
"kind": "QUESTION",
"title": "Host Nginx 에서 Keycloak 까지 packet 은 실제로 어디를 지나는가",
"version": null
},
"1a996f8d-b04e-49d2-88ef-4877e21f7c0e": {
"kind": "QUESTION",
"title": "이 가상 머신들의 virtio-net 에 multi-queue 가 켜져 있는가",
"version": null
},
"bded6560-dc8d-473d-966c-5b037bb84c4c": {
"kind": "QUESTION",
"title": "QEMU backend 와 vhost-net 의 차이가 이 호스트에서 실제로 보이는가",
"version": null
},
"32d0be7d-d88e-4760-8d91-35d3a233a99a": {
"kind": "CASE",
"title": "Fetch 타입이 아닌 조회 방식으로 인한 N+1",
"version": null
},
"c1158754-e3d2-47b8-bb41-81787c0ca84b": {
"kind": "CASE",
"title": "Collection Fetch Join Pagination의 In-memory Paging",
"version": null
},
"1b59e6de-16f1-42b2-81bb-01d6198e34bf": {
"kind": "QUESTION",
"title": "QEMU 메모리는 vCPU 가 도는 NUMA node 와 같은 node 에 있는가",
"version": null
},
"6a60abe7-ea2c-46c2-bb9f-8281bfbe9644": {
"kind": "QUESTION",
"title": "balloon target 을 바꾸면 게스트가 쓸 수 있는 메모리는 어떻게 따라 움직이는가",
"version": null
},
"a05fe2a6-eb7c-4bcc-a7da-58e729436468": {
"kind": "QUESTION",
"title": "호스트 메모리 압박이 게스트 애플리케이션 지연을 실제로 밀어 올리는가",
"version": null
},
"380d4975-3be5-451d-aede-2e24c5a2a09e": {
"kind": "QUESTION",
"title": "NUMA remote access 가 이 작업의 지연을 실제로 바꾸는가",
"version": null
},
"63aef96b-03ef-4dbd-9ba6-9e61c6e695e1": {
"kind": "QUESTION",
"title": "이 가상 머신들에 virtio-balloon 이 붙어 있는가",
"version": null
},
"074a1cb4-cea8-4dad-bc4c-899692ff9aa9": {
"kind": "QUESTION",
"title": "게스트 page fault 증가는 workload 변화를 따라가는가",
"version": null
},
"a2cbc828-59ab-4e70-916a-d053e71960ae": {
"kind": "QUESTION",
"title": "호스트 major fault 와 스토리지 지연은 같은 시간축에서 함께 움직이는가",
"version": null
},
"50730203-df5f-4604-8535-7e93ab6fb724": {
"kind": "QUESTION",
"title": "이 호스트의 THP 정책과 huge page 상태는 무엇인가",
"version": null
},
"fa5b3782-9fcf-4113-8f51-a55c8023b2db": {
"kind": "QUESTION",
"title": "QEMU 프로세스가 실제로 붙잡고 있는 호스트 메모리는 얼마이고 어떻게 나뉘어 있는가",
"version": null
},
"67285d51-7eaf-4540-9577-cfd0b273b40e": {
"kind": "QUESTION",
"title": "이 호스트의 가상 머신들은 설정한 메모리와 지금 잡고 있는 메모리가 얼마나 다른가",
"version": null
},
"63fa33e5-9957-4d28-8d3c-c75735a73bd6": {
"kind": "REFERENCE",
"title": "메모리 증상 하나로 계층을 단정하지 않는다",
"version": null
},
"ded41b52-ec08-4231-a54c-86d6c80d38f0": {
"kind": "REFERENCE",
"title": "메모리 실험 결과에는 Host · VM · Workload 조건을 함께 남긴다",
"version": null
},
"f460e9a2-35ef-46ff-81cf-b36627a1b231": {
"kind": "QUESTION",
"title": "가상 머신 RAM 이 HugeTLB 로 명시적으로 backing 되어 있는가",
"version": null
},
"d3e8501f-e1b5-4f8f-aa9a-89f9c89a308e": {
"kind": "QUESTION",
"title": "지금 게스트와 호스트에서 swap 이 실제로 오가고 있는가",
"version": null
},
"647d6d11-5bb2-4028-a530-b10c8925aa11": {
"kind": "CONCEPT",
"title": "Host 메모리 압박이 Guest 와 스토리지까지 내려가는 경로",
"version": null
},
"68811272-5352-4409-aa20-7e22e601ede8": {
"kind": "CONCEPT",
"title": "NUMA 에서는 vCPU 배치와 메모리 배치를 함께 본다",
"version": null
},
"ebdf93d7-2dcb-4fc8-91b9-2364ac8196e6": {
"kind": "CONCEPT",
"title": "virtio-balloon 이 Guest 메모리를 회수하고 돌려주는 방식",
"version": null
},
"2f2ffafc-91ff-4971-a6a8-7c0e7d844ca2": {
"kind": "CONCEPT",
"title": "Guest 메모리 주소가 물리 RAM 에 닿기까지 — GVA · GPA · HPA",
"version": null
},
"efce9a6e-5f5d-45ec-86e5-056d8cb1b8d2": {
"kind": "CONCEPT",
"title": "Huge Page 를 VM 에서 볼 때 갈라지는 세 자리",
"version": null
},
"e80853fa-98b8-4cc3-a2df-427c7147794b": {
"kind": "CONCEPT",
"title": "VM 에서 page fault 는 세 계층에서 따로 일어난다",
"version": null
},
"688c6c02-c1bd-4cd2-a615-2396db145386": {
"kind": "QUESTION",
"title": "이 호스트의 NUMA 토폴로지는 가상 머신 성능을 고려해야 할 구조인가",
"version": null
},
"d103bb81-45df-402f-86e5-e41d66ed7f8d": {
"kind": "QUESTION",
"title": "Keycloak 작업의 VM Exit 분포는 idle · CPU-bound · I/O-bound 와 어떻게 다른가",
"version": null
},
"62319064-17ef-4b2f-bf8b-e363cd0e36a6": {
"kind": "QUESTION",
"title": "CPU pinning 전후로 Keycloak 지연과 vCPU 스케줄링 변동이 달라지는가",
"version": null
},
"116b806c-b176-401c-9363-1489fffb721c": {
"kind": "QUESTION",
"title": "cgroup CPU 제한과 호스트 vCPU 경쟁을 지표로 가를 수 있는가",
"version": null
},
"723d8930-9e82-4552-8376-038a6446ec71": {
"kind": "QUESTION",
"title": "vCPU 를 늘릴수록 이 호스트에서 Keycloak 처리량도 계속 오르는가",
"version": null
},
"2d058fb5-01f1-4be8-80bb-0e377868573c": {
"kind": "QUESTION",
"title": "가상 머신 두 대를 CPU-bound 로 만들면 게스트 steal time 은 얼마나 오르는가",
"version": null
},
"2acec7d5-3eaf-4115-9d61-45647c4ec01e": {
"kind": "QUESTION",
"title": "운영 서버는 CPU 가상화 계층의 영향을 받는 구조인가",
"version": null
},
"cd58d35d-3a93-4682-82b8-b64ce3a8813c": {
"kind": "QUESTION",
"title": "pinning 없이 vCPU 스레드는 호스트 논리 CPU 사이를 옮겨 다니는가",
"version": null
},
"61b7486f-5dab-41df-8f4f-e8fc21d88617": {
"kind": "QUESTION",
"title": "실제 작업에서 주로 발생하는 VM Exit 은 무엇인가",
"version": null
},
"4a2d3332-81c3-44fa-a4fb-216a41abb504": {
"kind": "QUESTION",
"title": "게스트가 유휴 상태일 때 vCPU 스레드는 이 테스트 환경에서 어떻게 보이는가",
"version": null
},
"da3a7b56-5691-4a9f-90c5-3a1a5d0f0013": {
"kind": "QUESTION",
"title": "Keycloak 동시 refresh 실험 중 CPU 가상화 계층이 결과를 흔들 만큼 포화되는가",
"version": null
},
"eba4a877-276a-4050-9a97-d32614546c71": {
"kind": "QUESTION",
"title": "가상 머신 두 대에 동시에 부하를 주면 이 호스트에서 vCPU 경쟁이 실제로 생기는가",
"version": null
},
"a0e1cc05-92b3-4dac-bce1-513ab8cd862b": {
"kind": "CASE",
"title": "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유",
"version": null
},
"488ce49b-afa4-42a5-a2ce-de2e0653cd82": {
"kind": "CASE",
"title": "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출",
"version": null
},
"bf675775-4f3e-4744-8014-f0efff51422a": {
"kind": "CASE",
"title": "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우",
"version": null
},
"036563a1-3a43-4931-a017-0e693b591e89": {
"kind": "REFERENCE",
"title": "패턴 검증을 실제로 돌릴 때의 안전한 순서",
"version": null
},
"4c9c3b90-bc89-4300-9334-088ea95d37d8": {
"kind": "CASE",
"title": "Projection 이후에도 1,509행을 읽은 Row Over-fetch",
"version": null
},
"7ed75172-fd56-42bf-956a-8f9fc1cca235": {
"kind": "CASE",
"title": "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제",
"version": null
},
"a3493786-d3fb-4b01-b1c5-ecb23c3d5497": {
"kind": "CONCEPT",
"title": "Forward-Auth와 Nginx auth_request의 동작",
"version": null
},
"5c8f12d5-1ead-469b-8e91-2de69401df48": {
"kind": "CONCEPT",
"title": "Cookie로 인증하는 요청에서 CSRF token이 하는 일",
"version": null
},
"d85bd6af-7599-4ef7-9407-6609927d5b5c": {
"kind": "CASE",
"title": "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정",
"version": null
},
"e1e0e2a0-c6b6-45bf-be42-f697ba5e2fff": {
"kind": "QUESTION",
"title": "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가",
"version": null
},
"5159c415-232d-424a-970a-b0db52746767": {
"kind": "QUESTION",
"title": "Round Trip과 Row Volume을 독립 측정할 것인가",
"version": null
},
"b0b55ac9-c0a3-4c01-ba84-0aa478923ace": {
"kind": "REFERENCE",
"title": "JPA N+1 정량 진단 기준",
"version": null
},
"ae6c9bea-d3a3-46e1-bbd4-8d580d336394": {
"kind": "PROJECT_DECISION",
"title": "Query Plan은 실제 PostgreSQL에서 측정한다",
"version": null
},
"d99fdec9-fe9e-4e0f-a50b-6fb9b9ed5719": {
"kind": "CONCEPT",
"title": "외부 IdP Brokering의 동작",
"version": null
},
"bf5f2462-0e94-4723-bdc8-f7dd709b2dbb": {
"kind": "REFERENCE",
"title": "Top-N-per-group 선택 기준",
"version": null
},
"e8c2e9ea-cd87-46f8-9469-849dbd433d86": {
"kind": "REFERENCE",
"title": "PostgreSQL Query Plan 측정 기준",
"version": null
},
"06788903-3dfa-4f70-b159-f1224384fd0b": {
"kind": "REFERENCE",
"title": "Keyset Pagination 설계 기준",
"version": null
},
"51095f6e-2cc8-439c-8648-065033614215": {
"kind": "REFERENCE",
"title": "Fetch Type과 Fetch Strategy 구분",
"version": null
},
"db99cbc5-9123-4599-b368-39ff3170e81d": {
"kind": "REFERENCE",
"title": "Fetch Join · Batch · Projection 선택 기준",
"version": null
},
"635fcedd-d402-4297-bcf3-9fcdf4200d28": {
"kind": "REFERENCE",
"title": "Feed Visibility Query Pattern",
"version": null
},
"30a37f34-b406-4061-b924-e22e0be0c3bf": {
"kind": "PROJECT_DECISION",
"title": "화면 조회는 Read Projection을 사용한다",
"version": null
},
"4e3200c8-eff5-4442-ae84-ae7b7fa92c8b": {
"kind": "PROJECT_DECISION",
"title": "Query Strategy는 FeedQueryPort 뒤에서 소유한다",
"version": null
},
"5e4d033c-d6fe-4257-a4dc-1ade44473c72": {
"kind": "PROJECT_DECISION",
"title": "Collection Fetch Join과 Pagination을 같이 사용하지 않는다",
"version": null
},
"1dbce381-f0dc-4d49-ad68-bd31d205677e": {
"kind": "PROJECT_DECISION",
"title": "Feed Pagination은 Keyset을 사용한다",
"version": null
},
"7f248f68-ce2b-43ec-94ce-82324d0bd1a7": {
"kind": "PROJECT_DECISION",
"title": "현재 Read Model은 CQRS-lite로 유지한다",
"version": null
},
"08a74b35-10c3-4874-8fbc-209b0b6e942e": {
"kind": "PROJECT_DECISION",
"title": "Entity Graph 조회에는 Batch Fetch를 사용한다",
"version": null
},
"5088ce14-b096-41d3-abba-64b7afb48bb9": {
"kind": "QUESTION",
"title": "feed_visible을 Production CQRS로 승격할 것인가",
"version": null
},
"b099ca65-bf9f-4d61-814c-74722453fa3c": {
"kind": "QUESTION",
"title": "Highlight 없는 FeedItem을 허용할 것인가",
"version": null
},
"6cbe963f-86f8-4df6-be5a-900712970d01": {
"kind": "QUESTION",
"title": "실제 동시 트래픽에서도 이 구조가 안정적인가",
"version": null
},
"e6715e81-6dbd-4287-8e19-946c334f38fb": {
"kind": "CASE",
"title": "Visibility OR이 Keyset Index를 깨뜨린 문제",
"version": null
},
"3f886154-1b85-407b-bda4-57d28370e745": {
"kind": "REFERENCE",
"title": "OAuth/OIDC 인증 패턴 선택 기준",
"version": null
},
"19b55c39-c583-4161-9775-df954280a568": {
"kind": "PROJECT_DECISION",
"title": "BFF가 OAuth Token을 관리하는 조건",
"version": null
},
"7ff40767-a00b-4db2-98f6-0cdfce8c8936": {
"kind": "QUESTION",
"title": "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가",
"version": null
},
"18a5cde2-dd1e-4bff-9f1c-997577ae438f": {
"kind": "QUESTION",
"title": "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가",
"version": null
},
"66c18e42-116c-459f-86bd-b7e4bf394866": {
"kind": "REFERENCE",
"title": "OAuth Token과 Application Session을 구분하는 기준",
"version": null
},
"97eddd97-1096-426a-a2c6-a6c5bf1cd09f": {
"kind": "REFERENCE",
"title": "BFF 인증 구조 설계 기준",
"version": null
},
"004dd0a2-5fb3-4f25-80c9-576f709de331": {
"kind": "REFERENCE",
"title": "Forward-Auth에서 Identity Header를 신뢰하기 위한 조건",
"version": null
},
"1a00a640-8987-4075-a9e4-7ec023cdffbb": {
"kind": "REFERENCE",
"title": "외부 IdP 연동과 Application 인증 구조의 경계",
"version": null
},
"ede6b9ce-eeed-40c8-9175-9e8116029395": {
"kind": "REFERENCE",
"title": "Public Client와 Confidential Client 구분 기준",
"version": null
},
"c72656b5-842d-45d9-b5f6-82b66b09d0b9": {
"kind": "QUESTION",
"title": "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가",
"version": null
},
"bb5c37ae-2d94-48f7-ad4e-a37c61c3fd07": {
"kind": "CONCEPT",
"title": "브라우저가 credential을 보관하는 위치와 그 성질",
"version": null
},
"87000d59-b69f-4010-9481-0b71c8bde32d": {
"kind": "CONCEPT",
"title": "Bearer JWT가 인증된 principal이 되기까지",
"version": null
},
"75c6c657-3e03-47a0-a9d0-5637fce9dd3f": {
"kind": "CONCEPT",
"title": "Authorization Code와 PKCE가 보호하는 구간",
"version": null
},
"8c1ebea7-204e-445c-9812-0421d9eb0e9c": {
"kind": "PROJECT_DECISION",
"title": "외부 IdP와의 연동이라도 별도의 인증 방식이 아니다.",
"version": null
},
"9ae4ec71-a32e-49a7-88c2-f7368541c28d": {
"kind": "QUESTION",
"title": "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가",
"version": null
},
"39fdf472-82c4-43ed-abec-73de672f08ae": {
"kind": "REFERENCE",
"title": "Authorization Code Flow의 Endpoint와 Credential 이동 기준",
"version": null
}
}
}
+790
View File
@@ -0,0 +1,790 @@
{
"asked": 87,
"missing": 0,
"published": 2,
"byId": {
"bf169fef-f900-4247-88f8-427742ae3fe9": {
"kind": "SETUP",
"version": 4,
"slug": "reproduce-a2-database-loss",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "f2183920-6573-4148-90ae-8a510500d862",
"relations": 0
},
"91ce17ad-758d-4a63-be5b-489786609557": {
"kind": "SETUP",
"version": 4,
"slug": "reproduce-a3-database-crash",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "f2183920-6573-4148-90ae-8a510500d862",
"relations": 0
},
"d845adc8-be2c-4471-aa1d-7e4db864c471": {
"kind": "SETUP",
"version": 4,
"slug": "reproduce-a4-node-loss",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "f2183920-6573-4148-90ae-8a510500d862",
"relations": 0
},
"b90d719f-39fb-4bab-a263-0e32eedb2b36": {
"kind": "SETUP",
"version": 4,
"slug": "reproduce-a5-asymmetric-partition",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "f2183920-6573-4148-90ae-8a510500d862",
"relations": 0
},
"cf2e783c-424b-4167-aa27-3bd6b5f46ee2": {
"kind": "SETUP",
"version": 4,
"slug": "reproduce-a6-latency-injection",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "f2183920-6573-4148-90ae-8a510500d862",
"relations": 0
},
"7dc48b91-e31b-455c-9a9d-c766f95ff491": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-d1-backup-restore",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "cf1fe22a-af52-4d57-9833-559f537d924b",
"relations": 0
},
"e53c5947-e1df-400a-ad79-e9d55b1da452": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-d2-version-upgrade",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "cf1fe22a-af52-4d57-9833-559f537d924b",
"relations": 0
},
"186443e8-a32a-4a94-8609-845a4247d120": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-d3-secret-exposure",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "cf1fe22a-af52-4d57-9833-559f537d924b",
"relations": 0
},
"9349a3fe-5234-48ae-af9f-029ffc0d2296": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-d4-certificate-renewal",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "cf1fe22a-af52-4d57-9833-559f537d924b",
"relations": 0
},
"4f32b469-185a-4dea-8eba-599864a3b476": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-d4a-deploy-hook",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "cf1fe22a-af52-4d57-9833-559f537d924b",
"relations": 0
},
"4f56ed58-fc82-4ed1-ac87-c356b30c34f7": {
"kind": "SETUP",
"version": 3,
"slug": "reproduce-a0-session-sharing-path",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "2ee927ff-217b-429f-b081-d313df65a5de",
"relations": 0
},
"95e29500-b535-4246-86db-d569ed814904": {
"kind": "SETUP",
"version": 3,
"slug": "reproduce-a1-jgroups-transport-block",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "2ee927ff-217b-429f-b081-d313df65a5de",
"relations": 0
},
"b8d7db33-afdc-4e49-9eab-ed4edbbe398e": {
"kind": "SETUP",
"version": 3,
"slug": "reproduce-a7-volatile-comparison",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "2ee927ff-217b-429f-b081-d313df65a5de",
"relations": 0
},
"21dce25a-a165-47dc-bb40-2ed9f6f9efea": {
"kind": "SETUP",
"version": 3,
"slug": "reproduce-a7a-volatile-cause",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "2ee927ff-217b-429f-b081-d313df65a5de",
"relations": 0
},
"0b64d23a-f82a-44b4-ad54-e079578977c4": {
"kind": "SETUP",
"version": 3,
"slug": "reproduce-a8-rolling-restart",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "2ee927ff-217b-429f-b081-d313df65a5de",
"relations": 0
},
"9bb0b051-c158-497f-a744-b24769c93783": {
"kind": "SETUP",
"version": 3,
"slug": "reproduce-b4-forged-identity-headers",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "116708bb-fcb8-4c7e-9814-a8661dfb1c52",
"relations": 0
},
"d53181b9-bb28-4703-97ed-98adfb5b18dc": {
"kind": "SETUP",
"version": 3,
"slug": "reproduce-b7-cookie-secret-rotation",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "116708bb-fcb8-4c7e-9814-a8661dfb1c52",
"relations": 0
},
"8d6b8a7f-e08e-4c9f-9772-a14a9d569260": {
"kind": "SETUP",
"version": 4,
"slug": "reproduce-b7a-orphan-session",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "116708bb-fcb8-4c7e-9814-a8661dfb1c52",
"relations": 0
},
"3421185f-5f3c-4263-9455-6243306e9fc9": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-c1-multi-app-sso",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "116708bb-fcb8-4c7e-9814-a8661dfb1c52",
"relations": 0
},
"5296a106-4c42-437d-b721-33a5e53a045c": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-c2-backchannel-logout",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "116708bb-fcb8-4c7e-9814-a8661dfb1c52",
"relations": 0
},
"4ef91d43-1f1d-481a-8b6f-5e015d068578": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-b0-default-session-store",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "3e8e8f8d-1f94-48ba-a821-b7403b35eba5",
"relations": 0
},
"1e5d05fa-88f4-4ef5-a402-4a520ae4a52d": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-b1-redis-session-store",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "3e8e8f8d-1f94-48ba-a821-b7403b35eba5",
"relations": 0
},
"df2ee798-7f31-4570-a355-11f96c0eea84": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-b2-jdbc-token-store",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "3e8e8f8d-1f94-48ba-a821-b7403b35eba5",
"relations": 0
},
"405c4206-9b59-491f-aed1-8b97cfd9f584": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-b3-refresh-contention",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "3e8e8f8d-1f94-48ba-a821-b7403b35eba5",
"relations": 0
},
"af9645a0-8ca8-481d-9624-69fce6449b7c": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-b5-redis-loss",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "3e8e8f8d-1f94-48ba-a821-b7403b35eba5",
"relations": 0
},
"7447ccbf-1800-43a4-a9d2-8ac774965c4b": {
"kind": "SETUP",
"version": 2,
"slug": "reproduce-b6-key-rotation",
"publication": null,
"projectId": "9a8ed8be-ecb1-4e40-8e11-4b5641e98ded",
"topicId": "3e8e8f8d-1f94-48ba-a821-b7403b35eba5",
"relations": 0
},
"d103bb81-45df-402f-86e5-e41d66ed7f8d": {
"kind": "QUESTION",
"version": 2,
"slug": "exit-distribution-for-keycloak-workload",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"688c6c02-c1bd-4cd2-a615-2396db145386": {
"kind": "QUESTION",
"version": 2,
"slug": "host-numa-topology",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"4a2d3332-81c3-44fa-a4fb-216a41abb504": {
"kind": "QUESTION",
"version": 2,
"slug": "idle-vcpu-thread-appearance",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"2acec7d5-3eaf-4115-9d61-45647c4ec01e": {
"kind": "QUESTION",
"version": 2,
"slug": "is-production-on-a-hypervisor",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"62319064-17ef-4b2f-bf8b-e363cd0e36a6": {
"kind": "QUESTION",
"version": 2,
"slug": "pinning-before-and-after",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"2d058fb5-01f1-4be8-80bb-0e377868573c": {
"kind": "QUESTION",
"version": 2,
"slug": "steal-time-increase-under-two-cpu-bound-vms",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"116b806c-b176-401c-9363-1489fffb721c": {
"kind": "QUESTION",
"version": 2,
"slug": "throttling-vs-contention",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"723d8930-9e82-4552-8376-038a6446ec71": {
"kind": "QUESTION",
"version": 2,
"slug": "throughput-vs-vcpu-count",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"cd58d35d-3a93-4682-82b8-b64ce3a8813c": {
"kind": "QUESTION",
"version": 2,
"slug": "vcpu-thread-migration-without-pinning",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"da3a7b56-5691-4a9f-90c5-3a1a5d0f0013": {
"kind": "QUESTION",
"version": 2,
"slug": "virtualization-layer-saturation-during-refresh",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"61b7486f-5dab-41df-8f4f-e8fc21d88617": {
"kind": "QUESTION",
"version": 2,
"slug": "vm-exit-distribution-by-workload",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "e42456e2-9efd-41ee-b676-df82e90f2f53",
"relations": 0
},
"f972ca27-7e18-41c1-9494-59cc6f676ae2": {
"kind": "SETUP",
"version": 3,
"slug": "create-three-guests-with-cloud-init",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "886181b9-5d25-47bf-81a4-813084e3d9d7",
"relations": 0
},
"74a7bacf-e5d8-4129-926a-c8cf5cacb8c9": {
"kind": "SETUP",
"version": 5,
"slug": "edge-nginx-and-host-dnat",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "886181b9-5d25-47bf-81a4-813084e3d9d7",
"relations": 0
},
"5e629c2f-e653-4dd9-b1c9-1fe6b2bb181d": {
"kind": "SETUP",
"version": 6,
"slug": "install-k3s-server-and-agent",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "886181b9-5d25-47bf-81a4-813084e3d9d7",
"relations": 0
},
"7b113a04-180a-40ec-9270-033531c22221": {
"kind": "SETUP",
"version": 5,
"slug": "keycloak-two-nodes-and-postgres-on-k3s",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "886181b9-5d25-47bf-81a4-813084e3d9d7",
"relations": 0
},
"7c66a553-0008-4294-a27a-687bd1bda0c1": {
"kind": "SETUP",
"version": 5,
"slug": "prepare-the-lab-host-for-virtualization",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "886181b9-5d25-47bf-81a4-813084e3d9d7",
"relations": 0
},
"0cb0f195-b06b-4f8e-b52e-675eb0918805": {
"kind": "SETUP",
"version": 5,
"slug": "prometheus-and-grafana-for-the-lab",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "886181b9-5d25-47bf-81a4-813084e3d9d7",
"relations": 0
},
"975a6d61-4e34-4034-a0d2-01fea3b498a3": {
"kind": "SETUP",
"version": 5,
"slug": "wildcard-certificate-with-dns-01-and-a-deploy-hook",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "886181b9-5d25-47bf-81a4-813084e3d9d7",
"relations": 0
},
"efce9a6e-5f5d-45ec-86e5-056d8cb1b8d2": {
"kind": "CONCEPT",
"version": 3,
"slug": "huge-pages-in-a-vm",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 1
},
"647d6d11-5bb2-4028-a530-b10c8925aa11": {
"kind": "CONCEPT",
"version": 3,
"slug": "memory-pressure-reclaim-swap-oom",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 3
},
"68811272-5352-4409-aa20-7e22e601ede8": {
"kind": "CONCEPT",
"version": 2,
"slug": "numa-locality-for-vcpu-and-memory",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 3
},
"e80853fa-98b8-4cc3-a2df-427c7147794b": {
"kind": "CONCEPT",
"version": 2,
"slug": "page-fault-layers-in-a-vm",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 1
},
"ebdf93d7-2dcb-4fc8-91b9-2364ac8196e6": {
"kind": "CONCEPT",
"version": 3,
"slug": "virtio-balloon-memory-reclaim",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 1
},
"6a60abe7-ea2c-46c2-bb9f-8281bfbe9644": {
"kind": "QUESTION",
"version": 3,
"slug": "balloon-target-vs-guest-available-memory",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 4
},
"074a1cb4-cea8-4dad-bc4c-899692ff9aa9": {
"kind": "QUESTION",
"version": 2,
"slug": "guest-page-fault-vs-workload",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 4
},
"a2cbc828-59ab-4e70-916a-d053e71960ae": {
"kind": "QUESTION",
"version": 2,
"slug": "host-major-fault-vs-storage-latency",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 5
},
"a05fe2a6-eb7c-4bcc-a7da-58e729436468": {
"kind": "QUESTION",
"version": 3,
"slug": "host-memory-pressure-vs-guest-latency",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 5
},
"50730203-df5f-4604-8535-7e93ab6fb724": {
"kind": "QUESTION",
"version": 3,
"slug": "host-thp-policy",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 3
},
"380d4975-3be5-451d-aede-2e24c5a2a09e": {
"kind": "QUESTION",
"version": 2,
"slug": "numa-remote-access-vs-workload-latency",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 4
},
"1b59e6de-16f1-42b2-81bb-01d6198e34bf": {
"kind": "QUESTION",
"version": 3,
"slug": "qemu-memory-numa-placement",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 4
},
"fa5b3782-9fcf-4113-8f51-a55c8023b2db": {
"kind": "QUESTION",
"version": 3,
"slug": "qemu-resident-memory-distribution",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 4
},
"d3e8501f-e1b5-4f8f-aa9a-89f9c89a308e": {
"kind": "QUESTION",
"version": 2,
"slug": "swap-activity-in-guest-and-host",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 3
},
"63aef96b-03ef-4dbd-9ba6-9e61c6e695e1": {
"kind": "QUESTION",
"version": 2,
"slug": "virtio-balloon-configured",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 3
},
"67285d51-7eaf-4540-9577-cfd0b273b40e": {
"kind": "QUESTION",
"version": 3,
"slug": "vm-configured-vs-current-memory",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 4
},
"f460e9a2-35ef-46ff-81cf-b36627a1b231": {
"kind": "QUESTION",
"version": 2,
"slug": "vm-ram-backed-by-hugetlb",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 4
},
"63fa33e5-9957-4d28-8d3c-c75735a73bd6": {
"kind": "REFERENCE",
"version": 3,
"slug": "memory-symptom-needs-layer-separation",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 6
},
"ded41b52-ec08-4231-a54c-86d6c80d38f0": {
"kind": "REFERENCE",
"version": 3,
"slug": "record-the-conditions-with-every-memory-experiment",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "5675627f-3616-409c-990b-c537d3c68ed6",
"relations": 4
},
"5b1de31c-0495-4226-8607-73ed7284b314": {
"kind": "CONCEPT",
"version": 160,
"slug": "guest-packet-path-to-physical-nic",
"publication": "PUBLISHED",
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 10
},
"600a2621-f6d8-42df-9629-7db65011545d": {
"kind": "QUESTION",
"version": 2,
"slug": "actual-packet-path-nginx-to-keycloak",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 5
},
"484fd832-c7d3-413e-897a-97892e4e10ac": {
"kind": "QUESTION",
"version": 4,
"slug": "is-vhost-net-actually-in-use",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 4
},
"79ac61d6-cbe6-483b-9961-02ca8c02b5c2": {
"kind": "QUESTION",
"version": 2,
"slug": "network-virtualization-cpu-cost-under-load",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 5
},
"bded6560-dc8d-473d-966c-5b037bb84c4c": {
"kind": "QUESTION",
"version": 2,
"slug": "qemu-backend-vs-vhost-net-on-this-host",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 4
},
"397c4789-0272-449b-86d6-2c4a3a122401": {
"kind": "QUESTION",
"version": 4,
"slug": "tap-interface-to-vm-mapping",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 4
},
"1a996f8d-b04e-49d2-88ef-4877e21f7c0e": {
"kind": "QUESTION",
"version": 2,
"slug": "virtio-net-multi-queue-enabled",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 3
},
"38716d7f-1aa1-48d8-ab4c-3fc505332128": {
"kind": "QUESTION",
"version": 4,
"slug": "vm-network-mode-bridge-nat-or-routed",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 4
},
"68a848c7-eaff-4421-ad25-de21673ca40c": {
"kind": "REFERENCE",
"version": 4,
"slug": "bisect-the-packet-path-with-capture-points",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 5
},
"ca36f0db-9028-4761-91a8-afb7eb30f2b9": {
"kind": "REFERENCE",
"version": 4,
"slug": "verify-the-network-path-before-blaming-the-application",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "56089eec-a603-4eac-bbb6-6fb44392ea63",
"relations": 5
},
"3b05c1f0-13e9-414a-97a6-2078fb4bab26": {
"kind": "CONCEPT",
"version": 84,
"slug": "guest-block-io-path-to-virtqueue",
"publication": "PUBLISHED",
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 7
},
"ecaaa37d-7b33-4cbf-9bc5-4c6caad14ee5": {
"kind": "CONCEPT",
"version": 4,
"slug": "host-block-stack-under-the-vm",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 7
},
"892b6087-8961-486c-b4d9-d3dd46bc2605": {
"kind": "CONCEPT",
"version": 4,
"slug": "qemu-block-backend-forms",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 7
},
"8fafaeea-8746-4537-87a4-679a3a91aa4c": {
"kind": "CONCEPT",
"version": 5,
"slug": "write-completion-is-not-durability",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 6
},
"ae69e2e1-1a3b-4ae3-ae2a-572af07b95d7": {
"kind": "QUESTION",
"version": 3,
"slug": "disk-image-format-and-actual-host-usage",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 4
},
"be3c2c58-1ed4-4eae-876a-3114faa61c94": {
"kind": "QUESTION",
"version": 2,
"slug": "guest-fsync-latency-vs-host-storage-latency",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 7
},
"eead2379-94fe-463a-9e5e-06a01bf2105d": {
"kind": "QUESTION",
"version": 3,
"slug": "host-block-device-under-the-disk-image",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 6
},
"77aa0188-5c2b-4f47-bda7-7c66b084968d": {
"kind": "QUESTION",
"version": 3,
"slug": "host-io-scheduler",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 4
},
"480f86ac-86fb-4290-9bf5-584763aa1363": {
"kind": "QUESTION",
"version": 3,
"slug": "qemu-disk-cache-mode",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 4
},
"1f76d54b-bb17-4174-8128-0c2d71d4256f": {
"kind": "QUESTION",
"version": 3,
"slug": "vm-disk-backend-mapping",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 5
},
"ed64d4ff-aa01-48cc-ae68-6a683b025cde": {
"kind": "QUESTION",
"version": 3,
"slug": "vm1-storage-load-vs-vm2-latency",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 6
},
"decbab8d-9c98-45c6-b5b8-e6162e407299": {
"kind": "REFERENCE",
"version": 4,
"slug": "a-speedup-that-removed-durability-is-not-an-optimization",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 5
},
"274c5636-33f6-4c19-a54b-d2a96af31289": {
"kind": "REFERENCE",
"version": 3,
"slug": "confirm-the-disk-backend-on-the-host",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 6
},
"b1cda8c4-d31d-4ae4-a4d9-4103a0ab3109": {
"kind": "REFERENCE",
"version": 4,
"slug": "read-storage-before-blaming-cpu-for-latency",
"publication": null,
"projectId": "8a638411-2b21-4202-8bf0-720180d77897",
"topicId": "23314be2-7350-44f5-8989-575c3b40898f",
"relations": 6
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,167 @@
2026-09-17 에 기반 가이드 7단계로 실험대를 처음부터 다시 세우고 A-0 을 그대로 다시
밟은 기록이다. 명령과 출력은 그때 화면에 나온 것을 그대로 옮겼다.
a0-01-baseline.txt 기준선 1~5 — 노드·파드 배치, 파드 IP, ISPN000094 (2), jgroups_ping coord 하나, \dt 100행, 세션 0행
a0-02-cache-entries.txt 기준선 6 — Prometheus 의 vendor_statistics_approximate_entries_unique 통째로와 tr/grep 으로 자른 형태
a0-03-inject-reset.txt 주입 — 세션 행 삭제와 rollout restart, 새 파드 IP
a0-04-test0.txt 시험 0 ①~⑦ — 로그인, 토큰 길이, 클레임 디코드, sid, CID, 두 노드에 grep -c
a0-05-claims.txt JWT 클레임 전체 (패딩을 채워 디코드) — sub 이 없다는 것을 확인
a0-06-test0-cross-node.txt 시험 0 ⑧⑨ — 반대편 refresh 200 · 같은 sid, 로그아웃 204, 발급 노드에서 400 Session not active
a0-07-test0b-cache-delta.txt 시험 0b — 로그인 한 번에 발급 노드만 +1, 반대편 +0
a0-08-test0c-ownership.txt 시험 0c — 몰아준 쪽만 늘어 9 + 5 = 14, DB 총계 14
a0-09-test0d-sql.txt 시험 0d 첫 시도 — 문장 로깅을 켜고 요청 직후에 걸렀더니 반대편 문장이 안 나왔다
a0-10-test0d-retry.txt 같은 필터를 몇 초 뒤에 — 그래도 안 나온 판. 09 와 함께 「너무 일찍 걸렀다」의 증거다
a0-11-test0d-window.txt 그 시간대에 반대편이 낸 모든 줄 — 디스커버리 질의뿐이었다
a0-12-test0d-controlled.txt 대기를 넣고 다시 — 반대편 pid 가 낸 sid 줄 6개, 세션 테이블 문장 6/12
a0-13-test0d-sql-lines.txt 반대편이 날린 여덟 문장과 pid 로 거른 BEGIN~COMMIT 경계. 끝에 문장 로깅을 되돌렸다
비밀은 옮기지 않았다. 관리자 비밀번호는 길이(19)만 확인했고 값은 어느 파일에도 없다.
A-1 (JGroups 7800 차단)
a1-01-inject.txt 매니페스트 원문과 apply · 적용 시각
a1-02-verify.txt 정책이 잡은 파드, 포트 셋(9000=200 · 8080=200 · 7800=exit 7), 클러스터는 아직 2, /proc/net/tcp6 의 1E78
a1-03-conntrack.txt conntrack 을 두 노드에서 — 빈 화면이 나왔다
a1-04-conntrack-missing.txt 그 빈 화면의 까닭 — conntrack 미설치. 2>/dev/null 이 command not found 를 지웠다
a1-05-conntrack-installed.txt 처방 검증 — 깔고 나니 ESTABLISHED [ASSURED] 두 줄이 나온다
a1-06-forced-partition.txt 파드를 지워 분단 확정 — 두 노드가 각각 1 을 보고
a1-07-readiness.txt 분단된 노드가 0/1 · Ready=False, Endpoints 가 하나로, health/ready 503 과 Failing since
a1-08-readiness-commands.txt 문서가 적은 명령 그대로 — describe 는 조건표를 내고, custom-columns 는 zsh 에서 안 돈다
a1-09-quoting-and-health.txt 따옴표를 씌운 형태와 헬스 본문을 내는 명령. 정문은 Host 헤더로 200
a1-10-cross-node-under-partition.txt 분단 중 네 단계 — 200 · 204 · 200. 로그아웃이 전파되지 않았다 (토큰은 길이만 남기고 가렸다)
a1-11-recover.txt 정책 삭제 뒤 30~60초 안에 두 노드가 다시 2
A-2 (데이터베이스 정상 종료)
a2-01-baseline-probe.txt 상주 탐침 a2-probe 와 정지 전 토큰 (길이만)
a2-02-inject.txt 정지 전 네 경로 200×4, replicas=0, 파드 삭제, Deployment 0/0
a2-03-during-outage.txt 정지 직후 — 네 경로 500×4, JWKS·well-known 200, Connection refused, 그런데 ready 는 아직 true
a2-04-outage-progression.txt 약 50초 뒤 ready false,false · 정문 503 으로 뒤집히는 자리
a2-05-recover.txt replicas=1 에서 36초 만에 ready true,true · 정문 200 · 재시작 0
A-3 (데이터베이스 크래시)
a3-01-design-check.txt 로그인 트랜잭션에도 SET LOCAL synchronous_commit TO OFF 가 붙는다 · WAL 설정 네 줄
a3-02-prepare.txt 세션 비우기, 파드 셋 상태, 루프 파일 주입
a3-03-attempt1.txt 시도 ① 첫 판 — /tmp/sids 가 0줄이었다 (아래 04 가 그 까닭)
a3-04-sids-no-newline.txt busybox sed 가 끝 개행을 안 붙인다 — 9384바이트 0줄. echo 로 감싼 처방을 재서 10줄 확인
a3-05-attempt1-redo.txt 고친 루프로 다시 — 8초에 109건, force delete 는 유실 0
a3-06-attempt2.txt 시도 ② kill -9 1 — RESTARTS 0, 400 = 400, 유실 0
a3-07-attempt3.txt 시도 ③ 백엔드 SIGKILL — crash recovery 로그와 395 vs 392
a3-08-loss-count.txt 문서의 comm 방법으로 센 유실 3건과 그 sid 세 개
a3-09-revert.txt 문장 로깅 off 확인, 세션 정리, 파드 재시작, 탐침 삭제
A-4 (노드 상실)
a4-01-baseline.txt virsh uri·VM 셋·파드 배치·PV 가 kc-lab-2 에 고정된 것·관측 스택 위치
a4-02-inject-4a.txt kc-lab-2 영향 범위와 virsh destroy. 하이퍼바이저는 shut off 인데 쿠버네티스는 +10초에도 Ready
a4-03-4a-progression.txt +28초 Ready/정문 000 → +66초 NotReady/정문 503. 이벤트 NodeNotReady 와 context deadline exceeded
a4-04-eviction-and-recover.txt +4분 49초에 deletionTimestamp. Deployment 는 대체 파드(Pending), StatefulSet 은 대체 없이 Terminating
a4-05-recovery-progression.txt VM 을 켠 뒤 Ready 로 돌아오고 파드가 서고 정문이 200 이 되기까지
a4-06-agent-token-missing.txt 복구가 막힌 진짜 까닭 — 유닛이 지워진 ~/node-token 을 기다린다. 파일을 되돌리자 Ready
a4-07-token-remedy.txt 처방 — /etc/rancher/node-token(root 600)으로 옮기고 유닛을 돌리면 홈 사본을 지워도 뜬다
A-5 (비대칭 분단)
a5-01-attempt1.txt 시도 ① filter FORWARD — 카운터는 9·22 로 올라가는데 클러스터는 2 그대로. 게스트 셸의 $K1 이 비어 있는 것도 여기서 확인
a5-02-attempt2-3.txt 시도 ② 보내는 쪽 raw — 카운터 0. 시도 ③ 받는 쪽 raw — 카운터 16·20, 멤버 1
a5-03-one-direction-watch.txt 한 방향만 막은 채 2분 — 잠깐 1 이었다가 2 로 돌아온다 (문서의 「열린 방향으로 다시 붙는다」)
a5-04-bidirectional.txt 양방향 — 멤버 1·1 과 jgroups_ping 의 coord 가 둘 다 t
a5-05-revert.txt 두 체인을 비우자 2 와 coord 하나로 복귀
A-6 (지연 주입)
a6-01-attempt1-2.txt 시도 ① eth0 가 없다 · ip -brief link 목록 · enp1s0 는 노드 IP 만, flannel.1 은 파드 IP 가 보인다 (문서가 미검증으로 둔 두 줄)
a6-02-inject.txt 주입 전 로그인 0.064~0.068s, flannel.1 에 prio·netem·filter 세 줄
a6-03-effect.txt 주입 뒤 1.86~1.91s (문서의 1,872ms 와 28배가 그대로) · netem 83 pkt · tc filter show 의 match 0a2a011d
a6-04-revert.txt qdisc del 뒤 noqueue 로 돌아가고 시간도 0.04~0.07s 로 복귀
A-7 (volatile 로 A층 넷을 다시)
a7-01-switch-volatile.txt 세션 비우기, args 를 --features-disabled=persistent-user-sessions 로 patch, 롤아웃
a7-02-cross-node-volatile.txt 교차 노드 refresh 200 인데 DB 세션 행은 (0 rows)
a7-03-block-7800.txt 캐시가 keycloak-0 에만 1, 그 뒤 NetworkPolicy 로 7800 차단 — 멤버 1·1
a7-04-partition-breaks-sharing.txt 분단 + volatile 에서 400·400·400 (persistent 의 200·204·200 과 정반대)
a7-05-revert.txt NetworkPolicy 삭제와 args 원복
A-7a (그 500 의 원인과 캐시 온도 셋)
a7a-01-setup.txt 문장 로깅 on, volatile 전환, StatefulSet 밖의 탐침
a7a-02-repro-A.txt 재현 A 완전 냉시동 — 로그인이 400 unauthorized_client
a7a-03-repro-A-sql.txt 문서의 grep 이 한 줄도 못 잡는다. 속 예외가 JDBCConnectionException 쪽이었다
a7a-04-repro-B.txt 재현 B 로그인 한 번으로 덥힌 뒤 DB 정지 — refresh 가 500 unknown_error
a7a-05-repro-C-and-revert.txt 재현 C — DB 를 켜니 같은 refresh 가 200. 로깅 off 와 args 원복까지
A-8 (롤링 재시작)
a8-01-baseline.txt 파드·args·DB 0건, 토큰과 sid 를 탐침에 담고 대조군 refresh 200, 그 sid 의 DB 행
a8-02-restart.txt 재시작 전 캐시 0, rollout restart, AGE 로 교체 확인. (12번은 탐침의 낡은 K0 때문에 000 이 났다)
a8-03-main-test.txt 새 IP 로 다시 — 재시작 전 토큰이 200, created_on 은 그대로이고 last_session_refresh 만 갱신 (토큰은 길이만 남기고 가렸다)
D-3 (비밀 노출 네 경로)
d3-01-secret-paths.txt Secret 목록·describe 의 길이 표시·키 이름·길이, 카나리아 주입, 암호화 Disabled, state.db 0 / -wal 1
d3-02-paths-and-revert.txt strings 미설치, RBAC 는 둘 다 no, 카나리아 삭제 뒤 -wal 이 1 → 7 로 늘었다
d3-03-strings-missing.txt binutils 를 깔면 strings 가 생기고, 안 깔아도 grep -c 로 같은 7 이 나온다
B층 준비 (B-0 의 실행 가능한 부분)
b0-01-image-import.txt 워크스테이션에서 두 이미지를 빌드해 docker save | ssh | k3s ctr images import 로 두 노드에
b0-02-realm-and-user.txt kcadm 로그인, realm keycloak-patterns · client bff-confidential · user labuser 생성
b0-03-deploy-bff.txt bff-redis.yaml 적용 — Secret·PVC·redis·bff·Service·Ingress
b0-04-bff-status.txt bff 두 replica 와 redis 가 1/1. imagePullPolicy Never 로 반입 이미지가 붙었다
B-3 (refresh 회전 경쟁) — 전제 검증까지
b3-01-setup.txt realm 세 값(revokeRefreshToken false · maxReuse 0 · lifespan 60), 탐침, CS길이 14
b3-02-secret-and-token.txt B-0 이 없다고 한 비밀 대조를 만들어 「두 값이 같다」. 그런데 토큰 요청이 unauthorized_client
b3-03-direct-grant-missing.txt 원인 — directAccessGrantsEnabled 가 false. 켜니 invalid_grant 로 바뀐다
b3-04-account-not-fully-set-up.txt 둘째 원인 — labuser 의 emailVerified false. 고치니 토큰이 나온다 (rt=735자)
b3-05-race.txt 회전을 켜고 같은 토큰으로 동시에 5번 — 400 넷·200 하나, 이긴 토큰도 400 (토큰 조각은 가렸다)
b3-06-revert.txt revokeRefreshToken 원복과 탐침 삭제
D-1 (백업과 복구)
d1-01-backup.txt 기준선 101 테이블·세션 6건, pg_dump 로 976873 bytes·8569 줄·CREATE TABLE 101
d1-02-destroy-restore.txt DROP SCHEMA 뒤 남은 테이블 0 인데 정문은 200, 6초 만에 복구되고 ERROR 0, 101/6 으로 복귀
D-2 (판올림과 롤백)
d2-01-baseline.txt 태그 26.7.0, databasechangelog 210, 마지막 다섯 줄, 세션 6, quay 에 26.7.3 존재(200)
d2-02-upgrade.txt 26.7.3 정방향 85초 — 스키마 210 그대로, 세션 보존, 정문 200
d2-03-rollback-and-reverse.txt 롤백 67초 성공. 역방향 26.0 은 liquibase Validation Failed 로 keycloak-1 만 못 뜨고 정문은 200
d2-04-revert.txt 체크섬 줄(1 changesets check sum)과 26.7.0 원복
B-6 (키 회전) — 전제까지만
b6-01-deploy-echo.txt echo.yaml 로 header-lab 네임스페이스와 echo 두 replica (반입 이미지로 뜬다)
b6-02-jwks-before.txt JWKS 를 Host 헤더로 받는다. RS256 1개, kid 둘
b6-03-providers.txt kcadm 세션이 파드 교체로 사라진 것과 재로그인. 문서가 「빈 결과」라던 질의가 실제로는 결과를 낸다
b6-04-old-token.txt 토큰 1397자. echo 는 유효한 토큰에 401, 토큰 없이는 200
b6-05-echo-401.txt 그 까닭 — echo 의 issuer/jwk URI 가 https 이고 닿지 않는다. kid 를 뽑으려면 공백을 허용해야 한다
b6-06-kid-and-issuer.txt 두 URI 원문, issuer=000(exit 7), 헤더의 "kid" : "…" 공백 형태와 두 sed 비교
B-4 (신원 헤더 위조) — TLS 를 Host 헤더로 대체해 실행
b4-01-forged-headers.txt Ingress 네 줄, echo 응답 전문(들여쓰고 콜론 양옆에 공백), 대조군은 비어 있다
b4-02-forged-headers-retry.txt 동명 헤더 둘 · 값 안의 쉼표 · 위조한 신원이 전부 그대로 도착. 문서의 콜론 붙인 grep 은 한 줄도 못 잡는다
C-2 (백채널 로그아웃) — 브라우저 없이 되는 두 단계
c2-01-backchannel-probe.txt 세 후보 경로가 전부 302 이고 Location 이 로그인 시작점. 워크스테이션 grep 도 빈 출력(디렉터리는 있다)
B-7a / B-7 (엣지 세션) — 전제에서 막힌 자리를 쟀다
b7a-01-oauth2-proxy.txt oauth2-proxy 배포가 롤아웃 타임아웃. redis-cli dbsize 0 과 빈 스캔은 정상 동작
b7a-02-oauth2-proxy-blocked.txt 까닭 — 기동 시 OIDC 디스커버리가 https://auth.hyeonworks.com 으로 가고 connection refused. CrashLoopBackOff
B-0 / B-1 / B-2 — 브라우저 없이 되는 측정
b1-01-beans.txt /actuator/beans 빈 437개, authorizedClientService 가 JdbcOAuth2AuthorizedClientService, RedisSessionConfiguration 존재. 문서의 미검증 두 줄이 돈다
b2-01-jdbc-table.txt oauth2_authorized_client 표는 생겨 있고 행은 0 (행은 브라우저 로그인 뒤에 생긴다)
D-4 / D-4a (인증서 갱신과 배포 훅) — 인증서 없이 되는 부분
d4-01-renewal-automation.txt certbot 은 kc-lab-edge 에 있고 test-server 에는 타이머가 없다. 유닛 이름은 certbot.timer/.service (certbot-renew.* 아님). 기본 유닛에 ExecStartPost 도 --deploy-hook 도 없고 훅 디렉터리도 비었다. certificates 는 No certificates found
d4a-01-hook-state.txt test-server 에 nginx 프로세스가 없다(엣지에 있다). 훅 디렉터리 total 8. lab host 와 엣지의 시계가 94초 어긋나 있다
B-5 (Redis 상실)
b5-01-baseline.txt Redis PONG·키 없음·save 와 appendonly·/data 가 PVC redis-data
b5-02-redis-down.txt replicas=0 뒤 health 는 DOWN 인데 readiness 는 UP. 파드 둘 다 1/1, 엔드포인트도 ready true,true
b5-03-revert.txt replicas=1 로 되살리자 health 가 다시 UP
C-1 (여러 앱의 SSO) — 브라우저 없이 되는 세션 정리 구간
c1-01-clear-sessions.txt logout-all 을 쳐도 세션이 6건 그대로. 자식 1633 · 부모 6 순서로 지워야 한다. flushall·롤아웃 뒤 둘 다 0. 롤아웃이 kcadm 세션을 날린다
이 폴더가 덮는 범위
-------------------
2026-09-17 에 기반 가이드 7단계로 실험대를 철거하고 새로 세운 뒤 kss 26편을 순서대로
밟은 기록이다. 끝까지 밟은 편이 17, 되는 데까지 밟은 편이 9 다. 어느 편이 어디까지인지는
SSOT 의 「2026-09-17 재현 — 어디까지 밟았고 무엇이 막았나」와 각 기록의
「이 실험대에서 아직 못 밟은 단계」에 적혀 있다.
막은 것은 하나다 — https://auth.hyeonworks.com 이 서지 않는다. 와일드카드 인증서
(Cloudflare API 토큰)와 호스트의 libvirt guest_input 구멍(호스트 sudo)이 둘 다 필요하다.
비밀은 어느 파일에도 값으로 들어 있지 않다. 비밀번호는 길이(19·22)만 적었고, 화면에
찍혀 나온 토큰은 <REDACTED n자> 로 가렸다.
@@ -0,0 +1,131 @@
=== a0 기준선 1 노드·파드 ===
NAME STATUS ROLES AGE VERSION
kc-lab-1 Ready control-plane 18m v1.36.4+k3s1
kc-lab-2 Ready <none> 16m v1.36.4+k3s1
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
keycloak-0 1/1 Running 0 8m46s 10.42.1.3 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 8m46s 10.42.0.9 kc-lab-1 <none> <none>
postgres-7b474b88c8-s6vdh 1/1 Running 0 8m46s 10.42.1.5 kc-lab-2 <none> <none>
=== a0 기준선 2 파드 IP ===
10.42.1.3 10.42.0.9
=== a0 기준선 3 클러스터 뷰 ===
2026-09-17 04:35:25,386 INFO [org.infinispan.CLUSTER] (executor-thread-1) ISPN000094: Received new cluster view for channel ISPN: [keycloak-1-3159(v=16.0.12)|1] (2) [keycloak-1-3159(v=16.0.12), keycloak-0-13057(v=16.0.12)]
2026-09-17 04:35:25,333 INFO [org.infinispan.CLUSTER] (jgroups-5,keycloak-1-3159(v=16.0.12)) ISPN000094: Received new cluster view for channel ISPN: [keycloak-1-3159(v=16.0.12)|1] (2) [keycloak-1-3159(v=16.0.12), keycloak-0-13057(v=16.0.12)]
=== a0 기준선 4 디스커버리 ===
name | ip | coord
------------------+----------------+-------
keycloak-0-13057 | 10.42.1.3:7800 | f
keycloak-1-3159 | 10.42.0.9:7800 | t
(2 rows)
=== a0 기준선 5 테이블 목록 ===
List of relations
Schema | Name | Type | Owner
--------+-------------------------------+-------+----------
public | admin_event_entity | table | keycloak
public | associated_policy | table | keycloak
public | auth_session | table | keycloak
public | authentication_execution | table | keycloak
public | authentication_flow | table | keycloak
public | authenticator_config | table | keycloak
public | authenticator_config_entry | table | keycloak
public | broker_link | table | keycloak
public | client | table | keycloak
public | client_attributes | table | keycloak
public | client_auth_flow_bindings | table | keycloak
public | client_initial_access | table | keycloak
public | client_node_registrations | table | keycloak
public | client_scope | table | keycloak
public | client_scope_attributes | table | keycloak
public | client_scope_client | table | keycloak
public | client_scope_role_mapping | table | keycloak
public | cluster_event | table | keycloak
public | component | table | keycloak
public | component_config | table | keycloak
public | composite_role | table | keycloak
public | credential | table | keycloak
public | databasechangelog | table | keycloak
public | databasechangeloglock | table | keycloak
public | default_client_scope | table | keycloak
public | event_entity | table | keycloak
public | fed_issued_ver_credential | table | keycloak
public | fed_user_attribute | table | keycloak
public | fed_user_consent | table | keycloak
public | fed_user_consent_cl_scope | table | keycloak
public | fed_user_credential | table | keycloak
public | fed_user_group_membership | table | keycloak
public | fed_user_required_action | table | keycloak
public | fed_user_role_mapping | table | keycloak
public | fed_user_ver_credential | table | keycloak
public | federated_identity | table | keycloak
public | federated_user | table | keycloak
public | group_attribute | table | keycloak
public | group_role_mapping | table | keycloak
public | identity_provider | table | keycloak
public | identity_provider_config | table | keycloak
public | identity_provider_mapper | table | keycloak
public | idp_mapper_config | table | keycloak
public | issued_ver_credential | table | keycloak
public | jgroups_ping | table | keycloak
public | keycloak_group | table | keycloak
public | keycloak_role | table | keycloak
public | login_failure | table | keycloak
public | migration_model | table | keycloak
public | offline_client_session | table | keycloak
public | offline_user_session | table | keycloak
public | org | table | keycloak
public | org_domain | table | keycloak
public | org_invitation | table | keycloak
public | outbox_entry | table | keycloak
public | policy_config | table | keycloak
public | protocol_mapper | table | keycloak
public | protocol_mapper_config | table | keycloak
public | realm | table | keycloak
public | realm_attribute | table | keycloak
public | realm_default_groups | table | keycloak
public | realm_enabled_event_types | table | keycloak
public | realm_events_listeners | table | keycloak
public | realm_localizations | table | keycloak
public | realm_required_credential | table | keycloak
public | realm_smtp_config | table | keycloak
public | realm_supported_locales | table | keycloak
public | redirect_uris | table | keycloak
public | required_action_config | table | keycloak
public | required_action_provider | table | keycloak
public | resource_attribute | table | keycloak
public | resource_policy | table | keycloak
public | resource_scope | table | keycloak
public | resource_server | table | keycloak
public | resource_server_perm_ticket | table | keycloak
public | resource_server_policy | table | keycloak
public | resource_server_resource | table | keycloak
public | resource_server_scope | table | keycloak
public | resource_uris | table | keycloak
public | revoked_token | table | keycloak
public | role_attribute | table | keycloak
public | root_auth_session | table | keycloak
public | scope_mapping | table | keycloak
public | scope_policy | table | keycloak
public | server_config | table | keycloak
public | single_use_object | table | keycloak
public | user_attribute | table | keycloak
public | user_consent | table | keycloak
public | user_consent_client_scope | table | keycloak
public | user_entity | table | keycloak
public | user_federation_config | table | keycloak
public | user_federation_mapper | table | keycloak
public | user_federation_mapper_config | table | keycloak
public | user_federation_provider | table | keycloak
public | user_group_membership | table | keycloak
public | user_required_action | table | keycloak
public | user_role_mapping | table | keycloak
public | user_ver_credential | table | keycloak
public | web_origins | table | keycloak
public | workflow_state | table | keycloak
(100 rows)
=== a0 기준선 5b 세션 수 ===
offline_flag | count
--------------+-------
(0 rows)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,19 @@
=== a0 주입 1-① 세션 행을 지운다 ===
DELETE 0
=== a0 주입 1-② 재시각과 롤아웃 ===
13:45:12 재시작
statefulset.apps/keycloak restarted
=== a0 주입 1-③ 다 설 때까지 ===
Waiting for partitioned roll out to finish: 0 out of 2 new pods have been updated...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
Waiting for partitioned roll out to finish: 1 out of 2 new pods have been updated...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...
=== a0 주입 검증 — 새 파드와 새 IP ===
keycloak-0 1/1 Running 0 21s 10.42.1.7 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 43s 10.42.0.13 kc-lab-1 <none> <none>
10.42.1.7 10.42.0.13
@@ -0,0 +1,13 @@
### 탐침 파드 환경변수 (길이만)
K0=10.42.1.7 K1=10.42.0.13 PW길이=19
### 시험 0-① 발급 노드에 로그인하고 응답을 그대로 본다
{"access_token":"<REDACTED 198자 · 화면에 잘려 나온 조각>"exp":1789620334,"iat":1789620274,"jti":"onltro:32cc3942-8475-c164-0629-9512d7325f7c","iss":"https://auth.hyeonworks.com/realms/master","typ":"Bearer","azp":"admin-cli","sid":"MjPiYcA-AWfeos1E8AaUtSeD","scope":"profile email
### 시험 0-④ sid 만 뽑는다 (문서가 미검증으로 둔 줄)
SID=MjPiYcA-AWfeos1E8AaUtSeD
### 시험 0-⑥ CID (문서가 미검증으로 둔 줄)
CID=2d7893fb-6003-4f0e-b6a8-d6e7e9e8fa25
### 시험 0-⑦ 같은 질문을 두 노드에 (문서가 미검증으로 둔 루프)
10.42.1.7 : 1
10.42.0.13 : 1
### 반대편 노드에 refresh 를 먼저 쓴다
{"access_token":"<REDACTED 91자 · 화면에 잘려 나온 조각>
@@ -0,0 +1,8 @@
### 클레임 전체 (패딩을 채워 디코드)
{"exp":1789620347,"iat":1789620287,"jti":"onltro:ba0c11cf-5e4a-a378-7268-4efd5cef32b8","iss":"https://auth.hyeonworks.com/realms/master","typ":"Bearer","azp":"admin-cli","sid":"jbFsOn6E0EPyjFf8PwNJ-1q7","scope":"profile email"}
### 클레임 이름만
azp exp iat iss jti scope sid typ
### sub 이 있는가
0
### 문서의 형태(패딩 없이)는 어디서 끊기나
0EPyjFf8PwNJ-1q7","scope":"profile email
@@ -0,0 +1,9 @@
발급 sid = 71afhj32x97hsvCFqQYB2KaV
### 시험 0-⑧ 반대편 노드에서 refresh (기대 200, sid 같음)
200
갱신 sid = 71afhj32x97hsvCFqQYB2KaV
### 시험 0-⑨ 반대편에서 로그아웃 (기대 204)
204
### 그 뒤 발급 노드에서 갱신 시도 (기대 400 invalid_grant / Session not active)
{"error":"invalid_grant","error_description":"Session not active"}
400
@@ -0,0 +1,13 @@
=== 시험 0b 전값 ===
keycloak-0 clientSessions 3
keycloak-0 sessions 3
keycloak-1 clientSessions 0
keycloak-1 sessions 0
=== 발급 노드(keycloak-0) 에만 로그인 한 번 ===
200
=== 30초 기다린다 (스크레이프 간격 15초) ===
=== 시험 0b 후값 ===
keycloak-0 clientSessions 4
keycloak-0 sessions 4
keycloak-1 clientSessions 0
keycloak-1 sessions 0
@@ -0,0 +1,27 @@
keycloak-0 = 10.42.1.7
keycloak-1 = 10.42.0.13
=== 시험 0c 시작값 ===
keycloak-0 clientSessions 4
keycloak-0 sessions 4
keycloak-1 clientSessions 0
keycloak-1 sessions 0
=== ① 반대편(keycloak-1) 에 로그인 5회 ===
200 200 200 200 200
=== 5회 뒤 ===
keycloak-0 clientSessions 4
keycloak-0 sessions 4
keycloak-1 clientSessions 5
keycloak-1 sessions 5
=== ② 발급 노드(keycloak-0) 에 로그인 5회 ===
200 200 200 200 200
=== 5회 더 뒤 ===
keycloak-0 clientSessions 9
keycloak-0 sessions 9
keycloak-1 clientSessions 5
keycloak-1 sessions 5
=== 대조: PostgreSQL 에는 몇 건인가 ===
count
-------
14
(1 row)
@@ -0,0 +1,32 @@
=== 0d-① 문장 로깅을 켠다 ===
ALTER SYSTEM
ALTER SYSTEM
pg_reload_conf
----------------
t
(1 row)
=== 0d-② 적용됐는지 읽는다 ===
log_statement
---------------
all
(1 row)
log_line_prefix
-----------------
%m [%p] %h
(1 row)
K0=10.42.1.7 K1=10.42.0.13
=== 0d-③ 로그인 한 번 · 반대편에서 refresh 한 번 ===
SID=wITYiY7I4t8Ovy9wzwM1ewOA
REFRESH_ON_K1=200
=== 반대편 노드가 날린 문장만 ===
2026-09-17 04:47:30.547 UTC [916] 10.42.0.13 LOG: execute <unnamed>: SELECT address, name, ip, coord, coordinated_by, last_update FROM JGROUPS_PING WHERE cluster_name=$1
=== 그 sid 가 들어간 줄만 120자씩 ===
2026-09-17 04:47:31.285 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '0', $2 = 'wITYiY7I4t8Ovy9wzwM1ewOA'
2026-09-17 04:47:31.288 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '2d7893fb-6003-4f0e-b6a8-d6e7e9e8fa25', $2 = 'loca
2026-09-17 04:47:31.294 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '0', $2 = 'wITYiY7I4t8Ovy9wzwM1ewOA'
2026-09-17 04:47:31.295 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '2d7893fb-6003-4f0e-b6a8-d6e7e9e8fa25', $2 = 'loca
2026-09-17 04:47:31.296 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = NULL, $2 = '1789620451', $3 = '{"ipAddress":"10.42
2026-09-17 04:47:31.296 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '{"authMethod":"openid-connect","notes":{"clientId
@@ -0,0 +1,10 @@
=== 좀 지난 뒤 같은 필터를 다시 (K1=10.42.0.13) ===
2026-09-17 04:47:43.568 UTC [915] 10.42.0.13 LOG: execute S_2: COMMIT
2026-09-17 04:47:43.580 UTC [915] 10.42.0.13 LOG: execute S_2: COMMIT
=== 그 sid 가 들어간 줄 전부 ===
2026-09-17 04:47:31.285 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '0', $2 = 'wITYiY7I4t8Ovy9wzwM1ewOA'
2026-09-17 04:47:31.288 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '2d7893fb-6003-4f0e-b6a8-d6e7e9e8fa25', $2 = 'loca
2026-09-17 04:47:31.294 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '0', $2 = 'wITYiY7I4t8Ovy9wzwM1ewOA'
2026-09-17 04:47:31.295 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '2d7893fb-6003-4f0e-b6a8-d6e7e9e8fa25', $2 = 'loca
2026-09-17 04:47:31.296 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = NULL, $2 = '1789620451', $3 = '{"ipAddress":"10.42
2026-09-17 04:47:31.296 UTC [948] 10.42.1.7 DETAIL: parameters: $1 = '{"authMethod":"openid-connect","notes":{"clientId
@@ -0,0 +1,21 @@
=== 04:47:31~04:47:50 사이 10.42.0.13 이 낸 모든 줄 ===
2026-09-17 04:47:40.548 UTC [915] 10.42.0.13 LOG: execute S_34: SELECT address, name, ip, coord, coordinated_by, last_update FROM JGROUPS_PING WHERE cluster_name=$1
2026-09-17 04:47:40.548 UTC [915] 10.42.0.13 DETAIL: parameters: $1 = 'ISPN'
2026-09-17 04:47:40.548 UTC [916] 10.42.0.13 LOG: statement:
2026-09-17 04:47:43.566 UTC [915] 10.42.0.13 LOG: statement: BEGIN
2026-09-17 04:47:43.566 UTC [915] 10.42.0.13 LOG: execute S_36: DELETE from JGROUPS_PING WHERE address=$1
2026-09-17 04:47:43.566 UTC [915] 10.42.0.13 DETAIL: parameters: $1 = 'uuid://00000000-0000-0000-0000-000000000003'
2026-09-17 04:47:43.567 UTC [915] 10.42.0.13 LOG: execute S_37: INSERT INTO JGROUPS_PING (address, name, cluster_name, ip, coord, last_update, coordinated_by) values ($1
2026-09-17 04:47:43.567 UTC [915] 10.42.0.13 DETAIL: parameters: $1 = 'uuid://00000000-0000-0000-0000-000000000003', $2 = 'keycloak-1-50073', $3 = 'ISPN', $4 = '10.42.0.
2026-09-17 04:47:43.568 UTC [915] 10.42.0.13 LOG: execute S_2: COMMIT
2026-09-17 04:47:43.578 UTC [915] 10.42.0.13 LOG: statement: BEGIN
2026-09-17 04:47:43.578 UTC [915] 10.42.0.13 LOG: execute S_36: DELETE from JGROUPS_PING WHERE address=$1
2026-09-17 04:47:43.578 UTC [915] 10.42.0.13 DETAIL: parameters: $1 = 'uuid://00000000-0000-0000-0000-000000000004'
2026-09-17 04:47:43.580 UTC [915] 10.42.0.13 LOG: execute S_37: INSERT INTO JGROUPS_PING (address, name, cluster_name, ip, coord, last_update, coordinated_by) values ($1
2026-09-17 04:47:43.580 UTC [915] 10.42.0.13 DETAIL: parameters: $1 = 'uuid://00000000-0000-0000-0000-000000000004', $2 = 'keycloak-0-25228', $3 = 'ISPN', $4 = '10.42.1.
2026-09-17 04:47:43.580 UTC [915] 10.42.0.13 LOG: execute S_2: COMMIT
2026-09-17 04:47:48.315 UTC [915] 10.42.0.13 LOG: execute S_34: SELECT address, name, ip, coord, coordinated_by, last_update FROM JGROUPS_PING WHERE cluster_name=$1
2026-09-17 04:47:48.315 UTC [915] 10.42.0.13 DETAIL: parameters: $1 = 'ISPN'
=== 같은 창의 줄 수 (pod 별) ===
17
26
@@ -0,0 +1,20 @@
K0(keycloak-0)=10.42.1.7 K1(keycloak-1)=10.42.0.13
=== 캐시 전값 ===
keycloak-0 clientSessions 10
keycloak-0 sessions 10
keycloak-1 clientSessions 5
keycloak-1 sessions 5
SID=j7eRVo7v08st1EAoQj1WLcjG
MARK_BEFORE_REFRESH
REFRESH_ON_K1=200
=== 그 sid 가 들어간 DB 줄 — 어느 파드가 냈나 ===
6 [915]
6 [948]
=== 같은 창에서 세션 테이블을 건드린 문장을 파드별로 ===
6 [915]
12 [948]
=== 캐시 후값 (refresh 를 받은 keycloak-1 이 늘었나) ===
keycloak-0 clientSessions 11
keycloak-0 sessions 11
keycloak-1 clientSessions 5
keycloak-1 sessions 5
@@ -0,0 +1,57 @@
=== 반대편 노드(keycloak-1, pid 915) 가 날린 문장 — 문서의 여덟 줄과 대조 ===
[915] 10.42.0.13 LOG: statement:
[916] 10.42.0.13 LOG: statement:
[915] 10.42.0.13 LOG: statement: BEGIN
[915] 10.42.0.13 LOG: execute S_2: COMMIT
[915] 10.42.0.13 LOG: statement: BEGIN
[915] 10.42.0.13 LOG: execute S_2: COMMIT
[916] 10.42.0.13 LOG: statement:
[915] 10.42.0.13 LOG: statement:
[916] 10.42.0.13 LOG: statement:
[917] 10.42.0.13 LOG: statement:
[916] 10.42.0.13 LOG: statement:
[916] 10.42.0.13 LOG: statement:
[915] 10.42.0.13 LOG: statement: BEGIN
[915] 10.42.0.13 LOG: execute S_54/C_73: select puse1_0.OFFLINE_FLAG,puse1_0.USER_SESSION_ID,puse1_0.BROKER_SESSION_ID,puse1_0.CREATED_ON,puse1_0.DATA,puse1_0.LAST_
[915] 10.42.0.13 LOG: execute <unnamed>/C_74: select puse1_0.VERSION from OFFLINE_USER_SESSION puse1_0 where puse1_0.USER_SESSION_ID=$1 and puse1_0.OFFLINE_FLAG=$2
[915] 10.42.0.13 LOG: execute S_56/C_75: select pcse1_0.CLIENT_ID,pcse1_0.CLIENT_STORAGE_PROVIDER,pcse1_0.EXTERNAL_CLIENT_ID,pcse1_0.OFFLINE_FLAG,pcse1_0.USER_SESSI
[915] 10.42.0.13 LOG: execute <unnamed>/C_76: select pcse1_0.VERSION from OFFLINE_CLIENT_SESSION pcse1_0 where pcse1_0.USER_SESSION_ID=$1 and pcse1_0.OFFLINE_FLAG=$
[915] 10.42.0.13 LOG: execute <unnamed>: update OFFLINE_CLIENT_SESSION set TIMESTAMP=$1,VERSION=$2 where CLIENT_ID=$3 and CLIENT_STORAGE_PROVIDER=$4 and EXTERNAL_CL
[915] 10.42.0.13 LOG: execute <unnamed>: update OFFLINE_USER_SESSION set LAST_SESSION_REFRESH=$1,VERSION=$2 where OFFLINE_FLAG=$3 and USER_SESSION_ID=$4 and VERSION
[915] 10.42.0.13 LOG: execute <unnamed>: SET LOCAL synchronous_commit TO OFF
[915] 10.42.0.13 LOG: execute S_2: COMMIT
[915] 10.42.0.13 LOG: statement: BEGIN
[915] 10.42.0.13 LOG: execute S_2: COMMIT
[915] 10.42.0.13 LOG: statement: BEGIN
[915] 10.42.0.13 LOG: execute S_2: COMMIT
[916] 10.42.0.13 LOG: statement:
[916] 10.42.0.13 LOG: statement:
[916] 10.42.0.13 LOG: statement:
[915] 10.42.0.13 LOG: statement: BEGIN
[915] 10.42.0.13 LOG: execute S_2: COMMIT
[915] 10.42.0.13 LOG: statement: BEGIN
[915] 10.42.0.13 LOG: execute S_2: COMMIT
[916] 10.42.0.13 LOG: statement:
=== 그 트랜잭션의 경계 (pid 915 의 BEGIN~COMMIT) ===
2026-09-17 04:48:13.896 UTC [915] 10.42.0.13 LOG: statement: BEGIN
2026-09-17 04:48:13.898 UTC [915] 10.42.0.13 LOG: execute S_2: COMMIT
2026-09-17 04:48:13.908 UTC [915] 10.42.0.13 LOG: statement: BEGIN
2026-09-17 04:48:13.912 UTC [915] 10.42.0.13 LOG: execute S_2: COMMIT
2026-09-17 04:48:45.920 UTC [915] 10.42.0.13 LOG: statement: BEGIN
2026-09-17 04:48:45.922 UTC [915] 10.42.0.13 LOG: execute S_2: COMMIT
2026-09-17 04:48:45.933 UTC [915] 10.42.0.13 LOG: statement: BEGIN
2026-09-17 04:48:45.934 UTC [915] 10.42.0.13 LOG: execute S_2: COMMIT
=== 되돌리기 — 문장 로깅을 끈다 ===
ALTER SYSTEM
ALTER SYSTEM
pg_reload_conf
----------------
t
(1 row)
log_statement
---------------
none
(1 row)
@@ -0,0 +1,50 @@
=== a1 주입 1-① 매니페스트를 먼저 읽는다 ===
# Experiment A-1 — cut the JGroups transport (TCP 7800) while leaving discovery alone.
#
# The point is to separate two things that are easy to conflate:
#
# discovery how the nodes FIND each other -> PostgreSQL JGROUPS_PING table
# transport how they actually TALK -> TCP 7800
#
# Blocking only the transport produces a state that cannot happen on a single
# node: both members stay registered in the database, so each believes the other
# exists, yet no message gets through.
#
# kubectl apply -f deploy/lab/k8s/a1-block-jgroups-transport.yaml
# kubectl -n keycloak-lab delete networkpolicy a1-block-jgroups-transport
#
# NetworkPolicy is an ALLOWLIST, not a firewall with deny rules. There is no way
# to write "deny 7800". The moment a pod is selected by a policy carrying
# policyTypes: [Ingress], every inbound port is denied unless a rule permits it.
# So 7800 is blocked by *omission*: 8080 and 9000 are listed, 7800 is not.
#
# That makes the two allow rules load-bearing — get them wrong and the experiment
# measures a dead Keycloak instead of a partitioned cluster:
#
# 8080 the HTTP endpoint. Traefik, the other pod's REST calls, and the probe
# traffic all arrive here.
# 9000 the management port: /health/started, /health/ready, /health/live and
# /metrics. Losing it means the kubelet fails the readiness probe and
# kills the pod — the cluster would break for the wrong reason.
#
# Both rules deliberately omit `from:`, which allows those ports from any source.
# Narrowing the source is not the subject here; the 2-hop experiment already
# established how to do that by label when it matters.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: a1-block-jgroups-transport
namespace: keycloak-lab
spec:
podSelector:
matchLabels:
app: keycloak
policyTypes: [Ingress]
ingress:
- ports:
- { port: 8080, protocol: TCP } # HTTP — must stay open
- { port: 9000, protocol: TCP } # health + metrics — must stay open
# 7800 is absent on purpose. That is the whole experiment.
=== a1 주입 1-② 적용하고 시각 ===
networkpolicy.networking.k8s.io/a1-block-jgroups-transport created
13:53:51 적용
@@ -0,0 +1,29 @@
=== a1 검증 1 정책 ===
NAME POD-SELECTOR AGE
a1-block-jgroups-transport app=keycloak 11s
Name: a1-block-jgroups-transport
Namespace: keycloak-lab
Created on: 2026-09-17 13:52:16 +0900 KST
Labels: <none>
Annotations: <none>
Spec:
PodSelector: app=keycloak
Allowing ingress traffic:
To Port: 8080/TCP
To Port: 9000/TCP
From: <any> (traffic not restricted by source)
Not affecting egress traffic
Policy Types: Ingress
=== 파드가 죽지 않았는지 ===
keycloak-0 1/1 Running 0 8m25s 10.42.1.7 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 8m47s 10.42.0.13 kc-lab-1 <none> <none>
=== a1 검증 2 포트 셋 (K0=10.42.1.7) ===
9000 200
8080 200
7800 000
exit=7
=== a1 검증 3 클러스터가 깨졌나 ===
{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"vendor_cluster_size","cache_manager":"keycloak","exported_node":"keycloak-1-50073","instance":"10.42.0.13:9000","job":"keycloak","node":"kc-lab-1","pod":"keycloak-1"},"value":[1789620750.808,"2"]},{"metric":{"__name__":"vendor_cluster_size","cache_manager":"keycloak","exported_node":"keycloak-0-25228","instance":"10.42.1.7:9000","job":"keycloak","node":"kc-lab-2","pod":"keycloak-0"},"value":[1789620750.808,"2"]}]}}
=== 7800 은 16진수로 1E78 ===
4: 0000000000000000FFFF000007012A0A:1E78 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 47139 1 00000000a06c8407 100 0 0 10 0
11: 0000000000000000FFFF000007012A0A:1E78 0000000000000000FFFF00000D002A0A:E94D 01 00000000:00000000 02:000A3CB8 00000000 1000 0 47142 2 00000000c8c90f53 22 4 30 10 -1
@@ -0,0 +1,3 @@
=== a1 검증 4 conntrack — kc-lab-1 ===
--- 같은 것을 2>/dev/null 없이 (요약 줄이 보인다) ---
=== a1 검증 4 conntrack — kc-lab-2 ===
@@ -0,0 +1,12 @@
=== conntrack 이 게스트에 있나 ===
sudo: conntrack: command not found
(exit 0)
--- 실행 파일 ---
ls: cannot access '/usr/sbin/conntrack': No such file or directory
dpkg-query: no packages found matching conntrack
=== nf_conntrack 모듈과 표는 있나 ===
6
218 /proc/net/nf_conntrack
=== 대안: /proc/net/nf_conntrack 에서 7800 을 찾는다 ===
ipv4 2 tcp 6 86399 ESTABLISHED src=10.42.0.13 dst=10.42.1.7 sport=59725 dport=7800 src=10.42.1.7 dst=10.42.0.13 sport=7800 dport=59725 [ASSURED] mark=0 zone=0 use=2
ipv4 2 tcp 6 85891 ESTABLISHED src=10.42.0.13 dst=10.42.1.7 sport=38439 dport=57800 src=10.42.1.7 dst=10.42.0.13 sport=57800 dport=38439 [ASSURED] mark=0 zone=0 use=2
@@ -0,0 +1,11 @@
=== 처방 검증: 두 노드에 conntrack 을 깐다 ===
--- kc-lab-1 ---
Setting up conntrack (1:1.4.7-1+b2) ...
Processing triggers for man-db (2.11.2-2) ...
tcp 6 86398 ESTABLISHED src=10.42.0.13 dst=10.42.1.7 sport=59725 dport=7800 src=10.42.1.7 dst=10.42.0.13 sport=7800 dport=59725 [ASSURED] mark=0 use=1
tcp 6 85885 ESTABLISHED src=10.42.0.13 dst=10.42.1.7 sport=38439 dport=57800 src=10.42.1.7 dst=10.42.0.13 sport=57800 dport=38439 [ASSURED] mark=0 use=1
--- kc-lab-2 ---
Setting up conntrack (1:1.4.7-1+b2) ...
Processing triggers for man-db (2.11.2-2) ...
tcp 6 86397 ESTABLISHED src=10.42.0.13 dst=10.42.1.7 sport=59725 dport=7800 src=10.42.1.7 dst=10.42.0.13 sport=7800 dport=59725 [ASSURED] mark=0 use=1
tcp 6 85884 ESTABLISHED src=10.42.0.13 dst=10.42.1.7 sport=38439 dport=57800 src=10.42.1.7 dst=10.42.0.13 sport=57800 dport=38439 [ASSURED] mark=0 use=1
@@ -0,0 +1,12 @@
=== a1 6-① 시각을 남기고 파드를 지운다 ===
13:55:06 재시작
pod "keycloak-0" deleted from keycloak-lab namespace
=== a1 6-② 새 파드와 새 주소 ===
keycloak-1 1/1 Running 0 9m54s 10.42.0.13 kc-lab-1 <none> <none>
=== 60초 뒤 파드 상태 (분단된 노드가 Ready 를 잃었나) ===
keycloak-0 1/1 Running 0 60s 10.42.1.17 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 10m 10.42.0.13 kc-lab-1 <none> <none>
=== 멤버 수 ===
{"status":"success","data":{"resultType":"vector","result":[{"metric":{"__name__":"vendor_cluster_size","cache_manager":"keycloak","exported_node":"keycloak-1-50073","instance":"10.42.0.13:9000","job":"keycloak","node":"kc-lab-1","pod":"keycloak-1"},"value":[1789620875.437,"1"]},{"metric":{"__name__":"vendor_cluster_size","cache_manager":"keycloak","exported_node":"keycloak-0-53888","instance":"10.42.1.17:9000","job":"keycloak","node":"kc-lab-2","pod":"keycloak-0"},"value":[1789620875.437,"1"]}]}}
=== Service 뒤에 누가 남았나 ===
Endpoints: 10.42.0.13:8080,10.42.1.17:8080
@@ -0,0 +1,40 @@
=== 분단이 확정되고 몇 분 뒤 ===
14:22:18
keycloak-0 0/1 Running 0 27m 10.42.1.17 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 37m 10.42.0.13 kc-lab-1 <none> <none>
--- Ready 조건 ---
PodReadyToStartContainers=True Initialized=True Ready=False ContainersReady=False PodScheduled=True
PodReadyToStartContainers=True Initialized=True Ready=True ContainersReady=True PodScheduled=True
--- readiness 프로브 설정 ---
{"failureThreshold":3,"httpGet":{"path":"/health/ready","port":"management","scheme":"HTTP"},"periodSeconds":10,"successThreshold":1,"timeoutSeconds":1}
--- Service Endpoints ---
Endpoints: 10.42.0.13:8080
--- 두 노드의 health/ready 를 직접 ---
keycloak-0 ready=503
keycloak-1 ready=200
--- keycloak-0 의 실패 항목 ---
{
"status": "DOWN",
"checks": [
{
"name": "Graceful Shutdown",
"status": "UP"
},
{
"name": "Keycloak Initialized",
"status": "UP"
},
{
"name": "Keycloak cluster health check",
"status": "DOWN",
"data": {
"Failing since": "2026-09-17 04:54:25,769"
}
},
{
"name": "Keycloak database connections async health check"
--- 멤버 수 ---
"pod":"keycloak-1"}
"1"]}
"pod":"keycloak-0"}
"1"]}]}}
@@ -0,0 +1,11 @@
=== 문서가 적은 명령 그대로: describe pod | grep -A5 Conditions ===
Conditions:
Type Status
PodReadyToStartContainers True
Initialized True
Ready False
ContainersReady False
=== 문서의 Ready 칸 뽑기 형태 ===
zsh:5: no matches found: custom-columns=NAME:.metadata.name,READY:.status.containerStatuses[0].ready,RESTARTS:.status.containerStatuses[0].restartCount
=== EndpointSlice 형태 ===
zsh:7: no matches found: custom-columns=NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready
@@ -0,0 +1,35 @@
=== 이 호스트의 셸 ===
/bin/zsh
zsh
=== 따옴표를 씌우면 ===
keycloak-0 false 0
keycloak-1 true 0
NAME ADDR READY
keycloak-lxk8h [10.42.0.13],[10.42.1.17] true,false
=== 헬스 본문을 내는 명령 (문서가 JSON 을 실은 자리의 진짜 출처) ===
{
"status": "DOWN",
"checks": [
{
"name": "Graceful Shutdown",
"status": "UP"
},
{
"name": "Keycloak Initialized",
"status": "UP"
},
{
"name": "Keycloak cluster health check",
"status": "DOWN",
"data": {
"Failing since": "2026-09-17 04:54:25,769"
}
},
{
"name": "Keycloak database connections async health check",
"status": "UP"
}
]
}
=== 밖에서 정문 (Host 헤더로 TLS 없이) ===
200
@@ -0,0 +1,9 @@
분단 상태 K0=10.42.1.17 K1=10.42.0.13
# [1] keycloak-0 에서 로그인
sid=XliiKcW3DYw4hY_a3m2FX6e4# [2] keycloak-1 에서 refresh
200
# [3] keycloak-1 에서 로그아웃
204
# [4] keycloak-0 에서 재갱신 시도
{"access_token":"<REDACTED 758자>","expires_in":60,"refresh_expires_in":1800,"refresh_token":"<REDACTED 612자>","token_type":"Bearer","not-before-policy":0,"session_state":"XliiKcW3DYw4hY_a3m2FX6e4","scope":"profile email"}
200
@@ -0,0 +1,14 @@
a1 토큰 노출 경고 추가
=== a1 되돌리기 1-① 시각을 남기고 정책을 지운다 ===
14:23:52 해제
networkpolicy.networking.k8s.io "a1-block-jgroups-transport" deleted from keycloak-lab namespace
=== 30초 간격으로 멤버 수 ===
14:24:22 "pod":"keycloak-1"} "1"]} "pod":"keycloak-0"} "1"]}]}}
14:24:52 "pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}}
14:25:22 "pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}}
14:25:52 "pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}}
=== 파드 상태 ===
keycloak-0 1/1 Running 0 30m 10.42.1.17 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 40m 10.42.0.13 kc-lab-1 <none> <none>
[exited with code 0]
@@ -0,0 +1,9 @@
K0=10.42.1.17 K1=10.42.0.13
=== a2 상주 탐침 파드 ===
pod/a2-probe created
pod/a2-probe condition met
=== 환경변수 (길이만) ===
K0=10.42.1.17 K1=10.42.0.13 PW길이=19
=== 정지 전 네 경로 — 토큰을 파드 안 파일에 담는다 ===
rt0=613자 rt1=613자 at=759자
CID=2d7893fb-6003-4f0e-b6a8-d6e7e9e8fa25
@@ -0,0 +1,18 @@
=== 정지 전 네 경로 ===
① 200
② 200
③ 200
④ 200
=== a2 주입 ① 시각과 replicas=0 ===
14:27:23 정지
deployment.apps/postgres scaled
=== ② 파드가 사라질 때까지 ===
pod/postgres-7b474b88c8-s6vdh condition met
14:27:24 삭제완료
=== 검증 1 파드와 Deployment ===
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
a2-probe 1/1 Running 0 10s 10.42.1.22 kc-lab-2 <none> <none>
keycloak-0 1/1 Running 0 32m 10.42.1.17 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 42m 10.42.0.13 kc-lab-1 <none> <none>
NAME READY UP-TO-DATE AVAILABLE AGE
postgres 0/0 0 0 51m
@@ -0,0 +1,22 @@
=== 정지 직후 네 경로 ===
① 500
② 500
③ 500
④ 500
=== 500 의 본문 ===
{"error":"unknown_error","error_description":"For more on this error consult the server log."}
=== 서명 검증만 필요한 두 경로 ===
JWKS 200
well-known 200
=== 커넥션 풀 로그 ===
Caused by: java.net.ConnectException: Connection refused
at org.postgresql.core.v3.ConnectionFactoryImpl.tryConnect(ConnectionFactoryImpl.java:219)
at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl.java:365)
=== Ready 와 재시작 ===
keycloak-0 true 0
keycloak-1 true 0
=== Endpoints ===
NAME ADDR READY
keycloak-lxk8h [10.42.0.13],[10.42.1.17] true,true
=== 정문 (TLS 없이 Host 헤더로) ===
200
@@ -0,0 +1,3 @@
14:28:13 ready=false,false, 정문=503 엔드포인트=false,false
14:28:43 ready=false,false, 정문=503 엔드포인트=false,false
14:29:13 ready=false,false, 정문=503 엔드포인트=false,false
@@ -0,0 +1,19 @@
=== a2 복구 시각과 replicas=1 ===
14:29:20 복구
deployment.apps/postgres scaled
Waiting for deployment "postgres" rollout to finish: 0 out of 1 new replicas have been updated...
Waiting for deployment "postgres" rollout to finish: 0 of 1 updated replicas are available...
deployment "postgres" successfully rolled out
14:29:56 ready=true,true, 정문=200 재시작=0,0,
14:30:21 ready=true,true, 정문=200 재시작=0,0,
14:30:46 ready=true,true, 정문=200 재시작=0,0,
14:31:12 ready=true,true, 정문=200 재시작=0,0,
=== 복구 뒤 네 경로 (④ 는 토큰이 만료됐을 수 있다) ===
① 200
② 200
③ 200
④ 401
=== 탐침 파드를 지운다 ===
pod "a2-probe" deleted from keycloak-lab namespace
[exited with code 0]
@@ -0,0 +1,44 @@
=== a3 설계 확인 ① 문장 로깅 켜기 ===
t
(1 row)
log_statement
---------------
all
(1 row)
=== ② 탐침 파드 ===
pod/a3-probe created
pod/a3-probe condition met
=== ③ 길이만 ===
19
K0=10.42.1.17 PW길이=19
=== ④ 로그인 한 번 ===
200
=== ⑤ 그 로그인의 트랜잭션 ===
2026-09-17 05:31:55.093 UTC [41] LOG: statement: BEGIN
2026-09-17 05:31:55.099 UTC [41] LOG: execute S_6: COMMIT
2026-09-17 05:31:55.101 UTC [41] LOG: statement: BEGIN
2026-09-17 05:31:55.104 UTC [41] LOG: execute <unnamed>: insert into OFFLINE_USER_SESSION (BROKER_SESSION_ID,CREATED_ON,DATA,LAST_SESSION_REFRESH,REALM_ID,REMEMBER_ME,USER_ID,VERSION,OFFLINE_FLAG,USER_SESSION_ID) values ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)
2026-09-17 05:31:55.104 UTC [41] LOG: execute <unnamed>: insert into OFFLINE_CLIENT_SESSION (DATA,REALM_ID,TIMESTAMP,VERSION,CLIENT_ID,CLIENT_STORAGE_PROVIDER,EXTERNAL_CLIENT_ID,OFFLINE_FLAG,USER_SESSION_ID) values ($1,$2,$3,$4,$5,$6,$7,$8,$9)
2026-09-17 05:31:55.104 UTC [41] LOG: execute <unnamed>: SET LOCAL synchronous_commit TO OFF
2026-09-17 05:31:55.104 UTC [41] LOG: execute S_6: COMMIT
2026-09-17 05:31:59.694 UTC [40] LOG: statement: BEGIN
2026-09-17 05:31:59.696 UTC [40] LOG: execute S_1: COMMIT
=== ⑥ 문장 로깅 끄기 ===
(1 row)
log_statement
---------------
none
(1 row)
=== 커밋·WAL 설정 네 줄 ===
name | setting | unit | source
------------------------+---------+------+---------
commit_delay | 0 | | default
synchronous_commit | on | | default
wal_writer_delay | 200 | ms | default
wal_writer_flush_after | 128 | 8kB | default
(4 rows)
@@ -0,0 +1,15 @@
=== a3 4-① 세션 비우기 ===
DELETE 5
count
-------
0
(1 row)
=== 5. 파드 셋 ===
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
a3-probe 1/1 Running 0 30s 10.42.1.24 kc-lab-2 <none> <none>
keycloak-0 1/1 Running 0 38m 10.42.1.17 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 48m 10.42.0.13 kc-lab-1 <none> <none>
postgres-7b474b88c8-sspdm 1/1 Running 0 4m38s 10.42.1.23 kc-lab-2 <none> <none>
=== 주입 1-② 루프 파일을 파드에 밀어 넣는다 ===
18 /tmp/a3-login-loop.sh
@@ -0,0 +1,18 @@
=== 8초 뒤 쌓인 수 ===
0 /tmp/sids
=== 시도 ① --grace-period=0 --force ===
14:34:15.621 종료
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
pod "postgres-7b474b88c8-sspdm" force deleted from keycloak-lab namespace
14:34:15.722 반환
=== 루프 출력 ===
=== 파드 상태 ===
NAME READY RESTARTS AGE
postgres-7b474b88c8-mh86k true 0 2026-09-17T05:32:42Z
=== 클라이언트가 200 받은 수 vs DB 행 수 ===
0 /tmp/sids
count
-------
217
(1 row)
@@ -0,0 +1,12 @@
=== 왜 0 줄인가 — 파일에 개행이 없다 ===
0 /tmp/sids
9384 /tmp/sids
0022240 K K E Q - g O y
0022250
=== busybox sed 가 끝에 개행을 안 붙인다 ===
0000000 A B C
0000003
=== 처방: echo 로 감싼다 ===
처방 결과: 10 줄 / 250 바이트
iAxu1SRCu_c7uWTAka5MK-Xa
4PU8l__EjhBWQVQTCnjyg_fD
@@ -0,0 +1,18 @@
19 /tmp/a3-login-loop.sh
=== 8초 뒤 쌓인 수 ===
109 /tmp/sids
=== 시도 ① --grace-period=0 --force ===
14:35:47.693 종료
pod "postgres-7b474b88c8-mh86k" force deleted from keycloak-lab namespace
14:35:47.797 반환
=== 루프 출력 ===
=== 새 파드 ===
NAME READY RESTARTS
postgres-7b474b88c8-prlxx true 0
=== 클라이언트 200 수 vs DB 행 수 ===
292 /tmp/sids
count
-------
296
(1 row)
@@ -0,0 +1,19 @@
=== 시도 ① 검증 — 재시작 횟수와 파드 나이 ===
NAME RESTARTS START
postgres-7b474b88c8-prlxx 0 2026-09-17T05:34:14Z
=== crash recovery 로그가 있나 ===
2026-09-17 05:34:14.924 UTC [1] LOG: starting PostgreSQL 16.15 on x86_64-pc-linux-musl, compiled by gcc (Alpine 15.2.0) 15.2.0, 64-bit
2026-09-17 05:34:14.939 UTC [27] LOG: database system was shut down at 2026-09-17 05:34:13 UTC
=== 시도 ② kill -9 1 ===
113 /tmp/sids
14:36:29.106 SIGKILL
=== 시도 ② 결과 ===
NAME READY RESTARTS
postgres-7b474b88c8-prlxx true 0
400 /tmp/sids
count
-------
400
(1 row)
@@ -0,0 +1,34 @@
=== 시도 ③ 준비 — ps 로 무엇을 죽일지 본다 ===
PID USER TIME COMMAND
1 postgres 0:00 postgres
25 postgres 0:00 postgres: checkpointer
26 postgres 0:00 postgres: background writer
28 postgres 0:00 postgres: walwriter
29 postgres 0:00 postgres: autovacuum launcher
30 postgres 0:00 postgres: logical replication launcher
39 postgres 0:00 postgres: keycloak keycloak 10.42.1.17(44654) idle
40 postgres 0:00 postgres: keycloak keycloak 10.42.1.17(44660) idle
41 postgres 0:00 postgres: keycloak keycloak 10.42.1.17(44670) idle
42 postgres 0:00 postgres: keycloak keycloak 10.42.1.17(44686) idle
43 postgres 0:00 postgres: keycloak keycloak 10.42.1.17(44690) idle
=== 죽이기 직전 쌓인 수 ===
113 /tmp/sids
죽일 백엔드 PID=39
14:37:10.925 백엔드 SIGKILL
=== 시도 ③ 결과 ===
NAME READY RESTARTS
postgres-7b474b88c8-prlxx true 0
--- crash recovery 로그 ---
2026-09-17 05:35:36.810 UTC [1] LOG: server process (PID 39) was terminated by signal 9: Killed
2026-09-17 05:35:36.810 UTC [1] LOG: terminating any other active server processes
2026-09-17 05:35:36.817 UTC [1] LOG: all server processes terminated; reinitializing
2026-09-17 05:35:36.863 UTC [206] LOG: database system was not properly shut down; automatic recovery in progress
2026-09-17 05:35:36.885 UTC [207] LOG: checkpoint starting: end-of-recovery immediate wait
--- 클라이언트 200 수 ---
395 /tmp/sids
--- DB 행 수 ---
count
-------
392
(1 row)
@@ -0,0 +1,32 @@
=== 크래시 흔적 여섯 줄 ===
2026-09-17 05:34:14.944 UTC [1] LOG: database system is ready to accept connections
2026-09-17 05:35:36.810 UTC [1] LOG: server process (PID 39) was terminated by signal 9: Killed
2026-09-17 05:35:36.817 UTC [1] LOG: all server processes terminated; reinitializing
2026-09-17 05:35:36.863 UTC [206] LOG: database system was not properly shut down; automatic recovery in progress
2026-09-17 05:35:36.865 UTC [206] LOG: redo starts at 0/1F80A00
2026-09-17 05:35:36.873 UTC [206] LOG: redo done at 0/21D72B8 system usage: CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s
2026-09-17 05:35:36.909 UTC [207] LOG: checkpoint complete: wrote 243 buffers (1.5%); 0 WAL file(s) added, 0 removed, 1 recycled; write=0.008 s, sync=0.009 s, total=0.029 s; sync files=26, longest=0.007 s, average=0.001 s; distance=2397 kB, estimate=2397 kB; lsn=0/21D8048, redo lsn=0/21D8048
2026-09-17 05:35:36.912 UTC [1] LOG: database system is ready to accept connections
=== 관찰 ① 클라이언트 sid 목록 ===
395 /tmp/client-sids.txt
uyMM_LOM5QgdNXZPymOt_d5-
iNI2VuirdOjA-FT7iwt7F21I
01uQ99zaBSaNtNt4v5tT-ned
=== ② 총계 ===
count
-------
392
(1 row)
=== ③ 값만 파일로 ===
392 /tmp/db-sids.txt
=== ④ 정렬해서 왼쪽 전용만 ===
395 /tmp/a.txt
392 /tmp/b.txt
787 total
=== ⑤ 유실 건수 ===
3
=== 유실된 sid (있으면) ===
KvrWnKhKue4DlxozYc63HU4E
S1iYLwU5oASbsePYH2Q1SB_A
cUFSAEsClRh-OS-KvNnaa1Lu
@@ -0,0 +1,15 @@
=== a3 되돌리기 ===
log_statement
---------------
none
(1 row)
DELETE 392
statefulset.apps/keycloak restarted
pod "a3-probe" deleted from keycloak-lab namespace
=== DB 건강 ===
count
-------
1
(1 row)
@@ -0,0 +1,29 @@
=== a4 1-② virsh uri ===
qemu:///system
=== 주입 전 ① VM 전원 ===
Id Name State
-----------------------------
13 kc-lab-1 running
14 kc-lab-2 running
15 kc-lab-edge running
=== ② 노드와 파드 배치 ===
NAME STATUS ROLES AGE VERSION
kc-lab-1 Ready control-plane 72m v1.36.4+k3s1
kc-lab-2 Ready <none> 71m v1.36.4+k3s1
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
keycloak-0 1/1 Running 0 37s 10.42.1.27 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 60s 10.42.0.14 kc-lab-1 <none> <none>
postgres-7b474b88c8-prlxx 1/1 Running 0 3m29s 10.42.1.26 kc-lab-2 <none> <none>
=== PVC 와 그 PV 가 요구하는 노드 ===
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
postgres-data Bound pvc-fe269834-3dd1-4b1f-82f6-a53413310a30 5Gi RWO local-path <unset> 63m
PV=pvc-fe269834-3dd1-4b1f-82f6-a53413310a30
{"required":{"nodeSelectorTerms":[{"matchExpressions":[{"key":"kubernetes.io/hostname","operator":"In","values":["kc-lab-2"]}]}]}}
=== 관측 스택이 어느 노드에 ===
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
grafana-845b5678cf-bblgz 1/1 Running 0 59m 10.42.0.11 kc-lab-1 <none> <none>
node-exporter-82mmn 1/1 Running 0 59m 192.168.122.12 kc-lab-2 <none> <none>
node-exporter-g688j 1/1 Running 0 59m 192.168.122.11 kc-lab-1 <none> <none>
prometheus-6774f94f7c-pj8w9 1/1 Running 0 59m 10.42.0.12 kc-lab-1 <none> <none>
@@ -0,0 +1,27 @@
=== 4a ① kc-lab-2 에 무엇이 있나 (영향 범위) ===
keycloak-lab keycloak-0
keycloak-lab postgres-7b474b88c8-prlxx
kube-system svclb-traefik-72a92206-5jpxg
observability node-exporter-82mmn
=== ② 차단 시각 ===
14:39:28 차단
=== ③ 전원을 뽑는다 ===
Domain 'kc-lab-2' destroyed
=== 하이퍼바이저가 뭐라 하나 ===
Id Name State
------------------------------
13 kc-lab-1 running
15 kc-lab-edge running
- kc-lab-2 shut off
=== 10초 뒤 쿠버네티스 ===
NAME STATUS ROLES AGE VERSION
kc-lab-1 Ready control-plane 72m v1.36.4+k3s1
kc-lab-2 Ready <none> 71m v1.36.4+k3s1
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
keycloak-0 1/1 Running 0 58s 10.42.1.27 kc-lab-2 <none> <none>
keycloak-1 1/1 Running 0 81s 10.42.0.14 kc-lab-1 <none> <none>
postgres-7b474b88c8-prlxx 1/1 Running 0 3m50s 10.42.1.26 kc-lab-2 <none> <none>
=== 정문 (터미널 C: 밖에서) ===
000
@@ -0,0 +1,20 @@
--- 14:39:56 ---
kc-lab-1 Ready kc-lab-2 Ready | 파드: keycloak-0 Running kc-lab-2;keycloak-1 Running kc-lab-1;postgres-7b474b88c8-prlxx Running kc-lab-2; | 정문=000
--- 14:40:34 ---
kc-lab-1 Ready kc-lab-2 NotReady | 파드: keycloak-0 Running kc-lab-2;keycloak-1 Running kc-lab-1;postgres-7b474b88c8-prlxx Running kc-lab-2; | 정문=503
--- 14:41:08 ---
kc-lab-1 Ready kc-lab-2 NotReady | 파드: keycloak-0 Running kc-lab-2;keycloak-1 Running kc-lab-1;postgres-7b474b88c8-prlxx Running kc-lab-2; | 정문=503
--- 14:41:38 ---
kc-lab-1 Ready kc-lab-2 NotReady | 파드: keycloak-0 Running kc-lab-2;keycloak-1 Running kc-lab-1;postgres-7b474b88c8-prlxx Running kc-lab-2; | 정문=503
--- 14:42:08 ---
kc-lab-1 Ready kc-lab-2 NotReady | 파드: keycloak-0 Running kc-lab-2;keycloak-1 Running kc-lab-1;postgres-7b474b88c8-prlxx Running kc-lab-2; | 정문=503
--- 14:42:41 ---
kc-lab-1 Ready kc-lab-2 NotReady | 파드: keycloak-0 Running kc-lab-2;keycloak-1 Running kc-lab-1;postgres-7b474b88c8-prlxx Running kc-lab-2; | 정문=503
uccessfulCreate statefulset/keycloak Create Pod keycloak-0 in StatefulSet keycloak successful
3m38s Normal Started pod/keycloak-0 Container started
3m38s Normal Created pod/keycloak-0 Container created
3m38s Normal Pulled pod/keycloak-0 Container image "quay.io/keycloak/keycloak:26.7.0" already present on machine and can be accessed by the pod
3m28s Warning Unhealthy pod/keycloak-0 Startup probe failed: Get "http://10.42.1.27:9000/health/started": dial tcp 10.42.1.27:9000: connect: connection refused
117s Warning NodeNotReady pod/keycloak-0 Node is not ready
117s Warning NodeNotReady pod/postgres-7b474b88c8-prlxx Node is not ready
6s Warning Unhealthy pod/keycloak-1 Readiness probe failed: Get "http://10.42.0.14:9000/health/ready": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
@@ -0,0 +1,34 @@
=== 차단 +약 6분 — 축출이 일어났나 ===
14:46:15
keycloak-0 Terminating kc-lab-2
keycloak-1 Running kc-lab-1
postgres-7b474b88c8-5zp6v Pending <none>
postgres-7b474b88c8-prlxx Terminating kc-lab-2
kc-lab-1 Ready
kc-lab-2 NotReady
--- StatefulSet 파드와 Deployment 파드의 처지가 다른가 ---
keycloak-0 Running 2026-09-17T05:44:17Z
keycloak-1 Running <none>
postgres-7b474b88c8-5zp6v Pending <none>
postgres-7b474b88c8-prlxx Running 2026-09-17T05:44:17Z
=== 복구: VM 을 다시 켠다 ===
14:46:15 복구
Domain 'kc-lab-2' started
kc-lab-1 Ready
kc-lab-2 NotReady
keycloak-0 1/1 Terminating kc-lab-2
keycloak-1 0/1 Running kc-lab-1
postgres-7b474b88c8-5zp6v 0/1 Pending <none>
postgres-7b474b88c8-prlxx 1/1 Terminating kc-lab-2
정문=503
[exited with code 0]
--- 14:47:55 복구 확인 ---
kc-lab-1 Ready
kc-lab-2 NotReady
keycloak-0 1/1 Terminating kc-lab-2
keycloak-1 0/1 Running kc-lab-1
postgres-7b474b88c8-5zp6v 0/1 Pending <none>
postgres-7b474b88c8-prlxx 1/1 Terminating kc-lab-2
정문=503
@@ -0,0 +1,12 @@
14:48:49 kc-lab-1=Ready kc-lab-2=NotReady | keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-5zp6v:Pending postgres-7b474b88c8-prlxx:Terminating | 정문=503
14:49:34 kc-lab-1=Ready kc-lab-2=NotReady | keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-5zp6v:Pending postgres-7b474b88c8-prlxx:Terminating | 정문=503
14:50:19 kc-lab-1=Ready kc-lab-2=NotReady | keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-5zp6v:Pending postgres-7b474b88c8-prlxx:Terminating | 정문=503
14:51:05 kc-lab-1=Ready kc-lab-2=Ready | keycloak-0:Unknown keycloak-1:Running postgres-7b474b88c8-5zp6v:ContainerCreating postgres-7b474b88c8-prlxx:Unknown | 정문=503
14:51:50 kc-lab-1=Ready kc-lab-2=Ready | keycloak-0:Running keycloak-1:Running postgres-7b474b88c8-5zp6v:Running | 정문=200
14:48:49 kc-lab-1=Ready kc-lab-2=NotReady | keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-5zp6v:Pending postgres-7b474b88c8-prlxx:Terminating | 정문=503
14:49:34 kc-lab-1=Ready kc-lab-2=NotReady | keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-5zp6v:Pending postgres-7b474b88c8-prlxx:Terminating | 정문=503
14:50:19 kc-lab-1=Ready kc-lab-2=NotReady | keycloak-0:Terminating keycloak-1:Running postgres-7b474b88c8-5zp6v:Pending postgres-7b474b88c8-prlxx:Terminating | 정문=503
14:51:05 kc-lab-1=Ready kc-lab-2=Ready | keycloak-0:Unknown keycloak-1:Running postgres-7b474b88c8-5zp6v:ContainerCreating postgres-7b474b88c8-prlxx:Unknown | 정문=503
14:51:50 kc-lab-1=Ready kc-lab-2=Ready | keycloak-0:Running keycloak-1:Running postgres-7b474b88c8-5zp6v:Running | 정문=200
[exited with code 0]
@@ -0,0 +1,10 @@
=== 원인: 유닛이 --token-file 로 지운 파일을 가리킨다 ===
'--token-file' \
'/home/donghyeon/node-token' \
=== 처방 ① 그 파일을 되돌려 놓는다 (값은 이 기계 밖으로 안 나간다) ===
109
=== agent 가 뜨나 ===
active
kc-lab-1 Ready
kc-lab-2 Ready
@@ -0,0 +1,22 @@
=== 처방 ② 토큰을 root 만 읽는 자리로 옮기고 유닛을 그쪽으로 돌린다 ===
bash: line 1: /etc/rancher/node-token: Permission denied
'--token-file' \
'/etc/rancher/node-token' \
=== 홈의 파일을 지운 뒤에도 뜨나 ===
ls: cannot access '/home/donghyeon/node-token': No such file or directory
active
kc-lab-1 Ready
kc-lab-2 Ready
=== 재부팅을 견디는지 확인 — agent 를 완전히 다시 시작 ===
active
kc-lab-1 Ready
kc-lab-2 Ready
=== 옮긴 파일의 권한과 길이 ===
-rw------- 1 root root 109 Sep 17 05:50 /etc/rancher/node-token
109
=== 유닛이 가리키는 자리 ===
'--token-file' \
'/etc/rancher/node-token' \
=== 노드 ===
kc-lab-1 Ready
kc-lab-2 Ready
@@ -0,0 +1,17 @@
K0(keycloak-0, kc-lab-2)=10.42.1.30 K1(keycloak-1, kc-lab-1)=10.42.0.14
=== 문서의 [kc-lab-1] 블록을 그대로 — 게스트 셸에 $K1 이 있나 ===
게스트 셸의 K1=[]
=== 시도 ① filter FORWARD (보내는 쪽 노드에) ===
14:54:11 주입
--- 규칙과 카운터 ---
Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 9 516 DROP 6 -- * * 0.0.0.0/0 10.42.1.30 tcp dpt:57800
2 22 2767 DROP 6 -- * * 0.0.0.0/0 10.42.1.30 tcp dpt:7800
3 921 1849K KUBE-ROUTER-FORWARD 0 -- * * 0.0.0.0/0 0.0.0.0/0 /* kube-router netpol - TEMCG2JMHZYE7H7T */
--- 멤버 수 ---
"pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}}
=== 시도 ① 되돌리기 ===
Chain FORWARD (policy ACCEPT)
num target prot opt source destination
1 KUBE-ROUTER-FORWARD 0 -- 0.0.0.0/0 0.0.0.0/0 /* kube-router netpol - TEMCG2JMHZYE7H7T */
@@ -0,0 +1,15 @@
=== 시도 ② raw PREROUTING 을 보내는 쪽(kc-lab-2)에 ===
14:55:06 주입
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.30 tcp dpt:57800
2 0 0 DROP 6 -- * * 0.0.0.0/0 10.42.1.30 tcp dpt:7800
"pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}}
=== 시도 ② 되돌리기 ===
=== 시도 ③ raw PREROUTING 을 받는 쪽(kc-lab-1)에 — 목적지는 keycloak-1 ===
14:55:57 주입
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
num pkts bytes target prot opt in out source destination
1 16 916 DROP 6 -- * * 0.0.0.0/0 10.42.0.14 tcp dpt:57800
2 20 3257 DROP 6 -- * * 0.0.0.0/0 10.42.0.14 tcp dpt:7800
"pod":"keycloak-1"} "1"]} "pod":"keycloak-0"} "1"]}]}}
@@ -0,0 +1,7 @@
=== 한 방향만 막은 채로 2분 더 — 다시 붙나 ===
14:57:49 멤버="pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}} | coord=
14:58:24 멤버="pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}} | coord=
14:58:59 멤버="pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}} | coord=
14:59:34 멤버="pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}} | coord=
[exited with code 0]
@@ -0,0 +1,20 @@
=== a5 네 번째 주입 — 양방향 ===
15:00:13 주입
--- 두 노드의 규칙과 카운터 ---
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
21 1208 DROP 6 -- * * 0.0.0.0/0 10.42.0.14 tcp dpt:57800
21 3317 DROP 6 -- * * 0.0.0.0/0 10.42.0.14 tcp dpt:7800
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.30 tcp dpt:57800
19 2595 DROP 6 -- * * 0.0.0.0/0 10.42.1.30 tcp dpt:7800
--- 멤버 수 ---
"pod":"keycloak-1"} "1"]} "pod":"keycloak-0"} "1"]}]}}
--- coord 가 둘인가 ---
name | coord
------------------+-------
keycloak-0-13476 | t
keycloak-1-8002 | t
(2 rows)
@@ -0,0 +1,16 @@
=== a5 되돌리기 — 두 노드의 raw 체인을 비운다 ===
Chain PREROUTING (policy ACCEPT)
num target prot opt source destination
Chain PREROUTING (policy ACCEPT)
num target prot opt source destination
=== 다시 붙었나 ===
"pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}}
name | coord
------------------+-------
keycloak-0-13476 | f
keycloak-1-8002 | t
(2 rows)
keycloak-0 1/1 Running
keycloak-1 1/1 Running
postgres-7b474b88c8-5zp6v 1/1 Running
@@ -0,0 +1,29 @@
K0=10.42.1.30 K1=10.42.0.14 PG=10.42.1.29
=== a6 시도 ① eth0 (예제 그대로) ===
Cannot find device "eth0"
=== ② 이 게스트에 무엇이 있나 ===
lo UNKNOWN 00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
enp1s0 UP 52:54:00:aa:bb:12 <BROADCAST,MULTICAST,UP,LOWER_UP>
flannel.1 UNKNOWN da:f1:1f:7f:9e:da <BROADCAST,MULTICAST,UP,LOWER_UP>
cni0 UP c6:f2:76:f6:72:0e <BROADCAST,MULTICAST,UP,LOWER_UP>
veth5f5079ef@if2 UP 7a:c7:1b:85:f0:92 <BROADCAST,MULTICAST,UP,LOWER_UP>
veth5ef1875b@if2 UP 82:ef:56:9c:1d:52 <BROADCAST,MULTICAST,UP,LOWER_UP>
veth8d0ac36c@if2 UP 56:50:6c:30:63:ac <BROADCAST,MULTICAST,UP,LOWER_UP>
=== 시도 ② 미검증 두 줄 — 물리 쪽 ===
IP 10.42.1.30.7800 > 10.42.0.14.50799: Flags [.], ack 208, win 509, options [nop,nop,TS val 2179731813 ecr 4085265222], length 0
06:02:02.268760 IP 192.168.122.12.53654 > 192.168.122.11.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.1.30.7800 > 10.42.0.14.50799: Flags [P.], seq 238:336, ack 208, win 509, options [nop,nop,TS val 2179731905 ecr 4085265222], length 98
06:02:02.309848 IP 192.168.122.11.60237 > 192.168.122.12.8472: OTV, flags [I] (0x08), overlay 0, instance 1
IP 10.42.0.14.50799 > 10.42.1.30.7800: Flags [.], ack 336, win 823, options [nop,nop,TS val 4085265356 ecr 2179731905], length 0
5 packets captured
5 packets received by filter
0 packets dropped by kernel
=== 터널 쪽 ===
06:02:05.445308 IP 10.42.0.14.52916 > 10.42.1.29.5432: Flags [P.], seq 2469151212:2469151253, ack 2840346855, win 502, options [nop,nop,TS val 1040631333 ecr 678493794], length 41
06:02:05.445613 IP 10.42.1.29.5432 > 10.42.0.14.52916: Flags [P.], seq 1:343, ack 41, win 502, options [nop,nop,TS val 678500272 ecr 1040631333], length 342
06:02:05.446042 IP 10.42.0.14.52916 > 10.42.1.29.5432: Flags [.], ack 343, win 502, options [nop,nop,TS val 1040631334 ecr 678500272], length 0
06:02:08.968064 IP 10.42.0.14.52916 > 10.42.1.29.5432: Flags [P.], seq 41:82, ack 343, win 502, options [nop,nop,TS val 1040634856 ecr 678500272], length 41
06:02:08.968065 IP 10.42.0.14.52928 > 10.42.1.29.5432: Flags [P.], seq 617750272:617750278, ack 754926471, win 502, options [nop,nop,TS val 1040634856 ecr 678493795], length 6
5 packets captured
9 packets received by filter
0 packets dropped by kernel
@@ -0,0 +1,14 @@
PG=10.42.1.29 K1=10.42.0.14
=== 주입 전 기준 — 로그인 한 번의 시간 ===
200 0.623459s
200 0.067946s
200 0.064262s
=== a6 성공한 주입 — flannel.1 에 세 줄 ===
15:03:55 주입
=== 넣은 직후의 카운터 ===
qdisc prio 1: root refcnt 2 bands 3 priomap 1 2 2 2 1 2 0 0 1 1 1 1 1 1 1 1
Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
qdisc netem 30: parent 1:3 limit 1000 delay 200ms
Sent 0 bytes 0 pkt (dropped 0, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
@@ -0,0 +1,15 @@
=== 주입 뒤 — 같은 로그인 (keycloak-1 은 kc-lab-1 에 있고 DB 는 kc-lab-2 다) ===
200 1.905504s
200 1.903161s
200 1.860396s
200 1.867929s
200 1.872740s
=== 카운터가 올라갔나 ===
qdisc netem 30: parent 1:3 limit 1000 delay 200ms
Sent 8791 bytes 83 pkt (dropped 0, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
=== 필터 목록 (문서가 미검증으로 둔 줄) ===
filter parent 1: protocol ip pref 3 u32 chain 0
filter parent 1: protocol ip pref 3 u32 chain 0 fh 800: ht divisor 1
filter parent 1: protocol ip pref 3 u32 chain 0 fh 800::800 order 2048 key ht 800 bkt 0 *flowid 1:3 not_in_hw
match 0a2a011d/ffffffff at 12
@@ -0,0 +1,6 @@
=== a6 되돌리기 ===
qdisc noqueue 0: root refcnt 2
=== 되돌린 뒤 같은 로그인 ===
200 0.067775s
200 0.043802s
200 0.045763s
@@ -0,0 +1,19 @@
=== a7 7. 세션 테이블을 비운다 ===
DELETE 11
=== 8. args 를 volatile 로 (patch 형태) ===
statefulset.apps/keycloak patched
15:05:19 전환
Waiting for partitioned roll out to finish: 0 out of 2 new pods have been updated...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
Waiting for partitioned roll out to finish: 1 out of 2 new pods have been updated...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
Waiting for 1 pods to be ready...
partitioned roll out complete: 2 new pods have been updated...
=== 주입 검증 — args 가 바뀌었나 ===
["start","--features-disabled=persistent-user-sessions"]
keycloak-0 1/1 Running 10.42.1.34 kc-lab-2
keycloak-1 1/1 Running 10.42.0.15 kc-lab-1
postgres-7b474b88c8-5zp6v 1/1 Running 10.42.1.29 kc-lab-2
@@ -0,0 +1,15 @@
=== a7 관찰 — A-0 의 시험 0 을 글자 그대로 다시 ===
# [1] keycloak-0 에서 로그인
sid=_szXQIOVaE8_v2-b-frn9npt
# [2] keycloak-1 에서 refresh (교차 노드)
200
=== DB 세션 행은 몇 건인가 ===
user_session_id
-----------------
(0 rows)
=== 노드별 캐시 엔트리 ===
keycloak-0 clientSessions 0
keycloak-0 sessions 0
keycloak-1 clientSessions 0
keycloak-1 sessions 0
@@ -0,0 +1,14 @@
=== 스크레이프 뒤 캐시 — 세션이 어디 사나 ===
keycloak-0 clientSessions 1
keycloak-0 sessions 1
keycloak-1 clientSessions 0
keycloak-1 sessions 0
=== 7800 을 막는다 (A-1 의 NetworkPolicy 를 그대로) ===
networkpolicy.networking.k8s.io/a1-block-jgroups-transport created
15:06:59 적용
pod "keycloak-0" deleted from keycloak-lab namespace
partitioned roll out complete: 2 new pods have been updated...
=== 멤버 수 ===
"pod":"keycloak-1"} "1"]} "pod":"keycloak-0"} "1"]}]}}
[exited with code 0]
@@ -0,0 +1,16 @@
분단 + volatile 에서 K0=10.42.1.36 K1=10.42.0.15
=== A-1 의 네 단계를 그대로 (volatile 판) ===
# [1] keycloak-0 에서 로그인
sid=9tpo41BVY32uJdYOyv8maA5j# [2] keycloak-1 에서 refresh
400
# [3] keycloak-1 에서 로그아웃
400
# [4] keycloak-0 에서 재갱신 시도
{"error":"invalid_grant","error_description":"Invalid refresh token"}
400
=== DB 행 ===
count
-------
0
(1 row)
@@ -0,0 +1,7 @@
=== a7 복구 ① NetworkPolicy 를 지운다 ===
networkpolicy.networking.k8s.io "a1-block-jgroups-transport" deleted from keycloak-lab namespace
=== ② args 를 원래대로 ===
statefulset.apps/keycloak patched
15:09:07 원복
partitioned roll out complete: 2 new pods have been updated...
["start"]
@@ -0,0 +1,16 @@
=== a7a 6. 문장 로깅을 켠다 ===
(1 row)
log_statement
---------------
all
(1 row)
=== 7. volatile 로 바꾼다 ===
statefulset.apps/keycloak patched
partitioned roll out complete: 2 new pods have been updated...
["start","--features-disabled=persistent-user-sessions"]
=== 4. 탐침 파드를 StatefulSet 밖에 ===
pod/a7a-probe created
pod/a7a-probe condition met
K0=10.42.1.39 K1=10.42.0.17
@@ -0,0 +1,10 @@
=== a7a 재현 A — 완전 냉시동 ===
statefulset.apps/keycloak restarted
partitioned roll out complete: 2 new pods have been updated...
deployment.apps/postgres scaled
pod/postgres-7b474b88c8-5zp6v condition met
pod/a7a-probe condition met
=== ③ 로그인을 본문과 함께 (기대: 400 unauthorized_client) ===
{"error":"unauthorized_client","error_description":"Unexpected error when authenticating client"}
400
=== ④ 실패한 SQL 을 Keycloak 로그에서 ===
@@ -0,0 +1,10 @@
=== ④ 문서가 적은 형태 그대로 ===
(위가 비면 아래를 본다)
=== 더 넓게 ===
2026-09-17 06:11:31,802 WARN [org.keycloak.jgroups.protocol.KEYCLOAK_JDBC_PING2] (blocking-thread--p3-t2) Failed to fetch the cluster members from the database.: org.postgresql.util.PSQLException: FATAL: terminating connection due to administrator command
2026-09-17 06:11:59,291 ERROR [org.keycloak.services] (executor-thread-3) KC-SERVICES0015: Unexpected error when authenticating client: org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection [Connection to postgres:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.] [n/a]
=== 실제로 찍힌 오류 줄 ===
2026-09-17 06:11:49,114 ERROR [org.keycloak.jgroups.protocol.KEYCLOAK_JDBC_PING2] (Timer runner-2,keycloak-0-44929(v=16.0.12)) keycloak-0-44929(v=16.0.12): failed reading from the DB: org.postgresql.util.PSQLException: Connection to postgres:5432 refused. Chec
2026-09-17 06:11:55,190 ERROR [org.keycloak.jgroups.protocol.KEYCLOAK_JDBC_PING2] (Timer runner-2,keycloak-0-44929(v=16.0.12)) keycloak-0-44929(v=16.0.12): failed writing to DB: org.postgresql.util.PSQLException: Connection to postgres:5432 refused. Check that
2026-09-17 06:11:59,291 ERROR [org.keycloak.services] (executor-thread-3) KC-SERVICES0015: Unexpected error when authenticating client: org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection [Connection to postgres:5432 refused. Che
2026-09-17 06:11:59,295 WARN [org.keycloak.events] (executor-thread-3) type="LOGIN_ERROR", realmId="f9bed085-ff57-4ab0-92ab-af83adeb320e", realmName="master", clientId="admin-cli", userId="null", ipAddress="10.42.1.42", error="invalid_client_credentials", gra
@@ -0,0 +1,17 @@
=== 넓힌 grep 이 둘 다 잡나 ===
KC-SERVICES0015: Unexpected error when authenticating client: org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection [Connection to postgres:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.]
=== a7a 재현 B — 로그인 한 번으로 캐시를 덥힌 뒤 DB 를 내린다 ===
deployment.apps/postgres scaled
deployment "postgres" successfully rolled out
statefulset.apps/keycloak restarted
partitioned roll out complete: 2 new pods have been updated...
pod/a7a-probe condition met
--- 로그인 한 번 (DB 살아 있을 때) ---
rt=612자
--- DB 를 내린다 ---
deployment.apps/postgres scaled
pod/postgres-7b474b88c8-dg4vb condition met
--- refresh (기대: 500) ---
{"error":"unknown_error","error_description":"For more on this error consult the server log."}
500
@@ -0,0 +1,21 @@
=== a7a 재현 C — DB 를 살리고 같은 refresh 를 다시 ===
deployment.apps/postgres scaled
deployment "postgres" successfully rolled out
200
=== a7a 되돌리기 ===
(1 row)
log_statement
---------------
none
(1 row)
statefulset.apps/keycloak patched
partitioned roll out complete: 2 new pods have been updated...
pod "a7a-probe" deleted from keycloak-lab namespace
["start"]
keycloak-0 1/1 Running
keycloak-1 1/1 Running
postgres-7b474b88c8-48lrr 1/1 Running
[exited with code 0]
@@ -0,0 +1,21 @@
=== a8 1~3 기준선 ===
keycloak-0 1/1 Running 103s 10.42.1.47
keycloak-1 1/1 Running 2m17s 10.42.0.20
postgres-7b474b88c8-48lrr 1/1 Running 2m57s 10.42.1.46
["start"] replicas=2
count
-------
0
(1 row)
=== 4~6 탐침과 토큰 ===
pod/a8-probe condition met
rt=613자 sid=z_FpRg03WlwmkgqrpKZgzfNP
# 대조군 — 재시작 전 refresh
200
=== 7. 그 세션이 DB 에 있나 ===
user_session_id | created_on | last_session_refresh
--------------------------+------------+----------------------
z_FpRg03WlwmkgqrpKZgzfNP | 1789625835 | 1789625835
(1 row)
@@ -0,0 +1,28 @@
=== 8. 캐시와 클러스터 크기 ===
keycloak-0 clientSessions 0
keycloak-0 sessions 0
keycloak-1 clientSessions 0
keycloak-1 sessions 0
=== 9. 롤링 재시작 ===
15:18:57 재시작
statefulset.apps/keycloak restarted
partitioned roll out complete: 2 new pods have been updated...
=== 10. 파드가 진짜 바뀌었나 (AGE) ===
keycloak-0 1/1 Running 31s 10.42.1.49
keycloak-1 1/1 Running 63s 10.42.0.21
=== 12. 본 시험 — 재시작 전 토큰이 아직 통하는가 ===
000
command terminated with exit code 3
=== 13. DB 행의 두 시각 ===
user_session_id | created_on | last_session_refresh
--------------------------+------------+----------------------
z_FpRg03WlwmkgqrpKZgzfNP | 1789625835 | 1789625835
(1 row)
=== 14. 캐시와 클러스터 ===
keycloak-0 clientSessions 0
keycloak-0 sessions 0
keycloak-1 clientSessions 0
keycloak-1 sessions 0
"pod":"keycloak-1"} "2"]} "pod":"keycloak-0"} "2"]}]}}
@@ -0,0 +1,10 @@
새 K0=10.42.1.49 (탐침 안의 낡은 값=10.42.1.47)
=== 12. 본 시험 — 새 IP 를 명령줄로 넘긴다 ===
{"access_token":"<REDACTED 758자>","expires_in":60,"refresh_expires_in":1800,"refresh_token":"<REDACTED 612자>","token_type":"Bearer","not-before-policy":0,"session_state":"z_FpRg03WlwmkgqrpKZgzfNP","scope":"profile email"}
200
=== 13. DB 행의 두 시각 (갱신됐나) ===
user_session_id | created_on | last_session_refresh
--------------------------+------------+----------------------
z_FpRg03WlwmkgqrpKZgzfNP | 1789625835 | 1789625952
(1 row)
@@ -0,0 +1,20 @@
=== 이미지 두 개를 두 노드에 밀어 넣는다 (문서의 형태 그대로) ===
--- keycloak-pattern-bff:lab → kc-lab-1 ---
application/vnd.oci.image.index.v1+json sha256:9fb29178e7f8556a8d3b38cf6e23b55cfbc27396530451bb86dcea99206b7bbb
Importing elapsed: 7.8 s total: 0.0 B (0.0 B/s)
--- keycloak-pattern-bff:lab → kc-lab-2 ---
application/vnd.oci.image.index.v1+json sha256:9fb29178e7f8556a8d3b38cf6e23b55cfbc27396530451bb86dcea99206b7bbb
Importing elapsed: 6.9 s total: 0.0 B (0.0 B/s)
--- keycloak-pattern-api:lab → kc-lab-1 ---
application/vnd.oci.image.index.v1+json sha256:ff6ce2988e2fd19c44f38591c58e7cecc79c443368a502e328f356bee23d6ab3
Importing elapsed: 5.6 s total: 0.0 B (0.0 B/s)
--- keycloak-pattern-api:lab → kc-lab-2 ---
application/vnd.oci.image.index.v1+json sha256:ff6ce2988e2fd19c44f38591c58e7cecc79c443368a502e328f356bee23d6ab3
Importing elapsed: 5.8 s total: 0.0 B (0.0 B/s)
=== 두 노드의 이미지 목록 ===
--- kc-lab-1 ---
docker.io/library/keycloak-pattern-api:lab
docker.io/library/keycloak-pattern-bff:lab
--- kc-lab-2 ---
docker.io/library/keycloak-pattern-api:lab
docker.io/library/keycloak-pattern-bff:lab
@@ -0,0 +1,18 @@
=== b0 3-① kcadm 로그인 ===
Logging into http://localhost:8080 as user admin of realm master
(exit=0)
=== ③ realm 과 클라이언트 ===
Created new realm with id 'keycloak-patterns'
Created new client with id '7ae3362a-2bed-4902-b6af-d7307ea4ad0b'
=== ④ realm 세 칸 ===
{
"realm" : "keycloak-patterns",
"accessTokenLifespan" : 60,
"enabled" : true
}
=== 4. 사용자 ===
Created new user with id '07d3be2a-9f22-4779-a211-e530c8b7ff3b'
[ {
"username" : "labuser",
"enabled" : true
} ]
@@ -0,0 +1,29 @@
=== bff-redis.yaml 이 무엇을 만드나 ===
kind: Secret
name: bff-secrets
kind: PersistentVolumeClaim
name: redis-data
kind: Deployment
name: redis
kind: Service
name: redis
kind: Deployment
name: bff
kind: Service
name: bff
kind: Ingress
name: bff
=== 적용 ===
secret/bff-secrets created
persistentvolumeclaim/redis-data created
deployment.apps/redis created
service/redis created
deployment.apps/bff created
service/bff created
ingress.networking.k8s.io/bff created
bff-5d97897676-nmlqs 0/1 Running kc-lab-1
bff-5d97897676-zvfhp 0/1 Running kc-lab-2
keycloak-0 1/1 Running kc-lab-2
keycloak-1 1/1 Running kc-lab-1
postgres-7b474b88c8-48lrr 1/1 Running kc-lab-2
redis-f94bc6dbf-pjllr 0/1 ContainerCreating kc-lab-2
@@ -0,0 +1,26 @@
=== 파드 상태 ===
bff-5d97897676-nmlqs 1/1 Running kc-lab-1
bff-5d97897676-zvfhp 1/1 Running kc-lab-2
keycloak-0 1/1 Running kc-lab-2
keycloak-1 1/1 Running kc-lab-1
postgres-7b474b88c8-48lrr 1/1 Running kc-lab-2
redis-f94bc6dbf-pjllr 1/1 Running kc-lab-2
=== bff 가 안 뜨면 왜인지 ===
2026-09-17T06:24:23.717Z INFO 1 --- [keycloak-bff] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2026-09-17T06:24:23.718Z INFO 1 --- [keycloak-bff] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 2221 ms
2026-09-17T06:24:23.943Z INFO 1 --- [keycloak-bff] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2026-09-17T06:24:24.223Z INFO 1 --- [keycloak-bff] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@be6d228
2026-09-17T06:24:24.225Z INFO 1 --- [keycloak-bff] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2026-09-17T06:24:24.808Z INFO 1 --- [keycloak-bff] [ main] o.s.b.a.w.s.WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2026-09-17T06:24:25.272Z INFO 1 --- [keycloak-bff] [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 5 endpoints beneath base path '/actuator'
2026-09-17T06:24:25.909Z INFO 1 --- [keycloak-bff] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8083 (http) with context path '/'
2026-09-17T06:24:25.926Z INFO 1 --- [keycloak-bff] [ main] c.e.keycloakpattern.bff.BffApplication : Started BffApplication in 5.298 seconds (process running for 5.885)
2026-09-17T06:24:41.136Z INFO 1 --- [keycloak-bff] [nio-8083-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2026-09-17T06:24:41.137Z INFO 1 --- [keycloak-bff] [nio-8083-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2026-09-17T06:24:41.139Z INFO 1 --- [keycloak-bff] [nio-8083-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
=== 이미지 경로가 맞았나 ===
keycloak-pattern-bff:lab Never
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 2m39s default-scheduler Successfully assigned keycloak-lab/bff-5d97897676-nmlqs to kc-lab-1
@@ -0,0 +1,23 @@
=== BFF 파드: bff-5d97897676-nmlqs ===
=== /actuator/beans 를 파드 안에서 받는다 ===
155395 /tmp/beans.json
=== ① 빈의 개수 (문서가 미검증으로 둔 줄) ===
437
=== ② 저장소 계열 빈 (문서가 미검증으로 둔 줄) ===
"org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConfigurations$OAuth2AuthorizedClientServiceConfiguration": -> org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientConfigurations$OAuth2AuthorizedClientServiceConfiguration"
"authorizedClientManagerRegistrar": -> org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration$OAuth2AuthorizedClientManagerRegistrar"
"org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration$OAuth2AuthorizedClientManagerConfiguration": -> org.springframework.security.config.annotation.web.configuration.OAuth2ClientConfiguration$OAuth2AuthorizedClientManagerConfiguration"
"authorizedClientManager": -> org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager"
"authorizedClientService": -> org.springframework.security.oauth2.client.JdbcOAuth2AuthorizedClientService"
"authorizedClientRepository": -> org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository"
=== 세션 저장소 쪽도 ===
"reactiveRedisTemplate": -> org.springframework.data.redis.core.ReactiveRedisTemplate"
"redisConnectionDetails": -> org.springframework.boot.autoconfigure.data.redis.PropertiesRedisConnectionDetails"
"org.springframework.boot.autoconfigure.session.SessionAutoConfiguration$ServletSessionConfiguration$ServletSessionRepositoryConfiguration": -> org.springframework.boot.autoconfigure.session.SessionAutoConfiguration$ServletSessionConfiguration$ServletSessionRepositoryConfiguration"
"redisCustomConversions": -> org.springframework.data.redis.core.convert.RedisCustomConversions"
"org.springframework.boot.autoconfigure.session.RedisSessionConfiguration": -> org.springframework.boot.autoconfigure.session.RedisSessionConfiguration"
=== 이 BFF 의 저장소 설정 ===
SPRING_SESSION_STORE_TYPE=redis
BFF_DB_URL=jdbc:postgresql://postgres.keycloak-lab.svc:5432/keycloak
BFF_DB_USER=keycloak
BFF_DB_PASSWORD=
@@ -0,0 +1,15 @@
=== b2 — JDBC 토큰 저장소의 표가 있나 ===
Table "public.oauth2_authorized_client"
Column | Type | Collation | Nullable | Default
-------------------------+-----------------------------+-----------+----------+-------------------------
client_registration_id | character varying(100) | | not null |
principal_name | character varying(200) | | not null |
access_token_type | character varying(100) | | not null |
access_token_value | bytea | | not null |
access_token_issued_at | timestamp without time zone | | not null |
access_token_expires_at | timestamp without time zone | | not null |
access_token_scopes | character varying(1000) | | | NULL::character varying
refresh_token_value | bytea | | |
refresh_token_issued_at | timestamp without time zone | | |
=== 행 수 ===
0
@@ -0,0 +1,12 @@
=== b3 2. kcadm 세션 ===
Logging into http://localhost:8080 as user admin of realm master
=== 3. realm 의 세 값 ===
{
"revokeRefreshToken" : false,
"refreshTokenMaxReuse" : 0,
"accessTokenLifespan" : 60
}
=== 4-① 탐침 파드 ===
pod/b3-probe condition met
=== ② 길이만 ===
KC=http://keycloak.keycloak-lab.svc:8080/realms/keycloak-patterns/protocol/openid-connect/token CS길이=14
@@ -0,0 +1,6 @@
=== 문서가 없다고 한 그 대조 — 두 값이 같은가 (길이와 해시만) ===
매니페스트 쪽 길이=14 Keycloak 쪽 길이=14
두 값이 같다
=== b3 5. 토큰을 하나 받는다 ===
rt=0자
{"error":"unauthorized_client","error_description":"Client not allowed for direct access g
@@ -0,0 +1,15 @@
=== B-0 이 만든 클라이언트의 그랜트 설정 ===
{
"clientId" : "bff-confidential",
"standardFlowEnabled" : true,
"directAccessGrantsEnabled" : false,
"publicClient" : false
}
=== 켜 본다 ===
{
"clientId" : "bff-confidential",
"directAccessGrantsEnabled" : true
}
=== 켠 뒤 같은 토큰 요청 ===
rt=0자
{"error":"invalid_grant","error_description":"Account is not
@@ -0,0 +1,19 @@
=== 전체 오류 문구 ===
{"error":"invalid_grant","error_description":"Account is not fully set up"}
=== B-0 이 만든 사용자의 상태 ===
{
"username" : "labuser",
"emailVerified" : false,
"enabled" : true,
"requiredActions" : [ ]
}
=== 처방: 계정을 완전하게 만든다 ===
{
"username" : "labuser",
"emailVerified" : true,
"enabled" : true,
"requiredActions" : [ ]
}
=== 다시 토큰 요청 ===
rt=735자
{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2l
@@ -0,0 +1,16 @@
=== b3 주입 — 회전을 켠다 ===
15:29:22 회전 켬
{
"revokeRefreshToken" : true,
"refreshTokenMaxReuse" : 0
}=== 관찰 — 같은 토큰으로 동시에 다섯 번 ===
refresh=810자 SID=7yVb2FfztKznk461jR10VpIT
요청 1: HTTP 400 {"error":"invalid_grant","error_description":"Session doesn't have required client"}
요청 2: HTTP 400 {"error":"invalid_grant","error_description":"Session doesn't have required client"}
요청 3: HTTP 400 {"error":"invalid_grant","error_description":"Maximum allowed refresh token reuse exceeded
요청 4: HTTP 200 {"access_token":"<REDACTED 73자 조각>
요청 5: HTTP 400 {"error":"invalid_grant","error_description":"Session doesn't have required client"}
이긴 요청의 새 refresh 길이: 810
그 토큰을 다시 쓰면: 400
error_description=Session doesn't have required client
@@ -0,0 +1,6 @@
=== b3 되돌리기 ===
{
"revokeRefreshToken" : false,
"refreshTokenMaxReuse" : 0
}
pod "b3-probe" deleted from keycloak-lab namespace
@@ -0,0 +1,32 @@
=== b4 1. app1 이 경로에 따라 어디로 가는가 ===
NS NAME HOST PATH SVC
header-lab echo app1.hyeonworks.com /api echo
keycloak-lab bff app1.hyeonworks.com / bff
keycloak-lab keycloak auth.hyeonworks.com / keycloak
observability grafana app2.hyeonworks.com / grafana
=== 2. echo 응답을 통째로 (TLS 대신 Host 헤더) ===
{
"headers" : {
"host" : [ "app1.hyeonworks.com" ],
"user-agent" : [ "curl/8.21.0" ],
"accept" : [ "*/*" ],
"x-forwarded-host" : [ "app1.hyeonworks.com" ],
"x-forwarded-port" : [ "80" ],
"x-forwarded-proto" : [ "http" ],
"x-forwarded-server" : [ "traefik-59b7647586-t8fc7" ],
"x-real-ip" : [ "10.42.0.1" ],
"accept-encoding" : [ "gzip" ]
},
"remoteAddr" : "10.42.0.1",
"localAddr" : "10.42.1.58",
"scheme" : "http",
"secure" : false,
"serverName" : "app1.hyeonworks.com",
"serverPort" : 80,
"requestUrl" : "http://app1.hyeonworks.com/api/echo"
}
=== 3. 대조군 — 아무것도 안 붙였을 때 ===
(위가 비면 대조군에는 그 헤더가 없다)
=== 주입 1. 동명 헤더 두 개 ===
=== 주입 2. 값 안의 쉼표 ===
=== 주입 4. 신원 자체를 위조 ===
@@ -0,0 +1,11 @@
=== 주입 1. 동명 헤더 두 개 (문서의 grep 형태) ===
"x-auth-request-roles" : [ "admin", "editor" ]
=== 주입 2. 값 안의 쉼표 ===
"x-auth-request-roles" : [ "admin,editor,viewer" ]
"x-auth-request-roles" : [ "role-with,comma" ]
=== 주입 4. 신원 위조 ===
"x-auth-request-email" : [ "attacker@evil.invalid" ]
"x-auth-request-user" : [ "attacker" ]
=== 문서가 적은 x-forwarded-proto grep 형태 ===
(위가 비면 공백 때문이다)
"x-forwarded-proto" : [ "http" ]
@@ -0,0 +1,19 @@
=== b5 1. 파드 배치 ===
bff-5d97897676-nmlqs 1/1 Running kc-lab-1
bff-5d97897676-zvfhp 1/1 Running kc-lab-2
keycloak-0 1/1 Running kc-lab-2
keycloak-1 1/1 Running kc-lab-1
postgres-7b474b88c8-48lrr 1/1 Running kc-lab-2
redis-f94bc6dbf-pjllr 1/1 Running kc-lab-2
=== 2. Redis 안과 영속화 설정 ===
PONG
(위가 비면 키가 없다)
save
3600 1 300 100 60 10000
appendonly
yes
=== 3. /data 가 볼륨인가 ===
data=redis-data
kube-api-access-nk4c6=
data -> /data
kube-api-access-nk4c6 -> /var/run/secrets/kubernetes.io/serviceaccount
@@ -0,0 +1,23 @@
=== b5 5-① health 그룹 셋 (정상 상태, TLS 대신 Host 헤더) ===
/actuator/health -> {"status":"UP","groups":["liveness","readiness"],"components":{"db":{"status":"UP","details":{"database":"PostgreSQL","validationQuery":"isValid()"}},"diskSpace":{"status":"UP","details":{"total":20922114048,"free":15890649088,"threshold":10485760,"path":"/app/.","exists":true}},"livenessState":{"status":"UP"},"ping":{"status":"UP"},"readinessState":{"status":"UP"},"redis":{"status":"UP","details":{"version":"7.4.11"}},"ssl":{"status":"UP","details":{"validChains":[],"invalidChains":[]}}}}
/actuator/health/readiness -> {"status":"UP"}
/actuator/health/liveness -> {"status":"UP"}
=== ② kubelet 이 보는 경로 ===
/actuator/health/readiness
=== 주입 1. Redis 를 0대로 ===
15:52:37 정지
deployment.apps/redis scaled
pod/redis-f94bc6dbf-pjllr condition met
=== 정지 뒤 health 그룹 셋 ===
/actuator/health -> {"status":"DOWN","groups":["liveness","readiness"],"components":{"db":{"status":"UP","details":{"database":"PostgreSQL","validationQuery":"isValid()"}},"diskSpace":{"status":"UP","details":{"total":20922114048,"free":13710831616,"threshold":10485760,"path":"/app/.","exists":true}},"livenessState":{"status":"UP"},"ping":{"status":"UP"},"readinessState":{"status":"UP"},"redis":{"status":"DOWN","details":{"error":"org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis"}},"ssl":{"status":"UP","details":{"validChains":[],"invalidChains":[]}}}}
/actuator/health/readiness -> {"status":"UP"}
/actuator/health/liveness -> {"status":"UP"}
=== 파드 상태 ===
bff-5d97897676-nmlqs 1/1 Running kc-lab-1
bff-5d97897676-zvfhp 1/1 Running kc-lab-2
keycloak-0 1/1 Running kc-lab-2
keycloak-1 1/1 Running kc-lab-1
postgres-7b474b88c8-48lrr 1/1 Running kc-lab-2
=== Service 엔드포인트 ===
ADDR READY
[10.42.1.51],[10.42.0.22] true,true
@@ -0,0 +1,10 @@
=== b5 되돌리기 ===
deployment.apps/redis scaled
deployment "redis" successfully rolled out
/actuator/health -> "status":"UP"
bff-5d97897676-nmlqs 1/1 Running
bff-5d97897676-zvfhp 1/1 Running
keycloak-0 1/1 Running
keycloak-1 1/1 Running
postgres-7b474b88c8-48lrr 1/1 Running
redis-f94bc6dbf-s674z 1/1 Running
@@ -0,0 +1,19 @@
=== echo.yaml 이 무엇을 만드나 ===
kind: Namespace
name: header-lab
kind: Deployment
name: echo
namespace: header-lab
kind: Service
name: echo
namespace: header-lab
kind: Ingress
name: echo
namespace: header-lab
=== 적용 ===
namespace/header-lab created
deployment.apps/echo created
service/echo created
ingress.networking.k8s.io/echo created
echo-cd47dccc6-nq62j 1/1 Running kc-lab-2
echo-cd47dccc6-q46lk 1/1 Running kc-lab-1
@@ -0,0 +1,12 @@
=== b6 2. 공급자 목록 ===
=== 문서가 미검증으로 둔 「조용히 빈 결과」 형태 ===
No server specified. Use --server, or 'kcadm.sh config credentials'.
command terminated with exit code 1
(위가 비면 문서 말이 맞다. exit=0)
=== 3. JWKS 를 TLS 없이 (Host 헤더로) ===
{"keys":[{"kid":"HKy0uQhg-vlQackK6-oj3hW6vKbDj-95Wlvdgl37cGg","kty":"RSA","alg":"RS256","use":"sig","x5c":["MIICsTCCAZkCBgGgrgllnjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFrZXljbG9hay1wYXR0ZXJuczAeFw0yNjA5MTcwNjIyMjZaFw0zNjA5MTcwNjI0MDZaMBwxGjAYBgNVBAMMEWtleWNsb2FrLXBhdHRlcm5zMIIBIjANBgkqhkiG9w0BAQEFAAOC
=== 4. RS256 만 센다 ===
1
=== kid 목록 ===
{"keys":[{"kid":"HKy0uQhg-vlQackK6-oj3hW6vKbDj-95Wlvdgl37cGg"
{"kid":"5voCsAVhALEjGliTG9Z2bx6WSkHeAUFUXiYOYic-niI"
@@ -0,0 +1,12 @@
=== kcadm 세션이 왜 사라졌나 ===
ls: cannot access '/opt/keycloak/.keycloak/kcadm.config': No such file or directory
=== 다시 로그인 ===
Logging into http://localhost:8080 as user admin of realm master
=== b6 2. 공급자 목록 ===
,{"id":"807e653f-deaf-41cd-bbaa-5b19eaf74a2a","name":"hmac-generated-hs512","providerId":"hmac-generated"
,{"id":"27216d59-950c-4c8c-b0a8-e1d8108e0c0f","name":"aes-generated","providerId":"aes-generated"
,{"id":"a09ec151-6c8c-4e55-84c0-f02781221b83","name":"rsa-generated","providerId":"rsa-generated"
=== 문서가 미검증으로 둔 「조용히 빈 결과」 형태 ===
[ {
"id" : "d47b58ee-7c34-4be0-a3f3-30f49b4577c1",
"name" : "rsa-enc-generated",
@@ -0,0 +1,3 @@
at=1397자
kid=echo 에 그 토큰으로: 401
토큰 없이: 200
@@ -0,0 +1,9 @@
=== echo 가 어느 발행자를 보나 ===
{"name":"SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI"
{"name":"SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI"
=== echo 로그에서 401 의 까닭 ===
=== kid 를 따로 뽑아 본다 ===
헤더 길이=111
{"alg":"RS256","typ" : "JWT","kid" : "HKy0uQhg-vlQackK6-oj3hW6vKbDj-95Wlvdgl37cGgbase64: truncated input
@@ -0,0 +1,10 @@
=== echo 의 두 URI ===
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://auth.hyeonworks.com/realms/keycloak-patterns
SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_JWK_SET_URI=https://auth.hyeonworks.com/realms/keycloak-patterns/protocol/openid-connect/certs
=== echo 가 그 주소에 닿나 ===
issuer=000
command terminated with exit code 7
=== JWT 헤더는 공백이 들어간 형태다 ===
헤더: {"alg":"RS256","typ" : "JWT","kid" : "HKy0uQhg-vlQackK6-oj3hW6vKbDj-95Wlvdgl37cGg"}
문서 형태의 sed: []
공백을 허용한 sed: [HKy0uQhg-vlQackK6-oj3hW6vKbDj-95Wlvdgl37cGg]

Some files were not shown because too many files have changed in this diff Show More