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

34 KiB
Raw Permalink Blame History

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가 준비된다.
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를 끝까지 유지합니다.

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 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을 순서대로 실행합니다. 각 명령의 화면 지시와 정확히 일치하는 확인 문자열만 입력합니다.

: "${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인지 먼저 확인합니다.

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 계약입니다.

/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 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 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 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 scripts/bootstrap/create-observability-secrets.sh \
  --check-slack-recovery-evidence

현재 사용자가 승인한 operational risk path는 off-host Slack DR이 아직 deferred인 사실을 기록하고 deployment evidence를 만듭니다. 이 경로는 DR-complete을 주장하지 않습니다.

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 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를 실행하지 않습니다.

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 ]]
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는 다음과 같습니다.

target-initial: 79688d017d38eec9a6f100f8d0f784a5474e79802046ef1c2c11b30d170b0b0c
post-substrate: b1c3049206a1a88165ee672ae9aceac7945673a3bb9c3cf3670b7f0d56c3f291

이 두 SHA는 현재 cluster freshness artifact가 아니라 변경할 수 없는 historical metric/label provenance pair입니다. target-initialpost-substrate는 각각 exact target count 2130을 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을 실행하지 않습니다.

PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \
  bash scripts/bootstrap/apply-observability-access.sh
bash scripts/validate/test-apply-observability-access.sh

성공한 뒤에만 다음을 실행합니다.

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를 확인했다.

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/sudoregular|0:0|4755|1을 요구하는 유일한 owner-setuid 예외입니다. setgid·group/world write는 금지되고 다른 allowlisted executable은 setuid/setgid를 모두 금지합니다.

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을 요구합니다.

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 순서로 입증해야 합니다.

/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합니다.

/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=PASSTASK7_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으로만 받고 기록하지 않습니다.

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 defaultRECOVERY 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가 아닌 계획 확인으로 한 번만 실행합니다.

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에서만 수행합니다.

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를 요구합니다.

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를 사용하지 않습니다.

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로 각각 한 번만 실행합니다.

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을 시작하지 않습니다.