Files
service-catalog/doc/tenant-onboarding-guide.md
T
wbsong111 44c3888974 docs: add tenant onboarding guide (VictoriaMetrics + Perses multitenancy)
새 테넌트(accountID) 온보딩 시 계층별 생성 자원(Keycloak/vmagent/otelcol/
Perses)과 유저 RBAC, 오프보딩 절차 정리. demo01/demo02 실측 검증(2026-07-01)
기반. 자원 생성·유저 바인딩은 dip-console 온보딩 자동화 backlog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 16:09:32 +09:00

14 KiB
Raw Blame History

테넌트 온보딩 가이드 (VictoriaMetrics + Perses 멀티테넌시)

새 테넌트를 모니터링 스택에 추가할 때 생성해야 하는 자원유저 권한 설정 방법을 정리한다. dev 클러스터에서 demo01(accountID 1)·demo02(accountID 2)로 실측 검증한 결과를 기반으로 한다.

관련 문서: victoria-metrics-architecture.md(구조·멀티테넌시), monitoring-deploy-guide.md(배포 런북).

1. 멀티테넌시 모델 요약

격리 방식
메트릭 vmauth가 URL 경로 /select/{accountID}/ 로 라우팅 (쓰기: vmagent per-URL /insert/{accountID}/)
로그 vmauth가 AccountID 헤더 주입 (쓰기: otelcol이 AccountID/ProjectID 헤더로 전송)
유저별 구분 Perses RBAC(프로젝트 접근) + 프로젝트별 데이터소스(테넌트 자격증명)

핵심: 테넌트 = accountID. Perses는 로그인 유저의 토큰을 데이터소스로 전달하지 않고 프로젝트 데이터소스에 박힌 고정 자격증명(테넌트 SA) 으로 조회한다. 따라서 유저가 어떤 테넌트를 보는지는 그 유저가 접근 가능한 프로젝트(RBAC) 가 결정한다.

유저 로그인(OIDC) → Perses RBAC(RoleBinding) → 접근 가능한 프로젝트
                                              → 프로젝트 데이터소스(테넌트 accountID 고정)
                                              → vmauth → 해당 테넌트 메트릭/로그만

2. 테넌트 추가 시 생성 자원 (체크리스트)

새 테넌트 <T> (accountID <N>, 네임스페이스 접두 <T>-*) 추가 시:

# 계층 자원 담당 재배포
1 Keycloak 데이터소스용 SA client perses-vmauth-<T> (+vm-access scope, SA attr accountID=<N>) dip-console -
2 Keycloak (직접 API 접근 유저용) user attribute vm_metrics_account_id/vm_logs_account_id=<N> dip-console -
3 vmagent remoteWrite /insert/<N>/ + urlRelabelConfig + relabel ConfigMap 키 <T>.yaml(keep namespace=~<T>-.*) 카탈로그/dip-console vmagent 재배포
4 otelcol filter/<T>(namespace 정규식) + exporter otlphttp/vlogs-<N>(AccountID/ProjectID 헤더) + pipeline logs/<T> 카탈로그 otelcol 재배포
5 Perses Project <T> dip-console -
6 Perses Secret <T>-vmauth(OAuth → SA client) dip-console -
7 Perses Datasource victoriametrics(메트릭) + victorialogs(로그) dip-console -
8 Perses Role viewer + RoleBinding(테넌트 유저) dip-console -
9 Perses Dashboard N종(프로젝트별 복제) dip-console -

같은 테넌트의 새 네임스페이스(<T>-* 추가)는 정규식이 커버 → 무변경. 변경이 필요한 건 새 테넌트(새 accountID) 추가 시뿐이다.

3. 계층별 상세

3.1 Keycloak (paasup realm) — 데이터소스 자격증명

Perses 데이터소스는 테넌트별 service-account client(client_credentials)로 vmauth에 접근한다. 그 토큰의 vm_access 클레임이 accountID를 담는다.

# client perses-vmauth-<T> (serviceAccountsEnabled) + vm-access scope 연결
#   + SA user attribute: vm_metrics_account_id=<N>, vm_logs_account_id=<N>
# 절차는 victoria-metrics-auth/CUSTOM-README §3.2 (perses-vmauth 패턴) 동일, clientId만 테넌트별.

검증: 토큰 디코드 시 vm_access:{metrics_account_id:<N>, logs_account_id:<N>} 확인.

3.2 vmagent — 메트릭 쓰기 경로 (per-URL)

victoria-metrics-agent/custom-values.yaml remoteWrite에 테넌트 URL 추가 + vmagent-relabel-configs ConfigMap에 keep 규칙:

# remoteWrite 추가
- url: "http://...vminsert...:8480/insert/<N>/prometheus/api/v1/write"
  urlRelabelConfig: /relabel/<T>.yaml
# relabel ConfigMap 키 <T>.yaml
- action: keep
  source_labels: [namespace]
  regex: "<T>-.*"

⚠️ [](빈 규칙)은 격리가 아니다 — keep 규칙 없으면 전 accountID에 기록됨. 새 accountID 추가 시 vmagent 재배포 필요.

3.3 otelcol — 로그 쓰기 경로

opentelemetry-collector/custom-values.yaml에 테넌트 파이프라인 추가:

processors:
  filter/<T>:
    logs: { log_record: ['not IsMatch(attributes["namespace"], "^<T>-.*")'] }
exporters:
  otlphttp/vlogs-<N>:
    endpoint: "http://...vlinsert...:9481/insert/opentelemetry"
    headers: { AccountID: "<N>", ProjectID: "0" }   # ⚠️ VictoriaLogs 인식 헤더는 AccountID/ProjectID
service:
  pipelines:
    logs/<T>: { receivers: [filelog], processors: [memory_limiter, k8sattributes, filter/<T>, batch], exporters: [otlphttp/vlogs-<N>] }

⚠️ 헤더는 반드시 AccountID/ProjectID. VictoriaLogs-AccountID는 무시되어 전량 account 0으로 적재된다(실측 확인).

3.4 Perses — 프로젝트/데이터소스/RBAC/대시보드

⚠️ Datasource/Project/Secret/Role/RoleBinding/Dashboard는 Kubernetes CRD가 아니라 Perses 자체 리소스다(Perses DB에 저장). kubectl apply 대상이 아니다. 배포 경로:

  • 사이드카 provisioning(권장): 아래 리소스들을 K8s ConfigMap의 data:으로 넣고 ConfigMap에 라벨 perses.dev/resource: "true"perses-provisioning-sidecar/etc/perses/provisioning/에 기록 → Perses가 DB로 로드(10분 간격 upsert, prune 없음).
  • Perses REST API: POST /api/v1/projects/{project}/datasources 등. / percli: percli apply -f.
  • (대안) perses-operator를 쓰면 PersesDatasource 등 진짜 CRD가 생기나, 본 스택은 Helm 차트 + 사이드카 방식이라 CRD 없음.

ConfigMap 구성 (테넌트당 분리 권장): 전역 리소스(GlobalRole·globalAdmin)는 bootstrap ConfigMap, 테넌트별 리소스는 테넌트당 ConfigMap 1개(perses-tenant-<T>, 라벨 paasup.io/tenant: <T>)로 관리한다. 온보딩=ConfigMap 생성, 오프보딩=ConfigMap 삭제(§5). 테넌트 변경이 다른 테넌트/전역 설정을 건드리지 않는다.

데이터소스 이름은 대시보드가 참조하는 victoriametrics/victorialogs 로 맞춘다. 아래는 ConfigMap data 값(또는 API 바디)에 넣는 Perses 리소스다(전체 예시는 검증 매니페스트 tenant-verification/manifests/perses-tenant-demo01.yaml의 ConfigMap 래핑 참조):

# ↓ 아래는 K8s 매니페스트가 아니라, perses.dev/resource:true ConfigMap 의 data 값 (또는 API 바디)
kind: Project
metadata: { name: <T> }
---
kind: Secret            # 테넌트 OAuth (perses-vmauth-<T>)
metadata: { name: <T>-vmauth, project: <T> }
spec: { oauth: { clientID: perses-vmauth-<T>, clientSecret: "<from-infisical>",
        tokenURL: "https://keycloak.example.org/realms/paasup/protocol/openid-connect/token", scopes: ["openid"] } }
---
kind: Datasource        # 메트릭
metadata: { name: victoriametrics, project: <T> }
spec: { default: true, plugin: { kind: PrometheusDatasource, spec: { proxy: { kind: HTTPProxy, spec: {
        url: "http://...vmauth...:8427", allowedEndpoints: [{endpointPattern: "/api/v1/.*", method: GET},{endpointPattern: "/api/v1/.*", method: POST}], secret: <T>-vmauth } } } } }
---
kind: Datasource        # 로그
metadata: { name: victorialogs, project: <T> }
spec: { default: false, plugin: { kind: VictoriaLogsDatasource, spec: { proxy: { kind: HTTPProxy, spec: {
        url: "http://...vmauth...:8427", allowedEndpoints: [{endpointPattern: "/select/logsql/.*", method: GET},{endpointPattern: "/select/logsql/.*", method: POST}], secret: <T>-vmauth } } } } }
---
kind: Role
metadata: { name: viewer, project: <T> }
spec: { permissions: [{ actions: ["read"], scopes: ["*"] }] }
---
kind: RoleBinding
metadata: { name: <T>-viewer-binding, project: <T> }
spec: { role: viewer, subjects: [{ kind: User, name: "<preferred_username>" }] }
  • 대시보드 (테넌트 적정 정리): monitoring 프로젝트 대시보드를 metadata.project<T>로 바꿔 복제하되, 테넌트 계정에 데이터가 있는 것만 넣는다. 데이터소스가 프로젝트 스코프로 해석되어 자동으로 테넌트 accountID를 쓰므로, 내용 수정 없이 복제하면 테넌트 데이터만 렌더된다.

    • 테넌트에 복제: k8s-workloads, k8s-pod-diagnostics, k8s-pod-history (namespace/pod/workload 스코프 — 테넌트 계정에 존재. 검증: kube_pod_info/container_*/kube_deployment_* > 0)
    • 테넌트에서 제외: k8s-node-overview (node-exporter 메트릭은 namespace가 없어 테넌트 계정이 아니라 platform 계정(9000/0)에 적재 → 테넌트에선 빈 화면. 검증: acct1에서 node_uname_info=0). 노드/클러스터 레벨 대시보드는 platform(monitoring) 프로젝트에만 둔다.
  • namespace 변수(권한 있는 네임스페이스만 보이기): 대시보드의 namespace 변수(label_values(namespace))는 데이터소스 계정에 존재하는 namespace만 반환한다 → 테넌트 데이터소스(accountID 격리)를 쓰면 자동으로 그 테넌트의 namespace만 표시된다(별도 필터 불필요). 검증: 정리된 acct1에서 label_values(namespace) = ['demo01-nginx'].

    • ⚠️ 인덱스 오염 주의: 어느 시점에 keep 규칙 없이([]) 메트릭이 그 계정에 적재된 적이 있으면, label 인덱스에 외부 namespace가 남아 변수 드롭다운에 전부 표시된다(데이터 쿼리는 5분 lookback이라 안 보여도 인덱스는 남음). retention까지 유지되거나 수동 삭제 필요.
    • 정리: vmselect에 POST /delete/<N>/prometheus/api/v1/admin/tsdb/delete_series?match[]={namespace!~"<T>-.*"} 로 외부 시리즈 제거(검증됨: 삭제 후 변수가 테넌트 namespace만 표시). 애초에 keep 규칙이 처음부터 올바르면 오염이 없어 정리도 불필요.
  • provisioning 특성: 사이드카 ConfigMap → 10분 간격 upsert. prune 안 함(ConfigMap 삭제해도 DB 잔존 → 삭제는 API DELETE).

4. 유저 권한 부여 (핵심)

Perses는 OIDC로 인증만 받고 권한은 OIDC group/claim에서 자동 동기화하지 않는다. 권한은 Perses 내부 RBAC로만 부여한다.

  • 로그인 식별자 = OIDC preferred_username (v0.53.1 실측).
  • 부여 방법: 테넌트 프로젝트의 RoleBinding.spec.subjects{ kind: User, name: "<preferred_username>" } 추가. (바인딩 없는 유저는 guest_permissions 미설정 → fail-closed, 아무것도 못 봄.)
  • global admin: sidecar.globalAdminUsers(예: paasup) → GlobalRoleBinding으로 전 프로젝트 접근.
  • 유저는 최초 OIDC 로그인 시 Perses에 생성되며, RoleBinding은 로그인 전에 미리 만들어 둬도 로그인 시점에 매칭된다.

예시 — 기존 테넌트에 OIDC 유저 alice 추가:

# demo01 프로젝트 viewer 바인딩에 subject 추가 (Perses API 또는 provisioning ConfigMap)
#   subjects: [..., { kind: User, name: "alice" }]

검증: native 유저로 RBAC 강제 확인됨(테넌트 유저는 자기 프로젝트만, 타 프로젝트 403). OIDC 유저도 동일 메커니즘 — 식별자만 preferred_username.

5. 테넌트 오프보딩 (삭제)

Perses provisioning은 prune를 하지 않으므로 ConfigMap 삭제만으론 Perses DB에서 안 지워진다. 2단계로 정리한다.

  1. 재프로비저닝 중단: kubectl delete configmap perses-tenant-<T> -n monitoring (+ vmagent relabel 키·otelcol pipeline 제거 후 재배포)
  2. Perses DB 삭제(cascade): DELETE /api/v1/projects/<T> → 프로젝트 + 그 안의 데이터소스·시크릿·롤·롤바인딩·대시보드 일괄 삭제. (검증됨: 삭제 후 해당 프로젝트 데이터소스 조회 시 HTTP 400 metadata.project "<T>" doesn't exist)
  3. Keycloak: 테넌트 SA client perses-vmauth-<T> 삭제, 유저 attribute/그룹 정리.

유저만 제거(테넌트는 유지)하려면 프로젝트 RoleBinding의 subjects에서 해당 preferred_username만 제거.

6. 검증 방법

tenant-verification/(로컬, gitignore)의 스크립트로 재현:

  • 03-verify.sh — vmauth JWT 경로 메트릭·로그 격리(B1~B7)
  • 04-perses-verify.sh — Perses RBAC(유저별 프로젝트 가시성, 타 프로젝트 403)
  • 05-perses-complete.sh — 프로젝트별 victoriametrics/victorialogs 데이터소스 + 대시보드 복제

검증된 결과(2026-07-01, demo01/demo02):

  • 메트릭: acct1↔demo01, 교차 0. 로그: acct1 demo01=50건, demo02=0.
  • Perses: demo01-user는 demo01 프로젝트만, demo02 접근 403. demo01 데이터소스→demo01 데이터만(demo02=0).
  • 테넌트 유저가 메트릭+로그+대시보드 4종 모두 자기 테넌트로 조회.

7. 요약 — "무엇을 바꿔야 하나"

시나리오 Keycloak vmagent otelcol Perses 재배포
같은 테넌트 새 네임스페이스 - - - - 없음
새 테넌트(새 accountID) SA client + attr remoteWrite+relabel filter+exporter+pipeline project+secret+DS×2+role+binding+dashboards vmagent·otelcol
기존 테넌트에 유저 추가 (그룹/attr, 선택) - - RoleBinding subject 추가(preferred_username) 없음

이 자원 생성·유저 바인딩 일체는 dip-console 온보딩 자동화 대상이다. 정적 대시보드/데이터소스의 GitOps 편입(ArgoCD directory 소스 또는 차트 extraObjects)은 별도 backlog.