Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions TASKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,63 @@
4. F07 P5-M1 — PgBouncer 사이드카 + 독립 Deployment.
5. F08 P6-M1 — exporter + Grafana + PrometheusRule.
6. F02 P10-M1 — extension lifecycle e2e.

---

## 품질 개선 plan (2026-04-30 — Bitnami + Crunchy PGO 교차검증)

> 출처: `/Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md` (사용자 승인 plan)
> 19 권장사항을 P0(즉시) / P1(중기) / P2(장기) 우선순위로 분해.
> *영향 Pillar* 컬럼은 기존 F01~F14에 매핑.

### P0 (1-2 sprint, 즉시) — 6개

| ID | 권장 | 영향 Pillar | 단계 | 의존 | 비고 |
|----|------|------------|------|------|------|
| P0-1 | Status.Conditions reason 어휘 확장 (Promoting/Demoting/Election*/TopologyDrift/Rotating) | F04(P2), F03(P11), F09(P7) | **완료** | — | `internal/controller/status.go` 본 PR. P2-T3 신호 채널. |
| P0-2 | 데이터플레인 PodSecurityContext defaults (runAsUser=70, readOnlyRootFs, seccomp RuntimeDefault) | F01(P1) | 설계 | — | ADR 0006. `builders.go:184-198, 243-256`. |
| P0-3 | NetworkPolicy 데이터플레인 표준 템플릿 (coordinator↔workers, router→coordinator/workers) | F01(P1), F09(P7) | 설계 | P0-2 | RFC 0006 §NetworkPolicy. `config/network-policy/`. |
| P0-4 | Cascade Delete 회귀 테스트 (Finalizer 회피 정책) | F01(P1) | 설계 | — | ADR 0008. `test/e2e/cascade_delete_test.go`. |
| P0-5 | AuthPlugin.RotateSecret 인터페이스 추가 (additive, ADR 0005 §alpha rule) | F13(P13), F09(P7) | **완료** | — | `internal/plugin/api.go:205-` 본 PR. RFC 0006 §Auth Rotation Hook. |
| P0-6 | LibPQExecutor 구현 (Citus 차별화 코드 차원 잠금, P2 → P0 승격) | F03(P11) | 설계 | P0-1 | RFC 0002 Draft → Implemented. `internal/citus/exec.go`. |

### P1 (3-6 sprint, 중기) — 6개

| ID | 권장 | 영향 Pillar | 단계 | 의존 |
|----|------|------------|------|------|
| P1-1 | BackupJob CRD + reconciler (BackupPlugin 첫 호출자) | F06(P4) | 설계 | P0-2 |
| P1-2 | PgBouncer 사이드카 + cmd/router 통합 | F07(P5), F12(P12) | 설계 | P0-2 |
| P1-3 | Monitoring/Exporter 표준 통합 (ExporterPlugin 호출자) | F08(P6) | 설계 | P0-2 |
| P1-4 | Helm chart 패키징 (P14 → P1 앞당김, ADR 0007) | **신규 P1 트랙** (F14에서 분리) | 설계 | — |
| P1-5 | ClusterUpgrade CRD 시그니처 (in-place + blue/green) | F11(P9) | 설계 | P1-1 |
| P1-6 | pgBackRest 실행 모델 (BackupOptions.ExecutionMode: sidecar\|job) | F06(P4), F13(P13) | 설계 | P1-1 |

### P2 (6+ sprint, 장기 차별화) — 7개

| ID | 권장 | 영향 Pillar | 단계 | 의존 |
|----|------|------------|------|------|
| P2-1 | Citus rebalance / RebalanceJob CRD | F03(P11) | 설계 | P0-6 |
| P2-2 | Worker pool zero-downtime scale | F03(P11) | 설계 | P0-6, P2-1 |
| P2-3 | Plugin SDK wire-format golden test (reflect 기반 시그니처 hash) | F13(P13) | 설계 | — |
| P2-4 | gRPC out-of-process plugin + reference plugin (UDS, cosign) | F13(P13) | 설계 | P2-3, P0-2 |
| P2-5 | Declarative PgDatabase / PgRole CRD (PGO 미지원 차별화) | F10(P8), F03(P11) | 설계 | P0-5, P0-6 |
| P2-6 | Multi-region Standby Cluster (PGO Standby 차용 + Citus geo) | F14(P14)→독립 | 설계 | P1-1, P1-6 |
| P2-7 | Citus PGUpgrade orchestration | F11(P9), F03(P11) | 설계 | P1-5, P1-1, P2-6 |

### 거버넌스 산출물 매트릭스

| ID | 제목 | 트리거 권장 | 시작 상태 |
|----|------|-----------|----------|
| RFC 0002 | metadata-sync (기존) | P0-6 | Draft → **Implemented 예정** |
| RFC 0004 | Backup/PITR | P1-1, P1-6 | Draft (작성 예정) |
| RFC 0005 | QueryRouter | P1-2 | Draft (작성 예정) |
| RFC 0006 | Security/TLS (NetworkPolicy + Auth Rotation + Role/RBAC) | P0-3, P0-5, P2-5 | Draft (작성 예정) |
| RFC 0007 | Observability | P1-3 | Draft (작성 예정) |
| RFC 0008 | DistributedTable 의미론 | P2-1 | Draft (작성 예정) |
| RFC 0010 | Upgrade (Citus 절 포함) | P1-5, P2-2, P2-7 | Draft (작성 예정) |
| RFC 0012 | Plugin SDK 안정화 | P2-3, P2-4 | Draft (작성 예정) |
| RFC 0013 | Declarative DB/Role | P2-5 | Draft (작성 예정) |
| RFC 0014 | Multi-region & Standby | P2-6 | Draft (작성 예정) |
| ADR 0006 | Security Defaults Rationale | P0-2 | **Accepted (본 PR)** |
| ADR 0007 | Helm을 P14에서 P1로 분리 | P1-4 | **Accepted (본 PR)** |
| ADR 0008 | Finalizer 회피 정책 | P0-4 | **Accepted (본 PR)** |
73 changes: 73 additions & 0 deletions docs/adr/0006-security-defaults-rationale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# ADR 0006 — 데이터플레인 PodSecurityContext 기본값 (Security Defaults Rationale)

- **상태**: Accepted
- **날짜**: 2026-04-30
- **결정자**: @keiailab/maintainers
- **관련**: ADR 0001 v2 (PGO-class 패리티), Bitnami PostgreSQL Helm Chart 비교 (`/Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md` §4 P0-2)

## 컨텍스트

본 프로젝트의 *manager Pod*(operator 자체)은 `config/manager/manager.yaml:53-74`에서 강한 SecurityContext를 적용한다 — `runAsNonRoot=true`, `readOnlyRootFilesystem=true`, `seccompProfile=RuntimeDefault`, `capabilities.drop=[ALL]`. 그러나 *데이터플레인 Pod*(`buildPGStatefulSet:184-198`, `buildRouterDeployment:243-256`)에는 SecurityContext가 0개다.

이는 **비대칭 보안 부채**:
- PSS(Pod Security Standards) `restricted` 정책이 적용된 클러스터에서 admission 거부 가능
- `runAsNonRoot=false`(기본) 상태에서 PG 컨테이너가 root로 기동될 수 있음 — 호스트 escape 위험
- Bitnami PostgreSQL Helm Chart가 *기본값*으로 제공하는 보안 설정에 미달 → "PGO-class 패리티" 약속과 모순

## 결정

`buildPGStatefulSet`과 `buildRouterDeployment`가 생성하는 모든 데이터플레인 Pod에 다음 SecurityContext 기본값을 *항상* 적용한다:

```yaml
podSecurityContext:
runAsNonRoot: true
runAsUser: 70 # PG 표준 postgres user UID
runAsGroup: 70
fsGroup: 70
seccompProfile:
type: RuntimeDefault
container.securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
```

`readOnlyRootFilesystem=true` 동반 변경:
- emptyDir 마운트 추가 — `/tmp`, `/run`, `/var/run/postgresql` (PG가 lock/socket 작성 필요)
- `/var/lib/postgresql/data`는 PVC 마운트라 별도 emptyDir 불필요

## 근거

### 왜 UID 70인가
PostgreSQL 공식 컨테이너 이미지(postgres:*)가 `postgres` user를 UID/GID 70으로 정의. 본 프로젝트의 향후 cmd/instance 이미지도 동일 규약 따름.

### 왜 `readOnlyRootFilesystem=true`인가
- 컨테이너 내 임의 바이너리 작성 차단 → 공급망 공격 완화
- Bitnami chart 기본값
- emptyDir 마운트 비용은 미미(memory backed) → 트레이드오프 ↑

### 왜 *기본값*으로 강제인가
PostgresCluster CR에 `Spec.SecurityContext` override 필드를 두면 *opt-in 보안*이 됨 — 운영자가 잊으면 root 가능 상태. 본 ADR은 *opt-out*으로 강제: 기본값은 항상 위 설정, override는 webhook이 검증.

## 트레이드오프

- **`readOnlyRootFilesystem` 호환성**: 일부 PG extension(예: pg_cron, pg_stat_statements)이 디스크 임시 파일 생성. 해결: `/tmp` emptyDir + extension 별 PVC subpath. 본 ADR은 기본 emptyDir 3개로 충분, extension별 추가는 P10에서 처리.
- **사용자 정의 UID 요구**: 일부 K8s 환경(OpenShift 일부 SCC)이 random UID 강제. 해결: webhook이 `runAsUser`를 nil로 두면 K8s SCC가 채우도록 허용 (P0-2 implementation 시 webhook 추가).
- **기존 PVC 데이터의 ownership**: 기존 PG가 root로 쓴 데이터를 UID 70으로 읽으려면 fsGroup 변경. K8s `fsGroup`이 자동 처리하나 첫 transition은 시간 걸릴 수 있음.

## 결과

- `internal/controller/builders.go`의 `buildPGStatefulSet`과 `buildRouterDeployment`에 SecurityContext 주입 (P0-2 권장 적용 시).
- envtest assertion: 생성된 Pod에 `runAsNonRoot=true`, `runAsUser=70` 확인.
- e2e 회귀: restricted PSA 적용 namespace에서 admission 통과 검증.
- 본 ADR 변경(UID 변경, capabilities 추가)은 RFC 0006 "Security/TLS"의 일부로 처리.

## 강제 메커니즘

| 메커니즘 | 위치 | 도입 시점 |
|---|---|---|
| 기본값 주입 | `internal/controller/builders.go` | P0-2 implementation |
| webhook 검증 | `internal/webhook/v1alpha1/postgrescluster_webhook.go` | P0-2 후속 (override 시 최소값 강제) |
| envtest 회귀 | `internal/controller/builders_test.go` | P0-2 implementation |
| e2e 회귀 | `test/e2e/security_test.go` (신규) | P0-2 implementation |
77 changes: 77 additions & 0 deletions docs/adr/0007-helm-chart-promoted-to-p1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# ADR 0007 — Helm chart을 P14에서 P1 트랙으로 분리

- **상태**: Accepted
- **날짜**: 2026-04-30
- **결정자**: @keiailab/maintainers
- **관련**: roadmap.md (14 Pillar), Bitnami PostgreSQL Helm Chart 비교 (`/Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md` §5 P1-4)

## 컨텍스트

`docs/roadmap.md`는 Helm chart을 **P14 Distribution**에 배치 — install.yaml, OLM bundle, multi-arch image와 묶음. P14는 *v1.0 GA 마지막 단계*로 정의되어, 다른 모든 Pillar(P1~P13) M3 통과 후 진입.

문제: alpha/beta 단계에서 사용자가 operator를 *어떻게 설치*하는가? 현재는 Kustomize manifests만 제공 (`config/default`). 실제 사용자 채널은 *Helm chart가 사실상 표준*이라:

- alpha 단계 사용자가 시도조차 못 함 → feedback loop 차단
- "PGO-class" 패리티 약속에 *배포 채널 패리티*도 포함되나 P14까지 부재
- Bitnami는 chart가 *유일한 채널*이며 매우 mature — 같은 시장 사용자에게 "Kustomize 외 없음"은 진입 마찰

## 결정

Helm chart을 *P14에서 분리*하여 P1 Core Lifecycle 트랙의 후속 task(P1-T5)로 재배치한다. P14에는 *나머지* distribution 산출물만 남긴다:

| 변경 전 (P14) | 변경 후 |
|---|---|
| Helm chart | **P1 트랙으로 이동** (alpha 사용자 채널) |
| install.yaml | P14 유지 |
| OLM bundle | P14 유지 |
| multi-arch image | P14 유지 |

### chart 분리 모델

`charts/` 아래 두 chart 별도 패키징:

- `charts/postgresql-operator/` — operator 자체 (Deployment + RBAC + CRD + NetworkPolicy + ServiceAccount)
- `charts/postgrescluster/` — PostgresCluster CR 인스턴스 (선택, P1-T5 sub-task)

이는 Bitnami의 `postgresql` (CR 인스턴스) + 별도 operator chart 패턴의 *역배치* — operator chart가 *상위*, CR chart가 *옵션*. 사유: 사용자가 operator는 한 번 설치, CR은 namespace당 N개.

## 근거

### 왜 P14 전체 이동이 아닌 *분리*인가
P14의 install.yaml, OLM, multi-arch는 *후행 산출물* — 모든 CRD가 동결된 후에 생성하는 것이 안전. 그러나 Helm chart은 *현재 CRD 상태*를 패키징하면 되므로 *지금* 가능하다. 무리해서 OLM/multi-arch까지 앞당기면 *모든* CRD가 unstable한 alpha 상태에서 매번 재패키징.

### 왜 chart 두 개로 분리인가
- *operator*는 cluster-scope 한 번 설치
- *PostgresCluster CR*은 namespace당 N개 — chart가 다중 인스턴스를 지원하려면 helm release 별로 분리 필요
- Bitnami가 `postgresql` (CR 인스턴스 chart)을 메인으로 두는 이유와 동일

### 왜 *지금*인가 — 기존 P14 유지의 비용

| 비용 | 영향 |
|---|---|
| alpha 사용자 부재 | 실 사용 feedback 0, 회귀 발견 늦음 |
| Bitnami로 사용자 이탈 | "PGO-class" 약속의 인지도 손실 |
| chart 작성을 v1.0 직전에 모아서 처리 시 burden | 각 CRD별 template + values default 동시 결정 → 회귀 위험 |

## 트레이드오프

- **두 경로 동시 유지**: Kustomize(`config/default`) + Helm(`charts/postgresql-operator`) 동시 운영 → 변경 시 두 곳 갱신 부담. 완화: chart template이 `config/`의 manifests를 generate (Makefile 타겟에서 `kustomize build config/default | helmify`로 자동화 검토).
- **chart 안정성 약속**: alpha chart는 `Chart.yaml: appVersion`이 v0.x이므로 breaking change 가능. 사용자가 이를 인지해야 함. 완화: chart README에 "alpha 단계, breaking 가능" 명시.
- **OLM bundle 분리 유지**: OperatorHub 사용자는 여전히 P14까지 대기 — 사용자 세그먼트 차이를 인정하고 P14 우선순위는 유지.

## 결과

- `docs/roadmap.md`에서 P14 정의 갱신 — Helm chart을 *P1 트랙*으로 이동.
- 신규 디렉토리 `charts/postgresql-operator/` (P1-4 권장 implementation 시).
- Makefile에 `chart-package`, `chart-lint` 타겟 추가.
- TASKS.md에 P1-4 권장 등록.
- 본 ADR은 v1.0 GA 시점에 *재평가* — Helm chart 안정성과 OLM bundle 통합 시점 검토.

## 강제 메커니즘

| 메커니즘 | 위치 | 도입 시점 |
|---|---|---|
| roadmap.md 갱신 | `docs/roadmap.md` §14 Pillar | 본 ADR 동시 |
| chart 골격 | `charts/postgresql-operator/` | P1-4 |
| chart lint CI 단계 | Makefile + 로컬 hook | P1-4 |
| `helm install --dry-run` 회귀 | e2e | P1-4 |
74 changes: 74 additions & 0 deletions docs/adr/0008-finalizer-avoidance-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# ADR 0008 — Finalizer 회피 정책 (Cascade Delete via OwnerReference)

- **상태**: Accepted
- **날짜**: 2026-04-30
- **결정자**: @keiailab/maintainers
- **관련**: ADR 0002 (Patroni 미사용 — K8s API as DCS), Crunchy PGO 비교 (`/Users/phil/.claude/plans/1-https-artifacthub-io-packages-helm-bit-sunny-wozniak.md` §4 P0-4)

## 컨텍스트

본 프로젝트는 PostgresCluster reconciler가 모든 하위 자원(StatefulSet, Service, ConfigMap, PVC)에 `controllerutil.SetControllerReference`를 호출 (`internal/controller/postgrescluster_controller.go:262`). 이는 K8s GC가 PostgresCluster 삭제 시 *자동 cascade delete*하는 표준 패턴.

그러나:
- *Finalizer 도입 유혹*이 향후 PR에서 발생 가능 — "외부 자원(backup repo, S3 prefix, certificate) cleanup도 해야 하지 않나?"
- Finalizer는 *추가 reconcile loop* + *K8s API 의존도 증가* + *deletion 지연* 비용
- ADR 0002 "K8s API as DCS" 원칙이 *분산 합의 단순성*을 보존 — Finalizer는 그 원칙과 충돌 가능
- Crunchy PGO도 동일 패턴(controllerutil + GC TTL)을 ADR로 못박음 — 본 ADR이 그 결정을 *명시적으로 기록*

## 결정

본 프로젝트는 **Finalizer를 신규 도입하지 않는다.** 다음 두 메커니즘만 사용:

1. **OwnerReference + K8s GC**: 하위 자원에 `controllerutil.SetControllerReference`로 OwnerReference 설정. PostgresCluster 삭제 시 K8s GC가 cascade delete.
2. **외부 자원 cleanup은 별도 Job CRD**: backup repo cleanup, S3 prefix 정리 등 외부 자원이 PostgresCluster 삭제와 *별도 lifecycle*을 가질 때, 별도 `BackupCleanupJob` 같은 CRD에서 처리. PostgresCluster는 외부 자원을 *알지 못하는 상태*로 삭제 가능.

### 강제 회귀 테스트

`test/e2e/cascade_delete_test.go`에 다음 시나리오 추가:
1. PostgresCluster 생성 → StatefulSet/Service/ConfigMap 생성 확인
2. PostgresCluster 삭제
3. **60초 내** 모든 하위 자원이 GC 됨을 검증

`internal/controller/postgrescluster_controller_test.go`에 envtest 어셔션:
- 모든 하위 자원이 `controllerutil.HasControllerReference == true`

## 근거

### 왜 Finalizer가 *기본*에서 제외되는가
Finalizer는 다음 4가지 비용을 만든다:

1. **Deletion 지연**: 사용자가 `kubectl delete` 후 *Finalizer가 처리될 때까지* PostgresCluster 자원이 K8s API에 남음. 운영자 혼란.
2. **Stuck 위험**: Finalizer 처리 중 reconciler 다운 → 자원 영구 stuck. 강제 제거 시 외부 자원 leak.
3. **Reconcile 복잡도**: 모든 reconcile cycle에서 `if obj.DeletionTimestamp != nil { ... } else { ... }` 분기 추가.
4. **분산 합의 충돌**: ADR 0002 "K8s API as DCS"는 etcd가 단일 진실. Finalizer는 그 진실을 *지연* — 분산 모델에서 timing 가정 추가.

### 왜 외부 자원은 별도 Job CRD인가
PostgresCluster 삭제 ≠ backup repo 삭제. 사용자는 PostgresCluster를 삭제해도 *backup은 보존*하고 싶을 수 있음. 또는 *cluster 재생성*을 위해 backup만 보존. 이 분리를 강제하면:

- PostgresCluster lifecycle 단순화 (외부 의존 0)
- 사용자가 의도적으로 cleanup CRD 생성해야 외부 자원 삭제 → *명시적 의도 표현*
- Finalizer 부재로 deletion 즉시 완료

### 왜 *지금* ADR을 작성하는가
P0-4 회귀 테스트 추가 시점에 *왜 이 테스트가 존재하는가*의 근거를 ADR로 못박음. 향후 PR이 "외부 자원 cleanup 위해 Finalizer 추가"를 시도하면 본 ADR이 Reject 근거.

## 트레이드오프

- **외부 자원 leak 가능성**: Finalizer 부재 → PostgresCluster 삭제 후 외부 backup repo가 leak될 수 있음. 완화: 운영 가이드에서 *별도 cleanup CRD* 사용 권장. 또한 cloud provider 측 lifecycle policy (S3 expiration 등) 활용.
- **kubectl-cnpg 같은 도구의 *graceful drain* 부재**: PGO는 Finalizer로 PG가 graceful shutdown 후 삭제되도록 보장. 본 프로젝트는 fail-fast (ADR 0002 + RFC 0003 부록 A) 모델이라 graceful drain은 *재기동 신호로 운영자가 처리* — 동일 분산 합의 원칙.

## 결과

- 본 ADR은 *향후 모든 PR*의 Finalizer 도입을 reject할 근거.
- P0-4 권장 implementation 시 회귀 테스트 추가.
- 외부 자원 cleanup 필요한 시점(P4 Backup, P7 Security)에 *별도 Job CRD*로 설계.
- 본 ADR 변경(Finalizer 예외 도입)은 RFC 필수 — *광범위 영향* 관점에서 RFC 의무.

## 강제 메커니즘

| 메커니즘 | 위치 | 도입 시점 |
|---|---|---|
| 회귀 테스트 (e2e cascade delete) | `test/e2e/cascade_delete_test.go` | P0-4 implementation |
| envtest 어셔션 (OwnerReference) | `internal/controller/postgrescluster_controller_test.go` | P0-4 |
| golangci-lint 정책 (가능 시 — Finalizer import 차단) | `.custom-gcl.yml` | P13-T2 후속 |
| PR 리뷰 체크리스트 | `standards/checklist.md §3` | 본 ADR 채택과 동시 |
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ title: "Roadmap"
| **P11** | ⭐ Citus Topology | coord+workers, `pg_dist_node` sync, 4종 분산 CRD | P1, P2, P10 |
| **P12** | ⭐ QueryRouter | stateless 라우터 풀, PgBouncer 사이드카, HPA, metadata lag 메트릭 | P11, P5, P6 |
| **P13** | ⭐ Plugin SDK | 5종 Go 인터페이스 + gRPC out-of-process | P4, P6, P10 |
| **P14** | Distribution | Helm, install.yaml, OLM bundle, multi-arch 이미지 | 모두 |
| **P14** | Distribution | install.yaml, OLM bundle, multi-arch 이미지 (※ Helm chart는 ADR 0007에 따라 **P1 트랙으로 분리 — alpha 사용자 채널 조기 확보**) | 모두 |

## 의존 그래프

Expand Down
Loading
Loading