카탈로그 앱 내장 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:
@@ -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 를 쓰지 않는다.
|
||||
|
||||
@@ -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"` 를 함께 쓴다.
|
||||
|
||||
@@ -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` 신뢰 금지)
|
||||
Reference in New Issue
Block a user