# VictoriaMetrics 옵저버빌리티 스택 아키텍처
dip-catalog가 배포하는 **VictoriaMetrics 기반 옵저버빌리티 스택**(메트릭·로그 + 시각화·알림)의 구성, 데이터 흐름, 멀티테넌시, 인증을 정리한 문서다.
## 1. 개요
- **목적**: Grafana 계열(AGPLv3)을 대체하는 **Apache 2.0 기반** 옵저버빌리티 스택.
- **구성**: 카탈로그에 등록된 **10개 Helm 차트**. 메트릭(VictoriaMetrics), 로그(VictoriaLogs), 수집(vmagent/OpenTelemetry Collector), 알림(vmalert/Alertmanager), 시각화(Perses).
- **인증 전제**: 배포 환경에 항상 존재하는 **Keycloak `paasup` realm** 기반 JWT/OIDC. vmauth가 토큰을 검증하고 테넌트별로 라우팅한다.
- **네임스페이스**: `monitoring` (전 컴포넌트 공통).
- **범위**: 메트릭·로그 파이프라인. 분산 트레이싱은 본 스택 범위에 포함하지 않는다.
## 2. 전체 아키텍처
```mermaid
flowchart LR
subgraph collect[수집]
NE[node-exporter]
KSM[kube-state-metrics]
VMA[vmagent]
OTC[otelcol DaemonSet]
OTE[otelcol-events]
end
subgraph store[저장]
VMC[(vmcluster
vminsert/vmselect/vmstorage)]
VL[(vlogs
vlinsert/vlstorage/vlselect)]
end
subgraph access[인증·조회]
VAUTH[vmauth :8427
JWT/OIDC]
end
subgraph consume[시각화·알림]
PERSES[Perses]
VMALERT[vmalert]
AM[Alertmanager]
end
KC[(Keycloak
paasup realm)]
NE -->|scrape| VMA
KSM -->|scrape| VMA
VMA -->|remote_write| VMC
OTC -->|logs| VL
OTE -->|events| VL
PERSES -->|client_credentials| VAUTH
VAUTH -->|/select metrics| VMC
VAUTH -->|logs / AccountID hdr| VL
VMALERT -->|query multitenant| VMC
VMALERT -->|alerts| AM
AM -->|webhook| DISCORD[(Discord)]
KC -. OIDC 로그인 .-> PERSES
KC -. JWT 검증 .-> VAUTH
```
## 3. 컴포넌트 목록
| 컴포넌트 | 차트/버전 | 역할 | 포트 |
|---|---|---|---|
| vmcluster | victoria-metrics-cluster/0.43.0 | 메트릭 저장 (vminsert/vmselect/vmstorage) | 8480 / 8481 / 8482 |
| vmauth | victoria-metrics-auth/0.33.0 | 인증·라우팅 프록시 (JWT/OIDC) | 8427 |
| vlogs | victoria-logs-cluster/0.1.5 | 로그 저장 (vlinsert/vlstorage/vlselect) | 9481 / 9491 / 9471 |
| vmagent | victoria-metrics-agent/0.40.0 | 메트릭 스크레이프 → vminsert | - |
| vmalert | victoria-metrics-alert/0.41.0 | 알림 규칙 평가 | 8880 |
| otelcol | opentelemetry-collector/0.156.2 (DaemonSet) | 파드 로그 수집 | - |
| otelcol-events | opentelemetry-collector/0.156.2 (Deployment) | K8s 이벤트 수집 | - |
| kube-state-metrics | kube-state-metrics/7.4.0 | K8s 오브젝트 메트릭 | 8080 |
| node-exporter | prometheus-node-exporter/4.55.0 | 노드 시스템 메트릭 | 9100 |
| alertmanager | alertmanager/1.37.0 | 알림 라우팅·발송 | 9093 |
| perses | perses/0.21.0 | 대시보드/시각화 | 8080 |
각 컴포넌트의 상세 설정은 차트별 `CUSTOM-README.md` 참조(11장 링크).
## 4. 데이터 플로우
### 4.1 메트릭
```mermaid
flowchart LR
NE[node-exporter] --> VMA[vmagent]
KSM[kube-state-metrics] --> VMA
KUBELET[kubelet/cadvisor] --> VMA
PODS[pod annotations] --> VMA
VMA -->|"/insert/{accountID}/prometheus"| VMI[vminsert :8480]
VMI --> VMS[(vmstorage :8482)]
CLIENT[사용자/Grafana] -->|"Bearer JWT"| VAUTH[vmauth :8427]
PERSES[Perses] -->|"SA 토큰"| VAUTH
VAUTH -->|"/select/{accountID}/prometheus"| VSEL[vmselect :8481]
VMALERT[vmalert] -->|"/select/multitenant/"| VSEL
VSEL --> VMS
```
- **쓰기**: vmagent가 `/insert/{accountID}/prometheus/api/v1/write`로 직접 vminsert에 기록(vmauth 미경유).
- **조회**: vmauth가 JWT `vm_access.metrics_account_id`로 `/select/{accountID}/`에 라우팅.
- **vmalert**: 내부 서비스로 vmselect multitenant 엔드포인트 직결.
### 4.2 로그
```mermaid
flowchart LR
PODLOG[파드 로그] --> OTC[otelcol DaemonSet]
EVENTS[K8s 이벤트] --> OTE[otelcol-events]
OTC -->|"VictoriaLogs-AccountID 헤더"| VLI[vlinsert :9481]
OTE -->|"AccountID 0"| VLI
VLI --> VLS[(vlstorage :9491)]
PERSES[Perses] -->|"SA 토큰"| VAUTH[vmauth :8427]
VAUTH -->|"AccountID 헤더 + /select/logsql/"| VLSEL[vlselect :9471]
VLSEL --> VLS
```
- **쓰기**: otelcol이 namespace 기준으로 필터해 `VictoriaLogs-AccountID` 헤더(0/1/2)로 테넌트별 기록.
- **조회**: vmauth가 `AccountID` 헤더를 주입해 vlselect로 전달.
## 5. 멀티테넌시
vm과 vl은 **격리 메커니즘이 다르지만 단일 토큰으로 처리**된다. 토큰의 `vm_access` 클레임이 두 차원을 함께 담는다.
| 시그널 | 격리 방식 | vmauth 라우팅 | vm_access 필드 |
|---|---|---|---|
| 메트릭 | URL **경로** `/select/{accountID}/prometheus` | url_prefix 경로 치환 `{{.MetricsAccountID}}` | `metrics_account_id` |
| 로그 | HTTP **헤더** `AccountID` / `ProjectID` | 헤더 주입 `AccountID: {{.LogsAccountID}}` | `logs_account_id` |
```mermaid
flowchart TB
T["JWT vm_access
{metrics_account_id:1, logs_account_id:1}"]
T --> M["메트릭: /select/1/prometheus"]
T --> L["로그: vlselect + AccountID: 1"]
```
> **비대칭**: 메트릭은 `/select/multitenant/`로 전체 테넌트 집계가 가능하나, VictoriaLogs는 합산 엔드포인트가 없어 `(AccountID, ProjectID)` 단위로만 조회된다.
### 5.1 메트릭 쓰기 경로 테넌트 라우팅 결정 (per-URL)
위 표는 **조회(read)** 경로다. **쓰기(write)** 경로에서 vmagent가 테넌트를 어떻게 가르는지는 두 가지 선택지가 있고, **per-URL을 채택**한다.
| 방식 | 동작 | 온보딩 | 격리 |
|------|------|--------|------|
| **per-URL (채택)** | 테넌트마다 별도 remoteWrite `/insert/{accountID}/` + `urlRelabelConfig` keep 필터 | 새 테넌트 = vmagent 값 + relabel 수정 → **Git/PR** | **강** — URL이 테넌시 강제 |
| multitenant+라벨 (대안) | 단일 `/insert/multitenant/` + relabel로 `vm_account_id` 라벨 set | ConfigMap만 수정 | 약 — 라벨(=relabel 규칙)에 의존 |
**per-URL 채택 근거 (보안·격리 우선)**
- **스푸핑 차단**: 테넌시가 URL로 고정 → 스크레이프 대상이 `vm_account_id` 라벨을 노출해도 테넌트 위장 불가.
- **변경 통제**: 새 테넌트(=새 accountID) 추가가 Git/PR 리뷰·승인을 거침(ConfigMap 한 줄 수정보다 통제 강함).
- **장애·오설정 격리**: remoteWrite 큐가 테넌트별로 분리 → 한 테넌트 문제나 relabel 오타의 cross-tenant 파급 제한.
**트레이드오프(감수)**: 새 테넌트 추가 시 vmagent 값(remoteWrite)도 함께 수정·재배포해야 한다. (multitenant+라벨은 ConfigMap만으로 온보딩되지만 테넌시가 relabel 규칙에 의존해 격리가 약함 — 채택 안 함.)
> 같은 테넌트의 새 네임스페이스(`demo01-*`)는 prefix 정규식이 커버하므로 **무변경**. 변경이 필요한 건 **새 accountID(테넌트) 추가** 시뿐이다. 차트는 인라인 `urlRelabelConfig`도 지원하나, 위 근거로 per-tenant URL + 별도 relabel ConfigMap을 쓴다.
## 6. 인증/인가 (Keycloak `paasup` realm)
```mermaid
sequenceDiagram
participant U as 사용자/SA
participant KC as Keycloak (paasup)
participant VA as vmauth
participant VM as vmselect/vlselect
U->>KC: 인증 (OIDC / client_credentials)
KC-->>U: JWT (vm_access 클레임)
U->>VA: 요청 + Bearer JWT
VA->>KC: OIDC Discovery 공개키 fetch (최초 1회·rotate)
VA->>VA: 서명 검증 + vm_access 파싱
VA->>VM: accountID 기준 라우팅 (경로/헤더)
VM-->>U: 결과
```
- **Client Scope `vm-access`**: User Attribute 매퍼 2종 → `vm_access.metrics_account_id` / `vm_access.logs_account_id` (점 표기 중첩 JSON).
- **Clients**: `perses`(UI 로그인), `perses-vmauth`(datasource service-account, client_credentials), `vmauth-client`(선택, 직접 API).
- **vmauth**: `config.users[].jwt.oidc.issuer = https://keycloak.example.org/realms/paasup`.
- **테넌트 온보딩**: Keycloak Group + user attribute만 추가 → **재배포 불필요**.
- **Perses 권한**: OIDC groups 자동 동기화 미지원 → Perses 자체 RBAC(Project별 Role/RoleBinding, subject = Keycloak UUID) 수동 관리.
상세 절차: [victoria-metrics-auth CUSTOM-README](../manifests/helm/victoria-metrics-auth/0.33.0/CUSTOM-README.md) 3장.
## 7. 시각화 (Perses)
```mermaid
flowchart LR
USER[사용자] -->|OIDC 로그인| PERSES[Perses :8080]
subgraph prov[사이드카 프로비저닝]
DASH[대시보드 5종]
DS[데이터소스 3종]
end
PERSES --- prov
DS -->|client_credentials 토큰| VAUTH[vmauth :8427]
VAUTH --> VSEL[vmselect]
VAUTH --> VLSEL[vlselect]
```
- **로그인**: Keycloak OIDC(`config.security.authentication`).
- **데이터소스**: `victoriametrics`(Prometheus 호환)·`victorialogs`(LogsQL)가 vmauth를 경유하며, HTTPProxy `secret`으로 **client_credentials 토큰** 사용.
- **프로비저닝**: `perses.dev/resource: "true"` ConfigMap을 사이드카가 자동 로드. 대시보드 4종(k8s-node-overview, k8s-workloads, k8s-pod-diagnostics, k8s-pod-history).
소스: [perses/0.21.0/files/perses-provisioning.yaml](../manifests/helm/perses/0.21.0/files/perses-provisioning.yaml)
## 8. 알림
```mermaid
flowchart LR
VSEL[vmselect multitenant] --> VMALERT[vmalert]
VMALERT -->|"ALERTS 메트릭 /insert/0/"| VMI[vminsert]
VMALERT -->|"알림 발생"| AM[Alertmanager :9093]
AM -->|webhook_url_file| DISCORD[(Discord)]
```
- **규칙**: 클러스터 헬스 8종(NodeDown, NodeHighCPU, NodeHighMemory, NodeDiskPressure, PodCrashLooping, PodNotReady, DeploymentReplicasMismatch, PVCFillingUp), 평가 30s.
- **Alertmanager**: `[alertname, severity]` 그룹핑, critical→warning inhibit, Discord webhook(Secret 마운트).
## 9. 의존 관계
```mermaid
flowchart TB
vmcluster
vlogs
vmauth --> vmcluster
vmauth --> vlogs
vmagent --> vmcluster
kube-state-metrics -.scraped by.-> vmagent
node-exporter -.scraped by.-> vmagent
otelcol --> vlogs
otelcol-events --> vlogs
vmalert --> vmcluster
vmalert --> alertmanager
perses --> vmauth
vmauth -.JWT.-> Keycloak[(Keycloak paasup)]
perses -.OIDC.-> Keycloak
```
**배포 순서**: vmcluster → vmauth → vlogs → kube-state-metrics/node-exporter → vmagent → otelcol(+events) → alertmanager → vmalert → perses. (Keycloak `vm-access` scope·Client는 perses/vmauth 설치 전에 생성)
## 10. 스토리지·리소스
| 컴포넌트 | PVC | 보존기간 |
|---|---|---|
| vmstorage | 10Gi (longhorn) | 메트릭 1개월 |
| vlstorage | 5Gi | 로그 7일 |
| perses | 1Gi | 대시보드 영속 |
| alertmanager | 비활성(기본) | - |
리소스 티어(Small/Medium/Large)는 [doc/define-chart-resources.md](define-chart-resources.md) 43~52번 항목 참조.
## 11. 참고 (차트별 가이드)
| 컴포넌트 | CUSTOM-README |
|---|---|
| vmcluster | [link](../manifests/helm/victoria-metrics-cluster/0.43.0/CUSTOM-README.md) |
| vmauth | [link](../manifests/helm/victoria-metrics-auth/0.33.0/CUSTOM-README.md) |
| vlogs | [link](../manifests/helm/victoria-logs-cluster/0.1.5/CUSTOM-README.md) |
| vmagent | [link](../manifests/helm/victoria-metrics-agent/0.40.0/CUSTOM-README.md) |
| vmalert | [link](../manifests/helm/victoria-metrics-alert/0.41.0/CUSTOM-README.md) |
| opentelemetry-collector | [link](../manifests/helm/opentelemetry-collector/0.156.2/CUSTOM-README.md) |
| kube-state-metrics | [link](../manifests/helm/kube-state-metrics/7.4.0/CUSTOM-README.md) |
| prometheus-node-exporter | [link](../manifests/helm/prometheus-node-exporter/4.55.0/CUSTOM-README.md) |
| alertmanager | [link](../manifests/helm/alertmanager/1.37.0/CUSTOM-README.md) |
| perses | [link](../manifests/helm/perses/0.21.0/CUSTOM-README.md) |
## 12. 배포 아키텍처 (ArgoCD + dip-console)
### 12.1 배포 모델
GitOps 기반. 역할을 4개로 분리한다.
| 구성요소 | 역할 |
|---|---|
| **카탈로그 repo** (`dip-catalog`) | Helm 차트 + `dip-values.yaml`(템플릿) + 정적 대시보드 매니페스트 |
| **값 repo** (`dip/values-*`) | 환경별 값 파일(`$values/...`로 참조), `{{ .Domain }}`·`$VAR` 치환 |
| **ArgoCD ApplicationSet** | 10개 차트를 syncWave 순서로 선언적 배포(이중 소스) |
| **dip-console** | 클러스터 외부/차트 밖 리소스 오케스트레이션 — Keycloak 인증, Infisical 시크릿, 테넌트 프로젝트 |
### 12.2 책임 분담
| 영역 | 담당 | 방식 |
|---|---|---|
| 인증 (Keycloak `paasup`) | **dip-console** | `vm-access` scope·`perses`/`perses-vmauth` client·테넌트 그룹/attribute를 Admin API로 생성 |
| 시크릿 | **dip-console + Infisical** | Infisical 등록 → external-secrets가 `monitoring` ns Secret 동기화 → 차트는 `existingSecret` 참조 |
| root-ca-cert | **dip-console** | platform ns → `monitoring` 복사 |
| 테넌트 relabel (`vmagent-relabel-configs`) | **dip-console** | 테넌트(demo01/demo02/platform) 추가·수정 |
| 대시보드 ConfigMap | **카탈로그/ArgoCD** | 정적 → ArgoCD 경로 소스로 적용(사이드카 로드) |
| StorageClass / Ingress / cert-manager | **플랫폼 기본 배포** | 전제(longhorn, ingress controller, root-ca-issuer) |
| 알림 채널 | **dip-console (교체 가능)** | discord/email/slack 교체 — 12.5 |
### 12.3 ApplicationSet + syncWave
openmetadata ApplicationSet 패턴(list generator + 이중 소스 `$values/` + `argocd.argoproj.io/sync-wave`)을 재사용한다.
```mermaid
flowchart LR
subgraph w0[syncWave 0 · 무의존]
VMC[vmcluster]
VL[vlogs]
KSM[kube-state-metrics]
NE[node-exporter]
AM[alertmanager]
end
subgraph w1[syncWave 1 · 저장소·AM 의존]
VAUTH[vmauth]
VMA[vmagent]
OTC[otelcol]
OTE[otelcol-events]
VMALERT[vmalert]
end
subgraph w2[syncWave 2 · vmauth 의존]
PERSES[perses]
end
w0 --> w1 --> w2
```
배포 소스: 카탈로그 repo(`{{ .chartPath }}`) + 값 repo(`$values/{{ .valuesPath }}`). `syncPolicy.automated{prune,selfHeal}` + `CreateNamespace=true`. 네임스페이스 `monitoring` 고정.
### 12.4 시크릿 흐름
차트는 인라인 시크릿을 두지 않고 `existingSecret`만 참조한다. 실제 값은 dip-console → Infisical → external-secrets 경로로 채워진다.
```mermaid
flowchart LR
CONSOLE[dip-console] -->|등록| INF[(Infisical)]
INF -->|sync| ES[external-secrets]
ES -->|K8s Secret 생성| NS[monitoring ns Secret]
NS -->|existingSecret 참조| CHART[alertmanager / perses / ...]
```
대상: alertmanager 알림 자격증명, perses OIDC `client_secret`·`secret_key`(`secret.create=false`/`envVarsExternalSecretName`), perses-vmauth OAuth secret, root-ca-cert(dip-console 복사).
### 12.5 알림 채널 교체 가능 구조
Alertmanager `config.receivers`는 채널 무관하게 작성하고, 자격증명은 Infisical 시크릿을 파일로 마운트한다(평문 금지).
| 채널 | receiver 키 | 자격증명(파일 마운트) |
|---|---|---|
| Discord | `discord_configs.webhook_url_file` | webhook URL |
| Slack | `slack_configs.api_url_file` | webhook URL |
| Email | `email_configs`(smarthost/from/to) | `auth_password_file` |
**새 채널 추가 3단계**: ① Infisical에 자격증명 등록 → ② dip-values의 `config.receivers`(+ `extraSecretMounts`) 추가, `route.receiver` 지정 → ③ ArgoCD 재동기화. (상세 예시: [alertmanager CUSTOM-README](../manifests/helm/alertmanager/1.37.0/CUSTOM-README.md))
### 12.6 사전 준비 체크리스트
ArgoCD 동기화 전에 갖춰져야 하는 항목.
- **dip-console**: Keycloak `vm-access` scope·client 생성 / Infisical 시크릿 등록 / `root-ca-cert` 복사 / `vmagent-relabel-configs` 테넌트 ConfigMap 생성
- **플랫폼 기본**: 기본 StorageClass(longhorn), Ingress controller + cert-manager(`root-ca-issuer`), DNS
- **GitOps**: ArgoCD에 카탈로그 repo·값 repo 등록(자격증명), 값 repo에 `dip-values` 작성
- 누락 시 실패 지점: perses/vmauth(Keycloak·시크릿 미비), vmagent(relabel ConfigMap 미비), PVC(StorageClass 미비)