모니터링¶
대시보드¶
- 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 rho와replica 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_deny의 remote는 내부 프록시 주소일 수 있어 proxy_remote 필드로만 기록합니다.
Prometheus (rho)¶
- 리텐션: 30일
- Remote write receiver 활성화
- Scrape jobs:
vector: rho Vector exporterblackbox_exporter: eta blackbox exporter 자체 healthalertmanager: 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 Uptermblackbox_icmp: eta vantage ICMP probe for wg-admin host reachabilitynvidia-gpu: psi GPU exporterpostgresql: 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¶
공통 절차:
- Slack alert의
host,service,alert_category,dashboard_url,runbook_url를 확인합니다. - Grafana dashboard에서 같은 시간대의 metrics/logs를 확인합니다.
- 서비스 단위 장애면
systemctl status <unit>와journalctl -u <unit> -e를 먼저 확인합니다. - 사용자 영향이 있으면
#infra-alerts에 조사 시작/완료 시간을 남깁니다. - 원인을 모르면 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를 다시 시작하지 않습니다.
- tau의
postgresql.target을 중지합니다. /var/lib/postgresql/17을 timestamp가 붙은17.stale-*경로로 이동해 보존합니다.- rho의 inactive/lost
tauslot을 삭제하고 physical slot을 다시 만듭니다. inv deploy --hosts tau를 실행하면postgresql-replica-init이pg_basebackup --slot=tau로 새 replica를 구성합니다.- rho
pg_stat_replication의application_name=tau,state=streaming과 taupg_is_in_recovery()=true를 확인합니다. - 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-watchdog와infra-alert-bridge-heartbeat를 관리하고, sensitiveping_urloutput을 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입니다.
terraform/alert-bridgeapply 완료 확인packages/infra-alert-bridge에서 D1 migration 실행- Worker
GET /healthz와 cron heartbeat 확인 - Alertmanager receiver가 bridge
POST /alertmanager와 bearer token을 사용하는지 확인 #infra-alerts,#infra-audit에 firing/resolved/update 메시지가 같은 thread로 정상 생성되는지 확인- rollback window 동안 legacy Slack webhook secret 유지
- 안정화 후
incoming-webhookSlack 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으로 라우팅합니다.