diff --git a/doc/tenant-onboarding-guide.md b/doc/tenant-onboarding-guide.md new file mode 100644 index 0000000..2063ff2 --- /dev/null +++ b/doc/tenant-onboarding-guide.md @@ -0,0 +1,176 @@ +# 테넌트 온보딩 가이드 (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. 테넌트 추가 시 생성 자원 (체크리스트) + +새 테넌트 `` (accountID ``, 네임스페이스 접두 `-*`) 추가 시: + +| # | 계층 | 자원 | 담당 | 재배포 | +|---|------|------|------|--------| +| 1 | Keycloak | 데이터소스용 SA client `perses-vmauth-` (+vm-access scope, SA attr accountID=``) | dip-console | - | +| 2 | Keycloak | (직접 API 접근 유저용) user attribute `vm_metrics_account_id`/`vm_logs_account_id`=`` | dip-console | - | +| 3 | vmagent | remoteWrite `/insert//` + `urlRelabelConfig` + relabel ConfigMap 키 `.yaml`(keep namespace=~`-.*`) | 카탈로그/dip-console | **vmagent 재배포** | +| 4 | otelcol | `filter/`(namespace 정규식) + exporter `otlphttp/vlogs-`(AccountID/ProjectID 헤더) + pipeline `logs/` | 카탈로그 | **otelcol 재배포** | +| 5 | Perses | Project `` | dip-console | - | +| 6 | Perses | Secret `-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 | - | + +> **같은 테넌트의 새 네임스페이스**(`-*` 추가)는 정규식이 커버 → **무변경**. 변경이 필요한 건 **새 테넌트(새 accountID) 추가** 시뿐이다. + +## 3. 계층별 상세 + +### 3.1 Keycloak (paasup realm) — 데이터소스 자격증명 +Perses 데이터소스는 테넌트별 **service-account client**(client_credentials)로 vmauth에 접근한다. 그 토큰의 `vm_access` 클레임이 accountID를 담는다. + +```sh +# client perses-vmauth- (serviceAccountsEnabled) + vm-access scope 연결 +# + SA user attribute: vm_metrics_account_id=, vm_logs_account_id= +# 절차는 victoria-metrics-auth/CUSTOM-README §3.2 (perses-vmauth 패턴) 동일, clientId만 테넌트별. +``` +> 검증: 토큰 디코드 시 `vm_access:{metrics_account_id:, logs_account_id:}` 확인. + +### 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//prometheus/api/v1/write" + urlRelabelConfig: /relabel/.yaml +# relabel ConfigMap 키 .yaml +- action: keep + source_labels: [namespace] + regex: "-.*" +``` +⚠️ **`[]`(빈 규칙)은 격리가 아니다** — 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/: + logs: { log_record: ['not IsMatch(attributes["namespace"], "^-.*")'] } +exporters: + otlphttp/vlogs-: + endpoint: "http://...vlinsert...:9481/insert/opentelemetry" + headers: { AccountID: "", ProjectID: "0" } # ⚠️ VictoriaLogs 인식 헤더는 AccountID/ProjectID +service: + pipelines: + logs/: { receivers: [filelog], processors: [memory_limiter, k8sattributes, filter/, batch], exporters: [otlphttp/vlogs-] } +``` +> ⚠️ 헤더는 반드시 `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-`, 라벨 `paasup.io/tenant: `)로 관리한다. 온보딩=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: } +--- +kind: Secret # 테넌트 OAuth (perses-vmauth-) +metadata: { name: -vmauth, project: } +spec: { oauth: { clientID: perses-vmauth-, clientSecret: "", + tokenURL: "https://keycloak.example.org/realms/paasup/protocol/openid-connect/token", scopes: ["openid"] } } +--- +kind: Datasource # 메트릭 +metadata: { name: victoriametrics, project: } +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: -vmauth } } } } } +--- +kind: Datasource # 로그 +metadata: { name: victorialogs, project: } +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: -vmauth } } } } } +--- +kind: Role +metadata: { name: viewer, project: } +spec: { permissions: [{ actions: ["read"], scopes: ["*"] }] } +--- +kind: RoleBinding +metadata: { name: -viewer-binding, project: } +spec: { role: viewer, subjects: [{ kind: User, name: "" }] } +``` +- **대시보드 (테넌트 적정 정리)**: `monitoring` 프로젝트 대시보드를 `metadata.project`만 ``로 바꿔 복제하되, **테넌트 계정에 데이터가 있는 것만** 넣는다. 데이터소스가 프로젝트 스코프로 해석되어 자동으로 테넌트 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//prometheus/api/v1/admin/tsdb/delete_series?match[]={namespace!~"-.*"}` 로 외부 시리즈 제거(검증됨: 삭제 후 변수가 테넌트 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: "" }` 추가. (바인딩 없는 유저는 `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- -n monitoring` (+ vmagent relabel 키·otelcol pipeline 제거 후 재배포) +2. **Perses DB 삭제(cascade)**: `DELETE /api/v1/projects/` → 프로젝트 + 그 안의 데이터소스·시크릿·롤·롤바인딩·대시보드 **일괄 삭제**. (검증됨: 삭제 후 해당 프로젝트 데이터소스 조회 시 HTTP 400 `metadata.project "" doesn't exist`) +3. **Keycloak**: 테넌트 SA client `perses-vmauth-` 삭제, 유저 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.