Files
platform-core/bootstrap/manual/phase4-observability-access.md

702 lines
34 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Phase 4 관측성 접근·알림 전환 절차
상세 설계와 실행 원장은 `/home/donghyeon/workspace/docs/platform`에서 관리합니다.
이 문서는 현재 live substrate를 보존하면서 rules·alerts와 Host Nginx를 마지막에
전환하는 권위 실행 순서를 요약합니다. Secret 값, token, Cookie, 사용자 ID와
webhook 원문은 출력하거나 문서에 복사하지 않습니다.
## 현재 완료 경계
2026-08-15 기준 Blackbox substrate, metric target과 rules-alerts는 live입니다.
- Prometheus active target `30`, healthy `30`, unhealthy `0`
- Grafana와 Blackbox Exporter Deployment 각각 `1/1` Ready
- Host Nginx는 Grafana deny-only guard 상태
- Grafana OIDC workload와 `grafana-keycloak-oidc` Secret 참조는 live
- Slack Secret과 risk deployment evidence는 live이며 off-host Slack DR은 deferred
- platform dashboard ConfigMap `5`, platform PrometheusRule `4`,
`AlertmanagerConfig/platform-alertmanager` `1`이 rollback ID `20260814T145009Z`에서 accepted
- generated receiver는
`observability/platform-alertmanager/platform-slack` exact singleton
- full Nginx cutover와 browser OIDC·Slack firing/resolved·external-client acceptance는 미실행
rules-alerts 실행 때 다음 두 선행 조건은 모두 충족됐습니다. 이후 재실행이나 복구에서도
같은 gate를 생략하지 않습니다.
1. 아래 runbook URL이 HTTPS `200`으로 도달하고 모든 alert의 `runbook_url`과 일치한다.
2. 운영자가 만든 Slack webhook 입력 파일과 recovery evidence가 준비된다.
```text
https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md
```
## 1. 공통 rollback transaction
하나의 shell에서 같은 rollback ID를 끝까지 유지합니다.
```bash
cd /home/donghyeon/workspace/platform
OBS_ROLLBACK_ID="$(date -u +%Y%m%dT%H%M%SZ)"
[[ "$OBS_ROLLBACK_ID" =~ ^[0-9]{8}T[0-9]{6}Z$ ]]
[[ "$OBS_ROLLBACK_ID" != 20260814T080303Z ]]
OBS_ROLLBACK_ROOT="/var/lib/hyeonworks/platform-rollbacks/observability-$OBS_ROLLBACK_ID"
sudo -n /usr/bin/test ! -e "$OBS_ROLLBACK_ROOT"
sudo -n /usr/bin/test ! -L "$OBS_ROLLBACK_ROOT"
sudo -n /usr/bin/mkdir --mode=0700 -- "$OBS_ROLLBACK_ROOT"
[[ "$(sudo -n /usr/bin/stat -c '%F|%u:%g|%a' -- "$OBS_ROLLBACK_ROOT")" == \
'directory|0:0|700' ]]
export PLATFORM_OBSERVABILITY_ROLLBACK_ID="$OBS_ROLLBACK_ID"
[[ "$PLATFORM_OBSERVABILITY_ROLLBACK_ID" == "$OBS_ROLLBACK_ID" ]]
```
pre-absence, 단 한 번의 `mkdir`, metadata 또는 active-ID equality가 실패하면 STOP하고 그
collision/error ID를 재사용하지 않습니다. 자동 rollback 뒤에도 생성된 root를 삭제하지 않습니다.
## 2. 기존 substrate와 deny guard 확인
```bash
bash scripts/bootstrap/apply-private-dns.sh
bash scripts/bootstrap/apply-host-nginx-observability.sh
bash scripts/validate/validate-blackbox-edge-source.sh
```
private DNS와 metrics/deny guard 자체를 다시 적용해야 할 때만 다음 mutation을
순서대로 실행합니다. 각 명령의 화면 지시와 정확히 일치하는 확인 문자열만 입력합니다.
```bash
: "${CERTBOT_EMAIL:?set the operator-managed Certbot contact email}"
bash scripts/bootstrap/apply-private-dns.sh --execute
bash scripts/bootstrap/apply-host-nginx-observability.sh \
--execute --metrics-guard-only
metrics_status="$(curl --disable --silent --show-error --output /dev/null \
--write-out '%{http_code}' \
--resolve git.learn.hyeonworks.com:443:127.0.0.1 \
https://git.learn.hyeonworks.com/metrics)"
[[ "$metrics_status" == 404 ]]
bash scripts/bootstrap/apply-host-nginx-observability.sh \
--execute --certificate-only --certbot-email "$CERTBOT_EMAIL"
bash scripts/bootstrap/apply-host-nginx-observability.sh \
--execute --grafana-deny-guard-only
```
DNS/guard를 다시 적용했는지와 무관하게, **새 rollback ID마다** Blackbox source
proof를 반드시 새로 만듭니다. Task 6 retry에서는 failed ID `20260814T080303Z`를 거부하고
controller가 발급한 fresh ID와 현재 active environment가 exact equality인지 먼저 확인합니다.
```bash
FRESH_TASK6_ROLLBACK_ID="$OBS_ROLLBACK_ID"
[[ "$FRESH_TASK6_ROLLBACK_ID" != 20260814T080303Z ]]
[[ "${PLATFORM_OBSERVABILITY_ROLLBACK_ID:?active rollback ID is required}" == \
"$FRESH_TASK6_ROLLBACK_ID" ]]
set +e
bash scripts/validate/validate-blackbox-edge-source.sh \
--execute --context default
BLACKBOX_RC=$?
set -e
printf 'BLACKBOX_RC=%d\n' "$BLACKBOX_RC"
[[ "$BLACKBOX_RC" -eq 0 ]]
[[ "$(sudo -n /usr/bin/stat -c '%F|%u:%g|%a|%h' -- \
"/var/lib/hyeonworks/platform-rollbacks/observability-${FRESH_TASK6_ROLLBACK_ID}/blackbox-source-proof.env")" == \
'regular file|0:0|600|1' ]]
```
이 proof는 같은 rollback ID, active deny hash, 24시간 이내 시각과 세 private
hostname의 exact `403`에 결속되어야 합니다. validator는 한 번만 호출하고 operator가 exact
`PROVE BLACKBOX PRIVATE EDGE default`를 입력한 뒤 `BLACKBOX PRIVATE EDGE SOURCE PASS`, immediate
RC `0`, normalized proof metadata `regular|0:0|600|1`를 모두 확인합니다. 하나라도 실패하거나
불명확하면 STOP하고 new ID를 보존하며 같은 ID로 validator나 Task 6를 재시도하지 않습니다.
proof content는 읽지 않습니다.
preflight/live residue는 absolute zero가 아니라 다음 attested preexisting name-only baseline의
unchanged 계약입니다.
```text
/tmp/platform-k3s-encryption.Mskzy3
/tmp/platform-observability-access-apply.oeNcfI
/tmp/platform-observability-access-apply.Im02dz
/tmp/platform-observability-slack-gate.LYhYbv
```
`Mskzy3`는 8/1 empty evidence, `oeNcfI`/`Im02dz`는 8/12 recorded evidence,
`LYhYbv`는 private filename 두 개만 attested된 failed-live evidence입니다. 네 root는 content를
읽거나 삭제하지 않습니다. baseline name set unchanged, matching executable process `0`, current
preflight/live newly-created matching-root delta `0`을 요구합니다. unknown/new root는 broad
delete하지 않고 STOP/identity review합니다.
## 3. Secret과 Grafana OIDC recovery evidence
먼저 K3s encryption과 restore evidence를 각각 새 process에서 검사합니다.
```bash
bash scripts/validate/k3s-secret-encryption.sh --expect-reencrypted
bash scripts/validate/k3s-secret-encryption-restore-evidence.sh --check
```
입력 파일은 현재 사용자 소유 `0600`, non-symlink, link count 1이어야 합니다.
값을 shell 변수, argv, stdout 또는 runbook에 넣지 않습니다.
```bash
bash scripts/bootstrap/create-observability-secrets.sh \
--execute --grafana-admin \
--grafana-admin-user-file /home/donghyeon/.secrets/grafana/admin-user \
--grafana-admin-password-file /home/donghyeon/.secrets/grafana/admin-password
```
Slack Secret bootstrap 직전에 별도 내장 Windows SSD의 기존 KDBX에 same-host encrypted
disaster-recovery copy를 준비합니다. 이것은 일반 K3s restart나 host reboot용 사본이 아니라
datastore·Secret·bootstrap state 손실 때를 위한 것입니다. 기본 no-argument 실행은 고정
contract만 출력하며 SSD, KDBX, webhook, sudo에 접근하지 않습니다. 지원되는 interface는
다음 두 개뿐입니다.
```bash
bash scripts/bootstrap/backup-slack-webhook-recovery.sh
bash scripts/bootstrap/backup-slack-webhook-recovery.sh \
--execute \
--slack-webhook-file /home/donghyeon/.secrets/alertmanager/slack-webhook
```
execute에서 `SLACK_KEEPASS_RECOVERY=NOOP`는 exact entry의 verified read-only no-op이고,
`SLACK_KEEPASS_RECOVERY=COMMITTED`는 durable pre-change backup을 만든 verified commit입니다.
둘 다 source-based unmount proof와 private work/socket/helper cleanup 뒤에만 성공하며
`WINDOWS_SSD_UNMOUNTED=PASS`, `OFF_HOST_RECOVERY_SATISFIED=NO`를 출력합니다. lost response,
post-commit verification failure 또는 cleanup/unmount ambiguity는 자동 재시도하지 않고
`SLACK_KEEPASS_RECOVERY=MANUAL_RECOVERY_REQUIRED`로 중단하며 main과 backup을 보존합니다.
webhook payload, KeePassXC master password, hash·encoding·size·URL component 또는 protected
KDBX output을 terminal, argv, environment, log, runbook이나 plaintext 파일에 남기지 않습니다.
This local encrypted copy does not authorize RECOVERY SLACK default when the
approved gate requires off-host escrow. Do not continue the Secret bootstrap
until that independent prerequisite is literally true.
Slack에는 서로 다른 두 경로가 있습니다. off-host disaster recovery를 완료로 판정할 때만
strict recovery evidence를 검사합니다.
```bash
bash scripts/bootstrap/create-observability-secrets.sh \
--check-slack-recovery-evidence
```
현재 사용자가 승인한 operational risk path는 off-host Slack DR이 아직 deferred인 사실을
기록하고 deployment evidence를 만듭니다. 이 경로는 DR-complete을 주장하지 않습니다.
```bash
bash scripts/bootstrap/create-observability-secrets.sh \
--execute --slack-webhook \
--slack-webhook-file /home/donghyeon/.secrets/alertmanager/slack-webhook \
--accept-no-off-host-slack-recovery
```
도구가 요구하는 정확한 확인은 `ACCEPT NO OFF-HOST SLACK RECOVERY default`입니다.
이 risk path 밖에서 kubectl로 Secret을 수동 생성하지 않습니다. off-host 복구 증거가
없을 때 거짓 `RECOVERY SLACK default` 확인을 입력하지 않습니다.
위 risk path 또는 실제 off-host recovery evidence가 준비된 경우 Slack Secret bootstrap은
이미 완료된 상태이므로, deployment gate와 다음 checker만 실행합니다.
```bash
bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh --execute
bash scripts/bootstrap/create-observability-secrets.sh \
--check-grafana-recovery-evidence
bash scripts/bootstrap/create-observability-secrets.sh \
--check-slack-deployment-evidence
bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh \
--check-recovery-evidence
```
기존 Secret의 payload가 다르면 자동 rotation하지 않고 중단합니다. UID drift나 API
결과 불명도 자동 삭제로 처리하지 않습니다.
Grafana admin/OIDC object가 이미 exact live state이면 도구는 credential을 회전하거나
workload를 다시 쓰지 않고 기존 payload를 재사용하며 recovery evidence만 검증·갱신합니다.
exact state가 아닌데 권위 prior와 ownership을 증명할 수 없으면 자동 수렴시키지 않습니다.
## 4. 권위 inventory로 rules-alerts handoff 생성
현재 Blackbox substrate가 이미 live이므로 `target-initial`을 지금 다시 캡처하지
않습니다. 실행 당시 보존한 두 phase만 새 `0700` output root에 복제하고 renderer가
schema, phase, mode, link count와 hash를 다시 검증하게 합니다.
먼저 공개 runbook이 실제로 게시되었는지 확인합니다. `200`이 아니면 renderer와
rules-alerts apply를 실행하지 않습니다.
```bash
RUNBOOK_URL='https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md'
runbook_status="$(curl --disable --silent --show-error --location --output /dev/null \
--write-out '%{http_code}' --connect-timeout 3 --max-time 10 \
"$RUNBOOK_URL")"
[[ "$runbook_status" == 200 ]]
```
```bash
SOURCE_METRIC_ROOT=/tmp/platform-observability-metrics.VUpsZn
METRIC_ROOT="$(mktemp -d /tmp/platform-observability-metrics.XXXXXX)"
chmod 0700 "$METRIC_ROOT"
[[ "$(realpath --canonicalize-existing -- "$SOURCE_METRIC_ROOT")" == "$SOURCE_METRIC_ROOT" ]]
[[ "$(stat -c '%F|%u:%g|%a|%h' -- "$SOURCE_METRIC_ROOT")" == \
'directory|1000:1000|700|4' ]]
declare -A SOURCE_METRIC_IDENTITY=()
for phase in target-initial post-substrate; do
[[ -d "$SOURCE_METRIC_ROOT/$phase" && ! -L "$SOURCE_METRIC_ROOT/$phase" ]]
[[ "$(realpath --canonicalize-existing -- "$SOURCE_METRIC_ROOT/$phase")" == \
"$SOURCE_METRIC_ROOT/$phase" ]]
[[ "$(stat -c '%F|%u:%g|%a|%h' -- "$SOURCE_METRIC_ROOT/$phase")" == \
'directory|1000:1000|700|2' ]]
install -d -m 0700 -- "$METRIC_ROOT/$phase"
for file in inventory.json inventory.sha256; do
source_file="$SOURCE_METRIC_ROOT/$phase/$file"
destination_file="$METRIC_ROOT/$phase/$file"
[[ -f "$source_file" && ! -L "$source_file" ]]
[[ "$(realpath --canonicalize-existing -- "$source_file")" == "$source_file" ]]
[[ "$(stat -c '%F|%u:%g|%a|%h' -- "$source_file")" == \
'regular file|1000:1000|600|1' ]]
SOURCE_METRIC_IDENTITY["$phase/$file"]="$(stat -c '%d:%i|%F|%u:%g|%a|%h|%s|%Y|%Z' -- \
"$source_file")|$(sha256sum -- "$source_file" | awk '{print $1}')"
cp --no-dereference --reflink=never -- "$source_file" "$destination_file"
chmod 0600 "$destination_file"
[[ "$(stat -c '%F|%u:%g|%a|%h' -- "$destination_file")" == \
'regular file|1000:1000|600|1' ]]
cmp -s -- "$source_file" "$destination_file"
done
done
[[ "$(find "$METRIC_ROOT" -mindepth 1 -maxdepth 2 -printf '%P\n' | LC_ALL=C sort)" == \
$'post-substrate\npost-substrate/inventory.json\npost-substrate/inventory.sha256\ntarget-initial\ntarget-initial/inventory.json\ntarget-initial/inventory.sha256' ]]
for phase in target-initial post-substrate; do
for file in inventory.json inventory.sha256; do
source_file="$SOURCE_METRIC_ROOT/$phase/$file"
[[ "$(stat -c '%d:%i|%F|%u:%g|%a|%h|%s|%Y|%Z' -- "$source_file")|$(sha256sum -- \
"$source_file" | awk '{print $1}')" == "${SOURCE_METRIC_IDENTITY["$phase/$file"]}" ]]
done
done
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
bash scripts/validate/render-observability-access.sh \
--component rules-alerts --verified-output-dir "$METRIC_ROOT"
```
failed transaction handoff `/tmp/platform-observability-metrics.LNzksC`는 read-only immutable
evidence로 보존하고 source, destination 또는 live apply input으로 재사용하지 않습니다.
기존 attested path/fingerprint identity만 보존·비교하며 inventory body나 private file content를
다시 읽지 않습니다. fresh destination은 renderer 전 exact two phase directories만 가집니다.
renderer와 live apply는 이 절에서 byte-preserving copy와 metadata/hash/count revalidation을
마친 fresh `$METRIC_ROOT`만 사용합니다.
위 exact six-entry gate는 renderer 전 destination이 두 phase directory와 네 file만 갖는지
확인합니다. 각 source/destination pair는 byte-equal이며 copy 뒤 source inode/metadata/size/hash가
copy 전 fingerprint와 같아야 합니다. 이어지는 renderer는 current production pins와 exact target
counts `21/30`, checksum/schema/semantic contract, 그리고 reviewed rendered manifest set을 다시
검증합니다. source/destination metadata, fingerprint, byte equality, pins, counts 또는 entry set
중 하나라도 다르면 fresh root를 apply input으로 사용하지 않고 STOP합니다.
권위 inventory hash는 다음과 같습니다.
```text
target-initial: 79688d017d38eec9a6f100f8d0f784a5474e79802046ef1c2c11b30d170b0b0c
post-substrate: b1c3049206a1a88165ee672ae9aceac7945673a3bb9c3cf3670b7f0d56c3f291
```
이 두 SHA는 현재 cluster freshness artifact가 아니라 변경할 수 없는 historical
metric/label provenance pair입니다. `target-initial``post-substrate`는 각각 exact
target count `21``30`을 initial gate와 confirmation 뒤 first mutation 직전
last gate 모두에서 다시 검증합니다. `captured_at_utc`는 exact UTC-second
형식과 유효한 UTC calendar로 parse되어야 하며, 현재보다 300초를 초과해
미래인 시각만 거부합니다. 이 exact pair에는 과거 방향 24시간 상한을
적용하지 않으며, timestamp나 checksum을 현재 시각에 맞게 다시 쓰거나
inventory를 재수집해서는 안 됩니다. 이 예외는 2절 Blackbox source proof의
기존 24시간 freshness 계약에는 적용되지 않습니다.
## 5. rules-alerts 적용 — 2026-08-15 terminal PASS
먼저 no-argument dry-run과 focused test를 실행합니다. 둘 중 하나라도 끝나지 않거나
실패하면 mutation을 실행하지 않습니다.
```bash
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
bash scripts/bootstrap/apply-observability-access.sh
bash scripts/validate/test-apply-observability-access.sh
```
성공한 뒤에만 다음을 실행합니다.
```bash
case $- in *e*) TASK6_APPLY_ERREXIT_WAS_SET=1 ;; *) TASK6_APPLY_ERREXIT_WAS_SET=0 ;; esac
set +e
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm bash scripts/bootstrap/apply-observability-access.sh --execute --rules-alerts --verified-output-dir "$METRIC_ROOT"
TASK6_APPLY_RC=$?
printf 'TASK6_APPLY_RC=%d\n' "$TASK6_APPLY_RC"
(( TASK6_APPLY_ERREXIT_WAS_SET == 0 )) || set -e
[[ "$TASK6_APPLY_RC" -eq 0 ]]
```
operator만 exact `APPLY` confirmation을 입력합니다. apply는 위 exact one-line command로 한 번만
실행하고, 바로 다음 statement가 다른 command 없이 `TASK6_APPLY_RC=$?`를 capture합니다.
immediate printed RC `0`과 exact `OBSERVABILITY_ACCESS_RULES_ALERTS=PASS`를 모두 확인한 경우에만
후속 acceptance를 진행합니다. nonzero, missing/ambiguous RC 또는 PASS, response loss, rollback
ambiguity는 STOP하고 rollback ID와 evidence를 보존하며 같은 ID로 재시도하지 않습니다.
성공 조건은 다음 전부입니다.
- Prometheus와 Alertmanager owner/controller Ready
- Prometheus API의 desired alert·record exact set과 evaluation health 정상
- Alertmanager generated config의 `observability/platform-alertmanager/platform-slack` receiver exact singleton
- Grafana sidecar의 exact dashboard 5개와 source content hash 일치
- 기존 target·Grafana·Blackbox·Probe·Ingress·PVC·Secret 보존
- acceptance marker는 모든 증거 뒤에만 root-only로 기록
apply 도구는 deployment checker를 confirmation 전과 Slack Secret-consuming mutation 직전에
두 번 호출한다. rules-alerts acceptance ledger schema는
`platform-observability-rules-alerts-v2`이며 Slack deployment gate 값은 bare `RECOVERY` 또는
`RISK_ACCEPTED`만 기록한다. 이는 operational acceptance이며 off-host Slack DR-complete을
의미하지 않는다.
실제 terminal transaction은 fresh rollback ID `20260814T145009Z`와 fresh handoff
`/tmp/platform-observability-metrics.dw5gLZ`를 사용했다. argv는 exact six-element array로
attest됐고, operator가 exact `APPLY`를 입력한 단 한 번의 실행에서 다음 safe marker를 확인했다.
```text
target-initial SHA-256 = 79688d017d38eec9a6f100f8d0f784a5474e79802046ef1c2c11b30d170b0b0c
post-substrate SHA-256 = b1c3049206a1a88165ee672ae9aceac7945673a3bb9c3cf3670b7f0d56c3f291
OBSERVABILITY_ACCESS_RULES_ALERTS=PASS
TASK6_APPLY_RC=0
```
payload-free terminal audit는 dashboard `5`, platform PrometheusRule `4`, 전체 desired rule
`23`(`22` alerts + `1` recording) healthy, runbook URL `22/22`, AlertmanagerConfig `1`, exact
NetworkPolicy, target `30/30`, Grafana·Blackbox·Prometheus·Alertmanager Ready와 qualified receiver
exact singleton을 확인했다. acceptance schema와 `RISK_ACCEPTED` gate, root-only ledger의
object/mutation line `13/13` 및 metadata contract도 통과했다. 성공 transaction에는 rollback이
호출되지 않았고 rollback root와 handoff는 Task 7 종료까지 보존한다.
failed rollback ID `20260814T080303Z`와 argument paste가 파싱 전에 중단된
`20260814T140953Z`는 immutable evidence로 보존하고 재사용하지 않는다. 후자는
`--verified-output-dir` token이 줄바꿈으로 분리돼 usage RC `2`, shell-level RC `127`로 끝났으며
ledger·acceptance·Kubernetes mutation은 생성되지 않았다.
## 6. Task 7 operator boundary와 Host Nginx first cutover
Task 7은 성공 rollback ID `20260814T145009Z`와 original handoff
`/tmp/platform-observability-metrics.dw5gLZ`를 그대로 보존합니다. active state는 exact deny-only
SHA-256 `dbef6d443bcba58b26a5351ea76f6d09f6da8c2ef07a806e22745cf26c88f518`,
desired full은 `7d2de2a92c3597a0859775da1d2ccbf5a3d72c0b2af5cac2439c82311361f801`여야
합니다. full이 이미 active이거나 third state이면 STOP합니다.
어떤 external preparation command보다 먼저 fixed PATH를 export하고 command cache를 비운 뒤
reviewed command/launcher inventory를 byte-equal로 재검증합니다. ambient PATH command로 prep를
시작하지 않습니다. exact canonical `/usr/bin/sudo``regular|0:0|4755|1`을 요구하는
유일한 owner-setuid 예외입니다. setgid·group/world write는 금지되고 다른 allowlisted
executable은 setuid/setgid를 모두 금지합니다.
```bash
TASK7_OPERATOR_PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PATH=$TASK7_OPERATOR_PATH
export PATH
hash -r
cd /home/donghyeon/workspace/platform
TASK7_ID=20260814T145009Z
TASK7_METRIC_ROOT=/tmp/platform-observability-metrics.dw5gLZ
METRIC_ROOT=$TASK7_METRIC_ROOT
export PLATFORM_OBSERVABILITY_ROLLBACK_ID="$TASK7_ID"
[[ "$PLATFORM_OBSERVABILITY_ROLLBACK_ID" == "$TASK7_ID" ]]
[[ "$METRIC_ROOT" == "$TASK7_METRIC_ROOT" ]]
[[ "$(/usr/bin/readlink -f -- "$TASK7_METRIC_ROOT")" == "$TASK7_METRIC_ROOT" ]]
TASK7_HOST_DRY=(/usr/bin/bash)
TASK7_HOST_DRY+=(/home/donghyeon/workspace/platform/scripts/bootstrap/apply-host-nginx-observability.sh)
[[ "${#TASK7_HOST_DRY[@]}" -eq 2 ]]
case $- in *e*) TASK7_HOST_DRY_ERREXIT=1 ;; *) TASK7_HOST_DRY_ERREXIT=0 ;; esac
set +e
"${TASK7_HOST_DRY[@]}"
TASK7_HOST_DRY_RC=$?
printf 'TASK7_HOST_DRY_RC=%d\n' "$TASK7_HOST_DRY_RC"
(( TASK7_HOST_DRY_ERREXIT == 0 )) || set -e
```
no-arg는 source/hash/no-contact만 검증합니다. certificate/SAN, DNS, Kubernetes, proof
ID/age, NodePort와 network boundary는 execute 내부의 `APPLY` prompt 전 live gate입니다.
dry-run은 Host mutation `0`과 다음 exact output을 요구합니다.
```text
HOST_NGINX_ACTIVE_SHA256=dbef6d443bcba58b26a5351ea76f6d09f6da8c2ef07a806e22745cf26c88f518
HOST_NGINX_GRAFANA_DENY_GUARD_SHA256=dbef6d443bcba58b26a5351ea76f6d09f6da8c2ef07a806e22745cf26c88f518
HOST_NGINX_FULL_SHA256=7d2de2a92c3597a0859775da1d2ccbf5a3d72c0b2af5cac2439c82311361f801
HOST_NGINX_CERTIFICATE_EXPECTED_SAN=grafana.learn.hyeonworks.com
HOST_NGINX_CERTIFICATE_SAN=NOT_CHECKED_DRY_RUN
HOST_NGINX_GRAFANA_PUBLIC_DNS=NOT_CHECKED_DRY_RUN
HOST_NGINX_OBSERVABILITY_DRY_RUN=PASS
TASK7_HOST_DRY_RC=0
```
`HOST_NGINX_CERTIFICATE_EXACT_SAN=``HOST_NGINX_GRAFANA_PUBLIC_DNS=ABSENT`를 dry-run
결과로 받으면 STOP합니다. source proof는 exact ID/deny hash/status/IP/time에 bind되며
과거 24시간, 미래 300초 경계를 벗어나면 fresh rollback ID, fresh proof, complete Task 6를
다시 수행합니다. 같은 ID에서 proof만 바꾸거나 timestamp를 다시 쓰지 않습니다.
execute가 `APPLY`를 표시하기 전에 다음 네 path가 각각 `test -e`/`test -L` 모두에서
absent임을 no-follow, name-only 순서로 입증해야 합니다.
```text
/var/lib/hyeonworks/platform-rollbacks/observability-20260814T145009Z/host-nginx
/var/lib/hyeonworks/platform-rollbacks/observability-20260814T145009Z/host-nginx/stages.tsv
/var/lib/hyeonworks/platform-rollbacks/observability-20260814T145009Z/host-nginx/payloads
/var/lib/hyeonworks/platform-rollbacks/observability-20260814T145009Z/host-nginx/payloads/full-prior-0001.conf
```
일반 directory/file로 남은 ledger/payload도 reusable recovery state가 아니라 STOP residue입니다.
reviewed sudo identity를 다시 확인한 뒤에만 operator가 credential를 refresh합니다.
```bash
/usr/bin/sudo -v
/usr/bin/sudo -n /usr/bin/true
printf 'SUDO_READY\n'
TASK7_VOD=--verified
TASK7_VOD+=-output-dir
TASK7_HOST=(/usr/bin/bash)
TASK7_HOST+=(/home/donghyeon/workspace/platform/scripts/bootstrap/apply-host-nginx-observability.sh)
TASK7_HOST+=(--execute)
TASK7_HOST+=("$TASK7_VOD")
TASK7_HOST+=("$TASK7_METRIC_ROOT")
[[ "${#TASK7_HOST[@]}" -eq 5 ]]
printf 'TASK7_HOST_ARGC=%d\n' "${#TASK7_HOST[@]}"
case $- in *e*) TASK7_HOST_ERREXIT=1 ;; *) TASK7_HOST_ERREXIT=0 ;; esac
set +e
"${TASK7_HOST[@]}"
TASK7_HOST_RC=$?
printf 'TASK7_HOST_RC=%d\n' "$TASK7_HOST_RC"
(( TASK7_HOST_ERREXIT == 0 )) || set -e
```
operator만 exact `APPLY`를 입력합니다. 성공은 exact `HOST_NGINX_FULL_STAGE=PASS`
`TASK7_HOST_RC=0`이 모두 있을 때뿐입니다. `ALREADY_ACTIVE`, missing/ambiguous marker,
nonzero RC, response loss는 모두 실패이며 같은 ID로 재실행하지 않습니다.
1. prompt 전 실패: active deny unchanged, Host ledger/mutation `0`, rollback N/A.
2. prompt 뒤 `rollback_armed=true` 전 실패: active config/reload mutation `0`, root-owned
ledger/payload 또는 timestamp backup staging은 남을 수 있으며 rollback N/A. 전체 evidence를
보존합니다.
3. active install 뒤 실패: exact `HOST_NGINX_OBSERVABILITY_ROLLBACK=PASS`와 deny hash
복원을 요구합니다.
4. `ROLLBACK=FAIL`, `MANUAL_RECOVERY_REQUIRED=YES`, unknown stage/hash: 모든 후속 gate를
STOP합니다.
`stages.tsv`는 prior-payload recovery ledger이지 success marker가 아닙니다. ledger/payload/timestamp
staging 생성·검증·설치 중 하나라도 실패하면 현 ID/root를 보존하고 fresh ID,
source proof, complete Task 6를 다시 수행합니다. staged evidence를 repair/reuse하거나
failed fresh ID를 재사용하지 않습니다.
## 7. OIDC membership과 사람/external readiness
Host PASS 뒤 mutation 전에 서로 다른 admin, viewer, no-group, membership-removal test
identity, local break-glass 접근, Slack firing/resolved view, LAN/Tailscale 밖 proxy-disabled
external client를 모두 준비합니다. 하나라도 없으면 membership와 smoke를 시작하지
않습니다. username은 stdin으로만 받고 기록하지 않습니다.
```bash
read -r -p 'Grafana organization admin realm username: ' OBS_ADMIN_USER
read -r -p 'Grafana viewer realm username: ' OBS_VIEWER_USER
TASK7_OIDC=(/usr/bin/bash /home/donghyeon/workspace/platform/scripts/bootstrap/configure-keycloak-grafana-oidc.sh)
TASK7_OIDC+=(--execute)
TASK7_OIDC+=(--admin "$OBS_ADMIN_USER")
TASK7_OIDC+=(--viewer "$OBS_VIEWER_USER")
case $- in *e*) TASK7_OIDC_ERREXIT=1 ;; *) TASK7_OIDC_ERREXIT=0 ;; esac
set +e
"${TASK7_OIDC[@]}"
TASK7_OIDC_RC=$?
unset OBS_ADMIN_USER OBS_VIEWER_USER TASK7_OIDC
printf 'TASK7_OIDC_RC=%d\n' "$TASK7_OIDC_RC"
(( TASK7_OIDC_ERREXIT == 0 )) || set -e
```
operator는 exact `APPLY default``RECOVERY KEYCLOAK default`를 입력합니다.
`GRAFANA_OIDC_TRANSACTION=PASS`와 RC `0`을 모두 요구합니다.
`transaction_active=true` 전 실패는 managed Keycloak/OIDC Secret/membership mutation `0`,
rollback N/A입니다. active failure는 exact `GRAFANA_OIDC_ROLLBACK=PASS`를 요구합니다.
rollback FAIL, manual recovery 또는 unknown stage는 STOP입니다. OIDC rollback은 실행 중 private
snapshot을 사용하는 in-process rollback입니다. 성공 뒤 복원용 persistent Task 6 Keycloak
reversal ledger가 있다고 주장하거나 탐색하지 않습니다.
## 8. monolithic observability smoke exactly once
no-arg는 acceptance가 아닌 계획 확인으로 한 번만 실행합니다.
```bash
TASK7_SMOKE_DRY=(/usr/bin/bash)
TASK7_SMOKE_DRY+=(/home/donghyeon/workspace/platform/scripts/validate/observability-smoke.sh)
[[ "${#TASK7_SMOKE_DRY[@]}" -eq 2 ]]
case $- in *e*) TASK7_SMOKE_DRY_ERREXIT=1 ;; *) TASK7_SMOKE_DRY_ERREXIT=0 ;; esac
set +e
"${TASK7_SMOKE_DRY[@]}"
TASK7_SMOKE_DRY_RC=$?
printf 'TASK7_SMOKE_DRY_RC=%d\n' "$TASK7_SMOKE_DRY_RC"
(( TASK7_SMOKE_DRY_ERREXIT == 0 )) || set -e
```
RC `0`, `OBSERVABILITY_SMOKE_DRY_RUN=PASS`, `HUMAN_EXTERNAL_CLIENT=required`,
`MUTATION=NOT_REQUESTED`를 요구합니다. 그 뒤 machine, OIDC human/session, Slack
firing/resolved, true external-client attestation을 하나의 execute에서만 수행합니다.
```bash
TASK7_SMOKE=(/usr/bin/bash)
TASK7_SMOKE+=(/home/donghyeon/workspace/platform/scripts/validate/observability-smoke.sh)
TASK7_SMOKE+=(--execute)
case $- in *e*) TASK7_SMOKE_ERREXIT=1 ;; *) TASK7_SMOKE_ERREXIT=0 ;; esac
set +e
"${TASK7_SMOKE[@]}"
TASK7_SMOKE_RC=$?
printf 'TASK7_SMOKE_RC=%d\n' "$TASK7_SMOKE_RC"
(( TASK7_SMOKE_ERREXIT == 0 )) || set -e
```
operator만 requested identity와 exact dynamic confirmation을 입력합니다. 성공은 다음 전체
marker와 immediate RC를 요구합니다.
```text
OBSERVABILITY_MACHINE_ACCEPTANCE=PASS
OBSERVABILITY_OIDC_ACCEPTANCE=PASS
OBSERVABILITY_SLACK_ACCEPTANCE=PASS
OBSERVABILITY_EXTERNAL_BOUNDARY=PASS
OBSERVABILITY_SMOKE=PASS
TASK7_SMOKE_RC=0
```
RC `2` 또는 `OBSERVABILITY_EXTERNAL_BOUNDARY=PARTIAL`은 Task 7을 `부분 구현`으로 남깁니다.
external result를 server-side, LAN 또는 Tailscale probe로 대체하지 않습니다. cleanup ambiguity는
owned-object review 전 automatic rerun을 금지합니다.
## 9. fresh inventory-only renderer root와 단일 회귀 pass
fixed PATH를 다시 설치하고 `hash -r`, command inventory byte equality를 external prep 전에
확인합니다. original `dw5gLZ`의 canonical path, owner/mode/nlink, exact entry set, two
inventory hash와 three Task 6 YAML fingerprint를 보존합니다. complete publication에 original
root를 사용하지 않습니다.
```bash
TASK7_RENDER_ROOT="$(/usr/bin/mktemp -d /tmp/platform-observability-metrics.XXXXXX)"
/usr/bin/chmod 0700 "$TASK7_RENDER_ROOT"
for phase in target-initial post-substrate; do
/usr/bin/install -d -m 0700 -- "$TASK7_RENDER_ROOT/$phase"
for file in inventory.json inventory.sha256; do
source_file="$TASK7_METRIC_ROOT/$phase/$file"
destination_file="$TASK7_RENDER_ROOT/$phase/$file"
[[ -f "$source_file" && ! -L "$source_file" ]]
/usr/bin/cp --no-dereference --reflink=never -- "$source_file" "$destination_file"
/usr/bin/chmod 0600 "$destination_file"
/usr/bin/cmp -s -- "$source_file" "$destination_file"
done
done
unset source_file destination_file
```
destination은 exact six-entry topology, current owner, root/phase `0700`, file `0600`, nlink `1`,
byte equality와 known inventory hash를 요구합니다. copy 뒤 original fingerprint가 unchanged여야
합니다. 실패한 fresh root는 evidence로 보존하고 repair/reuse하지 않습니다.
core와 complete renderer, admin UI, AIStor S3, phase1, phase2, admin renderer를 다음 exact
array/envelope로 각각 한 번만 실행합니다.
```bash
TASK7_CORE=(/usr/bin/bash)
TASK7_CORE+=(/home/donghyeon/workspace/platform/scripts/validate/render-observability-core.sh)
[[ "${#TASK7_CORE[@]}" -eq 2 ]]
case $- in *e*) TASK7_CORE_ERREXIT=1 ;; *) TASK7_CORE_ERREXIT=0 ;; esac
set +e
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_CORE[@]}"
TASK7_CORE_RC=$?
(( TASK7_CORE_ERREXIT == 0 )) || set -e
printf 'TASK7_CORE_RC=%d\n' "$TASK7_CORE_RC"
TASK7_VOD=--verified
TASK7_VOD+=-output-dir
TASK7_COMPLETE=(/usr/bin/bash)
TASK7_COMPLETE+=(/home/donghyeon/workspace/platform/scripts/validate/render-observability-access.sh)
TASK7_COMPLETE+=(--component complete)
TASK7_COMPLETE+=("$TASK7_VOD" "$TASK7_RENDER_ROOT")
[[ "${#TASK7_COMPLETE[@]}" -eq 6 ]]
case $- in *e*) TASK7_COMPLETE_ERREXIT=1 ;; *) TASK7_COMPLETE_ERREXIT=0 ;; esac
set +e
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_COMPLETE[@]}"
TASK7_COMPLETE_RC=$?
(( TASK7_COMPLETE_ERREXIT == 0 )) || set -e
printf 'TASK7_COMPLETE_RC=%d\n' "$TASK7_COMPLETE_RC"
TASK7_ADMIN_UI=(/usr/bin/bash)
TASK7_ADMIN_UI+=(/home/donghyeon/workspace/platform/scripts/validate/admin-ui-smoke.sh)
[[ "${#TASK7_ADMIN_UI[@]}" -eq 2 ]]
case $- in *e*) TASK7_ADMIN_UI_ERREXIT=1 ;; *) TASK7_ADMIN_UI_ERREXIT=0 ;; esac
set +e
"${TASK7_ADMIN_UI[@]}"
TASK7_ADMIN_UI_RC=$?
(( TASK7_ADMIN_UI_ERREXIT == 0 )) || set -e
printf 'TASK7_ADMIN_UI_RC=%d\n' "$TASK7_ADMIN_UI_RC"
TASK7_AISTOR_S3=(/usr/bin/bash)
TASK7_AISTOR_S3+=(/home/donghyeon/workspace/platform/scripts/validate/aistor-s3-smoke.sh)
TASK7_AISTOR_S3+=(--execute)
[[ "${#TASK7_AISTOR_S3[@]}" -eq 3 ]]
case $- in *e*) TASK7_AISTOR_S3_ERREXIT=1 ;; *) TASK7_AISTOR_S3_ERREXIT=0 ;; esac
set +e
"${TASK7_AISTOR_S3[@]}"
TASK7_AISTOR_S3_RC=$?
(( TASK7_AISTOR_S3_ERREXIT == 0 )) || set -e
printf 'TASK7_AISTOR_S3_RC=%d\n' "$TASK7_AISTOR_S3_RC"
TASK7_PHASE1=(/usr/bin/bash)
TASK7_PHASE1+=(/home/donghyeon/workspace/platform/scripts/validate/render-phase1.sh)
[[ "${#TASK7_PHASE1[@]}" -eq 2 ]]
case $- in *e*) TASK7_PHASE1_ERREXIT=1 ;; *) TASK7_PHASE1_ERREXIT=0 ;; esac
set +e
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_PHASE1[@]}"
TASK7_PHASE1_RC=$?
(( TASK7_PHASE1_ERREXIT == 0 )) || set -e
printf 'TASK7_PHASE1_RC=%d\n' "$TASK7_PHASE1_RC"
TASK7_PHASE2=(/usr/bin/bash)
TASK7_PHASE2+=(/home/donghyeon/workspace/platform/scripts/validate/render-phase2.sh)
[[ "${#TASK7_PHASE2[@]}" -eq 2 ]]
case $- in *e*) TASK7_PHASE2_ERREXIT=1 ;; *) TASK7_PHASE2_ERREXIT=0 ;; esac
set +e
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_PHASE2[@]}"
TASK7_PHASE2_RC=$?
(( TASK7_PHASE2_ERREXIT == 0 )) || set -e
printf 'TASK7_PHASE2_RC=%d\n' "$TASK7_PHASE2_RC"
TASK7_ADMIN_RENDER=(/usr/bin/bash)
TASK7_ADMIN_RENDER+=(/home/donghyeon/workspace/platform/scripts/validate/render-admin-services.sh)
[[ "${#TASK7_ADMIN_RENDER[@]}" -eq 2 ]]
case $- in *e*) TASK7_ADMIN_RENDER_ERREXIT=1 ;; *) TASK7_ADMIN_RENDER_ERREXIT=0 ;; esac
set +e
PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm "${TASK7_ADMIN_RENDER[@]}"
TASK7_ADMIN_RENDER_RC=$?
(( TASK7_ADMIN_RENDER_ERREXIT == 0 )) || set -e
printf 'TASK7_ADMIN_RENDER_RC=%d\n' "$TASK7_ADMIN_RENDER_RC"
```
다섯 renderer의 exact Helm assignment을 생략하지 않습니다. 특히 phase1/phase2는 fixed
PATH에 Helm이 없으므로 `command -v helm` fallback을 허용하지 않습니다. 전체 RC `0`,
expected terminal PASS, complete seven-artifact publication, original fingerprint unchanged와 new
residue `0`을 요구합니다. 존재하지 않는 core smoke를 호출하지 않고 monolithic smoke
execute를 다시 실행하지 않습니다.
## 10. rollback·STOP 경계
Host/rules recovery evidence는 성공 Task 6 root에서 각 transaction 소유 범위만 사용합니다.
OIDC는 persistent Task 6 reversal ledger가 아니라 in-process private snapshot으로만 rollback합니다.
PVC, Secret, CRD, PV, Loki/Tempo object·bucket은 자동 삭제하지 않습니다. API timeout,
response loss, UID drift, third-state, controller 비수렴, ledger mismatch, rollback ambiguity는
`MANUAL_RECOVERY_REQUIRED=YES`로 STOP하고 evidence/root를 보존합니다.
## 11. 전체 완료 판정
Task 7은 Host/OIDC/smoke/renderer의 실제 RC·marker·cleanup과 independent review가 모두 있을
때만 완료로 표시합니다. admin, viewer, no-group, membership-removal, break-glass, Slack
firing/resolved, true external client 중 하나라도 미실행/실패면 `부분 구현`을 유지합니다.
Slack off-host DR은 Task 7 PASS와 무관하게 `deferred / not complete`이며 active exception을
유지합니다. 실제 terminal evidence의 independent review 전에는 중앙 Task 7 Step 16
checkbox를 체크하지 않고 Task 8을 시작하지 않습니다.