# VictoriaMetrics 모니터링 스택 배포 테스트 카탈로그의 VM 옵저버빌리티 스택(11개 Application)을 **ArgoCD 전체 GitOps 경로**로 배포·검증하는 절차와 기록 문서. 테스트 완료 후 본 문서를 바탕으로 이슈에 결과를 정리한다. - 아키텍처: [victoria-metrics-architecture.md](victoria-metrics-architecture.md) - ApplicationSet: [manifests/applicationset/victoria-metrics/1.0.0/](../manifests/applicationset/victoria-metrics/1.0.0/) - 카탈로그(테스트 업로드): `https://gitea.example.org/dip/service-catalog` (branch `master`) ## 표기 규칙 (중요) > 🔧 **수동 설정** — dip-console/ArgoCD가 아니라 **사람이 스크립트·명령어로 직접 설정**한 항목. 이 마커가 붙은 내용은 모두 **[§7 수동 설정 목록](#7-수동-설정-목록-이슈자동화-backlog)**에 모이며, 이슈 정리 + 향후 dip-console 자동화 대상이다. 새 수동 작업이 생기면 같은 마커로 표기하고 §7에 추가한다. 테스트 중 각 단계의 `[ ]` 체크박스와 결과란을 채운다. --- ## 1. 테스트 환경 / 전제 | 항목 | 값 / 상태 | |------|-----------| | 클러스터 | (기입: context / API 도달 여부) | | ArgoCD | (기입: 버전 / namespace) | | StorageClass | longhorn (전제) | | Ingress / cert-manager | apisix / `root-ca-issuer` (전제) | | Keycloak | `paasup` realm, 도달 URL (기입) | | 카탈로그 repo | `gitea.example.org/dip/service-catalog` @ `master` | | 값(values) repo | (기입: URL @ branch) | | 사전조건 준비 방식 | **수동 목(mock)** — dip-console 미사용 | > 경로: ArgoCD ApplicationSet 이중 소스(카탈로그 + 값 repo) + syncWave 0→1→2. 사전조건은 수동 목으로 준비한다. --- ## 2. 0단계 — 로컬 정적 검증 (클러스터 불필요) - [x] 오버레이 `$VAR`를 테스트값으로 치환 후 2단 병합 렌더 확인 — **11/11 PASS** ```sh # 차트마다: 베이스(custom-values) + 렌더된 오버레이 helm template manifests/helm// \ -f manifests/helm///custom-values.yaml \ -f /tmp/-overlay-rendered.yaml > /dev/null ``` - [x] vmauth 렌더 config에 issuer·`{{.MetricsAccountID}}`·로그 `AccountID` 헤더 포함 확인 — **PASS** - [x] `applicationset.yaml` / `perses-dashboards.yaml` / `perses-provisioning.yaml` YAML 유효성 — **PASS** 결과: ✅ **0단계 통과 (2026-06-25)** - 2단 병합 렌더 11/11 OK (vmcluster·vlogs·ksm·node-exporter·alertmanager·vmauth·vmagent·otelcol·otelcol-events·vmalert·perses). otelcol 로그전용(trace 포트 0), perses ingress 포함(docs=14). - vmauth 렌더 `auth.yml`: issuer `realms/paasup`, 메트릭 경로 `{{.MetricsAccountID}}`, 로그 헤더 `AccountID: {{.LogsAccountID}}`, `unauthorized_user` accountID 0 모두 확인($VAR 치환 후에도 vmauth 플레이스홀더 보존). - 매니페스트 3종 YAML 유효(applicationset / perses-dashboards / perses-provisioning). - 테스트 치환값: `$STORAGE_CLASS=longhorn`, `$KEYCLOAK_URL=https://keycloak.example.org`, `$KEYCLOAK_REALM=paasup`, `$DOMAIN=example.org` (검증용 임시값, 실제 배포는 dip-console 렌더). --- ## 3. 1단계 — 사전조건 수동 목 준비 > 🔧 **수동 설정** — 이 단계 전체가 수동 목. dip-console이 대신할 작업들이다. **3.1~3.5 모두 실행 완료(2026-06-25)**. 명령·결과는 각 절 + §7에 집계. > > 테스트 환경값: `DOMAIN=example.org`, `KEYCLOAK_URL=https://keycloak.example.org`, `KEYCLOAK_REALM=paasup`, `STORAGE_CLASS=longhorn`. `root-ca-cert` 소스 = `cert-manager/root-ca-secret`(`ca.crt`). ### 3.1 값(values) repo — ✅ 실행 완료 (2026-06-25) - [x] 오버레이 11종을 `$VAR` 치환(문자열 치환만, `{{.MetricsAccountID}}` 등 보존) → `~/project/tenant-catalog/victoria-metrics/`에 생성 후 push ```sh # dip-catalog 오버레이 → tenant-catalog 렌더 (sed 문자열 치환) for f in manifests/applicationset/victoria-metrics/1.0.0/*-values.yaml; do sed -e 's#$STORAGE_CLASS#longhorn#g' -e 's#$KEYCLOAK_URL#https://keycloak.example.org#g' \ -e 's#$KEYCLOAK_REALM#paasup#g' -e 's#$DOMAIN#example.org#g' \ -e 's#$INFISICAL_PERSES_CLIENT_SECRET#perses-mock-secret-changeme#g' \ -e 's#$INFISICAL_ALERTMANAGER_SECRET#alertmanager-notify#g' "$f" \ > ~/project/tenant-catalog/victoria-metrics/$(basename "$f") done cd ~/project/tenant-catalog && git add -A && git commit -m "Add victoria-metrics values (mock)" && git push origin master ``` 결과: `dip/tenant-catalog` @ master `510aa8e`, `victoria-metrics/*-values.yaml` 11개. **생성된 값 파일 내용** (환경 고유값 있는 차트만; 나머지 6종 `kube-state-metrics`/`node-exporter`/`otelcol`/`otelcol-events`/`vmagent`/`vmalert`-values.yaml = `{}` — 베이스 custom-values만 사용) `vmauth-values.yaml` ```yaml config: users: - jwt: oidc: issuer: "https://keycloak.example.org/realms/paasup" url_map: - src_paths: ["/api/v1/.*"] url_prefix: "http://vmcluster-victoria-metrics-cluster-vmselect.monitoring.svc.cluster.local:8481/select/{{.MetricsAccountID}}/prometheus" - src_paths: ["/select/logsql/.*"] url_prefix: "http://vlogs-victoria-logs-cluster-vlselect.monitoring.svc.cluster.local:9471" headers: - "AccountID: {{.LogsAccountID}}" - "ProjectID: 0" unauthorized_user: url_prefix: "http://vmcluster-victoria-metrics-cluster-vmselect.monitoring.svc.cluster.local:8481/select/0/prometheus" ``` `perses-values.yaml` ```yaml config: security: enable_auth: true authentication: providers: oidc: - slug_id: keycloak name: Keycloak client_id: perses client_secret: "perses-mock-secret-changeme" issuer: "https://keycloak.example.org/realms/paasup" redirect_uri: "https://perses.example.org/api/auth/providers/oidc/keycloak/callback" scopes: [openid, profile, email] ingress: enabled: true ingressClassName: "apisix" annotations: cert-manager.io/cluster-issuer: "root-ca-issuer" hosts: - host: "perses.example.org" paths: [{ path: /, pathType: Prefix }] tls: - secretName: perses-tls hosts: ["perses.example.org"] ``` `alertmanager-values.yaml` ```yaml extraSecretMounts: - name: alertmanager-notify mountPath: /etc/alertmanager/secrets subPath: "" secretName: "alertmanager-notify" readOnly: true ``` `vmcluster-values.yaml` / `vlogs-values.yaml` ```yaml # vmcluster vmstorage: { persistentVolume: { storageClass: "longhorn" } } # vlogs vlstorage: { persistentVolume: { storageClass: "longhorn" } } ``` ### 3.2 Keycloak (paasup realm) — ✅ 실행 완료 (2026-06-25) 확인됨: `paasup` realm 존재(HTTP 200), `perses`/`perses-vmauth` client·`vm-access` scope **없음**(신규 생성 대상). ```sh # ── 관리자 토큰 (platform Keycloak) ── KC=https://keycloak.example.org; REALM=paasup PW=$(kubectl get secret -n platform keycloak -o jsonpath='{.data.admin-password}' | base64 -d) TOKEN=$(curl -sk -X POST "$KC/realms/master/protocol/openid-connect/token" \ -d grant_type=password -d client_id=admin-cli -d username=admin --data-urlencode "password=$PW" | jq -r .access_token) H="Authorization: Bearer $TOKEN"; JSON="Content-Type: application/json" # ── 1) Client Scope vm-access + 매퍼 2종 ── curl -sk -X POST "$KC/admin/realms/$REALM/client-scopes" -H "$H" -H "$JSON" \ -d '{"name":"vm-access","protocol":"openid-connect"}' SID=$(curl -sk "$KC/admin/realms/$REALM/client-scopes" -H "$H" | jq -r '.[]|select(.name=="vm-access").id') for m in metrics logs; do curl -sk -X POST "$KC/admin/realms/$REALM/client-scopes/$SID/protocol-mappers/models" -H "$H" -H "$JSON" \ -d "{\"name\":\"vm-$m-account\",\"protocol\":\"openid-connect\",\"protocolMapper\":\"oidc-usermodel-attribute-mapper\",\"config\":{\"user.attribute\":\"vm_${m}_account_id\",\"claim.name\":\"vm_access.${m}_account_id\",\"jsonType.label\":\"int\",\"access.token.claim\":\"true\",\"id.token.claim\":\"false\"}}" done # ── 2) Client perses (로그인) — secret은 perses-values.yaml의 client_secret과 일치 ── curl -sk -X POST "$KC/admin/realms/$REALM/clients" -H "$H" -H "$JSON" -d '{ "clientId":"perses","enabled":true,"publicClient":false,"standardFlowEnabled":true, "secret":"perses-mock-secret-changeme", "redirectUris":["https://perses.example.org/api/auth/providers/oidc/keycloak/callback"]}' # ── 3) Client perses-vmauth (datasource service-account) ── curl -sk -X POST "$KC/admin/realms/$REALM/clients" -H "$H" -H "$JSON" -d '{ "clientId":"perses-vmauth","enabled":true,"publicClient":false, "standardFlowEnabled":false,"serviceAccountsEnabled":true,"secret":"perses-vmauth-secret-changeme"}' CID=$(curl -sk "$KC/admin/realms/$REALM/clients?clientId=perses-vmauth" -H "$H" | jq -r '.[0].id') curl -sk -X PUT "$KC/admin/realms/$REALM/clients/$CID/default-client-scopes/$SID" -H "$H" # vm-access 연결 SAU=$(curl -sk "$KC/admin/realms/$REALM/clients/$CID/service-account-user" -H "$H" | jq -r .id) curl -sk -X PUT "$KC/admin/realms/$REALM/users/$SAU" -H "$H" -H "$JSON" \ -d '{"attributes":{"vm_metrics_account_id":["0"],"vm_logs_account_id":["0"]}}' # 플랫폼 조회(account 0) # ── (선택) 테스트 유저 attribute: 테넌트 격리 검증용 ── # UID=$(curl -sk "$KC/admin/realms/$REALM/users?username=" -H "$H" | jq -r '.[0].id') # curl -sk -X PUT "$KC/admin/realms/$REALM/users/$UID" -H "$H" -H "$JSON" \ # -d '{"attributes":{"vm_metrics_account_id":["1"],"vm_logs_account_id":["1"]}}' ``` ### 3.3 시크릿 (평문 목 — external-secrets/Infisical 대체) — ✅ 실행 완료 (2026-06-25) > `monitoring` ns 선생성 필요(ArgoCD `CreateNamespace`는 sync 시점 → 사전 생성). perses OIDC `client_secret`은 perses-values.yaml에 인라인이라 별도 Secret 불필요(목). `root-ca-cert`·`alertmanager-notify`만 필요. > > ⚠️ `root-ca-cert`는 **vmauth·perses 양쪽**이 마운트(SSL_CERT_FILE)해 Keycloak OIDC TLS 검증에 사용. **누락 시 vmauth가 JWT 검증 불가(x509)** — 본 테스트에서 실제 발생(§8 참조). ```sh kubectl create namespace monitoring --dry-run=client -o yaml | kubectl apply -f - # alertmanager 알림 자격증명 (base custom-values가 /etc/alertmanager/secrets/webhook-url 참조) kubectl create secret generic alertmanager-notify -n monitoring \ --from-literal=webhook-url='https://discord.com/api/webhooks/' # Keycloak TLS 신뢰용 CA (perses base custom-values가 secretName: root-ca-cert, key ca.crt 마운트) # 사내 CA 소스: cert-manager/root-ca-secret (ca.crt, 1261 bytes) kubectl get secret root-ca-secret -n cert-manager -o jsonpath='{.data.ca\.crt}' | base64 -d > /tmp/ca.crt kubectl create secret generic root-ca-cert -n monitoring --from-file=ca.crt=/tmp/ca.crt ``` **생성된 Secret** (ns=monitoring) | Secret | 키 | 값 / 출처 | |--------|-----|-----------| | `alertmanager-notify` | `webhook-url` | `https://discord.com/api/webhooks/MOCK/MOCK` (mock) | | `root-ca-cert` | `ca.crt` | `cert-manager/root-ca-secret`의 `ca.crt` 복사 (1261 bytes) | ### 3.4 ConfigMap — ✅ 실행 완료 (2026-06-25) vmagent base custom-values가 `vmagent-relabel-configs`(테넌트별 demo01/demo02/platform) 마운트. 목은 빈 규칙으로도 기동 가능. ```sh # 테넌트별 write_relabel_configs (예: 최소 빈 규칙). 실제 규칙은 dip-console이 테넌트 온보딩 시 채움. for t in demo01 demo02 platform; do echo "[]" > /tmp/$t.yaml; done kubectl create configmap vmagent-relabel-configs -n monitoring \ --from-file=demo01.yaml=/tmp/demo01.yaml --from-file=demo02.yaml=/tmp/demo02.yaml --from-file=platform.yaml=/tmp/platform.yaml ``` **생성된 ConfigMap `vmagent-relabel-configs`** — 3개 키, 각 내용(mock): ```yaml # demo01.yaml [] # demo02.yaml [] # platform.yaml [] ``` ### 3.5 대시보드 — ✅ 실행 완료 (2026-06-25) 정적 대시보드 ConfigMap(사이드카 로드). 테스트에선 임시 apply, 정식은 ArgoCD directory 소스 Application. ```sh kubectl apply -n monitoring -f manifests/helm/perses/0.21.0/files/perses-dashboards.yaml # (데이터소스/시크릿은 perses-provisioning.yaml — 환경별이라 dip-console/Infisical 관리) ``` **생성된 ConfigMap `perses-dashboards`** — label `perses.dev/resource: "true"`, data 키 4종(대시보드 JSON): `dashboard-k8s-nodes.json`, `dashboard-k8s-pod-diagnostics.json`, `dashboard-k8s-pod-history.json`, `dashboard-k8s-workloads.json`. 원본: [perses-dashboards.yaml](../manifests/helm/perses/0.21.0/files/perses-dashboards.yaml). --- ## 4. 2단계 — ArgoCD 배포 — ✅ 실행 완료 (2026-06-25) > ArgoCD는 `platform` ns에 설치(release `argo-cd`). ApplicationSet도 `platform` ns에 apply. - [x] repo 등록: `service-catalog`는 기등록(insecure=true). `tenant-catalog`는 기존 repo secret 복제로 신규 등록 > 🔧 **수동 설정**: `kubectl get secret repo-3724134660 -n platform -o json | jq '.metadata.name="repo-tenant-catalog" | .data.url=("https://gitea.example.org/dip/tenant-catalog"|@base64)' | kubectl apply -f -` → §7 - [x] `applicationset.yaml` 조정: ns `argocd`→`platform`, repoURL `service-catalog`/`tenant-catalog`, `targetRevision: master` - [x] 로컬 변경분 `service-catalog` push (`d1f7c77`) 후 `kubectl apply -f .../applicationset.yaml` - [x] wave 0→1→2 순서 동기화, perses(wave2) vmauth 이후 기동 확인 결과: **15/15 파드 Running, PVC 3개 Bound, 11 Application 전부 Healthy**(9 Synced + 2 OutOfSync). | Application | wave | Sync | Health | |-------------|------|------|--------| | vmcluster | 0 | ✅ Synced¹ | ✅ Healthy | | vlogs | 0 | ✅ Synced¹ | ✅ Healthy | | kube-state-metrics | 0 | ✅ Synced | ✅ Healthy | | node-exporter | 0 | ✅ Synced | ✅ Healthy | | alertmanager | 0 | ✅ Synced | ✅ Healthy | | vmauth | 1 | ✅ Synced | ✅ Healthy | | vmagent | 1 | ✅ Synced | ✅ Healthy | | otelcol | 1 | ✅ Synced | ✅ Healthy | | otelcol-events | 1 | ✅ Synced | ✅ Healthy | | vmalert | 1 | ✅ Synced | ✅ Healthy | | perses | 2 | ✅ Synced | ✅ Healthy | > ¹ **vmcluster/vlogs (해결됨)** — 초기엔 `vmstorage`/`vlstorage` **StatefulSet `volumeClaimTemplates`**의 K8s 자동 기본값(`volumeMode`) diff로 영구 OutOfSync였음(immutable). **개선 반영**: ApplicationSet `ignoreDifferences`(StatefulSet `/spec/volumeClaimTemplates`) 추가 → **11/11 Synced** 확인(2026-06-25). --- ## 5. 3단계 — E2E 기능 검증 — ✅ 핵심 통과 (2026-06-25) vm-stack `test/` 패턴(port-forward + curl) 적응. | # | 항목 | 결과 | |---|------|------| | 1 | 파드 | ✅ **15/15 Running** | | 2 | 메트릭 | ✅ `count(up)`=**9**(acct0)/**36**(multitenant), status=success. vmagent 스크레이프→vminsert 적재 정상 | | 3 | 인증 라우팅 | ✅ (CA 수정 후 재검증) Keycloak `perses-vmauth` 토큰 `vm_access:{...:0}` → vmauth가 **실제 JWT 검증** 후 acct0 라우팅(count=9), 무토큰 401. ※ 초기엔 vmauth CA 미신뢰로 검증 실패→`unauthorized_user` 폴백이 가렸음(아래 발견사항) | | 4 | 로그 | ✅ vlselect `logsql`(AccountID:0) 데이터 존재. otelcol→vlinsert 적재 확인 | | 5 | 멀티테넌시 | ◐ 부분 — acct0/multitenant 라우팅 확인. demo01/demo02(acct1/2)는 워크로드 없어 데이터 없음 → 격리 양성검증만, 음성검증(타 테넌트 차단)은 워크로드 필요 | | 6 | perses | ◐ 로그인(`paasup`, global-admin)✅ + 대시보드 4종·데이터소스(victoriametrics/victorialogs) provisioning 적용✅. 인터랙티브 패널 렌더는 브라우저 로그인 필요(미실시) | | 7 | 알림 | ✅ vmalert 규칙 **8개**(cluster-health) 로드, alertmanager Running. Discord는 **mock**이라 실발송 미검증 | **발견사항 (E2E)** - ✅ **`unauthorized_user` 폴백 → 제거 완료(해결)**: 초기엔 무토큰/무효토큰도 acct0 200이었음. vmalert는 vmselect 직결이라 폴백 불필요 → **`unauthorized_user` 제거 반영**. 재검증: 무토큰/무효토큰 **모두 401**, 유효 JWT는 정상(2026-06-25). - perses 데이터소스/시크릿(`perses-provisioning`)은 §3.5에서 누락했다가 E2E 중 적용 → 정식 배포 시 ArgoCD 경로 또는 dip-console로 포함 필요. > 🔧 **수동 설정**(E2E 일회성): perses-provisioning apply(`kubectl apply -n monitoring -f .../perses-provisioning.yaml`), 검증 토큰 발급(perses-vmauth client_credentials), port-forward 다수 → §7 --- ## 6. 4단계 — 정리 (teardown) - [ ] ArgoCD Application/ApplicationSet 삭제(또는 prune) - [ ] 비-helm CR(있다면) → helm/Application → `monitoring` ns - [ ] longhorn PV/volume 명시 삭제(Retain) - [ ] Keycloak 목 리소스(perses client·테스트 그룹) 삭제 > 🔧 **수동 설정**: 삭제 명령 기록 → §7 --- ## 7. 수동 설정 목록 (이슈/자동화 backlog) > 위 🔧 항목에서 **실제 실행한 명령·스크립트**를 여기에 모은다. 이슈 정리 시 이 표를 그대로 사용하고, 각 항목은 dip-console 자동화 후보다. | # | 단계 | 대상 | 상태 | 명령 위치 | dip-console 이관 | |---|------|------|------|-----------|------------------| | 1 | 3.1 | 값 repo 렌더·push | ✅ 실행(2026-06-25) `tenant-catalog@510aa8e` | [§3.1](#31-값values-repo--✅-실행-완료-2026-06-25) | ✅ 예정 | | 2 | 3.2 | Keycloak vm-access scope(+매퍼2) + perses/perses-vmauth client + SA attr(acct 0) | ✅ 실행(2026-06-25) `paasup` realm | §3.2 | ✅ 예정 | | 3 | 3.3 | `monitoring` ns + `alertmanager-notify` + `root-ca-cert`(←cert-manager/root-ca-secret) | ✅ 실행(2026-06-25) | §3.3 | ✅ 예정(Infisical) | | 4 | 3.4 | `vmagent-relabel-configs`(demo01/demo02/platform, 빈 규칙) | ✅ 실행(2026-06-25) | §3.4 | ✅ 예정 | | 5 | 3.5 | `perses-dashboards` ConfigMap apply | ✅ 실행(2026-06-25) | §3.5 | (ArgoCD 경로) | | 6 | 4 | ArgoCD `tenant-catalog` repo secret 등록 + ApplicationSet apply(platform ns) | ✅ 실행(2026-06-25) | §4 | - | | 7 | 6 | teardown | ⬜ 미수행 | §6 | - | > 진행 메모(2026-06-25): **0~2단계 완료.** 1단계 mock 값(perses=`perses-mock-secret-changeme`, perses-vmauth=`perses-vmauth-secret-changeme`, alertmanager webhook·relabel placeholder). 2단계: ArgoCD=`platform` ns, `service-catalog`@`d1f7c77`/`tenant-catalog`@`510aa8e`, 11 App Healthy(15/15 파드). 남은 것: 3단계 E2E, 4단계 정리. --- ## 8. 테스트 결과 요약 (이슈용) 테스트 완료 후 채운다. - **일시 / 수행자**: (기입) - **환경**: (클러스터/ArgoCD/Keycloak) - **일시 / 수행자**: 2026-06-25 - **환경**: dev 클러스터(192.168.1.101), ArgoCD=`platform` ns, Keycloak `paasup` realm - **결과**: 0~3단계 통과 + 개선 반영. **11/11 Application Synced + Healthy**, **15/15 파드 Running**, E2E 핵심 항목 PASS(메트릭·로그·JWT 라우팅·알림 규칙), perses 로그인/admin OK - **발견 이슈 / 개선사항 (반영 결과)**: - ✅ **vmcluster/vlogs OutOfSync 해결** — ApplicationSet `ignoreDifferences`(StatefulSet `/spec/volumeClaimTemplates`) 추가 → 11/11 Synced. - ✅ **`unauthorized_user` 제거 완료** — vmauth 미인증 접근 차단(무토큰/무효토큰 401). vmalert는 vmselect 직결. - 🔴→✅ **vmauth가 JWT를 검증 못 하던 문제(중대)** — vmauth가 Keycloak OIDC discovery를 사내 CA 미신뢰로 가져오지 못해(x509) **OIDC verifier 초기화 실패 → 모든 JWT 검증 불가**. `unauthorized_user`가 이를 가려 그동안 acct0 폴백으로만 동작(=JWT 인증 실효 없음). **수정**: vmauth에 `root-ca-cert` 마운트 + `SSL_CERT_FILE=/ca/ca.crt` → OIDC verifier 정상, 유효 JWT 검증 성공·무토큰 401 재확인. (Perses 대시보드 unauthorized도 이 때문이었음) → **교훈**: Keycloak이 사내 CA면 **vmauth·perses 등 JWT/OIDC 소비자 모두 CA 신뢰 설정 필수**. - ◐ **perses 데이터소스/시크릿** — E2E 중 `perses-provisioning` 수동 apply. 정식 배포 시 ArgoCD 경로(또는 dip-console)로 포함 필요 (남은 backlog). - **Perses OIDC login = `preferred_username`** (v0.53.1 실측) — globalAdminUsers/RoleBinding은 username 기준(앞서 "sub UUID" 가정은 오류, 정정함). - ArgoCD 설치 위치 `platform` ns — ApplicationSet ns를 `platform`으로 조정함. - **자동화 backlog**: §7 수동 설정 목록 참조 (1단계 전부 dip-console 이관 대상) - **미완**: 4단계 정리(teardown), 멀티테넌시 음성검증(테넌트 워크로드 필요), perses 인터랙티브 렌더(브라우저)