diff --git a/.claude/deploy-test-procedure.md b/.claude/deploy-test-procedure.md index 9eb6b1d..48be6f8 100644 --- a/.claude/deploy-test-procedure.md +++ b/.claude/deploy-test-procedure.md @@ -48,6 +48,37 @@ IMAGE_NAME=quay.io/coreos/etcd:v3.7.1 bash scripts/deploy-test/deploy-test-etcd. 따른다. security-catalog 프로젝트에 상세 배포 테스트 기록(`doc/charts/etcd/deploy-test.md`)이 있으나 dip-catalog 에는 아직 이관되지 않았다. +## 카탈로그 앱 → cnpg-cluster 전환 배포 테스트 (`deploy-test-app-with-cnpg.sh`) + +airflow/lakekeeper/mlflow/superset처럼 내장 bitnami postgresql 서브차트를 cnpg-cluster +전용 인스턴스로 전환한 앱은 이 공용 스크립트로 검증한다. 각 앱의 ApplicationSet 배포 구조· +사전조건은 `manifests/applicationset///README.md`에 있다 — 여기서는 배포 +테스트 자체의 공통 절차만 다룬다. + +```sh +APP_CHART_DIR=manifests/helm// \ +APP_RELEASE= \ +DB_CUSTOM_VALUES=manifests/applicationset///-db-values.yaml \ +DB_SECRET_USER= DB_SECRET_PASSWORD= \ +TEST_NAMESPACE=-test-build \ +APP_EXTRA_VALUES=scripts/deploy-test/fixtures/-deploy-test-overrides.yaml \ +APP_POD_SELECTOR="" \ + bash scripts/deploy-test/deploy-test-app-with-cnpg.sh /tmp/deploy-test- +``` + +- operator(`cloudnative-pg`)는 상시 컴포넌트로 재사용한다. DB(cnpg-cluster)를 먼저 띄워 + healthy 대기 후 앱을 설치하고, 실제 DB 소비 파드가 Running+Ready 상태가 되고 로그에 DB + 연결 실패 패턴이 없으면 PASS. 정리는 성공/실패와 무관하게 항상 수행한다. +- `APP_POD_SELECTOR`는 반드시 지정한다 — 미지정 시 네임스페이스의 아무 파드나 Running이면 + 통과로 오판할 수 있다(DB와 무관한 다른 파드가 떠 있으면 실제 실패를 놓친다 — 실측). +- `APP_EXTRA_VALUES`(fixture, `scripts/deploy-test/fixtures/-deploy-test-overrides.yaml`)는 + DB 연결과 무관하지만 격리된 테스트 네임스페이스에서만 막히는 의존성을 끈다. 카탈로그의 + 실제 권장 설정(`custom-values.yaml`)은 건드리지 않는다. 앱별로 무엇을 왜 끄는지는 각 + fixture 파일 자체의 주석에 있다 — 문서를 이중으로 유지하지 않는다. +- PV뿐 아니라 **Longhorn Volume 커스텀 리소스**까지 지워야 한다 — PV만 지우면 Longhorn + 쪽에 실제 디스크가 계속 쌓인다. 상세: [pitfalls.md](pitfalls.md). +- ingress host 충돌 주의: [pitfalls.md](pitfalls.md) 참고. + ## 그 외 차트 — 수동 절차 1. 전용 네임스페이스를 새로 만든다 (`pg-test-`). 기존 워크로드가 있는 NS 를 쓰지 않는다. diff --git a/.claude/pitfalls.md b/.claude/pitfalls.md index 1dae07c..0624ffe 100644 --- a/.claude/pitfalls.md +++ b/.claude/pitfalls.md @@ -51,3 +51,41 @@ kubectl -n get clusters.postgresql.cnpg.io CNPG `Database` CRD 는 spec generation 이 바뀔 때만 reconcile 한다. 확장이 DB 에서 사라져도 `status.applied` 는 계속 `true` 다. 검증은 항상 실제 DB 에 질의해서 한다 (security-catalog 프로젝트에서 실측 — 상세 배포 테스트 기록은 dip-catalog 에 아직 없음). + +## PV 를 지워도 Longhorn Volume 은 남는다 + +`kubectl delete pv` 로 k8s PersistentVolume 오브젝트를 지워도 **Longhorn 자체의 +`volumes.longhorn.io` 커스텀 리소스는 남는다.** `kubectl get pv` 로는 "깨끗하다"고 보이지만 +Longhorn 의 `storageScheduled` 는 계속 누적되고, 결국 디스크에 여유가 충분한데도 +`insufficient storage`(`ReplicaSchedulingFailure`)로 새 볼륨 스케줄링이 막힌다. + +실측: 반복된 배포 테스트가 남긴 orphan 볼륨 112 개(~1TB)가 쌓여 airflow 배포가 볼륨 +attach 단계에서 멈췄다. PV 와 이름이 같은 Longhorn Volume 을 함께 지워야 한다. + +```sh +kubectl -n longhorn-system delete volumes.longhorn.io +``` + +이미 삭제된 네임스페이스의 orphan 볼륨을 찾을 때는 `.status.kubernetesStatus.namespace` 가 +현존하지 않는 네임스페이스를 가리키는 것만 골라낸다. 배포 테스트 스크립트 +(`deploy-test-app-with-cnpg.sh`, `deploy-test-cnpg-cluster.sh`)에는 이 정리가 포함돼 있다. + +## 배포 테스트의 ingress host 가 실제 배포와 충돌한다 + +카탈로그 `custom-values.yaml` 의 ingress host(`.example.org`)를 그대로 쓰면, 같은 host 를 +이미 쓰는 실제 배포가 있을 때 **apisix 에 동일 host 라우트가 2 개 등록되어 운영 트래픽 +라우팅과 충돌한다.** 실측: `defense-llm` 네임스페이스에 이미 배포된 lakekeeper 와 +`lakekeeper.example.org` 가 겹쳤다. + +배포 테스트는 항상 host 를 `-access-test.example.org` 처럼 별도 값으로 오버라이드한다 +(각 앱 fixture 에 반영돼 있다). 배포 전 `kubectl get ingress -A` 로 host 중복을 먼저 확인한다. + +## ingress 는 `ingressClassName` 을 명시해야 한다 + +이 클러스터의 ingress controller 는 **apisix 하나뿐이다**(kong 은 없다). 그런데 일부 카탈로그 +차트가 아직 `kubernetes.io/ingress.class: kong` 애노테이션 방식으로 남아 있었다 — 이 경우 +Ingress 리소스는 만들어지지만 `CLASS: ` 으로 뜨고 어떤 컨트롤러도 처리하지 않아 +접근 자체가 불가능하다(airflow·superset 에서 실측, 둘 다 수정함). + +또한 `path: /` 는 정확히 `/` 만 매치되므로 앱이 `/home` 등으로 리다이렉트하면 전부 404 가 +난다. `path: /.*` + `k8s.apisix.apache.org/use-regex: "true"` 를 함께 쓴다. diff --git a/.claude/skills/chart-to-cnpg/SKILL.md b/.claude/skills/chart-to-cnpg/SKILL.md new file mode 100644 index 0000000..f8fab3c --- /dev/null +++ b/.claude/skills/chart-to-cnpg/SKILL.md @@ -0,0 +1,82 @@ +--- +name: chart-to-cnpg +description: 카탈로그 차트가 내장한 bitnami postgresql 서브차트를 전용 cnpg-cluster 인스턴스로 전환할 때 사용한다. "postgresql 서브차트를 cnpg로 바꿔줘", "이 차트도 cnpg-cluster 쓰게 해줘", "bitnami postgresql 제거" 같은 요청, 또는 manifests/helm//의 custom-values.yaml에서 postgresql 서브차트를 끄고 외부 DB로 돌리는 작업에 해당한다. 남은 대상은 flowise, langflow-ide, langfuse, litellm, nemo다. +--- + +# 카탈로그 앱 → cnpg-cluster 전환 + +내장 bitnami postgresql 서브차트를 끄고, 앱 전용 `cnpg-cluster` 인스턴스를 외부 DB로 쓰도록 +바꾼다. airflow/lakekeeper/mlflow/superset 4개에서 실측 검증된 절차다(GitHub 이슈 #14). + +**helm/kubectl을 직접 실행하지 않는다** — 배포·검증은 `scripts/deploy-test/*.sh`가 전담한다 +(CLAUDE.md 설계 원칙 #1). 이 skill은 파일 편집과 판단만 담당한다. + +## 1. 대상 확인 + +`manifests/helm///custom-values.yaml`에 `postgresql:` 서브차트 설정이 있는지 본다. +`Chart.yaml`의 `dependencies`에 postgresql이 있는지도 확인한다. + +**범위 밖**: gitea·keycloak·dnsup는 서브차트가 아니라 공유 `postgresql-ha`를 외부 참조하며 +`paasup/dipup` 레포 관리 대상이다 — 건드리지 않는다(`doc/migrations/dipup-postgresql-ha-cnpg-handoff.md`). + +## 2. 외부 DB 연결 키 찾기 + +차트마다 키 구조가 다르다. `values.yaml`에서 `externalDatabase` / `external*` / `postgresql.enabled` +주변을 grep 한다. 이미 해결된 4개 사례: + +| 차트 | 외부 DB 키 | +|---|---| +| `airflow/1.16.0` | `data.metadataConnection.{host,user,pass,db,port,sslmode}` | +| `lakekeeper/0.11.0` | `externalDatabase.{host_read,host_write,port,database,user,password}` | +| `mlflow/2.1.0` | `externalDatabase.{dialectDriver,host,port,user,password,database}` | +| `superset/0.13.5` | `supersetNode.connections.db_{host,port,user,pass,name}` | + +**서비스명 규칙**: 쓰기는 `<릴리스>-rw`, 읽기는 `<릴리스>-r`. +읽기에 **`-ro`를 쓰면 안 된다** — `-ro`는 replica 전용이라 권장 기준인 `instances: 1`에서 +엔드포인트가 0개가 되어 앱이 영구 재시도에 빠진다(lakekeeper에서 실측). + +## 3. 파일 작업 (6종) + +내용은 여기 복붙하지 말고 **기존 4개 사례를 열어 그대로 따른다**. 문서 중복은 유지보수 부담만 는다. + +| 파일 | 작업 | 레퍼런스 | +|---|---|---| +| `manifests/helm///custom-values.yaml` | `postgresql.enabled: false` + 위 외부 DB 키. 기존 `postgresql.auth.*`/`image.*` 등 서브차트 전용 설정은 제거 | 4개 앱 | +| `manifests/helm///CUSTOM-README.md` | 전환 사실·사전조건(operator)·배포 순서 | 〃 | +| `manifests/applicationset///applicationset.yaml` | 신규 — DB(syncWave 0) → 앱(syncWave 1) | `manifests/applicationset/airflow/1.16.0/` | +| `manifests/applicationset///-db-values.yaml`
`-values.yaml` | 신규 — 비밀번호는 `$INFISICAL_SECRET` 플레이스홀더 | 〃 | +| `manifests/applicationset///README.md` | 신규 — 짧게. 배포 테스트 절차는 복제하지 말고 참조만 | 〃 | +| `scripts/deploy-test/fixtures/-deploy-test-overrides.yaml` | 신규 — 아래 4절 참고 | `scripts/deploy-test/fixtures/` 4개 | +| `doc/define-chart-resources.md` | 해당 차트 섹션의 postgres 리소스/볼륨 티어를 cnpg-cluster 참조로 교체 | 기존 4개 섹션 | + +## 4. 배포 테스트 fixture + +`custom-values.yaml`(카탈로그 권장값)은 절대 바꾸지 않고, 배포 테스트에서만 얹는 오버레이다. +격리된 테스트 네임스페이스에서 **DB 연결과 무관하게 막히는 의존성만** 끈다. + +- **ingress host 오버라이드는 필수다** — 카탈로그 기본 host가 이 클러스터의 다른 실제 배포와 + 겹치면 apisix에 동일 host 라우트가 2개 등록되어 운영 트래픽과 충돌한다(lakekeeper에서 실측). + `-access-test.example.org` 같은 별도 host를 쓴다. +- 외부 SSO/저장소 등 이 환경에 없는 의존성을 끈다(사례: airflow `dags.gitSync.enabled: false`, + lakekeeper `auth.oauth2` 비우기, superset `configOverrides.enable_oauth`). +- **왜 끄는지는 fixture 파일 주석에 남긴다** — README에 중복 기술하지 않는다. + +## 5. 배포 테스트 + +`.claude/deploy-test-procedure.md`의 "카탈로그 앱 → cnpg-cluster 전환 배포 테스트" 절을 따른다. +절차를 여기 복제하지 않는다. + +`APP_POD_SELECTOR`는 **DB에 실제로 연결하는 파드**를 가리켜야 한다(미지정 시 무관한 파드가 +Running이라는 이유로 통과 오판). 4개 사례: airflow `component=webserver`, +lakekeeper `app.kubernetes.io/component=catalog`, mlflow `app.kubernetes.io/component=tracking`, +superset `app=superset`. + +## 6. 마무리 체크리스트 + +- [ ] `helm template`으로 postgresql 서브차트 리소스가 더 이상 렌더링되지 않는지 확인 +- [ ] 읽기 엔드포인트가 `-ro`가 아니라 `-r`인지 확인(해당 차트에 읽기/쓰기 분리가 있을 때) +- [ ] fixture에 ingress host 오버라이드가 있는지 확인 +- [ ] 배포 테스트 PASS + 테스트 네임스페이스/PVC/PV/Longhorn 볼륨 잔여 없음 +- [ ] 운영 중인 네임스페이스(`platform`, `defense-llm`, `cnpg-system`)에 영향 없음 +- [ ] 환경 함정은 `.claude/pitfalls.md` 참고 (Longhorn 볼륨 잔여, ingress host 충돌, + `ingressClassName` 누락, 선언적 리소스 `status` 신뢰 금지) diff --git a/CLAUDE.md b/CLAUDE.md index 244e0be..dd44d28 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -109,6 +109,30 @@ python3 agent/update_catalog/skills/helm_diff/scripts/run.py \ --- +## Claude Code Skills (`.claude/skills/`) + +위 `agent/update_catalog/skills/`(결정론적 Python CLI 파이프라인)와 **용도가 다른 별개 +체계**다. 혼동하지 않는다. + +| 구분 | `agent/update_catalog/skills/` | `.claude/skills/` | +|------|-------------------------------|-------------------| +| 형식 | SKILL.md + `scripts/run.py` (JSON in/out) | Claude Code Agent Skill (frontmatter + 지시문) | +| 실행 | `python3 run.py --flags`, 파이프라인이 호출 | Claude Code 세션에서 관련 작업 시 자동 로드 | +| 용도 | 차트 버전 감지·diff·PR 생성 등 결정론적 자동화 | 차트마다 판단이 필요한 반복 편집·검증 절차 | + +`.claude/skills/`의 Skill도 설계 원칙 #1을 지킨다 — helm/kubectl 실행은 +`scripts/deploy-test/*.sh` 같은 스크립트가 전담하고, Skill은 편집·판단·검증 절차만 담당한다. + +| Skill | 용도 | +|-------|------| +| [chart-to-cnpg](.claude/skills/chart-to-cnpg/SKILL.md) | 카탈로그 차트의 내장 bitnami postgresql 서브차트를 전용 cnpg-cluster로 전환 | + +관련 참조 문서(Skill이 절차의 단일 출처로 삼는다): +[deploy-test-procedure.md](.claude/deploy-test-procedure.md) · +[pitfalls.md](.claude/pitfalls.md) · [image-authoring.md](.claude/image-authoring.md) + +--- + ## CVE/SBOM 게이트 작업 `manifests/helm/` 카탈로그가 참조하는 컨테이너 이미지의 SBOM·취약점을 스캔하고 diff --git a/doc/define-chart-resources.md b/doc/define-chart-resources.md index 0a6cc7c..fd2304a 100644 --- a/doc/define-chart-resources.md +++ b/doc/define-chart-resources.md @@ -16,12 +16,15 @@ DIP 카탈로그의 Helm 차트에 대한 상세한 리소스 요구사항을 | **Workers** | 500m/1Gi → 2000m/2Gi | 1000m/2Gi → 4000m/4Gi | 2000m/4Gi → 8000m/8Gi | | **Webserver** | 200m/256Mi → 500m/512Mi | 500m/512Mi → 1000m/1Gi | 1000m/1Gi → 2000m/2Gi | | **Redis** | 100m/128Mi → 200m/256Mi | 200m/256Mi → 500m/512Mi | 500m/512Mi → 1000m/1Gi | -| **PostgreSQL** | 250m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | + +> **PostgreSQL**: 내장 서브차트 대신 전용 `cnpg-cluster` 인스턴스를 외부 DB로 사용한다 +> (`postgresql.enabled: false` + `data.metadataConnection`). 리소스/볼륨 티어는 +> `manifests/helm/cnpg-cluster/1.0.0/dip-resources-quotas.yaml` · +> `dip-volumes-quotas.yaml` 참고. **볼륨 사이즈 권장사항:** | 컴포넌트 | Small | Medium | Large | |----------|-------|--------|-------| -| **PostgreSQL Data** | 10Gi | 50Gi | 200Gi | | **Redis Data** | 1Gi | 5Gi | 20Gi | | **Logs Volume** | 5Gi | 20Gi | 100Gi | | **DAGs Volume** | 1Gi | 5Gi | 20Gi | @@ -261,12 +264,15 @@ resources: {} | 컴포넌트 | Small | Medium | Large | |----------|-------|--------|-------| | **API Server** | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | 2000m/4Gi → 4000m/8Gi | -| **Database** | 250m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | + +> **Database**: 내장 서브차트 대신 전용 `cnpg-cluster` 인스턴스를 외부 DB로 사용한다 +> (`postgresql.enabled: false` + `externalPostgresql.*`). 리소스/볼륨 티어는 +> `manifests/helm/cnpg-cluster/1.0.0/dip-resources-quotas.yaml` · +> `dip-volumes-quotas.yaml` 참고. **볼륨 사이즈 권장사항:** | 컴포넌트 | Small | Medium | Large | |----------|-------|--------|-------| -| **Database Data** | 5Gi | 20Gi | 100Gi | | **Flow Storage** | 2Gi | 10Gi | 50Gi | **권장 사용 시나리오:** @@ -295,20 +301,19 @@ worker: # limits: # cpu: 100m # memory: 128Mi +# 내장 postgresql 대신 전용 cnpg-cluster 인스턴스를 외부 DB로 사용한다. +# flowise는 postgresql.enabled: false 만으로는 부족하고 externalPostgresql.enabled: true 를 +# 함께 켜야 한다. postgresql: + enabled: false + +externalPostgresql: enabled: true - primary: - persistence: - enabled: true - storageClass: "longhorn" - size: 5Gi - resources: {} - # limits: - # memory: 512Mi - # cpu: 500m - # requests: - # memory: 256Mi - # cpu: 250m + host: flowise-db-rw + port: 5432 + username: flowise + password: flowise + database: flowise ``` --- @@ -830,13 +835,16 @@ postgresql: |----------|-------|--------|-------| | **Tracking Server** | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | 2000m/4Gi → 4000m/8Gi | | **Run Container** | 250m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | -| **PostgreSQL** | 250m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | | **MinIO** | 250m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | +> **PostgreSQL**: 내장 서브차트 대신 전용 `cnpg-cluster` 인스턴스를 외부 DB로 사용한다 +> (`postgresql.enabled: false` + `externalDatabase.*`). 리소스/볼륨 티어는 +> `manifests/helm/cnpg-cluster/1.0.0/dip-resources-quotas.yaml` · +> `dip-volumes-quotas.yaml` 참고. + **볼륨 사이즈 권장사항:** | 컴포넌트 | Small | Medium | Large | |----------|-------|--------|-------| -| **PostgreSQL Data** | 5Gi | 20Gi | 100Gi | | **MinIO Storage** | 20Gi | 200Gi | 1Ti | | **Artifacts** | 10Gi | 100Gi | 500Gi | @@ -851,13 +859,6 @@ tracking: resources: {} run: resources: {} - -postgresql: - primary: - resources: {} - persistence: - storageClass: "" - size: 5Gi ``` @@ -1565,13 +1566,16 @@ resources: | **Superset Worker** | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | 2000m/4Gi → 4000m/8Gi | | **Celery Beat** | 100m/256Mi → 200m/512Mi | 200m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | | **Celery Flower** | 100m/256Mi → 200m/512Mi | 200m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | -| **PostgreSQL** | 250m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | | **Redis** | 200m/256Mi → 500m/512Mi | 500m/512Mi → 1000m/1Gi | 1000m/1Gi → 2000m/2Gi | +> **PostgreSQL**: 내장 서브차트 대신 전용 `cnpg-cluster` 인스턴스를 외부 DB로 사용한다 +> (`postgresql.enabled: false` + `supersetNode.connections.db_*`). 리소스/볼륨 티어는 +> `manifests/helm/cnpg-cluster/1.0.0/dip-resources-quotas.yaml` · +> `dip-volumes-quotas.yaml` 참고. + **볼륨 사이즈 권장사항:** | 컴포넌트 | Small | Medium | Large | |----------|-------|--------|-------| -| **PostgreSQL Data** | 8Gi | 50Gi | 200Gi | | **Redis Data** | 8Gi | 20Gi | 100Gi | **권장 사용 시나리오:** @@ -1974,14 +1978,18 @@ metadata: | 컴포넌트 | Small | Medium | Large | |----------|-------|--------|-------| | **Lakekeeper Catalog** | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | 2000m/4Gi → 4000m/8Gi | -| **PostgreSQL** | 250m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | | **OpenFGA** | 200m/256Mi → 500m/512Mi | 500m/512Mi → 1000m/1Gi | 1000m/1Gi → 2000m/2Gi | | **OpenFGA PostgreSQL** | 250m/512Mi → 500m/1Gi | 500m/1Gi → 1000m/2Gi | 1000m/2Gi → 2000m/4Gi | +> **Lakekeeper PostgreSQL**: 내장 `groundhog2k/postgres` 서브차트 대신 전용 `cnpg-cluster` +> 인스턴스를 외부 DB로 사용한다(`postgresql.enabled: false` + `externalDatabase.*`). 리소스/볼륨 +> 티어는 `manifests/helm/cnpg-cluster/1.0.0/dip-resources-quotas.yaml` · +> `dip-volumes-quotas.yaml` 참고. **OpenFGA PostgreSQL은 여전히 내장 bitnami 서브차트를 +> 사용한다** — 이번 전환 범위에 포함되지 않았다. + **볼륨 사이즈 권장사항:** | 컴포넌트 | Small | Medium | Large | |----------|-------|--------|-------| -| **Lakekeeper PostgreSQL** | 5Gi | 20Gi | 100Gi | | **OpenFGA PostgreSQL** | 8Gi | 30Gi | 150Gi | **권장 사용 시나리오:** @@ -2000,18 +2008,18 @@ catalog: # cpu: 500m # memory: 1Gi -# lakekeeper DB +# lakekeeper DB — 전용 cnpg-cluster 인스턴스를 외부 DB로 사용한다 postgresql: - storage: - className: "" - requestedSize: 5Gi - resources: {} - # limits: - # cpu: 500m - # memory: 1Gi - # requests: - # cpu: 250m - # memory: 512Mi + enabled: false + +externalDatabase: + type: postgres + host_read: "lakekeeper-db-r" # instances:1 에서는 -ro가 엔드포인트 0개라 -r을 쓴다 + host_write: "lakekeeper-db-rw" + port: 5432 + database: lakekeeper + user: lakekeeper + password: lakekeeper openfga: diff --git a/doc/migrations/dipup-postgresql-ha-cnpg-handoff.md b/doc/migrations/dipup-postgresql-ha-cnpg-handoff.md new file mode 100644 index 0000000..826dcae --- /dev/null +++ b/doc/migrations/dipup-postgresql-ha-cnpg-handoff.md @@ -0,0 +1,97 @@ +# dipup 인수인계 — postgresql-ha → cnpg-cluster (gitea/keycloak/dnsup) + +이 문서는 실행 문서가 아니다. dip-catalog가 아닌 `paasup/dipup` 레포에서 관리되는 +gitea/keycloak/dnsup의 DB 전환 작업을 위해, dip-catalog 쪽에서 파악한 내용을 정리해 +넘기는 참고 자료다. dip-catalog는 이 3개 앱과 `postgresql-ha`를 직접 변경하지 않는다. + +## 배경 + +카탈로그의 `postgresql-ha`(bitnami postgresql-repmgr + pgpool, +`manifests/helm/postgresql-ha/11.9.4/`)는 현재 `platform` 네임스페이스에 배포되어 있고, +`gitea`·`keycloak`·`dnsup` 3개 앱이 이 인스턴스의 4개 DB를 쓴다. + +| DB | 소비 앱 | owner role | +|---|---|---| +| `gitea` | gitea | gitea | +| `gitea_session` | gitea (세션 저장소) | gitea | +| `keycloak` | keycloak | keycloak | +| `paasup` | dnsup | paasup | + +GitHub 이슈 [#9](https://github.com/paasup/dip-catalog/issues/9)에서 이 인스턴스를 +CloudNativePG(cnpg-cluster + cloudnative-pg operator)로 대체하기로 이미 결정되었다 +(2026-07-31). dip-catalog에는 이미 `cloudnative-pg`(0.29.0, operator)·`cnpg-cluster` +(1.0.0, Cluster/Database 래퍼 차트)가 추가되어 있고, 이번 작업(카탈로그 앱 +airflow/lakekeeper/mlflow/superset 전환)에서 이 두 차트로 4개 앱의 내장 DB를 전용 +인스턴스로 옮기는 배포 테스트까지 실측 검증했다. + +## 현재 연결 정보 (변경 지점) + +3개 앱은 서브차트가 아니라 외부 호스트를 직접 참조한다. cnpg-cluster로 전환 시 아래 +지점만 바뀐다. + +| 앱 | 파일 | 현재 값 | 변경 후 (예: 릴리스명 `platform-db`) | +|---|---|---|---| +| gitea | `gitea.config.database.HOST` | `postgresql-postgresql-ha-postgresql:5432` | `platform-db-rw:5432` | +| gitea | `gitea.config.session.PROVIDER_CONFIG` | `host=postgresql-postgresql-ha-postgresql` | `host=platform-db-rw` | +| keycloak | `extraEnv` (`DB_ADDR`) | `postgresql-postgresql-ha-postgresql` | `platform-db-rw` | +| dnsup | `env.DB_HOST` | `postgresql-postgresql-ha-postgresql.platform.svc.cluster.local` | `platform-db-rw.platform.svc.cluster.local` | + +cnpg-cluster의 서비스 명명 규칙은 `<릴리스명>-rw`(쓰기/기본 연결)이다 — 자세한 내용은 +`manifests/helm/cnpg-cluster/1.0.0/CUSTOM-README.md` §3 참고. + +## 선결 과제 — 공유 인스턴스 하나로 4개 DB를 만들 수 없다 + +`cnpg-cluster` 차트의 `bootstrap.initdb`는 **DB 1개 + owner role 1개만** 최초 생성한다. +`databases[]`(Database CRD)로 DB를 추가할 수는 있지만 **owner role은 미리 존재해야 +한다** — role 자체를 새로 만들지 못한다. `postgresql-ha`의 `initdbScripts`처럼 +gitea/keycloak/paasup 3개 role을 한 번에 만들 방법이 지금 차트에는 없다. + +이번 dip-catalog 작업(airflow/lakekeeper/mlflow/superset)은 각 앱이 **자기 전용 +인스턴스**를 쓰는 구조라 이 문제를 만나지 않았다(인스턴스마다 role 1개만 필요). +gitea/keycloak/dnsup는 원래 인스턴스 1개를 공유하던 구조라 선택이 필요하다. + +### 옵션 A — 공유 인스턴스 유지 (기존 토폴로지와 동일) + +`bootstrap.initdb.postInitSQL`(CNPG 업스트림 API에 있는 필드 — superuser로 최초 1회, +DB 생성 전 단계에 실행되는 SQL. 기존 `postInitApplicationSQL`과 달리 `CREATE ROLE`/ +`CREATE DATABASE`에 적합)을 `manifests/helm/cnpg-cluster/1.0.0/templates/cluster.yaml`· +`values.yaml`에 추가해야 한다. 이 필드로 keycloak/paasup role을 만들고(gitea role은 +`bootstrap.initdb.owner`로 자동 생성), `databases[]`로 gitea/gitea_session/keycloak/ +paasup 4개 DB를 선언하는 방식이다. **이 작업은 dip-catalog 쪽 차트 변경이 선행되어야 +하므로, dipup에서 이 옵션을 원하면 dip-catalog에 먼저 요청이 필요하다.** + +### 옵션 B — 앱별 전용 인스턴스 3개 (이번 작업에서 검증된 패턴 그대로) + +gitea용, keycloak용, dnsup(paasup)용 `cnpg-cluster` 릴리스를 각각 배포한다. 각 인스턴스는 +`bootstrap.initdb.database`/`owner`만 채우면 되므로 **차트 수정이 필요 없다** — +`manifests/applicationset/airflow/1.16.0/airflow-db-values.yaml` 등과 동일한 패턴의 +값 오버레이 3개(dipup 쪽에 `gitea-db-values.yaml`, `keycloak-db-values.yaml`, +`dnsup-db-values.yaml` 격)만 있으면 된다. 이 값들은 ArgoCD ApplicationSet의 `valuesPath`로 +연결하는 것을 권장한다 — `manifests/applicationset/{airflow,lakekeeper,mlflow,superset}/`가 +그 구조의 실제 예시다(DB 차트 syncWave 0 → 앱 차트 syncWave 1). gitea의 `gitea_session` DB는 같은 인스턴스 +안에서 `databases[]`에 `owner: gitea`로 추가하면 된다(role은 이미 bootstrap로 생성됨). + +단점은 postgres 파드 수가 늘어나는 것(인스턴스 3개 × instances 설정값)이다 — 리소스는 +`manifests/helm/cnpg-cluster/1.0.0/dip-resources-quotas.yaml`의 `small`(instances:1) 기준을 +참고한다. + +**권장**: 별도 사유가 없다면 옵션 B — dip-catalog 차트 변경 없이 바로 시작할 수 있고, +이번 4개 앱 전환에서 동일 패턴이 실측 검증되었다. + +## 이번 작업에서 실측된 주의사항 (그대로 적용됨) + +- cnpg-cluster의 `-ro`(replica 전용) 서비스는 `instances:1`이면 엔드포인트가 0개다. + 읽기/쓰기를 분리해서 연결하는 앱이 있다면 읽기는 `-ro`가 아니라 `-r`(전체 인스턴스 + 라운드로빈)을 쓴다 — `lakekeeper/0.11.0/custom-values.yaml`에서 실측. +- `cloudnative-pg` operator(namespace `cnpg-system`, release `cnpg`)는 클러스터에 이미 + 1회 설치되어 있으므로 재사용하면 된다 — 새로 설치할 필요 없다. +- DB owner 계정의 비밀번호는 `bootstrap.initdb.secretName`으로 지정한 시크릿(키: + `username`/`password`)에서 온다. 앱 쪽 값(예: keycloak의 `DB_PASSWORD`)과 반드시 + 일치해야 한다. + +## 이번 작업 범위에 포함되지 않은 것 + +- gitea/keycloak/dnsup의 `custom-values.yaml` 실제 수정 (dipup 관리 대상) +- `postgresql-ha` 카탈로그 항목 제거 여부 판단 +- 실 데이터 마이그레이션(`pg_dump`/`pg_restore`) 및 컷오버 절차, 다운타임 계획, 롤백 계획 + — SSO(keycloak)·git(gitea) 서비스에 영향을 주는 운영 작업이라 별도 계획·승인이 필요하다. diff --git a/manifests/applicationset/airflow/1.16.0/README.md b/manifests/applicationset/airflow/1.16.0/README.md new file mode 100644 index 0000000..279fd86 --- /dev/null +++ b/manifests/applicationset/airflow/1.16.0/README.md @@ -0,0 +1,36 @@ +# airflow ArgoCD ApplicationSet + +`airflow-db`(cnpg-cluster, syncWave 0) → `airflow`(syncWave 1) 2단계로 배포한다. +전체 스키마는 [manifests/applicationset/openmetadata/1.12.1/](../../openmetadata/1.12.1/)와 +[manifests/applicationset/victoria-metrics/1.0.0/](../../victoria-metrics/1.0.0/)를 따른다. + +## 구성 + +- `applicationset.yaml` — list generator로 Application 2개 생성. +- `airflow-db-values.yaml` — cnpg-cluster 환경 오버레이 템플릿(DB/owner, 시크릿 참조). +- `airflow-values.yaml` — airflow 환경 오버레이 템플릿(DB 비밀번호만). + +두 파일 모두 **템플릿**이다 — dip-console이 렌더링해 값 repo(`dip/tenant-catalog`)의 +`airflow/*.yaml`로 커밋하는 것을 가정한다. + +## 값 적용 순서 (2단) + +각 Application은 `baseValues`(카탈로그 차트의 `custom-values.yaml`) + `valuesPath`(값 repo의 +환경 오버레이, 나중 파일 우선)를 병합한다. + +## 사전 준비 (ArgoCD 밖) + +- `cloudnative-pg` operator(`manifests/helm/cloudnative-pg/0.29.0`)가 클러스터에 먼저 + 설치되어 있어야 한다 — 없으면 `airflow-db`(Cluster CR)가 webhook 부재로 생성 자체가 거부된다. +- `airflow-db-values.yaml`의 `bootstrap.initdb.secretName`과 동일한 이름·값의 K8s Secret이 + 대상 네임스페이스에 미리 존재해야 한다(`username`/`password` 키). +- ArgoCD가 `Cluster.postgresql.cnpg.io` 커스텀 리소스의 상태를 "Healthy"로 판정하려면 + `argocd-cm`에 해당 CRD용 커스텀 헬스 체크(Lua)가 등록되어 있어야 sync-wave 1이 정상 + 대기한다 — 미등록 시 wave 0이 Progressing으로만 남아 wave 1로 못 넘어갈 수 있다. + +## 배포 테스트 + +절차와 주의사항은 [.claude/deploy-test-procedure.md](../../../../.claude/deploy-test-procedure.md) +"카탈로그 앱 → cnpg-cluster 전환 배포 테스트" 절을 따른다. +`APP_POD_SELECTOR="component=webserver"`, fixture는 +`scripts/deploy-test/fixtures/airflow-deploy-test-overrides.yaml`. diff --git a/manifests/applicationset/airflow/1.16.0/airflow-db-values.yaml b/manifests/applicationset/airflow/1.16.0/airflow-db-values.yaml new file mode 100644 index 0000000..b5eac5d --- /dev/null +++ b/manifests/applicationset/airflow/1.16.0/airflow-db-values.yaml @@ -0,0 +1,25 @@ +# airflow 전용 cnpg-cluster 환경 오버레이 템플릿. +# dip-console이 렌더링해 값 repo(dip/tenant-catalog)의 airflow/airflow-db-values.yaml로 +# 커밋하는 것을 가정한 시작점이다. 배포 테스트로 검증된 값 그대로다 +# (지난 세션: manifests/helm/cnpg-cluster/1.0.0/custom-values-airflow.yaml). +instances: 1 + +bootstrap: + initdb: + database: airflow + owner: airflow + secretName: "$INFISICAL_SECRET" + +databases: + - name: airflow + owner: airflow + ensure: present + reclaimPolicy: retain + +resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 1000m + memory: 2Gi diff --git a/manifests/applicationset/airflow/1.16.0/airflow-values.yaml b/manifests/applicationset/airflow/1.16.0/airflow-values.yaml new file mode 100644 index 0000000..675a6f4 --- /dev/null +++ b/manifests/applicationset/airflow/1.16.0/airflow-values.yaml @@ -0,0 +1,9 @@ +# airflow 환경 오버레이 템플릿 — custom-values.yaml(baseValues) 위에 얹힌다. +# DB 비밀번호처럼 환경별로 달라야 하는 민감값만 담는다. 나머지 연결 정보(host/db/user 등)는 +# custom-values.yaml에 이미 있으므로 여기서 중복 기입하지 않는다. +# +# airflow-db-values.yaml의 bootstrap.initdb.secretName과 실제 값이 일치해야 한다 +# ($INFISICAL_SECRET 토큰은 dip-console이 같은 시크릿 값으로 렌더링하는 것을 가정). +data: + metadataConnection: + pass: "$INFISICAL_SECRET" diff --git a/manifests/applicationset/airflow/1.16.0/applicationset.yaml b/manifests/applicationset/airflow/1.16.0/applicationset.yaml new file mode 100644 index 0000000..83fef57 --- /dev/null +++ b/manifests/applicationset/airflow/1.16.0/applicationset.yaml @@ -0,0 +1,61 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: airflow + namespace: platform +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - list: + # syncWave 0: 전용 cnpg-cluster DB 인스턴스 먼저 배포 + # syncWave 1: airflow 본체 (postgresql.enabled: false, 위 DB의 -rw 서비스를 바라봄) + elements: + - name: airflow-db + chartPath: manifests/helm/cnpg-cluster/1.0.0 + baseValues: custom-values.yaml + valuesPath: airflow/airflow-db-values.yaml + syncWave: "0" + - name: airflow + chartPath: manifests/helm/airflow/1.16.0 + baseValues: custom-values.yaml + valuesPath: airflow/airflow-values.yaml + syncWave: "1" + template: + metadata: + name: "{{ .name }}" + namespace: platform + annotations: + argocd.argoproj.io/sync-wave: "{{ .syncWave }}" + spec: + project: default + sources: + # Source 1: 카탈로그 repo (Helm 차트) + - repoURL: https://gitea.example.org/dip/service-catalog + targetRevision: master + path: "{{ .chartPath }}" + helm: + releaseName: "{{ .name }}" + valueFiles: + - "{{ .baseValues }}" # 카탈로그 차트의 정적 베이스값(custom-values) + - $values/{{ .valuesPath }} # Source 2의 환경 오버레이 (나중 = 우선) + # Source 2: 값(values) repo — dip-console이 렌더한 환경별 값 + - repoURL: https://gitea.example.org/dip/tenant-catalog + targetRevision: master + ref: values + destination: + server: https://kubernetes.default.svc + namespace: airflow + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + retry: + limit: 5 + backoff: + duration: 10s + factor: 2 + maxDuration: 3m diff --git a/manifests/applicationset/flowise/6.0.0/README.md b/manifests/applicationset/flowise/6.0.0/README.md new file mode 100644 index 0000000..d470520 --- /dev/null +++ b/manifests/applicationset/flowise/6.0.0/README.md @@ -0,0 +1,37 @@ +# flowise ArgoCD ApplicationSet + +`flowise-db`(cnpg-cluster, syncWave 0) → `flowise`(syncWave 1) 2단계로 배포한다. +전체 스키마는 [manifests/applicationset/openmetadata/1.12.1/](../../openmetadata/1.12.1/)와 +[manifests/applicationset/victoria-metrics/1.0.0/](../../victoria-metrics/1.0.0/)를 따른다. + +## 구성 + +- `applicationset.yaml` — list generator로 Application 2개 생성. +- `flowise-db-values.yaml` — cnpg-cluster 환경 오버레이 템플릿(DB/owner, 시크릿 참조). +- `flowise-values.yaml` — flowise 환경 오버레이 템플릿(DB 비밀번호만). + +두 파일 모두 **템플릿**이다 — dip-console이 렌더링해 값 repo(`dip/tenant-catalog`)의 +`flowise/*.yaml`로 커밋하는 것을 가정한다. + +## 값 적용 순서 (2단) + +각 Application은 `baseValues`(카탈로그 차트의 `custom-values.yaml`) + `valuesPath`(값 repo의 +환경 오버레이, 나중 파일 우선)를 병합한다. + +## 사전 준비 (ArgoCD 밖) + +- `cloudnative-pg` operator(`manifests/helm/cloudnative-pg/0.29.0`)가 클러스터에 먼저 + 설치되어 있어야 한다. +- `flowise-db-values.yaml`의 `bootstrap.initdb.secretName`과 동일한 이름·값의 K8s Secret이 + 대상 네임스페이스에 미리 존재해야 한다(`username`/`password` 키). +- ArgoCD가 `Cluster.postgresql.cnpg.io` 커스텀 리소스의 상태를 "Healthy"로 판정하려면 + `argocd-cm`에 해당 CRD용 커스텀 헬스 체크(Lua)가 등록되어 있어야 sync-wave 1이 정상 대기한다. +- flowise는 내장 postgresql을 끄는 것(`postgresql.enabled: false`)만으로는 부족하고 + `externalPostgresql.enabled: true`를 함께 켜야 한다 — 다른 차트와 다른 점이다. + +## 배포 테스트 + +절차와 주의사항은 [.claude/deploy-test-procedure.md](../../../../.claude/deploy-test-procedure.md) +"카탈로그 앱 → cnpg-cluster 전환 배포 테스트" 절을 따른다. +`APP_POD_SELECTOR="app.kubernetes.io/component=flowise"`, fixture는 +`scripts/deploy-test/fixtures/flowise-deploy-test-overrides.yaml`. diff --git a/manifests/applicationset/flowise/6.0.0/applicationset.yaml b/manifests/applicationset/flowise/6.0.0/applicationset.yaml new file mode 100644 index 0000000..8d6cc1a --- /dev/null +++ b/manifests/applicationset/flowise/6.0.0/applicationset.yaml @@ -0,0 +1,59 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: flowise + namespace: platform +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - list: + # syncWave 0: 전용 cnpg-cluster DB 인스턴스 먼저 배포 + # syncWave 1: flowise 본체 (postgresql.enabled: false, 위 DB의 -rw 서비스를 바라봄) + elements: + - name: flowise-db + chartPath: manifests/helm/cnpg-cluster/1.0.0 + baseValues: custom-values.yaml + valuesPath: flowise/flowise-db-values.yaml + syncWave: "0" + - name: flowise + chartPath: manifests/helm/flowise/6.0.0 + baseValues: custom-values.yaml + valuesPath: flowise/flowise-values.yaml + syncWave: "1" + template: + metadata: + name: "{{ .name }}" + namespace: platform + annotations: + argocd.argoproj.io/sync-wave: "{{ .syncWave }}" + spec: + project: default + sources: + - repoURL: https://gitea.example.org/dip/service-catalog + targetRevision: master + path: "{{ .chartPath }}" + helm: + releaseName: "{{ .name }}" + valueFiles: + - "{{ .baseValues }}" + - $values/{{ .valuesPath }} + - repoURL: https://gitea.example.org/dip/tenant-catalog + targetRevision: master + ref: values + destination: + server: https://kubernetes.default.svc + namespace: flowise + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + retry: + limit: 5 + backoff: + duration: 10s + factor: 2 + maxDuration: 3m diff --git a/manifests/applicationset/flowise/6.0.0/flowise-db-values.yaml b/manifests/applicationset/flowise/6.0.0/flowise-db-values.yaml new file mode 100644 index 0000000..392aaee --- /dev/null +++ b/manifests/applicationset/flowise/6.0.0/flowise-db-values.yaml @@ -0,0 +1,24 @@ +# flowise 전용 cnpg-cluster 환경 오버레이 템플릿. +# dip-console이 렌더링해 값 repo(dip/tenant-catalog)의 flowise/flowise-db-values.yaml로 +# 커밋하는 것을 가정한 시작점이다. +instances: 1 + +bootstrap: + initdb: + database: flowise + owner: flowise + secretName: "$INFISICAL_SECRET" + +databases: + - name: flowise + owner: flowise + ensure: present + reclaimPolicy: retain + +resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 1000m + memory: 2Gi diff --git a/manifests/applicationset/flowise/6.0.0/flowise-values.yaml b/manifests/applicationset/flowise/6.0.0/flowise-values.yaml new file mode 100644 index 0000000..97d66e4 --- /dev/null +++ b/manifests/applicationset/flowise/6.0.0/flowise-values.yaml @@ -0,0 +1,8 @@ +# flowise 환경 오버레이 템플릿 — custom-values.yaml(baseValues) 위에 얹힌다. +# DB 비밀번호처럼 환경별로 달라야 하는 민감값만 담는다. 나머지 연결 정보(host/db/user 등)는 +# custom-values.yaml에 이미 있으므로 여기서 중복 기입하지 않는다. +# +# flowise-db-values.yaml의 bootstrap.initdb.secretName과 실제 값이 일치해야 한다 +# ($INFISICAL_SECRET 토큰은 dip-console이 같은 시크릿 값으로 렌더링하는 것을 가정). +externalPostgresql: + password: "$INFISICAL_SECRET" diff --git a/manifests/applicationset/lakekeeper/0.11.0/README.md b/manifests/applicationset/lakekeeper/0.11.0/README.md new file mode 100644 index 0000000..ef4da56 --- /dev/null +++ b/manifests/applicationset/lakekeeper/0.11.0/README.md @@ -0,0 +1,35 @@ +# lakekeeper ArgoCD ApplicationSet + +`lakekeeper-db`(cnpg-cluster, syncWave 0) → `lakekeeper`(syncWave 1) 2단계로 배포한다. +전체 스키마는 [manifests/applicationset/openmetadata/1.12.1/](../../openmetadata/1.12.1/)와 +[manifests/applicationset/victoria-metrics/1.0.0/](../../victoria-metrics/1.0.0/)를 따른다. + +## 구성 + +- `applicationset.yaml` — list generator로 Application 2개 생성. +- `lakekeeper-db-values.yaml` — cnpg-cluster 환경 오버레이 템플릿(DB/owner, 시크릿 참조). +- `lakekeeper-values.yaml` — lakekeeper 환경 오버레이 템플릿(DB 비밀번호만). + +두 파일 모두 **템플릿**이다 — dip-console이 렌더링해 값 repo(`dip/tenant-catalog`)의 +`lakekeeper/*.yaml`로 커밋하는 것을 가정한다. + +## 값 적용 순서 (2단) + +각 Application은 `baseValues`(카탈로그 차트의 `custom-values.yaml`) + `valuesPath`(값 repo의 +환경 오버레이, 나중 파일 우선)를 병합한다. + +## 사전 준비 (ArgoCD 밖) + +- `cloudnative-pg` operator(`manifests/helm/cloudnative-pg/0.29.0`)가 클러스터에 먼저 + 설치되어 있어야 한다. +- `lakekeeper-db-values.yaml`의 `bootstrap.initdb.secretName`과 동일한 이름·값의 K8s Secret이 + 대상 네임스페이스에 미리 존재해야 한다(`username`/`password` 키). +- ArgoCD가 `Cluster.postgresql.cnpg.io` 커스텀 리소스의 상태를 "Healthy"로 판정하려면 + `argocd-cm`에 해당 CRD용 커스텀 헬스 체크(Lua)가 등록되어 있어야 sync-wave 1이 정상 대기한다. + +## 배포 테스트 + +절차와 주의사항은 [.claude/deploy-test-procedure.md](../../../../.claude/deploy-test-procedure.md) +"카탈로그 앱 → cnpg-cluster 전환 배포 테스트" 절을 따른다. +`APP_POD_SELECTOR="app.kubernetes.io/component=catalog"`, fixture는 +`scripts/deploy-test/fixtures/lakekeeper-deploy-test-overrides.yaml`. diff --git a/manifests/applicationset/lakekeeper/0.11.0/applicationset.yaml b/manifests/applicationset/lakekeeper/0.11.0/applicationset.yaml new file mode 100644 index 0000000..8b623e9 --- /dev/null +++ b/manifests/applicationset/lakekeeper/0.11.0/applicationset.yaml @@ -0,0 +1,59 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: lakekeeper + namespace: platform +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - list: + # syncWave 0: 전용 cnpg-cluster DB 인스턴스 먼저 배포 + # syncWave 1: lakekeeper 본체 (postgresql.enabled: false, 위 DB의 -rw/-r 서비스를 바라봄) + elements: + - name: lakekeeper-db + chartPath: manifests/helm/cnpg-cluster/1.0.0 + baseValues: custom-values.yaml + valuesPath: lakekeeper/lakekeeper-db-values.yaml + syncWave: "0" + - name: lakekeeper + chartPath: manifests/helm/lakekeeper/0.11.0 + baseValues: custom-values.yaml + valuesPath: lakekeeper/lakekeeper-values.yaml + syncWave: "1" + template: + metadata: + name: "{{ .name }}" + namespace: platform + annotations: + argocd.argoproj.io/sync-wave: "{{ .syncWave }}" + spec: + project: default + sources: + - repoURL: https://gitea.example.org/dip/service-catalog + targetRevision: master + path: "{{ .chartPath }}" + helm: + releaseName: "{{ .name }}" + valueFiles: + - "{{ .baseValues }}" + - $values/{{ .valuesPath }} + - repoURL: https://gitea.example.org/dip/tenant-catalog + targetRevision: master + ref: values + destination: + server: https://kubernetes.default.svc + namespace: lakekeeper + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + retry: + limit: 5 + backoff: + duration: 10s + factor: 2 + maxDuration: 3m diff --git a/manifests/applicationset/lakekeeper/0.11.0/lakekeeper-db-values.yaml b/manifests/applicationset/lakekeeper/0.11.0/lakekeeper-db-values.yaml new file mode 100644 index 0000000..686c1ce --- /dev/null +++ b/manifests/applicationset/lakekeeper/0.11.0/lakekeeper-db-values.yaml @@ -0,0 +1,25 @@ +# lakekeeper 전용 cnpg-cluster 환경 오버레이 템플릿. +# dip-console이 렌더링해 값 repo(dip/tenant-catalog)의 lakekeeper/lakekeeper-db-values.yaml로 +# 커밋하는 것을 가정한 시작점이다. 배포 테스트로 검증된 값 그대로다 +# (지난 세션: manifests/helm/cnpg-cluster/1.0.0/custom-values-lakekeeper.yaml). +instances: 1 + +bootstrap: + initdb: + database: lakekeeper + owner: lakekeeper + secretName: "$INFISICAL_SECRET" + +databases: + - name: lakekeeper + owner: lakekeeper + ensure: present + reclaimPolicy: retain + +resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 1000m + memory: 2Gi diff --git a/manifests/applicationset/lakekeeper/0.11.0/lakekeeper-values.yaml b/manifests/applicationset/lakekeeper/0.11.0/lakekeeper-values.yaml new file mode 100644 index 0000000..ea0598c --- /dev/null +++ b/manifests/applicationset/lakekeeper/0.11.0/lakekeeper-values.yaml @@ -0,0 +1,8 @@ +# lakekeeper 환경 오버레이 템플릿 — custom-values.yaml(baseValues) 위에 얹힌다. +# DB 비밀번호처럼 환경별로 달라야 하는 민감값만 담는다. 나머지 연결 정보(host_read/host_write/ +# database/user 등)는 custom-values.yaml에 이미 있으므로 여기서 중복 기입하지 않는다. +# +# lakekeeper-db-values.yaml의 bootstrap.initdb.secretName과 실제 값이 일치해야 한다 +# ($INFISICAL_SECRET 토큰은 dip-console이 같은 시크릿 값으로 렌더링하는 것을 가정). +externalDatabase: + password: "$INFISICAL_SECRET" diff --git a/manifests/applicationset/mlflow/2.1.0/README.md b/manifests/applicationset/mlflow/2.1.0/README.md new file mode 100644 index 0000000..ff38b3b --- /dev/null +++ b/manifests/applicationset/mlflow/2.1.0/README.md @@ -0,0 +1,35 @@ +# mlflow ArgoCD ApplicationSet + +`mlflow-db`(cnpg-cluster, syncWave 0) → `mlflow`(syncWave 1) 2단계로 배포한다. +전체 스키마는 [manifests/applicationset/openmetadata/1.12.1/](../../openmetadata/1.12.1/)와 +[manifests/applicationset/victoria-metrics/1.0.0/](../../victoria-metrics/1.0.0/)를 따른다. + +## 구성 + +- `applicationset.yaml` — list generator로 Application 2개 생성. +- `mlflow-db-values.yaml` — cnpg-cluster 환경 오버레이 템플릿(DB/owner, 시크릿 참조). +- `mlflow-values.yaml` — mlflow 환경 오버레이 템플릿(DB 비밀번호만). + +두 파일 모두 **템플릿**이다 — dip-console이 렌더링해 값 repo(`dip/tenant-catalog`)의 +`mlflow/*.yaml`로 커밋하는 것을 가정한다. + +## 값 적용 순서 (2단) + +각 Application은 `baseValues`(카탈로그 차트의 `custom-values.yaml`) + `valuesPath`(값 repo의 +환경 오버레이, 나중 파일 우선)를 병합한다. + +## 사전 준비 (ArgoCD 밖) + +- `cloudnative-pg` operator(`manifests/helm/cloudnative-pg/0.29.0`)가 클러스터에 먼저 + 설치되어 있어야 한다. +- `mlflow-db-values.yaml`의 `bootstrap.initdb.secretName`과 동일한 이름·값의 K8s Secret이 + 대상 네임스페이스에 미리 존재해야 한다(`username`/`password` 키). +- ArgoCD가 `Cluster.postgresql.cnpg.io` 커스텀 리소스의 상태를 "Healthy"로 판정하려면 + `argocd-cm`에 해당 CRD용 커스텀 헬스 체크(Lua)가 등록되어 있어야 sync-wave 1이 정상 대기한다. + +## 배포 테스트 + +절차와 주의사항은 [.claude/deploy-test-procedure.md](../../../../.claude/deploy-test-procedure.md) +"카탈로그 앱 → cnpg-cluster 전환 배포 테스트" 절을 따른다. +`APP_POD_SELECTOR="app.kubernetes.io/component=tracking"`, fixture는 +`scripts/deploy-test/fixtures/mlflow-deploy-test-overrides.yaml`. diff --git a/manifests/applicationset/mlflow/2.1.0/applicationset.yaml b/manifests/applicationset/mlflow/2.1.0/applicationset.yaml new file mode 100644 index 0000000..8b6fbcb --- /dev/null +++ b/manifests/applicationset/mlflow/2.1.0/applicationset.yaml @@ -0,0 +1,59 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: mlflow + namespace: platform +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - list: + # syncWave 0: 전용 cnpg-cluster DB 인스턴스 먼저 배포 + # syncWave 1: mlflow 본체 (postgresql.enabled: false, 위 DB의 -rw 서비스를 바라봄) + elements: + - name: mlflow-db + chartPath: manifests/helm/cnpg-cluster/1.0.0 + baseValues: custom-values.yaml + valuesPath: mlflow/mlflow-db-values.yaml + syncWave: "0" + - name: mlflow + chartPath: manifests/helm/mlflow/2.1.0 + baseValues: custom-values.yaml + valuesPath: mlflow/mlflow-values.yaml + syncWave: "1" + template: + metadata: + name: "{{ .name }}" + namespace: platform + annotations: + argocd.argoproj.io/sync-wave: "{{ .syncWave }}" + spec: + project: default + sources: + - repoURL: https://gitea.example.org/dip/service-catalog + targetRevision: master + path: "{{ .chartPath }}" + helm: + releaseName: "{{ .name }}" + valueFiles: + - "{{ .baseValues }}" + - $values/{{ .valuesPath }} + - repoURL: https://gitea.example.org/dip/tenant-catalog + targetRevision: master + ref: values + destination: + server: https://kubernetes.default.svc + namespace: mlflow + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + retry: + limit: 5 + backoff: + duration: 10s + factor: 2 + maxDuration: 3m diff --git a/manifests/applicationset/mlflow/2.1.0/mlflow-db-values.yaml b/manifests/applicationset/mlflow/2.1.0/mlflow-db-values.yaml new file mode 100644 index 0000000..f731d32 --- /dev/null +++ b/manifests/applicationset/mlflow/2.1.0/mlflow-db-values.yaml @@ -0,0 +1,25 @@ +# mlflow 전용 cnpg-cluster 환경 오버레이 템플릿. +# dip-console이 렌더링해 값 repo(dip/tenant-catalog)의 mlflow/mlflow-db-values.yaml로 +# 커밋하는 것을 가정한 시작점이다. 배포 테스트로 검증된 값 그대로다 +# (지난 세션: manifests/helm/cnpg-cluster/1.0.0/custom-values-mlflow.yaml). +instances: 1 + +bootstrap: + initdb: + database: mlflow + owner: mlflow + secretName: "$INFISICAL_SECRET" + +databases: + - name: mlflow + owner: mlflow + ensure: present + reclaimPolicy: retain + +resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 1000m + memory: 2Gi diff --git a/manifests/applicationset/mlflow/2.1.0/mlflow-values.yaml b/manifests/applicationset/mlflow/2.1.0/mlflow-values.yaml new file mode 100644 index 0000000..34d0a8e --- /dev/null +++ b/manifests/applicationset/mlflow/2.1.0/mlflow-values.yaml @@ -0,0 +1,8 @@ +# mlflow 환경 오버레이 템플릿 — custom-values.yaml(baseValues) 위에 얹힌다. +# DB 비밀번호처럼 환경별로 달라야 하는 민감값만 담는다. 나머지 연결 정보(host/db/user 등)는 +# custom-values.yaml에 이미 있으므로 여기서 중복 기입하지 않는다. +# +# mlflow-db-values.yaml의 bootstrap.initdb.secretName과 실제 값이 일치해야 한다 +# ($INFISICAL_SECRET 토큰은 dip-console이 같은 시크릿 값으로 렌더링하는 것을 가정). +externalDatabase: + password: "$INFISICAL_SECRET" diff --git a/manifests/applicationset/superset/0.13.5/README.md b/manifests/applicationset/superset/0.13.5/README.md new file mode 100644 index 0000000..ce58d96 --- /dev/null +++ b/manifests/applicationset/superset/0.13.5/README.md @@ -0,0 +1,39 @@ +# superset ArgoCD ApplicationSet + +`superset-db`(cnpg-cluster, syncWave 0) → `superset`(syncWave 1) 2단계로 배포한다. +전체 스키마는 [manifests/applicationset/openmetadata/1.12.1/](../../openmetadata/1.12.1/)와 +[manifests/applicationset/victoria-metrics/1.0.0/](../../victoria-metrics/1.0.0/)를 따른다. + +## 구성 + +- `applicationset.yaml` — list generator로 Application 2개 생성. +- `superset-db-values.yaml` — cnpg-cluster 환경 오버레이 템플릿(DB/owner, 시크릿 참조). +- `superset-values.yaml` — superset 환경 오버레이 템플릿(DB 비밀번호만). + +두 파일 모두 **템플릿**이다 — dip-console이 렌더링해 값 repo(`dip/tenant-catalog`)의 +`superset/*.yaml`로 커밋하는 것을 가정한다. + +## 값 적용 순서 (2단) + +각 Application은 `baseValues`(카탈로그 차트의 `custom-values.yaml`) + `valuesPath`(값 repo의 +환경 오버레이, 나중 파일 우선)를 병합한다. + +## 사전 준비 (ArgoCD 밖) + +- `cloudnative-pg` operator(`manifests/helm/cloudnative-pg/0.29.0`)가 클러스터에 먼저 + 설치되어 있어야 한다. +- `superset-db-values.yaml`의 `bootstrap.initdb.secretName`과 동일한 이름·값의 K8s Secret이 + 대상 네임스페이스에 미리 존재해야 한다(`username`/`password` 키). +- ArgoCD가 `Cluster.postgresql.cnpg.io` 커스텀 리소스의 상태를 "Healthy"로 판정하려면 + `argocd-cm`에 해당 CRD용 커스텀 헬스 체크(Lua)가 등록되어 있어야 sync-wave 1이 정상 대기한다. +- `custom-values.yaml`은 Keycloak OIDC 연동(`AUTH_TYPE = AUTH_OAUTH`)이 기본 활성화되어 + 있고 `client_secret`/keycloak URL이 플레이스홀더다 — 실제 배포 전 반드시 실제 값으로 + 채워야 한다. 채우지 않으면 로그인 화면에 "Keycloak으로 로그인" 버튼만 뜨고 실제로는 + 로그인이 불가능하다(아이디/비번 입력폼 자체가 사라짐). + +## 배포 테스트 + +절차와 주의사항은 [.claude/deploy-test-procedure.md](../../../../.claude/deploy-test-procedure.md) +"카탈로그 앱 → cnpg-cluster 전환 배포 테스트" 절을 따른다. +`APP_POD_SELECTOR="app=superset"`, fixture는 +`scripts/deploy-test/fixtures/superset-deploy-test-overrides.yaml`. diff --git a/manifests/applicationset/superset/0.13.5/applicationset.yaml b/manifests/applicationset/superset/0.13.5/applicationset.yaml new file mode 100644 index 0000000..283c859 --- /dev/null +++ b/manifests/applicationset/superset/0.13.5/applicationset.yaml @@ -0,0 +1,59 @@ +apiVersion: argoproj.io/v1alpha1 +kind: ApplicationSet +metadata: + name: superset + namespace: platform +spec: + goTemplate: true + goTemplateOptions: ["missingkey=error"] + generators: + - list: + # syncWave 0: 전용 cnpg-cluster DB 인스턴스 먼저 배포 + # syncWave 1: superset 본체 (postgresql.enabled: false, 위 DB의 -rw 서비스를 바라봄) + elements: + - name: superset-db + chartPath: manifests/helm/cnpg-cluster/1.0.0 + baseValues: custom-values.yaml + valuesPath: superset/superset-db-values.yaml + syncWave: "0" + - name: superset + chartPath: manifests/helm/superset/0.13.5 + baseValues: custom-values.yaml + valuesPath: superset/superset-values.yaml + syncWave: "1" + template: + metadata: + name: "{{ .name }}" + namespace: platform + annotations: + argocd.argoproj.io/sync-wave: "{{ .syncWave }}" + spec: + project: default + sources: + - repoURL: https://gitea.example.org/dip/service-catalog + targetRevision: master + path: "{{ .chartPath }}" + helm: + releaseName: "{{ .name }}" + valueFiles: + - "{{ .baseValues }}" + - $values/{{ .valuesPath }} + - repoURL: https://gitea.example.org/dip/tenant-catalog + targetRevision: master + ref: values + destination: + server: https://kubernetes.default.svc + namespace: superset + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true + retry: + limit: 5 + backoff: + duration: 10s + factor: 2 + maxDuration: 3m diff --git a/manifests/applicationset/superset/0.13.5/superset-db-values.yaml b/manifests/applicationset/superset/0.13.5/superset-db-values.yaml new file mode 100644 index 0000000..b62c034 --- /dev/null +++ b/manifests/applicationset/superset/0.13.5/superset-db-values.yaml @@ -0,0 +1,25 @@ +# superset 전용 cnpg-cluster 환경 오버레이 템플릿. +# dip-console이 렌더링해 값 repo(dip/tenant-catalog)의 superset/superset-db-values.yaml로 +# 커밋하는 것을 가정한 시작점이다. 배포 테스트로 검증된 값 그대로다 +# (지난 세션: manifests/helm/cnpg-cluster/1.0.0/custom-values-superset.yaml). +instances: 1 + +bootstrap: + initdb: + database: superset + owner: superset + secretName: "$INFISICAL_SECRET" + +databases: + - name: superset + owner: superset + ensure: present + reclaimPolicy: retain + +resources: + requests: + cpu: 500m + memory: 1Gi + limits: + cpu: 1000m + memory: 2Gi diff --git a/manifests/applicationset/superset/0.13.5/superset-values.yaml b/manifests/applicationset/superset/0.13.5/superset-values.yaml new file mode 100644 index 0000000..bddc770 --- /dev/null +++ b/manifests/applicationset/superset/0.13.5/superset-values.yaml @@ -0,0 +1,9 @@ +# superset 환경 오버레이 템플릿 — custom-values.yaml(baseValues) 위에 얹힌다. +# DB 비밀번호처럼 환경별로 달라야 하는 민감값만 담는다. 나머지 연결 정보(host/db/user 등)는 +# custom-values.yaml에 이미 있으므로 여기서 중복 기입하지 않는다. +# +# superset-db-values.yaml의 bootstrap.initdb.secretName과 실제 값이 일치해야 한다 +# ($INFISICAL_SECRET 토큰은 dip-console이 같은 시크릿 값으로 렌더링하는 것을 가정). +supersetNode: + connections: + db_pass: "$INFISICAL_SECRET" diff --git a/manifests/helm/airflow/1.16.0/CUSTOM-README.md b/manifests/helm/airflow/1.16.0/CUSTOM-README.md index 108eddc..2fe03a2 100644 --- a/manifests/helm/airflow/1.16.0/CUSTOM-README.md +++ b/manifests/helm/airflow/1.16.0/CUSTOM-README.md @@ -122,17 +122,40 @@ logs: ### 4) Postgresql 설정 -- 내장 postgresql을 사용하여 배포할 수 있다. +- 내장 bitnami postgresql 서브차트 대신, `cnpg-cluster`(전용 인스턴스)를 외부 DB로 사용한다. +- **사전 조건**: `cloudnative-pg` operator(`manifests/helm/cloudnative-pg/0.29.0`)가 클러스터에 + 먼저 설치되어 있어야 한다. +- **배포 순서** + ``` sh + # 1) airflow용 DB 계정 시크릿 생성 (아래 password는 metadataConnection.pass와 동일해야 한다) + kubectl create secret generic airflow-db-app-secret \ + --from-literal=username=airflow --from-literal=password=airflow -n airflow + + # 2) 전용 cnpg-cluster 배포 + helm upgrade airflow-db manifests/helm/cnpg-cluster/1.0.0 \ + -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ + -f manifests/applicationset/airflow/1.16.0/airflow-db-values.yaml \ + --install -n airflow + + # 3) airflow 배포 (기존 배포 방법과 동일) + helm upgrade airflow ./ -f custom-values.yaml --install -n airflow --create-namespace + ``` +- ArgoCD로 배포한다면 `manifests/applicationset/airflow/1.16.0/`의 ApplicationSet을 쓴다 — + `airflow-db`(syncWave 0) → `airflow`(syncWave 1) 순서로 자동 배포된다. 자세한 내용은 + 해당 디렉토리의 `README.md` 참고. - custom-values.yaml ``` yaml postgresql: - enabled: true - auth: - enablePostgresUser: true - postgresPassword: postgres # postgres 유저의 패스워드 설정 - primary: - persistence: - enabled: true - size: 8Gi # airflow에서 사용하는 DB의 볼륨 사이즈 설정 - storageClass: "" # DB 볼륨 배포를 위한 storage class 설정 - ``` \ No newline at end of file + enabled: false + + data: + metadataConnection: + user: airflow + pass: airflow # 위 시크릿 생성 시 사용한 password와 동일해야 한다 + protocol: postgresql + host: airflow-db-rw # cnpg-cluster 릴리스명이 airflow-db일 때의 rw 서비스명 + port: 5432 + db: airflow + sslmode: disable + ``` +- 참고: `manifests/applicationset/airflow/1.16.0/airflow-db-values.yaml` \ No newline at end of file diff --git a/manifests/helm/airflow/1.16.0/custom-values.yaml b/manifests/helm/airflow/1.16.0/custom-values.yaml index 0210743..565ba60 100644 --- a/manifests/helm/airflow/1.16.0/custom-values.yaml +++ b/manifests/helm/airflow/1.16.0/custom-values.yaml @@ -4,11 +4,17 @@ ingress: web: enabled: true + # Kong → APISIX 마이그레이션(issue #139, mlflow/lakekeeper와 동일). 이 클러스터에는 + # kong ingress controller가 없어 `kubernetes.io/ingress.class: kong` 애노테이션 방식은 + # 어떤 컨트롤러도 인식하지 않아 접근 자체가 불가능했다(실측 — CLASS: ). + ingressClassName: "apisix" + # 기본 path "/" 는 정확히 "/" 만 매치되어(exact) airflow가 "/home" 등으로 리다이렉트한 + # 뒤에는 전부 404가 난다(실측). lakekeeper와 동일하게 regex 전체 매치로 바꾼다. + path: "/.*" annotations: - cert-manager.io/issuer: "root-ca-issuer" - kubernetes.io/ingress.class: kong - konghq.com/protocols: https - konghq.com/https-redirect-status-code: "301" + cert-manager.io/cluster-issuer: "root-ca-issuer" + k8s.apisix.apache.org/ssl-redirect: "true" + k8s.apisix.apache.org/use-regex: "true" hosts: - name: "airflow.example.org" tls: @@ -81,13 +87,19 @@ logs: statsd: enabled: false +# 내장 bitnami postgresql 서브차트 대신 cnpg-cluster(전용 인스턴스)를 외부 DB로 사용한다. +# 사전 배포: helm install airflow-db manifests/helm/cnpg-cluster/1.0.0 \ +# -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ +# -f manifests/applicationset/airflow/1.16.0/airflow-db-values.yaml -n postgresql: - enabled: true - auth: - enablePostgresUser: true - postgresPassword: postgres - primary: - persistence: - enabled: true - size: 8Gi # airflow에서 사용하는 DB의 볼륨 사이즈 설정 - storageClass: "" \ No newline at end of file + enabled: false + +data: + metadataConnection: + user: airflow + pass: airflow + protocol: postgresql + host: airflow-db-rw + port: 5432 + db: airflow + sslmode: disable \ No newline at end of file diff --git a/manifests/helm/flowise/6.0.0/custom-values.yaml b/manifests/helm/flowise/6.0.0/custom-values.yaml index eec3461..c673748 100644 --- a/manifests/helm/flowise/6.0.0/custom-values.yaml +++ b/manifests/helm/flowise/6.0.0/custom-values.yaml @@ -34,13 +34,21 @@ ingress: - flowise.example.org secretName: flowise-tls-secret +# 내장 bitnami postgresql 서브차트 대신 cnpg-cluster(전용 인스턴스)를 외부 DB로 사용한다. +# 사전 배포: helm install flowise-db manifests/helm/cnpg-cluster/1.0.0 \ +# -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ +# -f manifests/applicationset/flowise/6.0.0/flowise-db-values.yaml -n postgresql: + enabled: false + +externalPostgresql: enabled: true - primary: - persistence: - enabled: true - size: 8Gi - + host: flowise-db-rw # cnpg-cluster 릴리스명이 flowise-db일 때의 rw 서비스명 + port: 5432 + username: flowise + password: flowise # cnpg-cluster 부트스트랩 시크릿의 password와 동일해야 한다 + database: flowise + redis: enabled: true diff --git a/manifests/helm/lakekeeper/0.11.0/CUSTOM-README.md b/manifests/helm/lakekeeper/0.11.0/CUSTOM-README.md index 969e1d3..3795dc0 100644 --- a/manifests/helm/lakekeeper/0.11.0/CUSTOM-README.md +++ b/manifests/helm/lakekeeper/0.11.0/CUSTOM-README.md @@ -68,12 +68,20 @@ catalog: secret: secretName: root-ca-secret -# lakekeeper DB +# lakekeeper DB — 내장 postgres 서브차트 대신 cnpg-cluster(전용 인스턴스)를 외부 DB로 사용한다. postgresql: - storage: - className: - requestedSize: 5Gi - resources: {} + enabled: false + +externalDatabase: + type: postgres + # -ro는 replica 전용이라 instances:1(권장 기준, small 티어)에서는 엔드포인트가 없다 — + # -r(전체 인스턴스 라운드로빈, primary 포함)을 대신 쓴다. 배포 테스트로 실측된 문제다. + host_read: "lakekeeper-db-r" + host_write: "lakekeeper-db-rw" + port: 5432 + database: lakekeeper + user: lakekeeper + password: lakekeeper # OIDC 인증 auth: @@ -162,11 +170,35 @@ extraVolumes: ### 4) PostgreSQL 데이터베이스 설정 +- 내장 postgres 서브차트(`groundhog2k/postgres`) 대신 `cnpg-cluster`(전용 인스턴스)를 외부 DB로 + 사용한다. **사전 조건**: `cloudnative-pg` operator가 클러스터에 먼저 설치되어 있어야 한다. +- **배포 순서** + ``` sh + kubectl create secret generic lakekeeper-db-app-secret \ + --from-literal=username=lakekeeper --from-literal=password=lakekeeper -n lakekeeper + + helm upgrade lakekeeper-db manifests/helm/cnpg-cluster/1.0.0 \ + -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ + -f manifests/applicationset/lakekeeper/0.11.0/lakekeeper-db-values.yaml \ + --install -n lakekeeper + + helm upgrade lakekeeper ./ -f custom-values.yaml --install -n lakekeeper --create-namespace + ``` +- ArgoCD로 배포한다면 `manifests/applicationset/lakekeeper/0.11.0/`의 ApplicationSet을 쓴다 — + `lakekeeper-db`(syncWave 0) → `lakekeeper`(syncWave 1) 순서로 자동 배포된다. 자세한 내용은 + 해당 디렉토리의 `README.md` 참고. + | Name | 설명 | 기본값 | | ------------------------- | ------------------------------------------------------------ | ----------- | -| `postgresql.storage.className` | 스토리지 클래스 | `""` | -| `postgresql.storage.requestedSize` | PostgreSQL 스토리지 크기 | `5Gi` | -| `postgresql.resources` | PostgreSQL 리소스 설정 | `{}` | +| `postgresql.enabled` | 내장 postgres 서브차트 사용 여부. cnpg-cluster 사용 시 `false` | `false` | +| `externalDatabase.host_write` | cnpg-cluster의 rw 서비스명(`-rw`) | `lakekeeper-db-rw` | +| `externalDatabase.host_read` | cnpg-cluster의 ro 서비스명(`-ro`) | `lakekeeper-db-ro` | +| `externalDatabase.database`, `.user`, `.password` | DB 계정 정보. cnpg-cluster 부트스트랩 시크릿과 동일해야 한다 | 참고 예시 | + +> 참고: `manifests/applicationset/lakekeeper/0.11.0/lakekeeper-db-values.yaml` +> +> OpenFGA 인증/권한 스토어(`internalOpenFGA: true`)는 여전히 자체 bitnami postgresql +> 서브차트(`openfga.postgresql`)를 내장 사용한다 — 이번 전환 범위에는 포함되지 않았다. ### 5) OIDC 인증 설정 diff --git a/manifests/helm/lakekeeper/0.11.0/custom-values.yaml b/manifests/helm/lakekeeper/0.11.0/custom-values.yaml index 42bd606..3fe1214 100644 --- a/manifests/helm/lakekeeper/0.11.0/custom-values.yaml +++ b/manifests/helm/lakekeeper/0.11.0/custom-values.yaml @@ -29,13 +29,27 @@ catalog: secret: secretName: root-ca-secret -# lakekeeper DB +# lakekeeper DB — 내장 postgres 서브차트(groundhog2k/postgres) 대신 +# cnpg-cluster(전용 인스턴스)를 외부 DB로 사용한다. +# 사전 배포: helm install lakekeeper-db manifests/helm/cnpg-cluster/1.0.0 \ +# -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ +# -f manifests/applicationset/lakekeeper/0.11.0/lakekeeper-db-values.yaml -n postgresql: - enabled: true - storage: - className: - requestedSize: 5Gi - resources: {} + enabled: false + +externalDatabase: + type: postgres + # cnpg-cluster의 -ro 서비스는 replica 파드에만 연결되는데, 권장 배포 기준인 + # instances: 1(small 티어)에서는 replica가 없어 -ro가 엔드포인트 0개인 서비스가 + # 된다 — lakekeeper의 read-connection이 영구히 재시도만 하며 멈춘다(배포 테스트로 실측). + # -r은 인스턴스 수와 무관하게 항상 primary를 포함해 라운드로빈하므로 이 값을 쓴다. + # instances를 3 이상으로 올려 실제 replica를 운용하는 경우에도 -r로 충분하다. + host_read: "lakekeeper-db-r" + host_write: "lakekeeper-db-rw" + port: 5432 + database: lakekeeper + user: lakekeeper + password: lakekeeper # OIDC 인증 diff --git a/manifests/helm/mlflow/2.1.0/CUSTOM-README.md b/manifests/helm/mlflow/2.1.0/CUSTOM-README.md new file mode 100644 index 0000000..ba2cc54 --- /dev/null +++ b/manifests/helm/mlflow/2.1.0/CUSTOM-README.md @@ -0,0 +1,85 @@ +# MLflow 배포 + +bitnami/mlflow 차트(2.1.0) 기반 배포 가이드다. + +## 1. 배포 방법 + +### 1) 사전 조건 + +- 내장 bitnami postgresql 서브차트 대신 `cnpg-cluster`(전용 인스턴스)를 외부 DB로 사용한다. + `cloudnative-pg` operator(`manifests/helm/cloudnative-pg/0.29.0`)가 클러스터에 먼저 설치되어 + 있어야 한다. + +### 2) 배포 순서 + +```sh +git clone https://github.com/paasup/dip-catalog.git +cd dip-catalog + +# 1) mlflow용 DB 계정 시크릿 생성 (password는 externalDatabase.password와 동일해야 한다) +kubectl create secret generic mlflow-db-app-secret \ + --from-literal=username=mlflow --from-literal=password=mlflow1234 -n mlflow + +# 2) 전용 cnpg-cluster 배포 +helm upgrade mlflow-db manifests/helm/cnpg-cluster/1.0.0 \ + -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ + -f manifests/applicationset/mlflow/2.1.0/mlflow-db-values.yaml \ + --install -n mlflow --create-namespace + +# 3) mlflow 배포 +helm upgrade mlflow manifests/helm/mlflow/2.1.0 -f custom-values.yaml --install -n mlflow +``` + +ArgoCD로 배포한다면 `manifests/applicationset/mlflow/2.1.0/`의 ApplicationSet을 쓴다 — +`mlflow-db`(syncWave 0) → `mlflow`(syncWave 1) 순서로 자동 배포된다. 자세한 내용은 해당 +디렉토리의 `README.md` 참고. + +## 2. custom-values.yaml 설명 + +### 1) 이미지 설정 + +private 환경 배포 시 registry를 변경한다. + +```yaml +image: + repository: bitnamilegacy/mlflow +gitImage: + repository: bitnamilegacy/git +waitContainer: + image: + repository: bitnamilegacy/os-shell +``` + +### 2) PostgreSQL 설정 + +내장 postgresql 서브차트를 끄고 `externalDatabase`로 cnpg-cluster를 가리킨다. + +```yaml +postgresql: + enabled: false + +externalDatabase: + dialectDriver: "postgresql" + host: "mlflow-db-rw" # cnpg-cluster 릴리스명이 mlflow-db일 때의 rw 서비스명 + port: 5432 + user: mlflow + password: mlflow1234 # 위 시크릿 생성 시 사용한 password와 동일해야 한다 + database: mlflow +``` + +참고: `manifests/applicationset/mlflow/2.1.0/mlflow-db-values.yaml` + +### 3) MinIO 설정 + +아티팩트 저장소로 내장 minio 서브차트를 사용한다(`minio.enabled: true`). private 환경에서는 +`minio.image.repository`를 내부 registry로 변경한다. + +```yaml +minio: + enabled: true + auth: + rootUser: admin + rootPassword: admin1234 + image: + repository: bitnamilegacy/minio +``` diff --git a/manifests/helm/mlflow/2.1.0/custom-values.yaml b/manifests/helm/mlflow/2.1.0/custom-values.yaml index b1d8450..fe99456 100644 --- a/manifests/helm/mlflow/2.1.0/custom-values.yaml +++ b/manifests/helm/mlflow/2.1.0/custom-values.yaml @@ -45,20 +45,20 @@ run: cpu: 250m memory: 512Mi +# 내장 bitnami postgresql 서브차트 대신 cnpg-cluster(전용 인스턴스)를 외부 DB로 사용한다. +# 사전 배포: helm install mlflow-db manifests/helm/cnpg-cluster/1.0.0 \ +# -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ +# -f manifests/applicationset/mlflow/2.1.0/mlflow-db-values.yaml -n postgresql: - auth: - username: mlflow - password: mlflow1234 - image: - repository: bitnamilegacy/postgresql - primary: - reousrces: - limits: - cpu: 500m - memory: 1Gi - requests: - cpu: 250m - memory: 512Mi + enabled: false + +externalDatabase: + dialectDriver: "postgresql" + host: "mlflow-db-rw" + port: 5432 + user: mlflow + password: mlflow1234 + database: mlflow minio: enabled: true auth: diff --git a/manifests/helm/superset/0.13.5/CUSTOM-README.md b/manifests/helm/superset/0.13.5/CUSTOM-README.md index 4ef354b..58d1611 100644 --- a/manifests/helm/superset/0.13.5/CUSTOM-README.md +++ b/manifests/helm/superset/0.13.5/CUSTOM-README.md @@ -247,34 +247,38 @@ bootstrapScript: | ### 6) postgresql -- subchart로 포함된 postgresql을 배포하기 위한 설정이다. +- 내장 bitnami postgresql 서브차트 대신 `cnpg-cluster`(전용 인스턴스)를 외부 DB로 사용한다. +- **사전 조건**: `cloudnative-pg` operator가 클러스터에 먼저 설치되어 있어야 한다. +- **배포 순서** + ``` sh + kubectl create secret generic superset-db-app-secret \ + --from-literal=username=superset --from-literal=password=superset -n superset -``` yaml -postgresql: - enabled: true - auth: - # DB의 계정 및 기본 생성되는 database를 설정한다. - username: superset - password: superset - database: superset - image: - # private 환경에서 배포할 때 사용할 registry로 설정을 변경한다. - registry: docker.io - primary: - # DB의 리소스를 설정한다. - resources: - limits: {} - requests: - memory: 256Mi - cpu: 250m - # DB에서 사용할 Disk 설정으로 StorageClass와 사이즈를 설정한다. - persistence: - enabled: true - storageClass: "" - accessModes: - - ReadWriteOnce - size: 8Gi -``` + helm upgrade superset-db manifests/helm/cnpg-cluster/1.0.0 \ + -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ + -f manifests/applicationset/superset/0.13.5/superset-db-values.yaml \ + --install -n superset + + helm upgrade superset ../superset --install -n superset --create-namespace \ + -f ./custom-values.yaml + ``` +- ArgoCD로 배포한다면 `manifests/applicationset/superset/0.13.5/`의 ApplicationSet을 쓴다 — + `superset-db`(syncWave 0) → `superset`(syncWave 1) 순서로 자동 배포된다. 자세한 내용은 + 해당 디렉토리의 `README.md` 참고. +- custom-values.yaml + ``` yaml + postgresql: + enabled: false + + supersetNode: + connections: + db_host: 'superset-db-rw' # cnpg-cluster 릴리스명이 superset-db일 때의 rw 서비스명 + db_port: "5432" + db_user: superset + db_pass: superset # 위 시크릿 생성 시 사용한 password와 동일해야 한다 + db_name: superset + ``` +- 참고: `manifests/applicationset/superset/0.13.5/superset-db-values.yaml` diff --git a/manifests/helm/superset/0.13.5/custom-values.yaml b/manifests/helm/superset/0.13.5/custom-values.yaml index 46dcd36..ce050f7 100644 --- a/manifests/helm/superset/0.13.5/custom-values.yaml +++ b/manifests/helm/superset/0.13.5/custom-values.yaml @@ -99,11 +99,17 @@ tolerations: [] ingress: enabled: true - annotations: - cert-manager.io/cluster-issuer: "selfsigned-issuer" - cert-manager.io/duration: 8760h + # airflow와 동일하게 실측된 문제 — ingressClassName 미지정 시 이 클러스터의 apisix가 + # 아예 인식하지 않아 CLASS: 으로 뜨고 접근이 불가능하다. path도 exact "/" 만 + # 매치되면 superset의 내부 리다이렉트(예: /login/) 이후 전부 404가 나 regex로 바꾼다. + ingressClassName: "apisix" + annotations: + cert-manager.io/cluster-issuer: "selfsigned-issuer" + cert-manager.io/duration: 8760h cert-manager.io/renew-before: 720h - path: / + k8s.apisix.apache.org/use-regex: "true" + k8s.apisix.apache.org/ssl-redirect: "true" + path: /.* pathType: ImplementationSpecific hosts: - superset.example.org @@ -133,8 +139,11 @@ supersetNode: redis_ssl: enabled: false ssl_cert_reqs: CERT_NONE - # You need to change below configuration incase bringing own PostgresSQL instance and also set postgresql.enabled:false - db_host: '{{ .Release.Name }}-postgresql' + # 내장 bitnami postgresql 서브차트 대신 cnpg-cluster(전용 인스턴스)를 외부 DB로 사용한다. + # 사전 배포: helm install superset-db manifests/helm/cnpg-cluster/1.0.0 \ + # -f manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml \ + # -f manifests/applicationset/superset/0.13.5/superset-db-values.yaml -n + db_host: 'superset-db-rw' db_port: "5432" db_user: superset db_pass: superset @@ -160,23 +169,7 @@ supersetCeleryFlower: resources: {} postgresql: - enabled: true - auth: - username: superset - password: superset - database: superset - image: - registry: docker.io - primary: - resources: - limits: {} - requests: - memory: 256Mi - cpu: 250m - persistence: - enabled: true - storageClass: "" - size: 8Gi + enabled: false redis: enabled: true diff --git a/scripts/deploy-test/deploy-test-app-with-cnpg.sh b/scripts/deploy-test/deploy-test-app-with-cnpg.sh new file mode 100755 index 0000000..327f618 --- /dev/null +++ b/scripts/deploy-test/deploy-test-app-with-cnpg.sh @@ -0,0 +1,291 @@ +#!/usr/bin/env bash +# ============================================================================= +# deploy-test-app-with-cnpg.sh +# 내장 bitnami postgresql 서브차트를 끄고 전용 cnpg-cluster 인스턴스를 외부 DB로 +# 쓰도록 바꾼 카탈로그 앱(airflow/lakekeeper/mlflow/superset)이 실제로 그 DB에 +# 연결해 기동하는지 격리된 테스트 네임스페이스에서 확인한다. +# +# scripts/deploy-test/deploy-test-cnpg-cluster.sh 와 같은 원칙을 따른다 — +# operator(cloudnative-pg)는 상시 컴포넌트로 재사용하고, 앱/DB 릴리스와 테스트 +# 네임스페이스는 매 실행 재생성한다. 정리는 성공/실패와 무관하게 항상 수행한다(trap). +# +# 이 스크립트가 확인하는 것은 "DB 연결이 되는가"이지 앱의 전체 기능 검증이 아니다. +# ingress/cert-manager/OIDC(keycloak) 등 DB와 무관한 의존성 때문에 앱 차트의 +# `helm install --wait`가 타임아웃할 수 있어, 앱 설치는 --wait 없이 진행하고 +# 파드 상태 + 로그의 DB 연결 실패 패턴 부재로 판단한다. +# +# 사용: +# APP_CHART_DIR=manifests/helm/airflow/1.16.0 \ +# APP_RELEASE=airflow \ +# DB_CUSTOM_VALUES=manifests/applicationset/airflow/1.16.0/airflow-db-values.yaml \ +# DB_SECRET_USER=airflow DB_SECRET_PASSWORD=airflow \ +# TEST_NAMESPACE=airflow-test-build \ +# bash scripts/deploy-test/deploy-test-app-with-cnpg.sh /tmp/deploy-test-out +# +# 환경변수: +# APP_CHART_DIR (필수) 앱 차트 경로 (예: manifests/helm/airflow/1.16.0) +# APP_RELEASE (필수) 앱 helm 릴리스명 +# APP_CUSTOM_VALUES 기본 "$APP_CHART_DIR/custom-values.yaml" +# DB_CUSTOM_VALUES (필수) cnpg-cluster용 앱 전용 오버레이 — 각 앱의 +# manifests/applicationset///-db-values.yaml +# (ArgoCD ApplicationSet의 valuesPath와 동일한 파일) +# DB_RELEASE 기본 "${APP_RELEASE}-db" +# DB_SECRET_NAME 기본 "${DB_RELEASE}-app-secret" (각 앱 -db-values.yaml의 +# bootstrap.initdb.secretName과 반드시 일치해야 한다) +# DB_SECRET_USER (필수) DB owner 계정명 +# DB_SECRET_PASSWORD (필수) DB owner 비밀번호 — 앱 custom-values.yaml의 평문 값과 동일해야 함 +# OPERATOR_CHART_DIR 기본 manifests/helm/cloudnative-pg/0.29.0 +# OPERATOR_NAMESPACE 기본 cnpg-system (상시 — 이 스크립트가 지우지 않는다) +# OPERATOR_RELEASE 기본 cnpg +# TEST_NAMESPACE (필수) 격리된 테스트 네임스페이스 (매 실행 재생성) +# DB_WAIT_TIMEOUT 기본 300 (초) — Cluster healthy 대기 한도 +# APP_WAIT_SECONDS 기본 180 (초) — 앱 파드 상태 관찰 시간 +# APP_EXTRA_VALUES 선택. custom-values.yaml 위에 추가로 얹을 배포-테스트 전용 values 파일 +# (예: 외부 keycloak 등 이 테스트 환경에 없는 의존성을 끄는 용도). +# 카탈로그의 실제 권장 설정(custom-values.yaml)은 건드리지 않는다 — +# scripts/deploy-test/fixtures/ 에 보관한다. +# APP_POD_SELECTOR (강력 권장) DB에 실제로 연결하는 파드를 가리키는 label selector +# (예: airflow "component=webserver", mlflow +# "app.kubernetes.io/component=tracking", superset "app=superset", +# lakekeeper "app.kubernetes.io/component=catalog"). 지정하면 이 +# selector 에 매치되는 파드가 Running+Ready 가 될 때까지만 기다린다 — +# git-sync/ingress 등 DB 와 무관한 다른 파드의 상태는 무시한다. +# 미지정 시 네임스페이스의 아무 파드나 1개 Running 이면 통과로 간주하는 +# 느슨한 판정으로 대체된다(권장하지 않음 — 실제 DB 소비 파드를 못 볼 수 있다). +# +# 산출물 (OUT_DIR): deploy-test.log +# 종료 코드: 0 = DB 연결 성공(파드 Running + 로그에 DB 연결 실패 패턴 없음), 그 외 = 실패. +# 정리는 종료 코드와 무관하게 항상 수행된다. +# ============================================================================= +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +OUT_DIR="${1:?사용법: deploy-test-app-with-cnpg.sh }" +mkdir -p "$OUT_DIR" +LOG="$OUT_DIR/deploy-test.log" +: > "$LOG" + +APP_CHART_DIR="${APP_CHART_DIR:?APP_CHART_DIR 환경변수가 필요하다 (앱 차트 경로)}" +APP_RELEASE="${APP_RELEASE:?APP_RELEASE 환경변수가 필요하다}" +APP_CUSTOM_VALUES="${APP_CUSTOM_VALUES:-$APP_CHART_DIR/custom-values.yaml}" +DB_CUSTOM_VALUES="${DB_CUSTOM_VALUES:?DB_CUSTOM_VALUES 환경변수가 필요하다 (cnpg-cluster 앱 전용 오버레이)}" +DB_RELEASE="${DB_RELEASE:-${APP_RELEASE}-db}" +DB_SECRET_NAME="${DB_SECRET_NAME:-${DB_RELEASE}-app-secret}" +DB_SECRET_USER="${DB_SECRET_USER:?DB_SECRET_USER 환경변수가 필요하다}" +DB_SECRET_PASSWORD="${DB_SECRET_PASSWORD:?DB_SECRET_PASSWORD 환경변수가 필요하다}" +OPERATOR_CHART_DIR="${OPERATOR_CHART_DIR:-$REPO_ROOT/manifests/helm/cloudnative-pg/0.29.0}" +OPERATOR_NAMESPACE="${OPERATOR_NAMESPACE:-cnpg-system}" +OPERATOR_RELEASE="${OPERATOR_RELEASE:-cnpg}" +TEST_NAMESPACE="${TEST_NAMESPACE:?TEST_NAMESPACE 환경변수가 필요하다 (격리된 테스트 네임스페이스)}" +DB_WAIT_TIMEOUT="${DB_WAIT_TIMEOUT:-300}" +# 이미지가 큰 앱(flowise 등)은 최초 pull 에만 5분 넘게 걸려 300s 로는 ImagePullBackOff 상태에서 +# 타임아웃한다(실측). 캐시된 뒤에는 1분 내로 끝나므로 넉넉히 잡아도 손해가 없다. +APP_WAIT_SECONDS="${APP_WAIT_SECONDS:-600}" +APP_EXTRA_VALUES="${APP_EXTRA_VALUES:-}" +APP_POD_SELECTOR="${APP_POD_SELECTOR:-}" +CNPG_CLUSTER_CHART_DIR="${CNPG_CLUSTER_CHART_DIR:-$REPO_ROOT/manifests/helm/cnpg-cluster/1.0.0}" + +log() { echo "$(date -u +%H:%M:%S) $*" | tee -a "$LOG"; } + +log "== 사전 확인 ==" +for t in kubectl helm python3; do + command -v "$t" >/dev/null || { log "::error:: $t 없음"; exit 2; } +done +[ -d "$OPERATOR_CHART_DIR" ] || { log "::error:: operator 차트 없음: $OPERATOR_CHART_DIR"; exit 2; } +[ -d "$APP_CHART_DIR" ] || { log "::error:: 앱 차트 없음: $APP_CHART_DIR"; exit 2; } +[ -f "$APP_CUSTOM_VALUES" ] || { log "::error:: 앱 custom-values 없음: $APP_CUSTOM_VALUES"; exit 2; } +[ -f "$DB_CUSTOM_VALUES" ] || { log "::error:: DB custom-values 없음: $DB_CUSTOM_VALUES"; exit 2; } +if ! kubectl get ns >/dev/null 2>>"$LOG"; then + log "::error:: 클러스터 연결 실패 — kubeconfig/네트워크 확인" + exit 2 +fi +log " OK (kubectl context: $(kubectl config current-context 2>/dev/null))" + +# --- operator: 상시 컴포넌트. 없으면 1회 설치, 있으면 재사용 -------------------- +ensure_operator() { + if helm status "$OPERATOR_RELEASE" -n "$OPERATOR_NAMESPACE" >/dev/null 2>>"$LOG"; then + log "== operator: 기존 릴리스 재사용 ($OPERATOR_RELEASE/$OPERATOR_NAMESPACE) ==" + return 0 + fi + if kubectl get validatingwebhookconfiguration cnpg-validating-webhook-configuration >/dev/null 2>&1; then + owner_ns="$(kubectl get validatingwebhookconfiguration cnpg-validating-webhook-configuration \ + -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-namespace}' 2>/dev/null)" + owner_rel="$(kubectl get validatingwebhookconfiguration cnpg-validating-webhook-configuration \ + -o jsonpath='{.metadata.annotations.meta\.helm\.sh/release-name}' 2>/dev/null)" + if [ "$owner_ns" != "$OPERATOR_NAMESPACE" ] || [ "$owner_rel" != "$OPERATOR_RELEASE" ]; then + log "::error:: 다른 CNPG operator 가 이미 클러스터에 있다 (release=$owner_rel ns=$owner_ns)." + exit 3 + fi + fi + log "== operator: 신규 설치 ($OPERATOR_RELEASE/$OPERATOR_NAMESPACE, 차트 기본값) ==" + helm install "$OPERATOR_RELEASE" "$OPERATOR_CHART_DIR" \ + -n "$OPERATOR_NAMESPACE" --create-namespace --wait --timeout 600s \ + >>"$LOG" 2>&1 || { log "::error:: operator 설치 실패 — $LOG 확인"; exit 1; } + log " OK" +} + +# --- 테스트 네임스페이스 정리 (선-정리·후-정리 공용) -------------------------- +cleanup_test_ns() { + kubectl get ns "$TEST_NAMESPACE" >/dev/null 2>&1 || return 0 + + log "== 정리: $TEST_NAMESPACE ==" + if helm status "$APP_RELEASE" -n "$TEST_NAMESPACE" >/dev/null 2>&1; then + helm uninstall "$APP_RELEASE" -n "$TEST_NAMESPACE" >>"$LOG" 2>&1 + fi + if helm status "$DB_RELEASE" -n "$TEST_NAMESPACE" >/dev/null 2>&1; then + helm uninstall "$DB_RELEASE" -n "$TEST_NAMESPACE" >>"$LOG" 2>&1 + fi + kubectl -n "$TEST_NAMESPACE" delete pvc --all --wait --timeout=120s >>"$LOG" 2>&1 + + # storageClass longhorn은 reclaimPolicy Retain이라 PVC 삭제만으로는 볼륨이 안 지워진다 + # (.claude/pitfalls.md) — 이 네임스페이스가 남긴 PV만 골라 지운다. + local pv + for pv in $(kubectl get pv -o json 2>/dev/null | python3 -c " +import json,sys +d=json.load(sys.stdin) +for p in d.get('items', []): + cr = p['spec'].get('claimRef') or {} + if cr.get('namespace') == '$TEST_NAMESPACE': + print(p['metadata']['name']) +" 2>/dev/null); do + kubectl delete pv "$pv" --ignore-not-found >>"$LOG" 2>&1 \ + || log "::warning:: PV $pv 삭제 실패 — 수동 확인 필요" + # k8s PV 오브젝트를 지워도 Longhorn 자체의 Volume 커스텀리소스는 남는다(실측 — + # storageScheduled가 계속 누적돼 결국 "insufficient storage"로 다른 배포가 막힘). + # PV와 이름이 같은 volumes.longhorn.io를 명시적으로 지워야 실제 디스크가 반환된다. + kubectl -n longhorn-system delete volumes.longhorn.io "$pv" --ignore-not-found >>"$LOG" 2>&1 + done + + kubectl delete ns "$TEST_NAMESPACE" --wait --timeout=120s >>"$LOG" 2>&1 + log " 완료" +} + +RESULT=1 +trap 'cleanup_test_ns' EXIT + +ensure_operator +cleanup_test_ns # 직전 실행이 비정상 종료했을 경우를 대비한 선-정리 + +log "== 테스트 네임스페이스 생성: $TEST_NAMESPACE ==" +kubectl create ns "$TEST_NAMESPACE" >>"$LOG" 2>&1 + +log "== DB 계정 시크릿 생성: $DB_SECRET_NAME (user=$DB_SECRET_USER) ==" +kubectl -n "$TEST_NAMESPACE" create secret generic "$DB_SECRET_NAME" \ + --from-literal=username="$DB_SECRET_USER" --from-literal=password="$DB_SECRET_PASSWORD" \ + >>"$LOG" 2>&1 || { log "::error:: 시크릿 생성 실패"; exit 1; } + +log "== DB: cnpg-cluster 설치 ($DB_RELEASE/$TEST_NAMESPACE) ==" +# DB_CUSTOM_VALUES는 ArgoCD ApplicationSet용 템플릿(manifests/applicationset/// +# -db-values.yaml)이라 secretName이 "$INFISICAL_SECRET" 플레이스홀더로 되어 있다 — +# dip-console이 실제 배포 시 렌더링해 치환하는 값이다. 여기서는 위에서 실제로 만든 +# $DB_SECRET_NAME 시크릿으로 --set 오버라이드한다. +if ! helm install "$DB_RELEASE" "$CNPG_CLUSTER_CHART_DIR" \ + -f "$CNPG_CLUSTER_CHART_DIR/custom-values.yaml" -f "$DB_CUSTOM_VALUES" \ + --set "bootstrap.initdb.secretName=$DB_SECRET_NAME" \ + -n "$TEST_NAMESPACE" --wait --timeout "${DB_WAIT_TIMEOUT}s" \ + >>"$LOG" 2>&1; then + log "::error:: cnpg-cluster 설치 실패 — $LOG 확인" + exit 1 +fi + +log "== DB: Cluster healthy 대기 (최대 ${DB_WAIT_TIMEOUT}s) ==" +# FQN 필수 — kubectl get cluster 는 Rancher/CAPI 의 동명 CRD 와 충돌한다(.claude/pitfalls.md) +instances="$(kubectl -n "$TEST_NAMESPACE" get clusters.postgresql.cnpg.io "$DB_RELEASE" \ + -o jsonpath='{.spec.instances}' 2>/dev/null)" +deadline=$((SECONDS + DB_WAIT_TIMEOUT)) +phase="" +ready=0 +while [ "$SECONDS" -lt "$deadline" ]; do + phase="$(kubectl -n "$TEST_NAMESPACE" get clusters.postgresql.cnpg.io "$DB_RELEASE" \ + -o jsonpath='{.status.phase}' 2>/dev/null)" + ready="$(kubectl -n "$TEST_NAMESPACE" get clusters.postgresql.cnpg.io "$DB_RELEASE" \ + -o jsonpath='{.status.readyInstances}' 2>/dev/null)" + log " phase=$phase ready=${ready:-0}/$instances" + [ "$phase" = "Cluster in healthy state" ] && [ "${ready:-0}" = "$instances" ] && break + sleep 10 +done +if [ "$phase" != "Cluster in healthy state" ] || [ "${ready:-0}" != "$instances" ]; then + log "::error:: 타임아웃 — DB Cluster 가 healthy 상태에 도달하지 못함" + kubectl -n "$TEST_NAMESPACE" get pods -l "cnpg.io/cluster=$DB_RELEASE" >>"$LOG" 2>&1 + exit 1 +fi + +log "== DB: 부트스트랩 확인 (role/database) ==" +primary_pod="$(kubectl -n "$TEST_NAMESPACE" get pods \ + -l "cnpg.io/cluster=$DB_RELEASE,cnpg.io/instanceRole=primary" \ + -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)" +kubectl -n "$TEST_NAMESPACE" exec "$primary_pod" -c postgres -- \ + psql -U postgres -Atc "\du" >>"$LOG" 2>&1 +kubectl -n "$TEST_NAMESPACE" exec "$primary_pod" -c postgres -- \ + psql -U postgres -Atc "\l" >>"$LOG" 2>&1 + +log "== 앱: 설치 ($APP_RELEASE/$TEST_NAMESPACE, --wait 없이 진행) ==" +# ingress/cert-manager/OIDC 등 DB 와 무관한 의존성 때문에 helm --wait 가 타임아웃할 수 있어 +# 설치만 하고 파드 상태는 아래에서 직접 관찰한다. +app_values_args=(-f "$APP_CUSTOM_VALUES") +if [ -n "$APP_EXTRA_VALUES" ]; then + [ -f "$APP_EXTRA_VALUES" ] || { log "::error:: APP_EXTRA_VALUES 파일 없음: $APP_EXTRA_VALUES"; exit 2; } + log " 추가 배포-테스트 전용 values 사용: $APP_EXTRA_VALUES" + app_values_args+=(-f "$APP_EXTRA_VALUES") +fi +if ! helm install "$APP_RELEASE" "$APP_CHART_DIR" "${app_values_args[@]}" \ + -n "$TEST_NAMESPACE" --timeout 300s \ + >>"$LOG" 2>&1; then + log "::error:: 앱 차트 설치 실패 — $LOG 확인" + exit 1 +fi + +log "== 앱: 파드 상태 관찰 (최대 ${APP_WAIT_SECONDS}s) — DB 연결 여부 판단 ==" +if [ -n "$APP_POD_SELECTOR" ]; then + log " 대상 파드 selector: $APP_POD_SELECTOR" +else + log " ::warning:: APP_POD_SELECTOR 미지정 — 네임스페이스의 아무 파드나 1개 Running 이면 통과로 판정한다" +fi +deadline=$((SECONDS + APP_WAIT_SECONDS)) +DB_ERROR_PATTERN='password authentication failed|could not connect to server|connection refused|no pg_hba.conf entry|FATAL:.*database|OperationalError|could not translate host name' +db_connected="" +while [ "$SECONDS" -lt "$deadline" ]; do + kubectl -n "$TEST_NAMESPACE" get pods >>"$LOG" 2>&1 + target_ready_count="$(kubectl -n "$TEST_NAMESPACE" get pods ${APP_POD_SELECTOR:+-l "$APP_POD_SELECTOR"} -o json 2>/dev/null | python3 -c " +import json,sys +d=json.load(sys.stdin) +n=0 +for p in d.get('items', []): + phase = p['status'].get('phase') + statuses = p['status'].get('containerStatuses') or [] + ready = bool(statuses) and all(c.get('ready') for c in statuses) + if phase == 'Running' and ready: + n += 1 +print(n) +" 2>/dev/null)" + # DB 관련 여부와 무관하게 네임스페이스 전체 로그에서 연결 실패 패턴을 찾는다(넓게 감지). + db_errors="$(kubectl -n "$TEST_NAMESPACE" get pods -o name 2>/dev/null | while read -r pod; do + kubectl -n "$TEST_NAMESPACE" logs "$pod" --all-containers --tail=200 2>/dev/null + done | grep -Ei "$DB_ERROR_PATTERN" | head -5)" + + if [ -n "$db_errors" ]; then + log "::error:: 파드 로그에서 DB 연결 실패 패턴 발견:" + echo "$db_errors" | tee -a "$LOG" + db_connected="false" + break + fi + if [ "${target_ready_count:-0}" -ge 1 ]; then + log " 대상 파드 Running/Ready ${target_ready_count}개 확인, DB 연결 실패 로그 없음" + db_connected="true" + break + fi + sleep 15 +done + +if [ "$db_connected" = "true" ]; then + log "== 결과: PASS (DB 연결 확인) ==" + RESULT=0 +else + log "::error:: 타임아웃 또는 DB 연결 실패 — 파드/로그 상세는 $LOG 참고" + kubectl -n "$TEST_NAMESPACE" get pods -o wide >>"$LOG" 2>&1 + RESULT=1 +fi + +exit "$RESULT" diff --git a/scripts/deploy-test/deploy-test-cnpg-cluster.sh b/scripts/deploy-test/deploy-test-cnpg-cluster.sh index 483befe..af8fe8c 100755 --- a/scripts/deploy-test/deploy-test-cnpg-cluster.sh +++ b/scripts/deploy-test/deploy-test-cnpg-cluster.sh @@ -128,6 +128,10 @@ for p in d.get('items', []): " 2>/dev/null); do kubectl delete pv "$pv" --ignore-not-found >>"$LOG" 2>&1 \ || log "::warning:: PV $pv 삭제 실패 — 수동 확인 필요" + # k8s PV 오브젝트를 지워도 Longhorn 자체의 Volume 커스텀리소스는 남는다(실측 — + # storageScheduled가 계속 누적돼 결국 "insufficient storage"로 다른 배포가 막힘). + # PV와 이름이 같은 volumes.longhorn.io를 명시적으로 지워야 실제 디스크가 반환된다. + kubectl -n longhorn-system delete volumes.longhorn.io "$pv" --ignore-not-found >>"$LOG" 2>&1 done kubectl delete ns "$TEST_NAMESPACE" --wait --timeout=120s >>"$LOG" 2>&1 diff --git a/scripts/deploy-test/fixtures/airflow-deploy-test-overrides.yaml b/scripts/deploy-test/fixtures/airflow-deploy-test-overrides.yaml new file mode 100644 index 0000000..f8b93f6 --- /dev/null +++ b/scripts/deploy-test/fixtures/airflow-deploy-test-overrides.yaml @@ -0,0 +1,21 @@ +# airflow 배포 테스트 전용 오버라이드 — custom-values.yaml 위에 얹어서 쓴다. +# 목적: DB(cnpg-cluster) 연결 여부만 확인한다. custom-values.yaml의 dags.gitSync는 +# 실제 운영 gitea 저장소(http://gitea-http.platform.svc.cluster.local:3000/dip/airflow-dags.git)를 +# 가리키므로, 격리된 테스트 네임스페이스에는 그 저장소가 없어 scheduler의 git-sync-init +# 컨테이너가 "Repository not found"로 영구 CrashLoopBackOff에 빠진다(실측 — DB 연결과 무관). +# +# 카탈로그의 실제 권장 설정(custom-values.yaml)은 변경하지 않는다. +dags: + gitSync: + enabled: false + +# 배포 테스트 시 host는 반드시 별도 값으로 덮어쓴다 — 카탈로그 기본값(airflow.example.org)이 +# 이 클러스터의 다른 실제 배포와 겹칠 수 있다(lakekeeper에서 실측 — defense-llm 네임스페이스가 +# 동일 패턴의 host를 이미 쓰고 있었다). +ingress: + web: + hosts: + - name: "airflow-access-test.example.org" + tls: + enabled: true + secretName: "airflow-tls" diff --git a/scripts/deploy-test/fixtures/flowise-deploy-test-overrides.yaml b/scripts/deploy-test/fixtures/flowise-deploy-test-overrides.yaml new file mode 100644 index 0000000..1534c1d --- /dev/null +++ b/scripts/deploy-test/fixtures/flowise-deploy-test-overrides.yaml @@ -0,0 +1,16 @@ +# flowise 배포 테스트 전용 오버라이드 — custom-values.yaml 위에 얹어서 쓴다. +# +# 배포 테스트 시 host는 반드시 별도 값으로 덮어쓴다 — 카탈로그 기본값(flowise.example.org)이 +# 이 클러스터의 다른 실제 배포와 겹칠 수 있다(lakekeeper에서 실측 — defense-llm 네임스페이스가 +# 동일 패턴의 host를 이미 쓰고 있었다). +# +# 카탈로그의 실제 권장 설정(custom-values.yaml)은 변경하지 않는다. +ingress: + hosts: + - host: flowise-access-test.example.org + paths: + - / + tls: + - hosts: + - flowise-access-test.example.org + secretName: flowise-tls-secret diff --git a/scripts/deploy-test/fixtures/lakekeeper-deploy-test-overrides.yaml b/scripts/deploy-test/fixtures/lakekeeper-deploy-test-overrides.yaml new file mode 100644 index 0000000..0d0d91c --- /dev/null +++ b/scripts/deploy-test/fixtures/lakekeeper-deploy-test-overrides.yaml @@ -0,0 +1,34 @@ +# lakekeeper 배포 테스트 전용 오버라이드 — custom-values.yaml 위에 얹어서 쓴다. +# 목적: DB(cnpg-cluster) 연결 여부만 확인한다. custom-values.yaml의 auth.oauth2는 +# 실제 운영 keycloak(https://keycloak.example.org/realms/paasup)을 가리키므로, +# 격리된 테스트 네임스페이스에서는 도달 불가능해 파드가 기동하지 못한다. +# +# lakekeeper values.yaml 주석(line 492-494): auth.oauth2.providerUri가 비어 있고 +# auth.kubernetes.enabled가 false면 인증 자체가 꺼진다 — 이 테스트에서 노리는 상태. +# +# custom-values.yaml의 catalog.extraVolumes(keycloak-tls)는 root-ca-secret을 참조하는데 +# 이 시크릿도 실제 keycloak 배포 시에만 생성되는 것이라 격리된 테스트 네임스페이스에는 +# 없다 — db-migration Job의 init 컨테이너가 FailedMount로 멈춘다. oauth2를 끄면서 이 볼륨도 +# 같이 비운다(Helm은 배열 값을 병합하지 않고 통째로 교체하므로 빈 배열로 덮어써야 한다). +# +# 카탈로그의 실제 권장 설정(custom-values.yaml)은 변경하지 않는다. +auth: + oauth2: + providerUri: "" + audience: "" + ui: + clientID: "" + scopes: "" + +catalog: + extraEnv: [] + extraVolumeMounts: [] + extraVolumes: [] + + # custom-values.yaml의 host(lakekeeper.example.org)는 이 클러스터에 실제 배포된 다른 + # lakekeeper 인스턴스(defense-llm 네임스페이스)가 이미 쓰고 있다 — 같은 host로 배포하면 + # apisix에 동일 host의 라우트가 2개 등록되어 실제 서비스의 트래픽 라우팅과 충돌한다 + # (실측 — apisix admin API에 두 서비스가 동일 host로 동시 등록된 것 확인). + # 배포 테스트는 반드시 별도 host를 쓴다. + ingress: + host: "lakekeeper-access-test.example.org" diff --git a/scripts/deploy-test/fixtures/mlflow-deploy-test-overrides.yaml b/scripts/deploy-test/fixtures/mlflow-deploy-test-overrides.yaml new file mode 100644 index 0000000..b73f1be --- /dev/null +++ b/scripts/deploy-test/fixtures/mlflow-deploy-test-overrides.yaml @@ -0,0 +1,14 @@ +# mlflow 배포 테스트 전용 오버라이드 — custom-values.yaml 위에 얹어서 쓴다. +# +# 배포 테스트 시 host는 반드시 별도 값으로 덮어쓴다 — 카탈로그 기본값(mlflow.example.org)이 +# 이 클러스터의 다른 실제 배포와 겹칠 수 있다(lakekeeper에서 실측 — defense-llm 네임스페이스가 +# 동일 패턴의 host를 이미 쓰고 있었다). +# +# 카탈로그의 실제 권장 설정(custom-values.yaml)은 변경하지 않는다. +tracking: + ingress: + hostname: mlflow-access-test.example.org + extraTls: + - hosts: + - mlflow-access-test.example.org + secretName: mlflow-tls-secret diff --git a/scripts/deploy-test/fixtures/superset-deploy-test-overrides.yaml b/scripts/deploy-test/fixtures/superset-deploy-test-overrides.yaml new file mode 100644 index 0000000..595641b --- /dev/null +++ b/scripts/deploy-test/fixtures/superset-deploy-test-overrides.yaml @@ -0,0 +1,28 @@ +# superset 배포 테스트 전용 오버라이드 — custom-values.yaml 위에 얹어서 쓴다. +# 목적: DB(cnpg-cluster) 연결 여부만 확인한다. custom-values.yaml의 configOverrides.enable_oauth는 +# 실제 운영 keycloak(플레이스홀더 URL/client_secret)을 가리키는 AUTH_TYPE = AUTH_OAUTH 설정이라, +# 격리된 테스트 네임스페이스에서 로그인 화면에 "Keycloak으로 로그인" 버튼만 뜨고 실제로는 +# 가짜 URL이라 로그인이 불가능하다(실측). 이 키를 비워 기본 AUTH_DB(아이디/비번) 로그인으로 +# 되돌린다 — 다른 configOverrides 키(secret, log_level)는 그대로 둔다(Helm은 맵을 키 단위로 +# 병합하므로 이 키만 덮어써진다). +# +# 카탈로그의 실제 권장 설정(custom-values.yaml)은 변경하지 않는다. +# +# 주의: configOverrides의 모든 키는 superset_config.py 파일 하나로 합쳐진다. log_level +# 키(LOG_LEVEL = logging.INFO)는 자체적으로 import를 안 하고 이 enable_oauth 키 맨 위의 +# `import logging`에 의존하고 있었다(실측 — 완전히 비우면 NameError로 전체 기동 실패). +# 그래서 완전히 비우지 않고 import만 남긴다. +configOverrides: + enable_oauth: | + import logging + +# 배포 테스트 시 host는 반드시 별도 값으로 덮어쓴다 — 카탈로그 기본값(superset.example.org)이 +# 이 클러스터의 다른 실제 배포와 겹칠 수 있다(lakekeeper에서 실측 — defense-llm 네임스페이스가 +# 동일 패턴의 host를 이미 쓰고 있었다). +ingress: + hosts: + - superset-access-test.example.org + tls: + - hosts: + - superset-access-test.example.org + secretName: superset-tls-secret