Files
service-catalog/doc/tenant-onboarding-guide.md
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

177 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 테넌트 온보딩 가이드 (VictoriaMetrics + Perses 멀티테넌시)
새 테넌트를 모니터링 스택에 추가할 때 **생성해야 하는 자원**과 **유저 권한 설정 방법**을 정리한다. dev 클러스터에서 demo01(accountID 1)·demo02(accountID 2)로 실측 검증한 결과를 기반으로 한다.
> 관련 문서: [victoria-metrics-architecture.md](victoria-metrics-architecture.md)(구조·멀티테넌시), [monitoring-deploy-guide.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를 담는다.
```sh
# 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](../manifests/helm/victoria-metrics-agent/0.40.0/custom-values.yaml) `remoteWrite`에 테넌트 URL 추가 + `vmagent-relabel-configs` ConfigMap에 keep 규칙:
```yaml
# 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](../manifests/helm/opentelemetry-collector/0.156.2/custom-values.yaml)에 테넌트 파이프라인 추가:
```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 래핑 참조):
```yaml
# ↓ 아래는 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` 추가:
```sh
# 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.