새 테넌트(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>
14 KiB
테넌트 온보딩 가이드 (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 규칙이 처음부터 올바르면 오염이 없어 정리도 불필요.
- ⚠️ 인덱스 오염 주의: 어느 시점에 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단계로 정리한다.
- 재프로비저닝 중단:
kubectl delete configmap perses-tenant-<T> -n monitoring(+ vmagent relabel 키·otelcol pipeline 제거 후 재배포) - Perses DB 삭제(cascade):
DELETE /api/v1/projects/<T>→ 프로젝트 + 그 안의 데이터소스·시크릿·롤·롤바인딩·대시보드 일괄 삭제. (검증됨: 삭제 후 해당 프로젝트 데이터소스 조회 시 HTTP 400metadata.project "<T>" doesn't exist) - 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.