콘텐츠로 이동

모니터링

대시보드

  • Grafana: https://logging.sjanglab.org (익명 Viewer 접근 가능, wg-admin 경유)
  • SjangLab Infrastructure (홈): 현재 건강 상태와 감사 요약 카운트
  • SjangLab Hosts: 호스트 리소스, 메트릭 freshness, Headscale 노드 상태
  • SjangLab Apps: Gatus 앱 smoke 상태와 앱 인증/거부 흐름
  • SjangLab Jobs: db-sync 및 batch/sync/backup 상태
  • SjangLab Access & Audit: SSH bastion, Authentik, Headscale 감사 드릴다운
  • SjangLab AI Resources: AI 서비스 smoke, psi 리소스, 데이터 동기화 상태
  • SjangLab PostgreSQL: rho primary, tau replica, streaming lag, slot WAL 여유, replication 감사 이력
  • Gatus: https://status.sjanglab.org (tailnet 내부 공개 상태 페이지)
  • PostgreSQL은 primary rhoreplica tau를 별도 endpoint로 표시합니다.

스택 구성

flowchart LR
  subgraph hosts["각 호스트"]
    vec["Vector"]
  end

  subgraph rho["rho"]
    prom["Prometheus"]
    loki["Loki"]
    alertmanager["Alertmanager"]
    graf["Grafana"]
  end

  subgraph eta["eta"]
    gatus["Gatus"]
  end

  bridge["Cloudflare alert bridge"]
  slack_alerts["Slack #infra-alerts"]
  slack_audit["Slack #infra-audit"]
  healthchecks["healthchecks.io"]

  vec -- "메트릭" --> prom
  vec -- "로그" --> loki
  prom --> graf
  loki --> graf
  prom -- "alerts" --> alertmanager
  alertmanager -- "ops/audit alerts" --> bridge
  bridge --> slack_alerts
  bridge --> slack_audit
  alertmanager -- "Watchdog ping" --> healthchecks
  bridge -- "heartbeat" --> healthchecks
  healthchecks --> slack_alerts
  hosts -- "헬스체크 Push" --> gatus

Vector (로그/메트릭 수집)

모든 호스트에서 실행됩니다:

수집 대상 전송처 주기
sshd 로그 Loki (rho:3100) 실시간
SSH bastion forward 매핑 Loki (rho:3100) 실시간
auditd 로그 Loki (rho:3100) 실시간
Authentik 감사 이벤트 (eta) Loki (rho:3100) 실시간
Headscale 컨트롤플레인 이벤트 (eta) Loki (rho:3100) 실시간
Headscale 노드 인벤토리 스냅샷 (eta) Loki (rho:3100) 300초
호스트 메트릭 Prometheus (rho:9090) 60초
psi job freshness/status snapshot Loki (rho:3100) 60초
PostgreSQL exporter metrics (rho/tau) Prometheus (rho:9090) 30초
PostgreSQL replication audit snapshot (rho/tau) Loki (rho:3100) 60초

eta는 SSH 인증 로그와 같은 PID의 outbound socket을 관찰해 ssh_bastion 로그를 생성하고 Loki로 직접 전송합니다. ProxyJump 때문에 대상 호스트가 eta의 내부 IP만 보더라도 실제 접속원 IP, bastion 사용자, 대상 호스트를 함께 조회할 수 있습니다.

{log_type="ssh_bastion", event="bastion_forward"}

대상 호스트의 SSH 로그와 맞출 때는 target_host, bastion_user, 시간대, bastion_local_port를 함께 봅니다. 대상 호스트 sshd 로그의 source_port가 eta에서 기록한 bastion_local_port입니다.

Job freshness (psi, rho)

psi는 db-sync-*.service, protected data backup, Nixbot PostgreSQL backup 상태를 60초마다 snapshot으로 기록합니다. rho는 PostgreSQL backup과 delayed mirror units 상태를 같은 형식으로 기록합니다 (log_type="systemd_status", event="job_snapshot"). 각 행은 health=OK|WARN|FAIL, health_reason, last_success_age_seconds, next_due_seconds, max_success_age_seconds를 포함합니다. FAIL은 systemd 실패/비정상 exit, WARN은 성공 기록이 없거나 마지막 성공이 freshness 한계를 넘은 상태입니다.

접근 감사 (Authentik / Headscale)

eta의 Vector가 journald에서 수집해 이벤트를 분류합니다 (modules/authentik/audit.nix, modules/headscale/audit.nix):

  • Authentik (log_type="authentik"): login, login_failed, logout, app_authorize(브라우저 앱 인가), admin_change, policy_error, forward_auth_deny
  • Headscale (log_type="headscale"): node_register, node_expire, preauth_key, oidc_denied, error — 컨트롤플레인/멤버십 감사이며 tailnet 데이터플레인 트래픽은 관측 대상이 아닙니다
  • Headscale 노드 인벤토리 (log_type="headscale_nodes"): 5분 주기 스냅샷 (event="node_snapshot")과 집계 행(event="nodes_summary")으로 node, user, IP, tags, online, health, health_reason, last_seen_seconds, expiry_seconds를 기록합니다
{log_type="authentik", event="login_failed"}
{log_type="headscale", event="node_register"}

레이블 정책: Loki 레이블은 bounded 값(host, log_type, event)만 사용합니다. user, source_ip, app, node 등 고카디널리티 값은 JSON 필드로만 저장하고 LogQL | json으로 조회합니다. 시크릿/쿠키/인증 헤더는 수집하지 않습니다. Authentik login/app_authorize 이벤트의 client_ip는 실제 클라이언트 IP로 보존하지만, forward_auth_denyremote는 내부 프록시 주소일 수 있어 proxy_remote 필드로만 기록합니다.

Prometheus (rho)

  • 리텐션: 30일
  • Remote write receiver 활성화
  • Scrape jobs:
  • vector: rho Vector exporter
  • blackbox_exporter: eta blackbox exporter 자체 health
  • alertmanager: rho Alertmanager health (Alertmanager SOPS keys가 있을 때)
  • blackbox_http: eta vantage public HTTPS probes (auth, hs, n8n)
  • blackbox_tailnet_http: eta vantage wg-admin HTTPS probes with Host/SNI override for tailnet-only apps (n8n-ui, grafana, nextcloud, etc.)
  • blackbox_tcp: eta vantage TCP probe for Upterm
  • blackbox_icmp: eta vantage ICMP probe for wg-admin host reachability
  • nvidia-gpu: psi GPU exporter
  • postgresql: rho/tau local postgres_exporter metrics relayed through Vector
  • Alert rules: 호스트 메트릭 freshness, 디스크 부족/심각 부족, 메모리 부족, 높은 CPU, generic scrape target down, Gatus non-app heartbeat down, PostgreSQL DB/exporter/role/streaming/lag/slot WAL 상태, blackbox exporter down, blackbox probe failed, GPU exporter down, Watchdog dead-man
  • Alert delivery: Alertmanager는 operational alert를 #infra-alerts로, audit alert를 #infra-audit로 Cloudflare Worker/D1 bridge를 통해 전송합니다. Watchdog은 healthchecks.io로 직접 ping하며, ping이 끊기면 healthchecks.io가 #infra-alerts에 알립니다.
  • Alert bridge: Bridge는 자체 healthchecks.io heartbeat를 사용합니다. healthchecks.io 알림은 별도 cutover 전까지 기존 Slack integration을 유지합니다.

Alert response runbook

공통 절차:

  1. Slack alert의 host, service, alert_category, dashboard_url, runbook_url를 확인합니다.
  2. Grafana dashboard에서 같은 시간대의 metrics/logs를 확인합니다.
  3. 서비스 단위 장애면 systemctl status <unit>journalctl -u <unit> -e를 먼저 확인합니다.
  4. 사용자 영향이 있으면 #infra-alerts에 조사 시작/완료 시간을 남깁니다.
  5. 원인을 모르면 silence하지 않습니다. 노이즈성 반복일 때만 만료 시간이 있는 silence를 설정합니다.

alert별 1차 확인:

Alert 1차 확인 다음 조치
HostMetricsMissing systemctl status vector, 네트워크, rho Prometheus target Vector 재시작 또는 wg-admin 연결 복구
DiskSpaceLow / DiskSpaceCritical df -h, du -xhd1 <mount> GC, 오래된 workspace 정리, root filesystem 증설
RustFSStoreMetricsMissing rho/tau Vector와 /srv mount 상태 Vector 또는 mount 복구 후 metrics 수신 확인
RustFSStoreSpaceLow / RustFSStoreSpaceCritical df -h /srv, RustFS object 증가량 불필요한 object 검토, 보관 정책 확인, 저장소 증설
MemoryLow / HighCPULoad Grafana Hosts, systemd-cgtop, ps 원인 process 확인 후 부하 제한·서비스 재시작
PrometheusTargetDown Prometheus Targets의 job/instance와 대상 service exporter/service 또는 wg-admin 연결 복구
GatusEndpointDown Gatus endpoint와 해당 push unit journal 서비스 health push unit 재시작
BlackboxExporterDown eta의 prometheus-blackbox-exporter.service exporter 재시작, listen address와 방화벽 확인
BlackboxProbeFailed eta blackbox exporter, DNS, nginx 인증서 ACME/프록시/방화벽 확인
TlsCertificateExpiringSoon eta ACME timer와 대상 acme-sync-* unit 인증서 갱신 후 대상 호스트 동기화 확인
NvidiaGpuExporterDown psi exporter service와 nvidia-smi NVIDIA driver/GPU 상태 확인 후 exporter 복구
PostgresqlDown / PostgresqlExporterMissing rho/tau postgresql.service, prometheus-postgres-exporter.service, Vector DB 또는 exporter 복구 후 pg_up 확인
PostgresqlReplicaNotStreaming / PostgresqlReplicationLagHigh rho pg_stat_replication, tau pg_stat_wal_receiver 연결·WAL 상태 확인, WAL 유실이면 tau 재-bootstrap
PostgresqlReplicationSlotInactive / PostgresqlReplicationSlotWalRisk rho pg_replication_slots tau 상태 복구. wal_status=lost면 새 basebackup 수행
PostgresqlAuditSnapshotsMissing rho/tau audit timer와 Vector/Loki timer·전송 경로 복구 후 누락 구간 기록
BackupJobFailed / BackupJobStale log_type="systemd_status", 대상 timer/service 백업 unit 수정 후 수동 실행, 새 snapshot 확인
AuditJobFailed / AuditJobStale log_type="systemd_status", 대상 audit timer/service 감사 수집 unit 복구 후 누락 구간 확인
AuditCorrelatorHeartbeatMissing rho audit correlator service와 Loki 수신 correlator·Loki 연결 복구 후 heartbeat 확인
NginxAccessLogsMissing nginx access log, Vector source, Loki 로그 파일 권한과 Vector pipeline 복구
HeadscaleNodeSnapshotsMissing eta snapshot timer/service와 Loki snapshot job 복구 후 새 node_snapshot 확인
SshLoginFailureBurst log_type="ssh", source IP, 대상 user 오탐 확인 후 계정/키 검토와 IP 차단
AuthentikLoginFailureBurst / AuthentikForwardAuthDenyBurst Authentik audit log의 user, app, source IP 사용자 영향과 계정 탈취 여부 확인
HeadscaleOidcDenied / HeadscaleNodeExpired Authentik group, users.yaml, node expiry 정당한 사용자면 inventory/expiry 수정 후 Terraform apply
Watchdog 미수신 Alertmanager, healthchecks.io ping URL, 네트워크 Alertmanager 복구. bridge 장애와 분리 확인

감사 alert는 #infra-audit에 도착합니다. 사용자명, source IP, app/node 정보를 기록하고 계정 탈취 가능성이 있으면 Authentik 계정 비활성화와 Headscale ACL apply를 우선합니다.

PostgreSQL replica recovery

일시적인 네트워크 장애는 slot이 보존한 WAL로 자동 복구됩니다. requested WAL segment ... has already been removed 또는 wal_status=lost이면 기존 PGDATA를 다시 시작하지 않습니다.

  1. tau의 postgresql.target을 중지합니다.
  2. /var/lib/postgresql/17을 timestamp가 붙은 17.stale-* 경로로 이동해 보존합니다.
  3. rho의 inactive/lost tau slot을 삭제하고 physical slot을 다시 만듭니다.
  4. inv deploy --hosts tau를 실행하면 postgresql-replica-initpg_basebackup --slot=tau로 새 replica를 구성합니다.
  5. rho pg_stat_replicationapplication_name=tau, state=streaming과 tau pg_is_in_recovery()=true를 확인합니다.
  6. Grafana와 Loki에서 byte lag 0, healthy audit snapshot, replica_basebackup_completed를 확인합니다.

stale PGDATA는 새 replica와 backup 검증 완료 전 삭제하지 않습니다.

Alert delivery bootstrap

Slack 앱 설정은 modules/monitoring/alerts/slack-app/slack-app-manifest.json이 source of truth입니다. modules/monitoring/alerts/slack-app 디렉터리에서 direnv를 허용하면 Slack CLI가 포함된 slack-deploy shell에 들어갑니다:

cd modules/monitoring/alerts/slack-app
direnv allow

운영 원칙:

  • Slack app/bot/scope: manifest JSON으로 선언하고 Slack CLI가 .slack/hooks.json을 통해 app을 생성/업데이트합니다.
  • Slack app install: Slack CLI로 app을 install하고, 필요하면 admin OAuth 승인을 완료합니다.
  • Slack channel: workspace resource라 수동으로 준비합니다 (#infra-alerts, #infra-audit).
  • Slack bot token: external alert bridge가 chat:write로 메시지를 생성/수정할 때 사용합니다. CI에 넣지 않습니다.
  • Slack webhook URL: bridge migration 동안만 유지되는 channel-bound secret입니다.
  • healthchecks.io checks: terraform/healthchecksio에서 rho-alertmanager-watchdoginfra-alert-bridge-heartbeat를 관리하고, sensitive ping_url output을 SOPS에 수동 저장합니다. healthchecks.io Slack integration은 bridge migration 동안 유지합니다.
  • Alert bridge: terraform/alert-bridge가 Cloudflare Worker/D1/secret binding/cron을 관리합니다. D1 migration은 Worker 배포 후 수동으로 실행합니다.
  • CI: manifest JSON syntax 검증만 합니다. Slack token이나 webhook URL을 CI에 넣지 않습니다.

필요한 SOPS keys:

alertmanager-bridge-token: ENC[...]
alertmanager-healthchecks-ping-url: ENC[...]

Rollback window 동안만 legacy Slack incoming webhook secrets를 유지합니다:

alertmanager-slack-infra-alerts-webhook: ENC[...]
alertmanager-slack-infra-audit-webhook: ENC[...]

자세한 Slack bootstrap/drift check 절차는 modules/monitoring/alerts/slack-app/README.md를 봅니다.

Alert bridge cutover

현재 Alertmanager 운영 경로는 Cloudflare Worker bridge입니다.

  1. terraform/alert-bridge apply 완료 확인
  2. packages/infra-alert-bridge에서 D1 migration 실행
  3. Worker GET /healthz와 cron heartbeat 확인
  4. Alertmanager receiver가 bridge POST /alertmanager와 bearer token을 사용하는지 확인
  5. #infra-alerts, #infra-audit에 firing/resolved/update 메시지가 같은 thread로 정상 생성되는지 확인
  6. rollback window 동안 legacy Slack webhook secret 유지
  7. 안정화 후 incoming-webhook Slack scope와 webhook SOPS key 제거

healthchecks.io webhook integration은 아직 legacy Slack integration을 유지합니다. 이후 bridge POST /healthchecks와 bearer token으로 전환합니다. Worker 장애가 Watchdog ping 자체를 막지 않도록 rho-alertmanager-watchdog은 healthchecks.io로 직접 ping합니다.

Loki (rho)

  • 리텐션: 기본 7일, 감사 스트림(log_type=~"ssh|ssh_bastion|access_audit|audit|authentik|headscale|postgresql_audit")은 90일
  • headscale_nodes 스냅샷은 반복 상태 데이터라 기본 7일 적용
  • 스토리지: 로컬 파일시스템 (/var/lib/loki)

Gatus (eta)

  • Pull 방식: eta에서 직접 접근 가능한 서비스 (Authentik, Headscale, Upterm 등)
  • Push 방식: 내부 서비스가 로컬/사용자 경로를 확인한 뒤 상태 보고
  • 저장소: SQLite (/var/lib/gatus/gatus.sqlite)로 재시작 후 uptime 유지
  • External endpoint heartbeat: 15분 동안 push가 없으면 실패 처리
  • 그룹: apps, ai, ci, monitoring, platform, storage
  • 기본 정렬: group 기준
  • 알림: 직접 전송 없음. Prometheus가 Gatus metrics를 평가하고, Alertmanager SOPS keys가 있을 때 Alertmanager가 Slack으로 라우팅합니다.