카탈로그 앱 내장 bitnami postgresql → cnpg-cluster 전환 (1차 5개 차트)

이슈 #9에서 결정된 CloudNativePG 채택을 카탈로그 앱에 확장 적용한다.
airflow/lakekeeper/mlflow/superset/flowise 5개 차트가 내장하던 bitnami
postgresql 서브차트를 끄고 앱 전용 cnpg-cluster 인스턴스를 외부 DB로 쓰도록
전환했다. 5개 모두 dev 클러스터 격리 네임스페이스에서 배포 테스트로 실측 검증했다.

gitea/keycloak/dnsup 는 서브차트가 아니라 공유 postgresql-ha 를 외부 참조하며
paasup/dipup 레포 관리 대상이라 제외했다 — 인수인계 문서만 추가했다.

배포 구조:
- ArgoCD ApplicationSet 으로 DB(syncWave 0) → 앱(syncWave 1) 순서를 보장한다.
  기존 openmetadata/victoria-metrics 관례를 따랐다. cnpg-cluster 차트는 범용
  상태로 유지하고 앱별 값은 manifests/applicationset/<app>/ 에 둔다.

검증 중 발견해 함께 고친 문제:
- lakekeeper: cnpg 의 -ro 는 replica 전용이라 instances:1 에서 엔드포인트가
  0개다. 읽기 연결을 -r(전체 라운드로빈)로 교체했다.
- airflow/superset: ingressClassName 누락 + kong 애노테이션 잔존으로 이
  클러스터(apisix 전용)에서 ingress 접근이 아예 불가능했다. apisix + regex
  path 로 교체했다.
- 배포 테스트가 PV 만 지우고 Longhorn Volume CR 을 남겨 storageScheduled 가
  누적됐다(orphan 112개 ~1TB 로 배포 차단). 두 스크립트의 정리 로직을 고쳤다.

재사용 구조화:
- .claude/skills/chart-to-cnpg/ 신규. 남은 4개 차트(langflow-ide, langfuse,
  litellm, nemo)에 같은 절차를 재사용한다. flowise 에 실제 적용해 검증했다.
- 배포 테스트 공통 절차는 .claude/deploy-test-procedure.md, 환경 함정은
  .claude/pitfalls.md 로 단일화하고 앱별 README 는 참조만 남겼다.

관련: #9, #14

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
wbsong111
2026-08-05 15:17:46 +09:00
parent 35ce2bb80d
commit 3ee1c1ee23
42 changed files with 1650 additions and 146 deletions
+31
View File
@@ -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/<app>/<version>/README.md`에 있다 — 여기서는 배포
테스트 자체의 공통 절차만 다룬다.
```sh
APP_CHART_DIR=manifests/helm/<app>/<version> \
APP_RELEASE=<app> \
DB_CUSTOM_VALUES=manifests/applicationset/<app>/<version>/<app>-db-values.yaml \
DB_SECRET_USER=<user> DB_SECRET_PASSWORD=<password> \
TEST_NAMESPACE=<app>-test-build \
APP_EXTRA_VALUES=scripts/deploy-test/fixtures/<app>-deploy-test-overrides.yaml \
APP_POD_SELECTOR="<DB에 실제로 연결하는 파드의 label selector>" \
bash scripts/deploy-test/deploy-test-app-with-cnpg.sh /tmp/deploy-test-<app>
```
- 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/<app>-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-<name>`). 기존 워크로드가 있는 NS 를 쓰지 않는다.
+38
View File
@@ -51,3 +51,41 @@ kubectl -n <ns> get clusters.postgresql.cnpg.io <name>
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 <pv-name>
```
이미 삭제된 네임스페이스의 orphan 볼륨을 찾을 때는 `.status.kubernetesStatus.namespace`
현존하지 않는 네임스페이스를 가리키는 것만 골라낸다. 배포 테스트 스크립트
(`deploy-test-app-with-cnpg.sh`, `deploy-test-cnpg-cluster.sh`)에는 이 정리가 포함돼 있다.
## 배포 테스트의 ingress host 가 실제 배포와 충돌한다
카탈로그 `custom-values.yaml` 의 ingress host(`<app>.example.org`)를 그대로 쓰면, 같은 host 를
이미 쓰는 실제 배포가 있을 때 **apisix 에 동일 host 라우트가 2 개 등록되어 운영 트래픽
라우팅과 충돌한다.** 실측: `defense-llm` 네임스페이스에 이미 배포된 lakekeeper 와
`lakekeeper.example.org` 가 겹쳤다.
배포 테스트는 항상 host 를 `<app>-access-test.example.org` 처럼 별도 값으로 오버라이드한다
(각 앱 fixture 에 반영돼 있다). 배포 전 `kubectl get ingress -A` 로 host 중복을 먼저 확인한다.
## ingress 는 `ingressClassName` 을 명시해야 한다
이 클러스터의 ingress controller 는 **apisix 하나뿐이다**(kong 은 없다). 그런데 일부 카탈로그
차트가 아직 `kubernetes.io/ingress.class: kong` 애노테이션 방식으로 남아 있었다 — 이 경우
Ingress 리소스는 만들어지지만 `CLASS: <none>` 으로 뜨고 어떤 컨트롤러도 처리하지 않아
접근 자체가 불가능하다(airflow·superset 에서 실측, 둘 다 수정함).
또한 `path: /` 는 정확히 `/` 만 매치되므로 앱이 `/home` 등으로 리다이렉트하면 전부 404 가
난다. `path: /.*` + `k8s.apisix.apache.org/use-regex: "true"` 를 함께 쓴다.
+82
View File
@@ -0,0 +1,82 @@
---
name: chart-to-cnpg
description: 카탈로그 차트가 내장한 bitnami postgresql 서브차트를 전용 cnpg-cluster 인스턴스로 전환할 때 사용한다. "postgresql 서브차트를 cnpg로 바꿔줘", "이 차트도 cnpg-cluster 쓰게 해줘", "bitnami postgresql 제거" 같은 요청, 또는 manifests/helm/<chart>/의 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/<chart>/<ver>/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/<chart>/<ver>/custom-values.yaml` | `postgresql.enabled: false` + 위 외부 DB 키. 기존 `postgresql.auth.*`/`image.*` 등 서브차트 전용 설정은 제거 | 4개 앱 |
| `manifests/helm/<chart>/<ver>/CUSTOM-README.md` | 전환 사실·사전조건(operator)·배포 순서 | 〃 |
| `manifests/applicationset/<app>/<ver>/applicationset.yaml` | 신규 — DB(syncWave 0) → 앱(syncWave 1) | `manifests/applicationset/airflow/1.16.0/` |
| `manifests/applicationset/<app>/<ver>/<app>-db-values.yaml`<br>`<app>-values.yaml` | 신규 — 비밀번호는 `$INFISICAL_SECRET` 플레이스홀더 | 〃 |
| `manifests/applicationset/<app>/<ver>/README.md` | 신규 — 짧게. 배포 테스트 절차는 복제하지 말고 참조만 | 〃 |
| `scripts/deploy-test/fixtures/<app>-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에서 실측).
`<app>-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` 신뢰 금지)
+24
View File
@@ -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·취약점을 스캔하고
+48 -40
View File
@@ -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:
@@ -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) 서비스에 영향을 주는 운영 작업이라 별도 계획·승인이 필요하다.
@@ -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`.
@@ -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
@@ -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"
@@ -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
@@ -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`.
@@ -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
@@ -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
@@ -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"
@@ -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`.
@@ -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
@@ -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
@@ -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"
@@ -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`.
@@ -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
@@ -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
@@ -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"
@@ -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`.
@@ -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
@@ -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
@@ -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"
+33 -10
View File
@@ -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 설정
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`
@@ -4,11 +4,17 @@
ingress:
web:
enabled: true
# Kong → APISIX 마이그레이션(issue #139, mlflow/lakekeeper와 동일). 이 클러스터에는
# kong ingress controller가 없어 `kubernetes.io/ingress.class: kong` 애노테이션 방식은
# 어떤 컨트롤러도 인식하지 않아 접근 자체가 불가능했다(실측 — CLASS: <none>).
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 <ns>
postgresql:
enabled: true
auth:
enablePostgresUser: true
postgresPassword: postgres
primary:
persistence:
enabled: true
size: 8Gi # airflow에서 사용하는 DB의 볼륨 사이즈 설정
storageClass: ""
enabled: false
data:
metadataConnection:
user: airflow
pass: airflow
protocol: postgresql
host: airflow-db-rw
port: 5432
db: airflow
sslmode: disable
@@ -34,12 +34,20 @@ 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 <ns>
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
@@ -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 서비스명(`<release>-rw`) | `lakekeeper-db-rw` |
| `externalDatabase.host_read` | cnpg-cluster의 ro 서비스명(`<release>-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 인증 설정
@@ -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 <ns>
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 인증
@@ -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
```
+12 -12
View File
@@ -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 <ns>
postgresql:
auth:
username: mlflow
enabled: false
externalDatabase:
dialectDriver: "postgresql"
host: "mlflow-db-rw"
port: 5432
user: mlflow
password: mlflow1234
image:
repository: bitnamilegacy/postgresql
primary:
reousrces:
limits:
cpu: 500m
memory: 1Gi
requests:
cpu: 250m
memory: 512Mi
database: mlflow
minio:
enabled: true
auth:
+31 -27
View File
@@ -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`
@@ -99,11 +99,17 @@ tolerations: []
ingress:
enabled: true
# airflow와 동일하게 실측된 문제 — ingressClassName 미지정 시 이 클러스터의 apisix가
# 아예 인식하지 않아 CLASS: <none>으로 뜨고 접근이 불가능하다. 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 <ns>
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
+291
View File
@@ -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/<app>/<version>/<app>-db-values.yaml
# (ArgoCD ApplicationSet의 valuesPath와 동일한 파일)
# DB_RELEASE 기본 "${APP_RELEASE}-db"
# DB_SECRET_NAME 기본 "${DB_RELEASE}-app-secret" (각 앱 <app>-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 <OUT_DIR>}"
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/<app>/<version>/
# <app>-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"
@@ -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
@@ -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"
@@ -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
@@ -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"
@@ -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
@@ -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