diff --git a/docs/runbooks/2026-07-31-observability-access-cutover.md b/docs/runbooks/2026-07-31-observability-access-cutover.md new file mode 100644 index 0000000..c5a7b3b --- /dev/null +++ b/docs/runbooks/2026-07-31-observability-access-cutover.md @@ -0,0 +1,216 @@ +# 관측성 운영 접근·알림 전환 Runbook + +## 목적 + +공용 관측성 플랫폼의 검증된 dashboard·rule·Alertmanager 설정을 적용한 뒤, 가장 +마지막에 Grafana private HTTPS 경로를 여는 절차와 rollback 경계를 고정한다. + +이 문서는 Secret 값, bearer token, Cookie, 사용자 ID, authorization code/state, +webhook 원문을 기록하지 않는다. 실행하지 않은 검사는 성공으로 기록하지 않는다. + +운영 alert의 `runbook_url`로 사용할 공개 위치는 다음 경로다. 이 파일이 해당 +저장소에 실제 게시되어 HTTPS `200`으로 도달하기 전에는 rules-alerts 적용을 시작하지 +않는다. + +```text +https://git.learn.hyeonworks.com/donghyeon.kang/project-infra/src/branch/main/docs/runbooks/2026-07-31-observability-access-cutover.md +``` + +## 현재 상태 + +기준 시각: `2026-08-12T07:58:23Z` + +상태: **부분 구현 — substrate와 metric target은 live, rules-alerts와 full Nginx +cutover는 미실행** + +- Prometheus active target `30`, healthy `30`, scrape pool `25`, unhealthy `0` +- Grafana와 Blackbox Exporter Deployment 각각 `1/1` Ready +- target-initial inventory SHA-256 + `79688d017d38eec9a6f100f8d0f784a5474e79802046ef1c2c11b30d170b0b0c` +- post-substrate inventory SHA-256 + `b1c3049206a1a88165ee672ae9aceac7945673a3bb9c3cf3670b7f0d56c3f291` +- Host Nginx active config는 Grafana deny-only guard SHA-256 + `dbef6d443bcba58b26a5351ea76f6d09f6da8c2ef07a806e22745cf26c88f518` +- full Host Nginx candidate SHA-256 + `7d2de2a92c3597a0859775da1d2ccbf5a3d72c0b2af5cac2439c82311361f801` +- Grafana OIDC Deployment는 exact `grafana-keycloak-oidc`의 `client-id`, + `client-secret`만 참조하며 `1/1` Ready +- 새 platform dashboard ConfigMap, 새 platform rule, AlertmanagerConfig와 Slack + webhook Secret은 아직 live acceptance 전이다. + +## 영향 + +rules-alerts 단계는 다음 object만 생성 또는 수정한다. + +- dashboard ConfigMap 5개 +- platform PrometheusRule 4개 +- AlertmanagerConfig 1개 +- kube-prometheus-stack Alertmanager/Prometheus owner object +- 필요한 observability NetworkPolicy + +Grafana·Blackbox workload, Probe, Ingress, PVC, Secret과 Host Nginx는 이 단계에서 +변경하지 않는다. full Nginx 단계는 rules-alerts acceptance가 끝난 뒤 Grafana +upstream을 deny endpoint에서 loopback Traefik으로 전환한다. + +## 사전 조건 + +다음 조건이 모두 통과해야 한다. + +1. current context와 API server가 각각 `default`, `https://127.0.0.1:6443`이다. +2. K3s Secret encryption은 `reencrypt_finished`이고 current restore evidence가 + 유효하다. +3. 같은 rollback ID 아래 root-only ledger root가 `root:root 0700`으로 존재한다. +4. Grafana local credential, Slack webhook, Keycloak의 recovery evidence가 각각 + 유효하다. +5. Slack Secret은 `observability/alertmanager-slack-webhook`, type `Opaque`, key는 + 정확히 `url` 하나다. 값은 검사 출력에 포함하지 않는다. +6. 두 inventory directory는 `0700`, 파일은 `0600`, non-symlink, link count 1이며 + 위 SHA-256과 일치한다. +7. Blackbox private-edge source proof는 같은 rollback ID와 active deny SHA에 + 결속되고 24시간 이내이며 세 hostname 모두 status `403`이다. +8. 이 runbook의 공개 URL이 HTTPS `200`으로 도달하고 모든 새 alert annotation이 + 해당 URL을 사용한다. +9. public Grafana A/AAAA는 없고 LAN·Tailscale·Pod resolver만 승인된 private IP를 + 반환한다. +10. Grafana exact-SAN 인증서, Keycloak discovery, Gitea/Admin endpoint, 내부 + Grafana health가 정상이다. + +## 절차 — 권위 실행 순서 + +아래 순서만 허용한다. 각 도구의 no-argument 실행과 focused test가 먼저 통과해야 +하며, 현재 독립 검토에서 `Ready: No`인 도구는 실행하지 않는다. + +```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$ ]] +: "${CERTBOT_EMAIL:?set the operator-managed Certbot contact email}" +sudo install -d -o root -g root -m 0700 \ + "/var/lib/hyeonworks/platform-rollbacks/observability-$OBS_ROLLBACK_ID" +export PLATFORM_OBSERVABILITY_ROLLBACK_ID="$OBS_ROLLBACK_ID" + +bash scripts/validate/observability-core-smoke.sh --execute +bash scripts/bootstrap/apply-private-dns.sh --execute + +bash scripts/bootstrap/apply-host-nginx-observability.sh \ + --execute --metrics-guard-only +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 +bash scripts/validate/validate-blackbox-edge-source.sh --execute --context default + +bash scripts/validate/k3s-secret-encryption.sh --expect-reencrypted +bash scripts/validate/k3s-secret-encryption-restore-evidence.sh --check + +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 +bash scripts/bootstrap/create-observability-secrets.sh \ + --execute --slack-webhook \ + --slack-webhook-file /home/donghyeon/.secrets/alertmanager/slack-webhook + +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-recovery-evidence +bash scripts/bootstrap/configure-keycloak-grafana-oidc.sh \ + --check-recovery-evidence + +SOURCE_METRIC_ROOT=/tmp/platform-observability-metrics.VUpsZn +METRIC_ROOT="$(mktemp -d /tmp/platform-observability-metrics.XXXXXX)" +chmod 0700 "$METRIC_ROOT" + +# Blackbox substrate가 이미 live이므로 target-initial을 현재 상태에서 재수집하면 안 된다. +# 실행 당시 보존한 두 authoritative inventory만 새 rules-alerts root로 복제한 뒤 renderer가 +# schema, phase, file metadata와 위 두 hash를 다시 검증하게 한다. +[[ -d "$SOURCE_METRIC_ROOT/target-initial" ]] +[[ -d "$SOURCE_METRIC_ROOT/post-substrate" ]] +cp -a -- "$SOURCE_METRIC_ROOT/target-initial" "$METRIC_ROOT/" +cp -a -- "$SOURCE_METRIC_ROOT/post-substrate" "$METRIC_ROOT/" +chmod 0700 "$METRIC_ROOT/target-initial" "$METRIC_ROOT/post-substrate" +chmod 0600 "$METRIC_ROOT/target-initial/"* "$METRIC_ROOT/post-substrate/"* + +PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \ + bash scripts/validate/render-observability-access.sh \ + --component rules-alerts --verified-output-dir "$METRIC_ROOT" + +PLATFORM_HELM_BIN=/home/donghyeon/.local/bin/helm \ + bash scripts/bootstrap/apply-observability-access.sh \ + --execute --rules-alerts --verified-output-dir "$METRIC_ROOT" + +bash scripts/bootstrap/apply-host-nginx-observability.sh \ + --execute --verified-output-dir "$METRIC_ROOT" +``` + +사용자 group membership은 위 transaction과 분리한다. 실제 기존 realm username은 +stdin으로 받고 명령 인자·이 문서에 기록하지 않는다. + +## 검증 — 자동 수락 조건 + +rules-alerts 성공 조건: + +- Prometheus와 Alertmanager controller가 Ready로 수렴한다. +- 모든 새 rule이 Prometheus API에서 구문·평가 가능하다. +- Alertmanager generated config에 exact `platform-slack` receiver가 있다. +- Grafana sidecar가 exact dashboard ConfigMap 5개를 실제로 읽었다. +- 기존 active target set은 유지되고 down/error target이 없다. +- Grafana·Blackbox·Probe·Ingress·Host Nginx·PVC·Secret의 UID와 계약이 보존된다. +- root-only acceptance marker는 위 조건이 모두 끝난 뒤에만 기록된다. + +full Nginx 성공 조건: + +- reload 뒤 연속 세 번 안정된 probe가 통과한다. +- 승인된 LAN/Tailscale source의 Grafana는 `200` 또는 로그인 redirect다. +- 비허용 local/public source는 `403`이다. +- Gitea와 Grafana 외부 `/metrics`는 `404`다. +- Keycloak·Gitea·storage-admin·db-admin 회귀가 없다. +- unknown SNI는 거부되고 LAN의 NodePort `30080/30443`은 계속 refused다. + +## 사람 확인 + +다음은 자동 검사로 대체하지 않는다. + +- observability admin group 사용자의 Grafana organization `Admin`, server admin + `false` +- viewer group 사용자의 `Viewer`와 edit/admin 동작 거부 +- 두 group이 모두 없는 별도 사용자의 로그인 거부 +- local break-glass 계정의 private path 로그인과 server admin 확인 +- Slack temporary test alert의 firing/resolved 메시지, cluster/namespace/alertname + grouping, severity와 이 runbook URL 확인 +- LAN/Tailscale 밖 별도 client에서 public boundary 확인 + +확인 결과에는 역할·시각·성공/실패만 기록한다. + +## 롤백 + +자동 rollback은 같은 `PLATFORM_OBSERVABILITY_ROLLBACK_ID`의 root-only ledger만 +권위 입력으로 사용한다. + +1. full Nginx를 exact Grafana deny-only guard payload/hash로 복원하고 `nginx -t`, + reload, 연속 probe를 확인한다. +2. rules-alerts ledger를 역순 처리한다. 이번 transaction의 `false/apply`만 UID + precondition으로 삭제하고 `true/apply`만 exact sanitized payload와 현재 UID/RV로 + 복원한다. `controller`는 owner 처리 뒤 수렴만 기다리고 `preserve`는 변경하지 않는다. +3. Alertmanager/Prometheus/Grafana sidecar 수렴과 기존 target set을 다시 확인한다. +4. target metric을 끄고 기존 endpoint 회귀를 확인하기 전에는 metrics guard보다 이전 + Nginx 설정으로 돌아가지 않는다. +5. PVC, Secret, CRD, PV, Loki/Tempo object와 bucket은 자동 삭제하지 않는다. + +UID 변경, API timeout 뒤 결과 불명, payload drift, controller 비수렴 또는 ledger +불일치는 자동 성공으로 처리하지 않는다. 보호 정책과 rollback root를 보존하고 +`MANUAL_RECOVERY_REQUIRED`로 에스컬레이션한다. + +## 에스컬레이션 + +- Slack webhook이나 recovery evidence가 없으면 rules-alerts mutation `0`으로 중단한다. +- runbook URL이 실제 HTTPS로 도달하지 않으면 rule source를 적용하지 않는다. +- Blackbox source가 `200/302`로 보이면 source allowlist를 완화하지 않고 네트워크 + 경계를 재설계한다. +- 외부 client 검증, 세 OIDC 역할 또는 Slack firing/resolved가 하나라도 없으면 전체 + Phase 4를 `부분 구현`으로 유지한다. +- Secret payload, token, Cookie 또는 원본 Nginx log line이 출력되면 즉시 중단하고 + 관련 credential을 별도 절차로 회전한다.