702 lines
34 KiB
Markdown
702 lines
34 KiB
Markdown
# 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 1–6
|
||
checkbox를 체크하지 않고 Task 8을 시작하지 않습니다.
|