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
@@ -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