34 KiB
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, healthy30, unhealthy0 - Grafana와 Blackbox Exporter Deployment 각각
1/1Ready - Host Nginx는 Grafana deny-only guard 상태
- Grafana OIDC workload와
grafana-keycloak-oidcSecret 참조는 live - Slack Secret과 risk deployment evidence는 live이며 off-host Slack DR은 deferred
- platform dashboard ConfigMap
5, platform PrometheusRule4,AlertmanagerConfig/platform-alertmanager1이 rollback ID20260814T145009Z에서 accepted - generated receiver는
observability/platform-alertmanager/platform-slackexact singleton - full Nginx cutover와 browser OIDC·Slack firing/resolved·external-client acceptance는 미실행
rules-alerts 실행 때 다음 두 선행 조건은 모두 충족됐습니다. 이후 재실행이나 복구에서도 같은 gate를 생략하지 않습니다.
- 아래 runbook URL이 HTTPS
200으로 도달하고 모든 alert의runbook_url과 일치한다. - 운영자가 만든 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-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을 실행하지 않습니다.
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-slackreceiver 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/sudo는 regular|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=PASS와
TASK7_HOST_RC=0이 모두 있을 때뿐입니다. ALREADY_ACTIVE, missing/ambiguous marker,
nonzero RC, response loss는 모두 실패이며 같은 ID로 재실행하지 않습니다.
- prompt 전 실패: active deny unchanged, Host ledger/mutation
0, rollback N/A. - prompt 뒤
rollback_armed=true전 실패: active config/reload mutation0, root-owned ledger/payload 또는 timestamp backup staging은 남을 수 있으며 rollback N/A. 전체 evidence를 보존합니다. - active install 뒤 실패: exact
HOST_NGINX_OBSERVABILITY_ROLLBACK=PASS와 deny hash 복원을 요구합니다. 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 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가 아닌 계획 확인으로 한 번만 실행합니다.
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 1–6
checkbox를 체크하지 않고 Task 8을 시작하지 않습니다.