자체 빌드 이미지 3종(cloudnative-pg/cnpg-postgresql/etcd) + 대응 헬름 차트 도입

security-catalog 프로젝트에서 첫 실사용 자체 빌드 이미지 3종을 포팅한다 — 전부
상위 태그·베이스 OS 교체로 해소 안 되는 CVE(Go 모듈 정적 링크 또는 미수정 CRITICAL/
HIGH)를 자체 빌드(소스 컴파일 또는 SUSE BCI 재설치)로 대응한다:

- images/cloudnative-pg: CNPG operator, release-1.30 소스 컴파일 + bci-micro
- images/cnpg-postgresql: PostgreSQL 18.4, bci-base + zypper 재설치
- images/etcd: etcd v3.7.1, 소스 컴파일(x/text 강제 업그레이드) + bci-micro

함께 추가:
- manifests/helm/{cloudnative-pg,cnpg-cluster,etcd} — 위 이미지를 참조하는 카탈로그 차트
- scripts/deploy-test/*.sh, .claude/deploy-test-procedure.md — CVE 0건과 별개로
  "실제로 뜨는가"를 검증하는 배포 스모크 테스트
- .claude/pitfalls.md — 자체 빌드/배포 테스트 중 실측한 함정 모음

검토 중 발견해 반영한 수정:
- cloudnative-pg 차트의 image 블록을 etcd와 동일한 registry/repository/tag 3필드+
  따옴표 포맷으로 통일 — 기존 포맷(repository에 registry+repo 결합, 따옴표 없음)은
  patch-catalog-tag.py 의 split 패처가 tag만 갱신하고 repository는 그대로 남기는
  조용한 부분 치환을 일으켜, 향후 레지스트리 마이그레이션 시 깨진 참조를 만들 수 있었다
- cnpg-cluster 차트의 SLES 커버리지 코멘트를 최신 실측(trivy가 SLES 15.7을 정상
  커버함, 2026-07-29 재측정)에 맞게 정정 — 폐기된 "측정 불가/OVAL 우회 필요" 결론이
  남아있었다
- CLAUDE.md/MEMORY.md 의 "images/ 디렉토리 없음" 서술을 갱신하고, 레지스트리
  마이그레이션(docker.io/wbsong111 → docker.io/paasup)·decisions/analysis 문서 이관·
  리소스 프로파일 추가를 다음 작업으로 기록

이 3개 이미지는 아직 dip-catalog 자체 CI(build-image.yml)로 빌드·게이트·push 를
실행해본 적이 없다 — 현재 참조 태그는 security-catalog 쪽에서 이미 검증된 것이다.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
wbsong111
2026-08-03 12:03:37 +09:00
parent a168a3c7e6
commit 6878b61efa
89 changed files with 36608 additions and 11 deletions
+61
View File
@@ -0,0 +1,61 @@
# 배포 테스트 절차
[CLAUDE.md](../CLAUDE.md) 에서 분리했다. 배포 테스트를 실제로 수행할 때만 참고한다.
security-catalog 프로젝트에서 포팅했다.
CVE 0건이어도 동작하지 않는 이미지는 카탈로그에 넣을 수 없다. 게이트 통과와 기능 검증은
둘 다 필수다.
## CNPG (`cnpg-postgresql` 이미지) — 자동화됨
`build-image.yml` 이 새 이미지로 카탈로그 PR 을 열면, 병합 전에 로컬 kubeconfig 로
`scripts/deploy-test/deploy-test-cnpg-cluster.sh` 를 실행해 "실제로 뜨는가"만 빠르게 확인한다(PR 본문에도
안내됨).
```sh
IMAGE_NAME=docker.io/wbsong111/cnpg-postgresql:<태그> bash scripts/deploy-test/deploy-test-cnpg-cluster.sh /tmp/deploy-test-out
```
- **Operator(`cloudnative-pg`)는 상시 컴포넌트다** — release `cnpg`, namespace
`cnpg-system` 에 1회만 설치되고 스크립트가 재사용한다. cloudnative-pg 차트의 웹훅
설정 이름이 릴리스와 무관하게 클러스터 전역 고정이라(`cnpg-validating-webhook-configuration`
등) 두 번째 설치가 원천적으로 불가능하다 — 그래서 상시 설치로 전환됐다.
- 앱(`cnpg-cluster`)만 고정 네임스페이스(`pg-test-build`)에 매번 재생성되고, 검증은
차트 기본값 + `postgresql.imageName` override만으로 이루어진다(`custom-values.yaml`
안 씀). 확인 범위는 "psql 로 응답하는가" 수준의 스모크 테스트다.
- 정리(uninstall + PVC/PV + 네임스페이스 삭제)는 성공/실패와 무관하게 항상 수행된다.
Operator 는 정리 대상이 아니다.
- CI(GitHub Actions) 통합은 아직 없다 — 러너에서 dev 클러스터로의 네트워크 경로가 없다.
- 백업/PITR, pooler, 동기 복제 등 스모크 테스트 밖의 항목은 아래 수동 절차를 따른다.
security-catalog 프로젝트에 상세 배포 테스트 기록(`doc/charts/cnpg/deploy-test.md`)이
있으나 dip-catalog 에는 아직 이관되지 않았다.
## etcd (`etcd` 이미지) — 스크립트 있음, 수동 실행
`cnpg` 와 달리 상시 오퍼레이터가 없다 — 단일 차트라 매 실행 테스트 네임스페이스
(`etcd-test-build`)에 설치하고 끝나면 지운다.
```sh
IMAGE_NAME=quay.io/coreos/etcd:v3.7.1 bash scripts/deploy-test/deploy-test-etcd.sh /tmp/deploy-test-out
```
- 차트 기본값 + `image.*`/`initImage.*`/`replicas` override 만으로 검증한다
(`custom-values.yaml` 안 씀 — cnpg 원칙과 동일).
- 확인 범위: StatefulSet 롤아웃, 컨테이너 실행 계정(uid 999 기대), 전 멤버 quorum
health(`etcdctl endpoint health --cluster`), 쓰기/읽기 왕복(`etcdctl put`/`get`).
- 정리(uninstall + PVC/PV + 네임스페이스 삭제)는 성공/실패와 무관하게 항상 수행된다.
- TLS·백업 등 스모크 테스트 밖의 항목은 `manifests/helm/etcd/1.1.12/CUSTOM-README.md`
따른다. security-catalog 프로젝트에 상세 배포 테스트 기록(`doc/charts/etcd/deploy-test.md`)이
있으나 dip-catalog 에는 아직 이관되지 않았다.
## 그 외 차트 — 수동 절차
1. 전용 네임스페이스를 새로 만든다 (`pg-test-<name>`). 기존 워크로드가 있는 NS 를 쓰지 않는다.
2. 오퍼레이터는 가능하면 권한을 축소해 설치한다 (CNPG 는 `config.clusterWide=false`).
3. 개발 환경 크기로 축소해 배포한다 (`--set storage.size=2Gi` 등). `custom-values.yaml`
자체를 개발용 값으로 바꾸지 않는다.
4. 검증 항목은 해당 차트의 `BUILD-README.md` "배포 검증" 절에 정의한다.
5. 결과는 실측값으로 기록한다(장소는 `MEMORY.md` 또는 PR 설명 — `doc/charts/` 관례는
dip-catalog 에 아직 없다). 통과 항목만 쓰지 말고 발견된 문제와 미검증 항목을 반드시
남긴다.
6. 정리한다. 정리 명령은 문서에 함께 기록한다.
+53
View File
@@ -0,0 +1,53 @@
# 이 환경에서 반복적으로 문제가 된 것들
security-catalog 프로젝트에서 CNPG/etcd 자체 빌드·배포 테스트 작업 중 실제로 겪은 함정을
포팅했다. [CLAUDE.md](../CLAUDE.md) 에서 분리했다 — 매 세션 필요한 내용이 아니라 해당
작업(자체 빌드 이미지, CNPG/etcd 배포 테스트)을 할 때만 참고한다.
## 스캐너 결과를 그대로 믿지 말 것
세 가지 실패 양상을 모두 겪었다. 게이트(`scripts/pipeline/cve-gate.py`)가 이것들을
잡도록 만들어져 있다.
| 양상 | 실측 |
| --- | --- |
| **벤더 하향 등급** | NVD 9.8 을 Debian 이 LOW(`CVE-2019-1010022`), Ubuntu 가 MEDIUM(`CVE-2026-8376`) 으로 둠 |
| **데이터 커버리지 부재** | Debian sid 기반 이미지가 149 패키지에 findings 0건 — 거짓 clean |
| **벤더 미평가 은폐** | Ubuntu 의 "Needs evaluation" CVE 는 보고되지 않음. `--detection-priority comprehensive` 로도 안 나옴 |
> dip-catalog 의 `scan-sbom.sh` 는 두 번째 양상(데이터 커버리지 부재)을 구분하는 자가진단
> (`CoverageProbe`)이 아직 없다 — `doc/sbom-pipeline.md` 참고.
## 이미지 태그의 베이스 OS 를 확인할 것
같은 앱 버전이라도 태그에 따라 베이스 OS 가 다르고 EOL 이 임박한 것이 섞여 있다.
오퍼레이터가 배포판 수명 테이블을 갖고 있으면 기동 로그에 남는다
(CNPG: `internal/cmd/manager/instance/run/osdb.go`).
## `kubectl get cluster` 는 쓰면 안 된다
dev 클러스터에 `clusters` 단축명을 쓰는 CRD 가 3개 있다(CNPG 배포 테스트 대상 클러스터
기준).
- `clusters.postgresql.cnpg.io` (CloudNativePG)
- `clusters.management.cattle.io` (Rancher)
- `clusters.cluster.x-k8s.io` (CAPI)
`kubectl get cluster` 는 CNPG 가 아닌 리소스를 조회해 **빈 결과를 반환한다.**
스크립트·런북에서는 항상 FQN 을 쓴다.
```sh
kubectl -n <ns> get clusters.postgresql.cnpg.io <name>
```
## 오퍼레이터 차트는 cluster-scoped 잔여물을 남긴다
`helm uninstall` 후에도 CRD 와 webhook 설정이 남는다. CNPG 는 webhook `failurePolicy: Fail`
이므로 **오퍼레이터를 먼저 지우면 CR 을 삭제할 수 없게 된다.**
정리 순서: CR → 오퍼레이터 → webhook 설정 → CRD.
## 선언적 리소스의 `status` 를 근거로 쓰지 말 것
CNPG `Database` CRD 는 spec generation 이 바뀔 때만 reconcile 한다. 확장이 DB 에서
사라져도 `status.applied` 는 계속 `true` 다. 검증은 항상 실제 DB 에 질의해서 한다
(security-catalog 프로젝트에서 실측 — 상세 배포 테스트 기록은 dip-catalog 에 아직 없음).
+7 -4
View File
@@ -121,10 +121,13 @@ extract-helm-images.sh → generate-sbom.sh → scan-sbom.sh → cve-gate.py
- **현재 warn-only**: 게이트가 실패해도 CI/PR 을 막지 않는다. 45+ 개 카탈로그 차트가
이 게이트로 트리아지된 적이 없다.
- 자체 빌드 프레임워크(`scripts/build/`, `.github/workflows/build-image.yml`)
도입만 됐고 실사용 이미지가 없다(`images/` 디렉토리 자체가 없음). 게이트가 상위
태그·베이스 OS 교체로 해소 안 되는 차단 CVE 를 찾으면 이 프레임워크로 자체 빌드를
검토한다 — 절차는 [.claude/image-authoring.md](.claude/image-authoring.md).
- 자체 빌드 프레임워크(`scripts/build/`, `.github/workflows/build-image.yml`)
`images/`에 이미지 3종(`cloudnative-pg`, `cnpg-postgresql`, `etcd`, 전부
security-catalog 프로젝트에서 포팅)이 있으나 dip-catalog 자체 CI 로는 한 번도
실행된 적이 없다(빌드·게이트·push 모두 미검증 — 현재 참조 태그는 security-catalog
쪽에서 이미 빌드된 것). 게이트가 상위 태그·베이스 OS 교체로 해소 안 되는 차단 CVE 를
찾으면 이 프레임워크로 자체 빌드를 검토한다 — 절차는
[.claude/image-authoring.md](.claude/image-authoring.md).
- 상세: [doc/sbom-pipeline.md](doc/sbom-pipeline.md) · 승인 예외: `doc/cve-exceptions.json`
· 현재 미결 사항: [MEMORY.md](MEMORY.md)
+32 -7
View File
@@ -28,12 +28,35 @@ dip-catalog 의 `scan-sbom.sh` 는 이 로직이 없다 — 게이트는 finding
차단 처리하는 구버전 경로로만 동작한다. 45+ 차트 규모에서 이 판정이 오탐을 얼마나
내는지 실측 후 이식 여부를 결정한다.
**자체 빌드 프레임워크(`scripts/build/`, `.github/workflows/build-image.yml`)는 도입만
했고 실사용 이미지가 없다.** `images/` 디렉토리 자체가 아직 없다. 실제 자체 빌드가
필요해지면(게이트가 상위 태그/베이스 OS 교체로 해소 안 되는 차단 CVE 를 찾을 때)
[.claude/image-authoring.md](.claude/image-authoring.md) 절차로 `images/<image>/`
신설한다. 이때 최종 런타임 베이스 OS 정책(security-catalog 는 SUSE BCI 로 고정했으나
dip-catalog 는 아직 미결)을 처음으로 정해야 한다.
**`images/`에 이미지 3종(`cloudnative-pg`, `cnpg-postgresql`, `etcd`)이 security-catalog
프로젝트에서 포팅됐지만 dip-catalog 자체 CI(`build-image.yml`)로는 아직 한 번도
빌드·게이트·push 를 실행해본 적이 없다.** 현재 카탈로그 values(`custom-values.yaml`/
`dip-values.yaml`)가 참조하는 태그는 전부 security-catalog 쪽에서 이미 빌드·게이트
PASS·push 된 실제 이미지(`docker.io/wbsong111/...`)를 그대로 재사용하는 것이다 —
dip-catalog 파이프라인으로 재현·재검증된 적은 없다. 최종 런타임 베이스 OS 정책은
security-catalog 의 SUSE BCI 고정 결정을 그대로 따랐다(ADR 자체는 미이관, 아래 참고).
**레지스트리 마이그레이션(`docker.io/wbsong111` → `docker.io/paasup`)이 필요하다.**
`build-image.yml``REGISTRY_HOST` 는 이미 `docker.io/paasup` 로 설정돼 있지만
(아래 항목 참고) 위 3개 이미지가 실제로 가리키는 곳은 아직 `docker.io/wbsong111` 다.
`REGISTRY=docker.io/paasup` 로 3개 이미지를 재빌드·재게이트·재검증해 PASS 를 확인한
뒤에만 6개 values 파일(`manifests/helm/{cloudnative-pg/0.29.0,cnpg-cluster/1.0.0,
etcd/1.1.12}/{custom-values,dip-values}.yaml`, cnpg-cluster 는 `dip-values.yaml`
`imageName` 오버라이드가 있음)의 이미지 참조를 새 태그로 교체한다. push 자격 증명
확인은 바로 아래 항목과 동일하다.
**`doc/decisions/`·`doc/analysis/` 디렉토리 자체가 dip-catalog 에 없다.** 포팅된 3개
이미지의 README·values 코멘트가 `doc/decisions/000X-*.md`, `doc/analysis/*.md`,
`doc/image-selection.md`, `doc/cve-zero-pipeline.md`, `doc/architecture/build-pipeline.md`
를 근거로 계속 인용하지만 이 경로들은 dip-catalog 에 하나도 없다(security-catalog
프로젝트에만 있음). 당장 급한 건 아니지만, 이 상태로는 이 레포만 보는 사람이 자체 빌드
결정의 CVE 실측·비교 근거를 확인할 방법이 없다 — 각 README 에 이미 요약된 근거(CVE
번호·후보 비교표)를 압축한 로컬 stub ADR 작성을 검토한다.
**`doc/define-chart-resources.md` 에 신규 차트 3종(`cloudnative-pg`, `cnpg-cluster`,
`etcd`) 의 Small/Medium/Large 리소스 프로파일이 없다.** CLAUDE.md 의 "신규 차트 추가"
규칙(리소스 프로파일 필수)을 아직 못 지켰다 — custom-values.yaml 의 기존
requests/limits/storage 값을 근거로 표를 추가하는 별도 작업으로 처리한다.
**`SBOM_PIPELINE_IMAGE` 재빌드가 보류돼 있다.** 이 마이그레이션으로 빌드 컨텍스트 경로가
`doc/scripts/Dockerfile``scripts/pipeline/Dockerfile` 로 바뀌었다. Dockerfile 내용
@@ -43,7 +66,9 @@ Repo Variable `SBOM_PIPELINE_IMAGE` 갱신은 git 커밋으로 되지 않는 수
**`build-image.yml``REGISTRY_HOST``docker.io/paasup` 로 설정했다.** 실제 이미지를
push 하려면 `DOCKERHUB_USER`/`DOCKERHUB_TOKEN` 시크릿이 `paasup` 조직 네임스페이스에
push 권한을 가져야 한다 — **미확인**, 실제 첫 자체 빌드 시도 전에 확인 필요.
push 권한을 가져야 한다 — **미확인**, 실제 첫 자체 빌드 시도 전에 확인 필요. 이제
`images/` 의 3개 이미지가 실사용 후보이므로 위 "레지스트리 마이그레이션" 항목이 이
확인을 실제로 필요로 하는 첫 사례다.
---
+93
View File
@@ -0,0 +1,93 @@
# cloudnative-pg — CloudNativePG 오퍼레이터 자체 빌드
CNPG 오퍼레이터(컨트롤러) 이미지를 업스트림 소스에서 직접 컴파일한다.
`manifests/helm/cloudnative-pg/0.29.0/custom-values.yaml`·`dip-values.yaml`
`image.repository`/`image.tag` 가 이 산출물을 가리킨다.
security-catalog 프로젝트에서 포팅했다. 목표 정의·CVE 조사 실측·이미지 채택 결정
근거(각각 `doc/cve-zero-pipeline.md`, `doc/analysis/cloudnative-pg-operator-cve.md`,
`doc/decisions/0005-cloudnative-pg-operator-self-build.md`)는 security-catalog 프로젝트에
있고 dip-catalog 에는 아직 이관되지 않았다 — 신규 자체 빌드 이미지 추가 절차 전반은
[.claude/image-authoring.md](../../.claude/image-authoring.md) 참고.
> **자체 빌드는 대응 우선순위 3번이다.** 상위 태그 교체·베이스 OS 교체로 목표를
> 만족할 수 있으면 그 쪽을 쓴다. 자체 빌드는 업스트림 서명·provenance·SBOM attestation 을
> 잃고 재빌드 책임을 지는 선택이다.
## 왜 자체 빌드하나 — 그리고 왜 `cnpg-postgresql` 과 다른 방법인가
업스트림 `ghcr.io/cloudnative-pg/cloudnative-pg:1.30.0` 은 실효 HIGH 3건
(`CVE-2026-39822` stdlib, `CVE-2026-56852` `golang.org/x/text`, `GHSA-hrxh-6v49-42gf`
`google.golang.org/grpc`)으로 차단된다. 상위 태그가 없다(2026-07-30 확인, 최신 릴리스가
여전히 `v1.30.0`). 업스트림 배포 이미지 베이스가 `gcr.io/distroless/static-debian13`
(OS 패키지 사실상 0개)라 **베이스 OS 를 바꾸는 것만으로는 고쳐지지 않는다** — CVE 는
바이너리에 정적 링크된 Go 모듈 버전이 원인이다. 자체 빌드(소스 컴파일)만 유효한 대응이다.
`cnpg-postgresql`(OS 베이스 교체 + zypper 패치)과 성격이 다르지만, **오케스트레이션은
동일한 [scripts/build/build-hardened-image.sh](../../scripts/build/build-hardened-image.sh)
하나를 공유한다.** 이 이미지가 그 스크립트의 계약(`build.env``DOCKERFILE`·`TARGET`·
`BUILD_ARGS`·`APP_VERSION` 선언, `verify.sh``VERIFY-OK` 로 종료)만 지키면, 이미지
종류(OS 패키지 설치형 vs 소스 컴파일형)는 스크립트가 몰라도 된다 — 근거는
[.claude/image-authoring.md](../../.claude/image-authoring.md) 참고.
## 소스·버전 관리
| 항목 | 값 |
| --- | --- |
| 소스 | `https://github.com/cloudnative-pg/cloudnative-pg.git` |
| pinned commit | `source.build.env``SOURCE_COMMIT` (release-1.30 브랜치) |
| 빌더 | 공식 `golang` 이미지 (`source.build.env``GO_BUILDER_TAG`, go.mod 요구 버전과 일치) |
| 최종 베이스 | `registry.suse.com/bci/bci-micro:15.7` — security-catalog 는 SUSE BCI 하나만 쓰기로 결정했다(ADR 미이관); 업스트림의 distroless 대신 여기 적용 |
빌더 스테이지(Go 컴파일)는 공식 `golang` 이미지를 그대로 쓴다 — 최종 이미지에 남는 것이
아니라 컴파일 산출물만 최종 스테이지로 넘어오므로 스캔·정책 대상이 아니다. `bci-micro`
는 SUSE BCI 중 가장 가벼운 변종이지만 `bci-base` 와 달리 `nonroot`(uid 65532) 계정이
미리 없어 `source.Dockerfile` 이 직접 만든다(`/etc/passwd`·`/etc/group` 에 추가).
`SOURCE_COMMIT`**자동 추적하지 않는다.** `cnpg-postgresql` 의 PGDG 버전처럼 사람이
업스트림 `release-1.30` 브랜치(또는 그다음 패치 릴리스가 나오면 그 태그)를 보고
`source.build.env` 를 고쳐 PR 을 여는 것 자체가 갱신 트리거다.
**권장 점검 주기**: 카탈로그 게이트가 이 이미지의 차단 CVE 를 다시 보고할 때, 또는
업스트림이 `v1.30.1`(혹은 그 이상) 을 릴리스했을 때 — 릴리스가 나오면 그쪽으로 갈아타는
것(대응 우선순위 a)이 이 자체 빌드를 유지하는 것보다 항상 우선이다.
## 빌드
```sh
# 로컬 빌드 (push 없음)
IMAGE=cloudnative-pg BASE_OS=source bash scripts/build/build-hardened-image.sh /tmp/out
# 레지스트리에 push 까지 (현재 실제 배포 이미지도 이 네임스페이스에 있다)
IMAGE=cloudnative-pg BASE_OS=source REGISTRY=docker.io/wbsong111 \
bash scripts/build/build-hardened-image.sh /tmp/out
```
수행 순서: **빌드 → 기능 검증(`verify.sh`) → SBOM → 전 심각도 스캔 → 게이트 판정.**
dip-catalog 의 `scan-sbom.sh` 는 커버리지 자가진단(`CoverageProbe`)이 없다는 점에
유의한다 — `doc/sbom-pipeline.md` 참고. `verify.sh``manager version` 출력에 pinned
commit 이 실제로 반영됐는지(ldflags 주입 확인), 이미지 `Config.User`
`65532:65532`(nonroot)인지, `--help` 가 정상 종료하는지를 확인한다 — 실제 컨트롤러
기동(k8s API 필요)은 이 스모크 테스트 범위 밖이며, dev 클러스터 배포 테스트
(`.claude/deploy-test-procedure.md`)가 담당한다.
### 파일 구성
| 파일 | 역할 |
| --- | --- |
| `source.Dockerfile` | 빌드 정의 — 소스 컴파일(builder 스테이지) + SUSE BCI(`bci-micro`) 패키징(final 스테이지) |
| `source.build.env` | pinned commit·버전·빌더 이미지 태그. `BUILD_ARGS` 에 나열한 이름만 `--build-arg` 로 전달된다 |
| `verify.sh` | 기능 검증. 호스트에서 bash 로 실행되며 직접 `docker run --entrypoint /manager` 를 호출한다(게스트 스크립트 주입 방식보다 상위 호환 — 최종 이미지에 셸이 없는 경우에도 그대로 동작한다) |
베이스 변종이 하나뿐이라 파일명이 `source.*` 로 고정돼 있다 — `cnpg-postgresql` 처럼
변종이 늘면 `<variant>.Dockerfile`/`<variant>.build.env` 로 분기한다.
### 태그
```
docker.io/wbsong111/cloudnative-pg:1.30.0-security-hardened-20260730
└ app ─┘└ 슬러그 ┘└ 하드닝 ┘└ 빌드일 ┘
```
이 태그는 security-catalog 프로젝트에서 이미 빌드·게이트 PASS·push 된 실제 이미지다.
dip-catalog 는 이를 그대로 재사용한다 — 재빌드·재푸시 여부는 `MEMORY.md` 참고.
+11
View File
@@ -0,0 +1,11 @@
# build-image.yml 이 읽는 카탈로그 반영 메타데이터. 빌드 정의(<variant>.build.env)와는
# 별개다 — 이건 "이 이미지가 어느 차트의 어느 필드를 가리키는가" 만 담는다.
CHART_DIRS="manifests/helm/cloudnative-pg/0.29.0"
# 태그 표기 스타일 — imageName(단일 필드 문자열) | split(registry/repository/tag 분리)
TAG_STYLE=split
TAG_BLOCK=image
# base_os 입력을 안 주면 쓸 기본 변종 (images/<image>/<DEFAULT_BASE_OS>.build.env)
DEFAULT_BASE_OS=source
+80
View File
@@ -0,0 +1,80 @@
# CloudNativePG 오퍼레이터 — 업스트림 소스를 pinned commit 으로 직접 컴파일한다.
#
# 업스트림(https://github.com/cloudnative-pg/cloudnative-pg)의 Dockerfile 은 이미 goreleaser
# 로 빌드된 바이너리(`dist/manager/manager_<arch>`)를 COPY 만 한다 — 컴파일 자체는 이 Dockerfile
# 밖(Makefile `docker-build` → goreleaser)에서 일어난다. 우리는 그 컴파일 단계를 Dockerfile
# 안으로 가져와 `go build` 로 직접 재현한다. `cnpg-postgresql/suse.Dockerfile` 이 "업스트림
# Dockerfile 을 다른 배포판으로 이식"한 것이라면, 이건 "업스트림이 Dockerfile 밖에서 하던
# 빌드를 Dockerfile 안으로 흡수"한 것이다.
#
# 왜 자체 빌드인가 — cloudnative-pg:1.30.0 이 게이트에서 차단하는 CVE 3건(stdlib·x/text·grpc)
# 은 OS 패키지가 아니라 바이너리에 정적 링크된 Go 모듈 버전이 원인이다. 배포 이미지 베이스가
# distroless(OS 패키지 사실상 0개)라 베이스 OS 교체로는 고칠 수 없다 — 소스를 다시 컴파일해야
# 한다. 세 CVE 모두 업스트림 release-1.30 브랜치에 이미 백포트돼 있다(SOURCE_COMMIT 이 그
# 커밋을 가리킨다) — 근거: doc/analysis/cloudnative-pg-operator-cve.md
#
# 업스트림과의 대응 관계 (release-1.30 브랜치 Dockerfile 기준)
# go build (Makefile build-manager) → 동일 (ldflags 까지 그대로)
# goreleaser 멀티아치(manager_amd64/arm64) → 단일 아키텍처(linux/amd64)만 직접 COPY.
# 심볼릭 링크 대신 같은 바이너리를 두 경로에 COPY (아래 "실측으로 드러난 필수 조건" 참고)
# distroless base(gcr.io/distroless/static-debian13:nonroot) → SUSE BCI(bci-micro)로 교체.
# 카탈로그는 SUSE BCI 하나만 쓴다(decisions/0001) — 최종 런타임 이미지에도 동일하게 적용한다.
# builder 스테이지(Go 컴파일)는 공식 golang 이미지를 그대로 쓴다 — 컴파일 결과물에는
# 영향이 없고, 최종 이미지에만 남는 게 무엇인지가 스캔·정책 대상이다
# syntax=docker/dockerfile:1
ARG GO_BUILDER_TAG=1.26.5-trixie
# FROM 에서 쓰는 ARG 는 반드시 첫 FROM 이전(전역 스코프)에 선언해야 한다 — 스테이지 안에서
# 선언하면(예: 이전엔 builder 스테이지 RUN 다음에 둠) 그 스테이지 지역 변수가 되어 다음
# FROM 의 이미지명 해석에 쓰이지 않는다(실측: "FROM argument 'RUNTIME_BASE' is not
# declared" 경고와 함께 빈 이미지명 에러 발생).
ARG RUNTIME_BASE=registry.suse.com/bci/bci-micro:15.7
# 호스트 네이티브 아키텍처로 빌더를 띄운다. Go 크로스컴파일은 에뮬레이션이 필요 없으므로
# --platform=$BUILDPLATFORM 로 고정해 에뮬레이션 오버헤드를 피한다(로컬 arm64 Docker 데스크톱
# 에서 linux/amd64 결과물을 만들 때 특히 중요).
FROM --platform=$BUILDPLATFORM golang:${GO_BUILDER_TAG} AS builder
ARG TARGETARCH
ARG SOURCE_COMMIT
ARG APP_VERSION
WORKDIR /src
# BuildKit 의 git context 지원 — tarball+checksum 관리 없이 git 자체가 커밋 무결성을 보장한다.
ADD https://github.com/cloudnative-pg/cloudnative-pg.git#${SOURCE_COMMIT} /src
# 업스트림 Makefile 의 LDFLAGS·.goreleaser.yml 의 build 설정을 그대로 재현한다.
# (release-1.30 기준 go.mod: go 1.26.5, google.golang.org/grpc v1.82.1, golang.org/x/text v0.39.0)
RUN --mount=type=cache,target=/root/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
set -eux; \
CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath \
-ldflags "-s -w \
-X github.com/cloudnative-pg/cloudnative-pg/pkg/versions.buildVersion=${APP_VERSION} \
-X github.com/cloudnative-pg/cloudnative-pg/pkg/versions.buildCommit=${SOURCE_COMMIT} \
-X github.com/cloudnative-pg/cloudnative-pg/pkg/versions.buildDate=$(date -u +%Y-%m-%d)" \
-o /out/manager ./cmd/manager
FROM ${RUNTIME_BASE} AS final
WORKDIR /
# bci-micro 는 root 만 있고 65532 사용자가 없다(distroless 의 nonroot 변종과 달리 nonroot
# 계정을 미리 만들어두지 않는다) — 직접 만든다. bci-micro 에 bash·coreutils 는 있다
# (zypper·rpm 은 없음 — "micro" 는 패키지 매니저가 빠진 것이지 셸까지 없는 건 아니다).
RUN set -eux; \
echo 'nonroot:x:65532:65532:nonroot:/home/nonroot:/bin/false' >> /etc/passwd; \
echo 'nonroot:x:65532:' >> /etc/group; \
mkdir -p /home/nonroot; \
chown 65532:65532 /home/nonroot
# 실측으로 드러난 필수 조건: 오퍼레이터는 `operator/manager_<GOARCH>` 를 런타임에 glob 해서
# "가용 아키텍처" 목록을 만든다(pkg/utils/discovery.go DetectAvailableArchitectures) — 이
# 목록이 비어 있으면 Cluster 리컨실이 "invalid architecture: amd64" 로 실패한다(배포
# 테스트 2026-07-30 에서 재현). 업스트림은 멀티아치 심볼릭 링크로 이걸 만들지만, 우리는
# 단일 아키텍처만 다루므로 같은 바이너리를 두 경로에 COPY 해 같은 효과를 낸다.
COPY --from=builder /out/manager /manager
COPY --from=builder /out/manager /operator/manager_amd64
COPY --from=builder /src/licenses /licenses
COPY --from=builder /src/LICENSE /licenses/LICENSE
USER 65532:65532
ENTRYPOINT ["/manager"]
+32
View File
@@ -0,0 +1,32 @@
# CloudNativePG 오퍼레이터 — 소스 컴파일형 자체 빌드 (유일한 변종)
#
# build-hardened-image.sh 가 source 한다. BUILD_ARGS 에 나열한 이름만 --build-arg 로 넘어간다.
# 이 이미지는 소스를 직접 컴파일하므로 "베이스 OS" 선택지가 없다 — BASE_OS=source 로 호출한다:
# IMAGE=cloudnative-pg BASE_OS=source bash scripts/build/build-hardened-image.sh <OUT_DIR>
#
# 왜 자체 빌드하는가·CVE 근거 → doc/analysis/cloudnative-pg-operator-cve.md
# 결정 → doc/decisions/0004-cloudnative-pg-operator-self-build.md
DOCKERFILE=source.Dockerfile
TARGET=final
TAG_SLUG=security
# build-hardened-image.sh 가 태그·verify.sh 전달용으로 요구하는 범용 필수값.
# 스톡 1.30.0 과는 태그 슬러그(TAG_SLUG=security)로 구분되므로 버전 문자열 자체는 그대로 둔다.
APP_VERSION=1.30.0
# release-1.30 브랜치 HEAD, 2026-07-30 확인 — CVE-2026-39822(stdlib)·CVE-2026-56852(x/text)·
# GHSA-hrxh-6v49-42gf(grpc) 가 모두 이 커밋에 백포트돼 있다. 갱신할 때는 release-1.30 의
# 최신 커밋으로 사람이 다시 고른다(자동 추적하지 않음 — cnpg-postgresql 의 PGDG 버전처럼
# 사람이 트리거해야 하는 갱신 축).
SOURCE_COMMIT=4463551204bc5cdb5af05b2c60a2d6b58ce9ff6a
# go.mod 의 `go 1.26.5` 요구를 만족하는 공식 golang 이미지 태그. 이 이미지는 builder
# 스테이지에서만 쓰이고 최종 이미지에는 남지 않는다.
GO_BUILDER_TAG=1.26.5-trixie
# 최종 런타임 베이스 — 카탈로그는 SUSE BCI 하나만 쓴다(decisions/0001). bci-base 대신
# 가장 가벼운 bci-micro 를 쓴다(패키지 매니저 없음, bash·coreutils 는 있음).
RUNTIME_BASE=registry.suse.com/bci/bci-micro:15.7
BUILD_ARGS="SOURCE_COMMIT GO_BUILDER_TAG APP_VERSION RUNTIME_BASE"
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# cloudnative-pg 오퍼레이터 이미지 기능 검증 — 호스트에서 bash 로 실행된다
# (build-hardened-image.sh 가 `env TAG=... PLATFORM=... SOURCE_COMMIT=... bash verify.sh`
# 형태로 호출한다).
#
# cnpg-postgresql/verify.sh 와 달리 게스트 셸을 쓰지 않는다 — 최종 이미지 베이스가
# gcr.io/distroless/static-debian13:nonroot 라 셸이 아예 없다(`/bin/sh` 없음, 업스트림
# Dockerfile 도 이 사실을 자기 빌더 스테이지 주석에 명시한다). 대신 `--entrypoint /manager`
# 로 바이너리를 직접 실행해 검증한다 — 실제 컨트롤러 기동(k8s API 필요)은 이 스모크
# 테스트 범위 밖이며, dev 클러스터 배포 테스트가 담당한다.
#
# 마지막 줄에 VERIFY-OK 를 출력하면 통과다. build-hardened-image.sh 가 그것으로 판정한다.
set -e
TAG="${TAG:?TAG 환경변수가 필요하다}"
PLATFORM="${PLATFORM:-linux/amd64}"
SOURCE_COMMIT="${SOURCE_COMMIT:?SOURCE_COMMIT 환경변수가 필요하다 (build-hardened-image.sh 가 build.env 에서 전달)}"
SHORT_COMMIT="${SOURCE_COMMIT:0:8}"
echo "== manager version =="
OUT="$(docker run --rm --platform "$PLATFORM" --entrypoint /manager "$TAG" version)"
echo " $OUT"
case "$OUT" in
*"$SHORT_COMMIT"*) ;;
*) echo "FAIL: version 출력에 pinned commit($SHORT_COMMIT) 이 없다 — ldflags 주입 확인 필요"; exit 1 ;;
esac
echo "== 실행 사용자 (nonroot, 이미지 메타데이터) =="
# 셸이 없어 컨테이너 안에서 id 를 실행할 수 없다 — 이미지가 선언한 기본 User 를 확인한다.
# 위 version 실행 자체가 이 기본 사용자로 이미 성공했다(권한 문제였다면 여기까지 오지 못한다).
USER_CFG="$(docker inspect --format '{{.Config.User}}' "$TAG")"
[ "$USER_CFG" = "65532:65532" ] || { echo "FAIL: 이미지 Config.User 가 65532:65532 가 아니다 (실제: $USER_CFG)"; exit 1; }
echo " Config.User=$USER_CFG"
echo "== --help 스모크 (k8s API 없이 도는 유일한 확인 범위) =="
docker run --rm --platform "$PLATFORM" --entrypoint /manager "$TAG" --help >/dev/null
echo " /manager --help 종료 코드 0"
echo "VERIFY-OK"
+132
View File
@@ -0,0 +1,132 @@
# cnpg-postgresql — CloudNativePG PostgreSQL 자체 빌드
CNPG 오퍼레이터가 관리하는 PostgreSQL 인스턴스 이미지를 직접 빌드한다.
`manifests/helm/cnpg-cluster/1.0.0/custom-values.yaml``imageName` 이 이 산출물을 가리킨다.
security-catalog 프로젝트에서 포팅했다. 목표 정의·이미지 선택 규칙·후보 비교 실측·이미지
채택 결정·빌드 자동화(CI) 문서(`doc/cve-zero-pipeline.md`, `doc/image-selection.md`,
`doc/analysis/cnpg-image-vuln-comparison.md`, `doc/decisions/0001-cnpg-postgresql-image.md`,
`doc/architecture/build-pipeline.md`)는 security-catalog 프로젝트에 있고 dip-catalog 에는
아직 이관되지 않았다.
> **자체 빌드는 대응 우선순위 3번이다.** 상위 태그 교체·베이스 OS 교체로 목표를
> 만족할 수 있으면 그 쪽을 쓴다. 자체 빌드는 업스트림 서명·provenance·SBOM attestation 을
> 잃고 재빌드 책임을 지는 선택이다.
---
## 왜 자체 빌드하나
업스트림 `ghcr.io/cloudnative-pg/postgresql:18.4-standard-trixie` 는 실효 CRITICAL/HIGH
23건을 갖고 있고, **전부 수정 버전이 없다**(Debian `unimportant` 9 / `no-dsa` 7 /
`postponed` 3 / 미분류 4). SUSE 는 같은 CVE 들을 이미 백포트했다 — 베이스 OS 를 바꾸면
벤더 판정이 달라진다.
**단, 수치가 낮아진 것이 실제 개선인지 벤더 미평가인지 반드시 구분해야 한다.**
`cve-gate.py` 의 교차 검증(`--crossref`)이 이를 돕는다. security-catalog 의
`scan-sbom.sh` 가 제공하던 커버리지 자가진단(`CoverageProbe`)은 dip-catalog 의
`scan-sbom.sh` 에는 아직 없다(`doc/sbom-pipeline.md` 참고).
## 베이스 OS
이 이미지는 **SUSE BCI 15.7** 로 빌드됐다(security-catalog 프로젝트 ADR, 미이관).
| 파일 | 베이스 | 패키지 관리자 | 측정 가능성 |
| --- | --- | --- | --- |
| `suse.Dockerfile` | `registry.suse.com/bci/bci-base:15.7` | zypper + PGDG rpm | ✅ trivy 로 완전 측정 |
`ubuntu:24.04` 기반 빌드(후보 B)는 security-catalog 프로젝트에서 검토 후 제거됐다 —
카탈로그가 채택하지 않아 재빌드되지 않은 채 낡아가고 있었다(빌드 하루 만에 findings
38 → 46). 관련 실측은 이관되지 않았다.
### 업스트림과의 차이
**원칙: 업스트림 Dockerfile 과의 차이를 최소로 유지한다.** 패키지 구성·uid·확장 목록을
임의로 줄이면 오퍼레이터가 이미지에 기대하는 조건이 깨진다.
`suse.Dockerfile` 은 업스트림 CNPG(Debian/apt/PGDG-deb)를 SUSE(zypper/PGDG-rpm)로
이식한 것이라 저장소 설정·패키지명·경로가 전부 다르다. 대응 관계는 파일 상단 주석 참고.
`patched` 단계의 `zypper update -y` 가 실제로 기여한 몫: 베이스 이미지는 주기적으로만
재빌드되므로 배포판 아카이브보다 뒤처져 있다. 이 단계가 없으면 그 시점의 미패치 취약점이
그대로 남는다 — 자동 재빌드가 필요한 이유가 이것이다.
### 업스트림 빌드 레시피 변경 점검 (수동, 자동화 대상 아님)
`cloudnative-pg/postgres-containers` 저장소가 자체 Dockerfile 구조를 바꾸면(새 확장,
새 하드닝 단계 등) 이 포트도 따라가야 한다. **CI 로 자동 감지하지 않는다** — 배포판이
달라(Debian vs SUSE) 의미 있는 diff 가 안 나온다.
**권장 점검 주기: CNPG 마이너 릴리스마다.** 업스트림 Dockerfile 을 훑어보고 구조가
바뀌었으면 `suse.Dockerfile` 을 손으로 다시 이식한다.
---
## 빌드
```sh
# 카탈로그가 쓰는 베이스 OS (기본값)
IMAGE=cnpg-postgresql BASE_OS=suse bash scripts/build/build-hardened-image.sh /tmp/out
# 레지스트리에 push 까지 (현재 실제 배포 이미지도 이 네임스페이스에 있다)
IMAGE=cnpg-postgresql BASE_OS=suse REGISTRY=docker.io/wbsong111 \
bash scripts/build/build-hardened-image.sh /tmp/out
# 교차 검증을 함께 (사각지대 후보를 뽑는다. CI 에는 아직 연결되지 않았다)
CROSSREF=/tmp/ref/standard-trixie.json IMAGE=cnpg-postgresql BASE_OS=suse \
bash scripts/build/build-hardened-image.sh /tmp/out
```
수행 순서: **빌드 → 기능 검증(`verify.sh`) → SBOM → 전 심각도 스캔 → 게이트 판정.**
기능 검증을 통과하지 못하면 스캔으로 넘어가지 않는다.
CVE 0건이어도 동작하지 않는 이미지는 카탈로그에 넣을 수 없다.
### 파일 구성
| 파일 | 역할 |
| --- | --- |
| `<base-os>.Dockerfile` | 빌드 정의 |
| `<base-os>.build.env` | 베이스·앱 버전·확장 목록. `BUILD_ARGS` 에 나열한 이름만 `--build-arg` 로 전달된다 |
| `verify.sh` | 기능 검증. **베이스 OS 공통** — CNPG 요구사항은 베이스와 무관하다 |
베이스 OS 가 늘어도 이 구조는 그대로다 — `build-hardened-image.sh``BASE_OS` 환경변수로
`<base-os>.build.env` 를 고른다.
### 태그에 빌드일을 넣는다
```
docker.io/wbsong111/cnpg-postgresql:18.4-bci15.7-hardened-20260729
└ 앱 ─┘└ 베이스 ─┘└ 하드닝 ┘└ 빌드일 ┘
```
같은 앱 버전이라도 `zypper update` 결과가 시점마다 다르다. 롤링 태그를 피하고
자체 이미지에도 같은 실수를 하지 않는다(빌드일 포함 고정 태그).
이 태그는 security-catalog 프로젝트에서 이미 빌드·게이트 PASS·push 된 실제 이미지다.
dip-catalog 는 이를 그대로 재사용한다 — 재빌드·재푸시 여부는 `MEMORY.md` 참고.
---
## 실측 (security-catalog, 2026-07-29 / trivy 0.72.0 기준)
| | `bci15.7-hardened-20260728` |
| --- | --- |
| 커버리지 자가진단 | ✅ `ok` |
| 전 심각도 findings | 0 |
| 실효 고유 C/H | **0 / 0** |
| 게이트 | **PASS** |
### 채택 당시 비교 (후보 A/B/C, security-catalog 기준)
| | 업스트림 `standard-trixie` (A) | Ubuntu 24.04 자체빌드 (B, 제거됨) | **SUSE BCI 15.7 (C, 채택)** |
| --- | --- | --- | --- |
| 패키지 수 | 148 | 147 | 182 |
| trivy findings (전 심각도) | 316 | 46 | 0 |
| 실효 고유 C/H | 23 | 0 | 0 |
| 미조사 사각지대 | 0 | 18건 | 18건 |
| 서명·attestation | ✅ | ❌ | ❌ |
| 배포 검증 | — | ✅ failover 2초 | ✅ failover 3초 |
| gid | `postgres` | ⚠️ `tape` | `postgres` |
이 수치는 dip-catalog 에서 재측정한 것이 아니라 security-catalog 프로젝트에서 이관한
기록이다 — dip-catalog 자체 게이트로 재확인은 아직 하지 않았다(`MEMORY.md` 참고).
+11
View File
@@ -0,0 +1,11 @@
# build-image.yml 이 읽는 카탈로그 반영 메타데이터. 빌드 정의(<variant>.build.env)와는
# 별개다 — 이건 "이 이미지가 어느 차트의 어느 필드를 가리키는가" 만 담는다.
# 이 이미지의 태그를 참조하는 차트 버전 디렉토리 (공백 구분, 여러 개 가능)
CHART_DIRS="manifests/helm/cnpg-cluster/1.0.0"
# 태그 표기 스타일 — imageName(단일 필드 문자열) | split(registry/repository/tag 분리)
TAG_STYLE=imageName
# base_os 입력을 안 주면 쓸 기본 변종 (images/<image>/<DEFAULT_BASE_OS>.build.env)
DEFAULT_BASE_OS=suse
+83
View File
@@ -0,0 +1,83 @@
# CloudNativePG PostgreSQL — SUSE BCI 기반 자체 빌드
#
# 업스트림 CNPG Dockerfile(Debian/apt/PGDG-deb)을 SUSE(zypper/PGDG-rpm)로 이식한 것이다.
# ARG BASE 만 바꾸는 수준이 아니라 패키지 관리자·저장소 설정·패키지명·경로가 모두 다르다.
#
# 업스트림과의 대응 관계
# postgresql-common + apt.postgresql.org.sh → rpm --import + zypper addrepo (PGDG zypp)
# postgresql-18 → postgresql18-server
# postgresql-18-pgaudit / -pgvector → pgaudit_18 / pgvector_18
# locales-all → glibc-locale
# usermod -u 26 postgres → 불필요 (PGDG RPM 이 uid 26 으로 생성)
# /usr/lib/postgresql/18/bin → /usr/pgsql-18/bin
#
# CNPG 호환성 근거 (실측)
# - uid 26: PGDG RPM 이 postgres 를 uid/gid 26 으로 만든다 (RPM 계열 관례)
# - 바이너리 탐색: CNPG 는 경로를 하드코딩하지 않고 PATH 로 찾는다.
# cloudnative-pg 소스에서 "usr/lib/postgresql" 는 테스트 픽스처에만 등장한다.
# 그래도 하드코딩 경로를 쓰는 코드가 생길 경우를 대비해 심볼릭 링크를 함께 둔다.
#
# 측정 가능성 — trivy 는 SLES 15.7 을 정상 커버한다.
# 2026-07-29 이전에는 "trivy 의 SUSE 데이터가 15.7 을 커버하지 않는다" 로 판단했으나
# 양성 대조(SBOM 사본에 취약 버전 주입 후 재스캔)로 재측정한 결과 오판이었다 — trivy 는
# 13건을 잡았다(SUSE-SU ID 포함). 0건은 이 이미지가 실제로 최신이라서다.
# scan-sbom.sh 의 커버리지 자가진단이 매 스캔마다 이를 확인한다.
# 경위: doc/analysis/sles-oval-measurement.md
ARG BASE=registry.suse.com/bci/bci-base:15.7
FROM $BASE AS minimal
ARG PG_MAJOR=18
ARG PG_VERSION=18.4-4200001PGDG.sles15.7
ARG SLE_REPO=sles-15.7-x86_64
ARG PGDG_KEY=PGDG-RPM-GPG-KEY-SLES15
ENV PATH=$PATH:/usr/pgsql-${PG_MAJOR}/bin
# PGDG 저장소는 GPG 키를 미리 import 해야 한다. addrepo 만 하면
# "Signature verification failed for repomd.xml" 로 저장소가 스킵된다.
RUN set -eux; \
rpm --import "https://download.postgresql.org/pub/repos/zypp/keys/${PGDG_KEY}"; \
zypper --non-interactive addrepo --refresh \
"https://download.postgresql.org/pub/repos/zypp/${PG_MAJOR}/suse/${SLE_REPO}/" pgdg; \
zypper --non-interactive refresh; \
zypper --non-interactive install -y --no-recommends \
"postgresql${PG_MAJOR}-server=${PG_VERSION}"; \
zypper clean --all; \
rm -rf /var/log/zypp /var/cache/zypp
# CNPG 는 PATH 로 바이너리를 찾지만, Debian 레이아웃을 가정하는 코드가 생길 경우를 대비한다.
RUN set -eux; \
mkdir -p /usr/lib/postgresql; \
ln -sfn "/usr/pgsql-${PG_MAJOR}" "/usr/lib/postgresql/${PG_MAJOR}"; \
id postgres | grep -q 'uid=26(' || { echo "postgres uid 가 26 이 아니다"; exit 1; }
USER 26
FROM minimal AS standard
ARG PG_MAJOR=18
# 업스트림 standard 와 동등한 구성: pgaudit, pgvector, contrib(pg_stat_statements), 전 로케일.
# pg-failover-slots 는 PGDG zypp 저장소에 없어 제외한다 (업스트림 Debian standard 와의 차이).
ARG EXTENSIONS="pgaudit_${PG_MAJOR} pgvector_${PG_MAJOR}"
USER 0
RUN set -eux; \
zypper --non-interactive install -y --no-recommends \
glibc-locale \
"postgresql${PG_MAJOR}-contrib" \
${EXTENSIONS}; \
zypper clean --all; \
rm -rf /var/log/zypp /var/cache/zypp
USER 26
# 베이스 이미지에 남은 구버전 패키지를 보안 업데이트로 올린다.
# Debian 판의 `apt-get upgrade` 단계에 대응한다 (그쪽에서 NVD 기준 HIGH 5건을 해소했다).
FROM standard AS patched
USER 0
RUN set -eux; \
zypper --non-interactive refresh; \
zypper --non-interactive update -y --no-recommends; \
zypper clean --all; \
rm -rf /var/log/zypp /var/cache/zypp
USER 26
+28
View File
@@ -0,0 +1,28 @@
# CNPG PostgreSQL — SUSE BCI / zypper 기반 자체 빌드 (카탈로그 채택 베이스 OS)
#
# build-hardened-image.sh 가 source 한다. BUILD_ARGS 에 나열한 이름만 --build-arg 로 넘어간다.
# BASE_OS 기본값이 이 파일(suse)이다 — 카탈로그의 imageName 이 실제로 이 정의로 빌드된다.
#
# 측정 가능성 — trivy 는 SLES 15.7 을 정상 커버한다 (2026-07-29 재측정, 이전 판단 정정).
# 상세: doc/analysis/sles-oval-measurement.md
DOCKERFILE=suse.Dockerfile
TARGET=patched
TAG_SLUG=bci15.7
# build-hardened-image.sh 가 태그·verify.sh 전달용으로 요구하는 범용 필수값.
# PG_VERSION(아래, PGDG 패키지의 정확한 EVR)에서 메이저.마이너만 뽑은 값과 같다 —
# 예전엔 스크립트가 PG_VERSION 을 파싱해 유도했지만, 이제는 이미지가 명시적으로 선언한다.
APP_VERSION=18.4
BASE=registry.suse.com/bci/bci-base:15.7
PG_MAJOR=18
# PGDG zypp 저장소의 정확한 EVR. apt 쪽과 형식이 다르다.
PG_VERSION=18.4-4200001PGDG.sles15.7
SLE_REPO=sles-15.7-x86_64
PGDG_KEY=PGDG-RPM-GPG-KEY-SLES15
# pg-failover-slots 는 PGDG zypp 저장소에 없어 제외한다 (apt 변종과의 차이).
EXTENSIONS="pgaudit_18 pgvector_18"
BUILD_ARGS="BASE PG_MAJOR PG_VERSION SLE_REPO PGDG_KEY EXTENSIONS"
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# CNPG PostgreSQL 이미지 기능 검증 — 호스트에서 bash 로 실행된다(build-hardened-image.sh 가
# `env TAG=... PLATFORM=... PG_MAJOR=... bash verify.sh` 형태로 호출한다). 이 이미지는 셸
# (`/bin/sh`)이 있으므로, 실제 점검은 컨테이너 안에서 게스트 셸 스크립트로 수행한다.
#
# 마지막 줄에 VERIFY-OK 를 출력하면 통과다. build-hardened-image.sh 가 그것으로 판정한다.
#
# 왜 이미지 디렉토리에 두나: 검증 항목은 이미지마다 다르다. 빌드 스크립트에 하드코딩하면
# 이미지가 둘 이상이 될 때 깨진다. 실제로 apt·suse 두 변종이 생겨 분리했다.
#
# CNPG 가 이미지에 요구하는 것(오퍼레이터 소스 기준)
# - uid 26 — 다르면 오퍼레이터가 관리하는 볼륨 권한이 깨진다
# - PATH 에서 initdb/pg_ctl/postgres 를 찾을 수 있어야 한다 (경로 하드코딩 아님)
# - 차트가 선언한 확장이 실제로 생성되어야 한다
set -e
TAG="${TAG:?TAG 환경변수가 필요하다}"
PLATFORM="${PLATFORM:-linux/amd64}"
PG_MAJOR="${PG_MAJOR:?PG_MAJOR 환경변수가 필요하다 (build-hardened-image.sh 가 build.env 에서 전달)}"
docker run --rm -i --platform "$PLATFORM" -e PG_MAJOR="$PG_MAJOR" --entrypoint sh "$TAG" <<'GUEST'
set -e
export PGDATA=/tmp/_verify
export PATH="/usr/lib/postgresql/${PG_MAJOR}/bin:/usr/pgsql-${PG_MAJOR}/bin:$PATH"
echo "== 실행 사용자 =="
uid=$(id -u)
gid=$(id -g)
gname=$(id -gn 2>/dev/null || echo '?')
echo " uid=$uid gid=$gid($gname)"
[ "$uid" = "26" ] || { echo "FAIL: uid=$uid — CNPG 는 26 을 요구한다"; exit 1; }
# gid 는 실패시키지 않고 경고만 한다. 실측 사례: Ubuntu 베이스에서 gid 26 이 `tape`
# 그룹에 선점되어 있어 postgres 가 tape 그룹으로 동작했다. 기능은 동작하지만 위생 문제다.
if [ "$gname" != "postgres" ]; then
echo " WARN: gid $gid 의 그룹명이 '$gname' 이다 (postgres 가 아님)"
fi
echo "== 바이너리 탐색 (PATH 기준) =="
for b in initdb pg_ctl postgres psql; do
p=$(command -v "$b") || { echo "FAIL: $b 를 PATH 에서 찾을 수 없다"; exit 1; }
echo " $b → $p"
done
echo "== initdb + 기동 =="
initdb -D "$PGDATA" -A trust >/dev/null
pg_ctl -D "$PGDATA" -w start >/dev/null \
-o "-c shared_preload_libraries=pgaudit,pg_stat_statements -c unix_socket_directories=/tmp"
psql -h /tmp -U postgres -Atc "SELECT version();" | sed 's/^/ /'
echo "== 확장 생성 =="
# 차트(cnpg-cluster)가 Database CRD 로 선언하는 것과 같은 목록이다.
psql -h /tmp -U postgres -q -c \
"CREATE EXTENSION pgaudit; CREATE EXTENSION vector; CREATE EXTENSION pg_stat_statements;"
psql -h /tmp -U postgres -Atc \
"SELECT extname||' '||extversion FROM pg_extension ORDER BY 1;" | sed 's/^/ /'
echo "== libxml2 링크 (XML 함수) =="
psql -h /tmp -U postgres -Atc \
"SELECT xml_is_well_formed_document('<a>x</a>');" | sed 's/^/ /'
echo "== 로케일 =="
# 업스트림 standard 는 전 로케일을 포함한다. 줄어들면 정렬·비교 동작이 달라진다.
n=$(psql -h /tmp -U postgres -Atc "SELECT count(*) FROM pg_collation;")
echo " pg_collation: ${n}개"
[ "$n" -gt 100 ] || echo " WARN: collation 이 ${n}개다 — 로케일 패키지 누락 의심"
pg_ctl -D "$PGDATA" -w stop >/dev/null
echo "VERIFY-OK"
GUEST
+95
View File
@@ -0,0 +1,95 @@
# etcd — 자체 빌드
etcd 서버/`etcdctl`/`etcdutl` 바이너리를 업스트림 소스에서 직접 컴파일한다.
`manifests/helm/etcd/1.1.12/custom-values.yaml``image.repository`/`image.tag`
이 산출물을 가리킨다.
security-catalog 프로젝트에서 포팅했다. 목표 정의·CVE 조사 실측·이미지 채택 결정
근거(각각 `doc/cve-zero-pipeline.md`, `doc/analysis/etcd-cve.md`,
`doc/decisions/0007-etcd-image-self-build.md`)는 security-catalog 프로젝트에 있고
dip-catalog 에는 아직 이관되지 않았다 — 신규 자체 빌드 이미지 추가 절차 전반은
[.claude/image-authoring.md](../../.claude/image-authoring.md) 참고.
> **자체 빌드는 대응 우선순위 3번이다.** 상위 태그 교체·베이스 OS 교체로 목표를
> 만족할 수 있으면 그 쪽을 쓴다. 자체 빌드는 업스트림 서명·provenance·SBOM attestation 을
> 잃고 재빌드 책임을 지는 선택이다.
## 왜 자체 빌드하나 — 그리고 왜 `cloudnative-pg` 와 같은 방법인가
업스트림 `quay.io/coreos/etcd:v3.7.1` 은 실효 HIGH 1건(`CVE-2026-56852`,
`golang.org/x/text` — 깨진 UTF-8 입력에 대한 `norm.Iter` 무한루프 DoS)으로 차단된다.
상위 태그가 없다(2026-07-31 확인, 최신 릴리스가 여전히 `v3.7.1`이고 `release-3.7`
브랜치도 아직 이 CVE 를 백포트하지 않았다). 업스트림 배포 이미지 베이스가
`gcr.io/distroless/static-debian12`(OS 패키지 사실상 0개)라 **베이스 OS 를 바꾸는 것만
으로는 고쳐지지 않는다** — CVE 는 바이너리에 정적 링크된 Go 모듈 버전이 원인이다.
자체 빌드(소스 컴파일)만 유효한 대응이다.
이는 `cloudnative-pg` 오퍼레이터 자체 빌드와 **완전히 같은 유형의 문제**(같은
CVE-2026-56852)이고 대응도 같다 — **오케스트레이션은 동일한
[scripts/build/build-hardened-image.sh](../../scripts/build/build-hardened-image.sh)
하나를 공유한다.**
## 소스·버전 관리
| 항목 | 값 |
| --- | --- |
| 소스 | `https://github.com/etcd-io/etcd.git` |
| pinned commit | `source.build.env``SOURCE_COMMIT``v3.7.1` 태그가 가리키는 실제 커밋 |
| 빌더 | 공식 `golang` 이미지 (`source.build.env``GO_BUILDER_TAG`, go.mod 요구 버전과 일치) |
| 최종 베이스 | `registry.suse.com/bci/bci-micro:15.7` — security-catalog 는 SUSE BCI 하나만 쓰기로 결정했다(ADR 미이관); 업스트림의 distroless 대신 여기 적용 |
빌더 스테이지(Go 컴파일)는 공식 `golang` 이미지를 그대로 쓴다 — 최종 이미지에 남는 것이
아니라 컴파일 산출물만 최종 스테이지로 넘어오므로 스캔·정책 대상이 아니다.
**업스트림과 다른 부분은 딱 하나다**`golang.org/x/text` 를 워크스페이스 전역
`go.work` `replace` 한 줄로 `0.39.0` 이상으로 강제 업그레이드한다(각 서브모듈의
`go.mod` 를 개별로 고치지 않는다). 그 외 빌드 절차(`server`/`etcdutl`/`etcdctl`
바이너리를 각각 컴파일, `CGO_ENABLED=0` 정적 링크)는 업스트림
`scripts/build_lib.sh``etcd_build()` 를 그대로 재현한다.
`SOURCE_COMMIT`**자동 추적하지 않는다.** `cloudnative-pg` 와 동일하게 사람이
업스트림 `release-3.7` 브랜치(또는 그다음 패치 릴리스가 나오면 그 태그)를 보고
`source.build.env` 를 고쳐 PR 을 여는 것 자체가 갱신 트리거다.
**권장 점검 주기**: 카탈로그 게이트가 이 이미지의 차단 CVE 를 다시 보고할 때, 또는
업스트림이 `v3.7.2`(혹은 그 이상, `x/text` 백포트 포함)를 릴리스했을 때 — 릴리스가
나오면 그쪽으로 갈아타는 것(대응 우선순위 a)이 이 자체 빌드를 유지하는 것보다 항상
우선이다.
## 빌드
```sh
# 로컬 빌드 (push 없음)
IMAGE=etcd BASE_OS=source bash scripts/build/build-hardened-image.sh /tmp/out
# 레지스트리에 push 까지 (현재 실제 배포 이미지도 이 네임스페이스에 있다)
IMAGE=etcd BASE_OS=source REGISTRY=docker.io/wbsong111 \
bash scripts/build/build-hardened-image.sh /tmp/out
```
수행 순서: **빌드 → 기능 검증(`verify.sh`) → SBOM → 전 심각도 스캔 → 게이트 판정.**
`verify.sh``etcd`/`etcdctl`/`etcdutl` 이 PATH 에 있는지, `etcd --version` 출력에
pinned commit 이 실제로 반영됐는지(ldflags 주입 확인), 단일 노드로 기동해 `etcdctl`
put/get 왕복이 실제로 동작하는지를 확인한다 — 다중 노드 쿼럼·TLS 등은 이 스모크 테스트
범위 밖이며, dev 클러스터 배포 테스트(`.claude/deploy-test-procedure.md`,
`scripts/deploy-test/deploy-test-etcd.sh`)가 담당한다.
### 파일 구성
| 파일 | 역할 |
| --- | --- |
| `source.Dockerfile` | 빌드 정의 — 소스 컴파일(builder 스테이지) + SUSE BCI(`bci-micro`) 패키징(final 스테이지) |
| `source.build.env` | pinned commit·버전·빌더 이미지 태그. `BUILD_ARGS` 에 나열한 이름만 `--build-arg` 로 전달된다 |
| `verify.sh` | 기능 검증. 호스트에서 bash 로 실행되며 `docker run --entrypoint sh` 로 게스트 셸 스크립트를 주입한다(`bci-micro` 는 bash·coreutils 가 있다 — `cnpg-postgresql/verify.sh` 와 같은 패턴, `cloudnative-pg` 의 셸 없는 distroless 와는 다르다) |
베이스 변종이 하나뿐이라 파일명이 `source.*` 로 고정돼 있다.
### 태그
```
docker.io/wbsong111/etcd:3.7.1-security-hardened-20260731
└ app ┘└ 슬러그 ┘└ 하드닝 ┘└ 빌드일 ┘
```
이 태그는 security-catalog 프로젝트에서 이미 빌드·게이트 PASS·push 된 실제 이미지다.
dip-catalog 는 이를 그대로 재사용한다 — 재빌드·재푸시 여부는 `MEMORY.md` 참고.
+11
View File
@@ -0,0 +1,11 @@
# build-image.yml 이 읽는 카탈로그 반영 메타데이터. 빌드 정의(<variant>.build.env)와는
# 별개다 — 이건 "이 이미지가 어느 차트의 어느 필드를 가리키는가" 만 담는다.
CHART_DIRS="manifests/helm/etcd/1.1.12"
# 태그 표기 스타일 — imageName(단일 필드 문자열) | split(registry/repository/tag 분리)
TAG_STYLE=split
TAG_BLOCK=image
# base_os 입력을 안 주면 쓸 기본 변종 (images/<image>/<DEFAULT_BASE_OS>.build.env)
DEFAULT_BASE_OS=source
+83
View File
@@ -0,0 +1,83 @@
# etcd — 업스트림 소스를 pinned commit 으로 직접 컴파일한다.
#
# 업스트림(https://github.com/etcd-io/etcd) 의 루트 Dockerfile 은 이미 빌드된 바이너리
# (`etcd`/`etcdctl`/`etcdutl`)를 ADD 만 한다 — 컴파일 자체는 `scripts/build.sh` →
# `scripts/build_lib.sh` 의 `etcd_build()` 가 Dockerfile 밖에서 수행한다. 우리는 그 빌드
# 단계를 Dockerfile 안으로 가져와 `go build` 로 직접 재현한다(cloudnative-pg 자체 빌드와
# 동일한 패턴 — doc/decisions/0005 참고).
#
# 왜 자체 빌드인가 — quay.io/coreos/etcd:v3.7.1 이 게이트에서 차단하는 HIGH 1건
# (CVE-2026-56852, golang.org/x/text)은 OS 패키지가 아니라 바이너리에 정적 링크된 Go
# 모듈 버전이 원인이다. etcd v3.7.1/release-3.7 브랜치는 go.mod 에 x/text v0.37.0(취약)을
# 고정하고 있고 상위 패치 릴리스가 아직 없다(2026-07-31 확인) — 베이스 OS 교체로는
# 고쳐지지 않으므로 자체 빌드가 유일한 대응이다. 근거: doc/analysis/etcd-cve.md
#
# 업스트림과의 대응 관계 (v3.7.1 태그, scripts/build_lib.sh 의 etcd_build() 기준)
# cd server && go build ... → 동일 (ldflags 는 GitSHA 대신 pinned commit 을 직접 주입 — 아래 참고)
# cd etcdutl && go build ... → 동일
# cd etcdctl && go build ... → 동일
# distroless/static-debian12 → SUSE BCI(bci-micro)로 교체. 카탈로그는 SUSE BCI 하나만
# 쓴다(decisions/0001) — builder 스테이지(Go 컴파일)는 공식 golang 이미지를 그대로 쓴다
#
# 업스트림과 다르게 하는 부분 — x/text 강제 업그레이드
# etcd 는 Go 워크스페이스(go.work)로 서버/etcdctl/etcdutl 모듈을 함께 관리한다.
# 각 go.mod 를 개별로 고치는 대신 go.work 에 워크스페이스 전역 replace 한 줄만 추가해
# golang.org/x/text 를 강제 업그레이드한다 — 업스트림과의 차이를 최소로 유지하기 위함
# (doc/image-selection.md 7절 "빌드 원칙: 업스트림과의 차이를 최소로 유지한다").
#
# ldflags — GitSHA 대신 pinned commit 을 직접 주입하는 이유
# 업스트림은 `git rev-parse --short HEAD` 로 얻은 값을 버전 문자열에 심는다. 우리는
# BuildKit 의 git context(`ADD ...#${SOURCE_COMMIT}`)로 체크아웃하므로 커밋을 이미 알고
# 있다 — 호스트의 verify.sh 가 굳이 컨테이너 안에서 git 을 실행하지 않고도 버전 문자열에
# pinned commit 이 반영됐는지 확인할 수 있도록, 알고 있는 전체 커밋 해시를 그대로 심는다
# (cloudnative-pg/source.Dockerfile 과 동일한 이유).
# syntax=docker/dockerfile:1
ARG GO_BUILDER_TAG=1.26.5-trixie
# FROM 에서 쓰는 ARG 는 반드시 첫 FROM 이전(전역 스코프)에 선언해야 한다 — .claude/image-authoring.md
# 3번 항목, cloudnative-pg/source.Dockerfile 에서 실제로 겪은 버그.
ARG RUNTIME_BASE=registry.suse.com/bci/bci-micro:15.7
# 호스트 네이티브 아키텍처로 빌더를 띄운다. Go 크로스컴파일은 에뮬레이션이 필요 없다.
FROM --platform=$BUILDPLATFORM golang:${GO_BUILDER_TAG} AS builder
ARG TARGETARCH
ARG SOURCE_COMMIT
ARG APP_VERSION
ARG XTEXT_FIX_VERSION
WORKDIR /src
# BuildKit 의 git context 지원 — tarball+checksum 관리 없이 git 자체가 커밋 무결성을 보장한다.
ADD https://github.com/etcd-io/etcd.git#${SOURCE_COMMIT} /src
# CVE-2026-56852 대응: golang.org/x/text 를 워크스페이스 전역으로 강제 업그레이드한다.
# `go work sync` 가 이 replace 를 각 모듈(server/etcdctl/etcdutl 등)의 go.sum 에 반영한다.
RUN echo "replace golang.org/x/text => golang.org/x/text v${XTEXT_FIX_VERSION}" >> go.work \
&& go work sync
# 업스트림 scripts/build_lib.sh 의 etcd_build() 를 그대로 재현한다.
# (GOARCH 는 TARGETARCH 로 대체, GitSHA 대신 pinned commit 전체 해시를 심는다 — 위 설명 참고)
RUN --mount=type=cache,target=/root/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
set -eux; \
mkdir -p /out; \
LDFLAGS="-X=go.etcd.io/etcd/api/v3/version.GitSHA=${SOURCE_COMMIT}"; \
( cd server && CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -installsuffix=cgo -ldflags="${LDFLAGS}" -o /out/etcd . ); \
( cd etcdutl && CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -installsuffix=cgo -ldflags="${LDFLAGS}" -o /out/etcdutl . ); \
( cd etcdctl && CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} go build -trimpath -installsuffix=cgo -ldflags="${LDFLAGS}" -o /out/etcdctl . )
FROM ${RUNTIME_BASE} AS final
# 업스트림 루트 Dockerfile 과 동일한 경로·구조 — chart(groundhog2k/etcd) 의 startup/liveness/
# readiness probe 가 `/usr/local/bin/etcdctl` 을 하드코딩해 호출하므로 경로를 바꾸면 안 된다.
COPY --from=builder /out/etcd /usr/local/bin/etcd
COPY --from=builder /out/etcdctl /usr/local/bin/etcdctl
COPY --from=builder /out/etcdutl /usr/local/bin/etcdutl
COPY --from=builder /src/LICENSE /licenses/LICENSE
WORKDIR /var/etcd/
WORKDIR /var/lib/etcd/
EXPOSE 2379 2380
CMD ["/usr/local/bin/etcd"]
+37
View File
@@ -0,0 +1,37 @@
# etcd — 소스 컴파일형 자체 빌드 (유일한 변종)
#
# build-hardened-image.sh 가 source 한다. BUILD_ARGS 에 나열한 이름만 --build-arg 로 넘어간다.
# 이 이미지는 소스를 직접 컴파일하므로 "베이스 OS" 선택지가 없다 — BASE_OS=source 로 호출한다:
# IMAGE=etcd BASE_OS=source bash scripts/build/build-hardened-image.sh <OUT_DIR>
#
# 왜 자체 빌드하는가·CVE 근거 → doc/analysis/etcd-cve.md
# 결정 → doc/decisions/0007-etcd-image-self-build.md
DOCKERFILE=source.Dockerfile
TARGET=final
TAG_SLUG=security
# build-hardened-image.sh 가 태그·verify.sh 전달용으로 요구하는 범용 필수값.
# 업스트림 stock 이미지(quay.io/coreos/etcd:v3.7.1)와는 태그 슬러그(TAG_SLUG=security)로
# 구분되므로 버전 문자열 자체는 그대로 둔다.
APP_VERSION=3.7.1
# v3.7.1 태그가 가리키는 실제 커밋(태그 객체가 아니라 peeled commit), 2026-07-31 확인:
# git ls-remote --tags https://github.com/etcd-io/etcd.git v3.7.1 v3.7.1^{}
SOURCE_COMMIT=5e7fd0de9a57db03ecc11794dc40403a734c07bb
# go.mod 의 `go 1.26`/`toolchain go1.26.5` 요구를 만족하는 공식 golang 이미지 태그.
# 이 이미지는 builder 스테이지에서만 쓰이고 최종 이미지에는 남지 않는다.
GO_BUILDER_TAG=1.26.5-trixie
# 최종 런타임 베이스 — 카탈로그는 SUSE BCI 하나만 쓴다(decisions/0001). 정적 링크
# 바이너리(CGO_ENABLED=0)라 패키지 매니저가 없는 가장 가벼운 bci-micro 로 충분하다.
RUNTIME_BASE=registry.suse.com/bci/bci-micro:15.7
# CVE-2026-56852 대응 — golang.org/x/text 를 이 버전 이상으로 강제 업그레이드한다.
# v3.7.1/release-3.7 브랜치는 아직 이 CVE 를 백포트하지 않았다(go.mod 실측 x/text
# v0.37.0). etcd 메인테이너가 release-3.7 브랜치에 패치를 백포트하면(그리고 v3.7.2 가
# 나오면) 이 자체 빌드는 걷어내고 상위 태그로 돌아간다(대응 우선순위 a 가 c 보다 우선).
XTEXT_FIX_VERSION=0.39.0
BUILD_ARGS="SOURCE_COMMIT GO_BUILDER_TAG APP_VERSION RUNTIME_BASE XTEXT_FIX_VERSION"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# etcd 이미지 기능 검증 — 호스트에서 bash 로 실행된다(build-hardened-image.sh 가
# `env TAG=... PLATFORM=... SOURCE_COMMIT=... XTEXT_FIX_VERSION=... bash verify.sh`
# 형태로 호출한다). 최종 베이스가 bci-micro 라 bash/coreutils 가 있으므로(패키지
# 매니저만 없음 — .claude/image-authoring.md), cnpg-postgresql/verify.sh 와 같은
# 게스트 셸 패턴을 쓴다(cloudnative-pg 처럼 셸 없는 distroless 가 아니다).
#
# 마지막 줄에 VERIFY-OK 를 출력하면 통과다. build-hardened-image.sh 가 그것으로 판정한다.
#
# 이 이미지에 요구하는 것
# - /usr/local/bin/etcd·etcdctl·etcdutl 이 PATH 에 있어야 한다 (chart 의 startup/
# liveness/readiness probe 가 /usr/local/bin/etcdctl 을 하드코딩해 호출한다)
# - 단일 노드로 기동해 put/get 왕복이 실제로 동작해야 한다 (게이트 0건이어도 못 쓰는
# 이미지는 무의미하다 — .claude/image-authoring.md 5번)
set -e
TAG="${TAG:?TAG 환경변수가 필요하다}"
PLATFORM="${PLATFORM:-linux/amd64}"
SOURCE_COMMIT="${SOURCE_COMMIT:?SOURCE_COMMIT 환경변수가 필요하다 (build-hardened-image.sh 가 build.env 에서 전달)}"
docker run --rm -i --platform "$PLATFORM" -e SOURCE_COMMIT="$SOURCE_COMMIT" --entrypoint sh "$TAG" <<'GUEST'
set -e
echo "== 바이너리 탐색 (PATH 기준) =="
for b in etcd etcdctl etcdutl; do
p=$(command -v "$b") || { echo "FAIL: $b 를 PATH 에서 찾을 수 없다"; exit 1; }
echo " $b -> $p"
done
echo "== 버전 (pinned commit 반영 확인) =="
# sed 가 없다(bci-micro 는 bash·coreutils 는 있지만 sed 는 별도 패키지라 빠져 있다 —
# 2026-07-31 CI 에서 실측: "sh: sed: command not found"). 순수 셸 루프로 들여쓴다.
OUT="$(etcd --version)"
while IFS= read -r line; do echo " $line"; done <<EOF
$OUT
EOF
case "$OUT" in
*"$SOURCE_COMMIT"*) ;;
*) echo "FAIL: 버전 출력에 pinned commit($SOURCE_COMMIT) 이 없다 — ldflags 주입 확인 필요"; exit 1 ;;
esac
echo "== 단일 노드 기동 =="
export ETCD_DATA_DIR=/tmp/verify-data
export ETCD_LISTEN_CLIENT_URLS=http://127.0.0.1:2379
export ETCD_ADVERTISE_CLIENT_URLS=http://127.0.0.1:2379
export ETCD_LISTEN_PEER_URLS=http://127.0.0.1:12380
export ETCD_INITIAL_ADVERTISE_PEER_URLS=http://127.0.0.1:12380
export ETCD_INITIAL_CLUSTER=default=http://127.0.0.1:12380
export ETCD_NAME=default
etcd >/tmp/etcd-verify.log 2>&1 &
ETCD_PID=$!
trap 'kill $ETCD_PID 2>/dev/null || true' EXIT
echo "== health 대기 (최대 30s) =="
ok=0
for i in $(seq 1 30); do
if etcdctl endpoint health >/dev/null 2>&1; then ok=1; break; fi
sleep 1
done
if [ "$ok" != "1" ]; then
echo "FAIL: etcd 가 30초 내에 healthy 상태가 되지 못했다"
cat /tmp/etcd-verify.log
exit 1
fi
HEALTH="$(etcdctl endpoint health)"
while IFS= read -r line; do echo " $line"; done <<EOF
$HEALTH
EOF
echo "== put/get 왕복 =="
etcdctl put smoke-test ok >/dev/null
answer="$(etcdctl get smoke-test --print-value-only)"
[ "$answer" = "ok" ] || { echo "FAIL: get 응답 이상 (got: '$answer')"; exit 1; }
echo " get smoke-test => '$answer'"
kill "$ETCD_PID" 2>/dev/null || true
trap - EXIT
echo "VERIFY-OK"
GUEST
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
@@ -0,0 +1,102 @@
# cloudnative-pg 버전 갱신 가이드
## 1. git 작업 환경 구성
```sh
git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog
git checkout -b update-cloudnative-pg/<신규버전>
```
## 2. helm chart 업데이트
### 1) 신규 버전 확인
```sh
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update cnpg
helm search repo cnpg/cloudnative-pg --versions | head
```
차트 버전과 operator 버전(appVersion)은 다르다. 대응 관계를 반드시 확인한다.
| 차트 버전 | operator(appVersion) |
| --- | --- |
| 0.29.0 | 1.30.0 |
| 0.28.3 | 1.29.1 |
| 0.27.1 | 1.28.1 |
### 2) 신규 버전 디렉토리 생성
버전별 독립 디렉토리다. 기존 디렉토리를 수정하지 않고 새로 만든다.
```sh
NEW=0.30.0
OLD=0.29.0
cd manifests/helm/cloudnative-pg
# 업스트림 차트를 신규 버전 디렉토리로 내려받는다
helm pull cnpg/cloudnative-pg --version "$NEW" --untar --untardir /tmp/cnpg-pull
mkdir -p "$NEW"
cp -R /tmp/cnpg-pull/cloudnative-pg/. "$NEW"/
# PaaSup 관리 파일을 이전 버전에서 승계한다
for f in custom-values.yaml dip-values.yaml dip-resources-quotas.yaml \
CUSTOM-README.md BUILD-README.md; do
cp "$OLD/$f" "$NEW/$f"
done
```
### 3) diff 확인
업스트림 `values.yaml` 변경으로 `custom-values.yaml` 의 키가 사라졌는지 확인한다.
이게 Breaking Change 판단의 핵심이다.
```sh
diff -u "$OLD/values.yaml" "$NEW/values.yaml" | less
# custom-values.yaml 의 각 키가 신규 values.yaml 에 존재하는지 검증
helm template test "$NEW" -f "$NEW/custom-values.yaml" >/dev/null && echo "렌더링 OK"
```
`values.schema.json` 이 있으므로 없는 키를 넘기면 렌더링이 실패한다. 위 명령이 통과해야 한다.
### 4) 렌더링 결과 비교
```sh
helm template cnpg "$OLD" -f "$OLD/custom-values.yaml" -n cnpg-system > /tmp/old.yaml
helm template cnpg "$NEW" -f "$NEW/custom-values.yaml" -n cnpg-system > /tmp/new.yaml
diff -u /tmp/old.yaml /tmp/new.yaml
```
RBAC 규칙 추가/삭제, webhook 경로 변경, CRD 필드 변경을 특히 주의해서 본다.
## 3. 문서 갱신
| 파일 | 갱신 내용 |
| --- | --- |
| `CUSTOM-README.md` | 차트/operator 버전 번호, 업그레이드 명령의 CRD URL, 변경된 values 키 |
| `BUILD-README.md` | 위 버전 대응 표에 신규 행 추가 |
| `doc/charts/cnpg/deploy-test.md` | 신규 버전으로 배포 테스트 재실행 후 결과 갱신 |
| `manifests/helm/cnpg-cluster/*/CUSTOM-README.md` | operator 버전 호환성 명시 부분 |
## 4. 배포 검증
`doc/charts/cnpg/deploy-test.md` 의 검증 절차를 신규 버전으로 재실행한다.
최소한 다음 4개는 통과해야 한다.
1. operator Pod `Running`, CRD 11개 생성
2. `cnpg-cluster` 차트로 3-instance Cluster 배포 → `readyInstances=3/3`
3. `-rw` / `-ro` 서비스 라우팅 (primary / replica 분리)
4. primary Pod 삭제 → failover 후 쓰기 복구, 데이터 정합성 유지
## 5. PR
```sh
git add manifests/helm/cloudnative-pg/<신규버전> doc/
git commit -m "cloudnative-pg <신규버전> 추가"
git push -u origin update-cloudnative-pg/<신규버전>
```
PR 생성 시 `helm-catalog-sbom` 워크플로가 변경 차트에 대해 SBOM·취약점 스캔을 수행한다.
CRITICAL 취약점이 있으면 내용을 확인하고 PR 본문에 판단 근거를 남긴다.
@@ -0,0 +1,169 @@
# CloudNativePG Operator 배포
차트 버전 `0.29.0` / operator 버전 `1.30.0`
CloudNativePG 는 PostgreSQL 을 Kubernetes 오퍼레이터로 운영하는 CNCF 프로젝트다.
이 차트는 **오퍼레이터만** 설치한다. 실제 DB 클러스터는 `cnpg-cluster` 차트로 배포한다.
```
cloudnative-pg (operator) → CRD + controller + webhook
↓ 감시
cnpg-cluster (Cluster CR) → PostgreSQL primary/replica Pod, PVC, Service
```
## 1. 배포 방법
### 1) 배포 시 주의 사항
- **CRD 와 webhook 은 cluster-scoped 리소스다.** 네임스페이스를 분리해도 클러스터 전체에 하나만 존재한다.
여러 팀이 각자 오퍼레이터를 설치하면 CRD 버전이 충돌한다. 클러스터당 오퍼레이터는 1개만 둔다.
- **webhook 의 `failurePolicy``Fail` 이다.** 오퍼레이터 Pod 가 없는 상태에서는 `Cluster` 리소스의
생성·수정·삭제가 모두 거부된다. 오퍼레이터를 제거할 때는 webhook 설정을 반드시 함께 삭제해야 한다
(아래 [4. 제거](#4-제거) 참고).
- **`helm upgrade` 는 CRD 를 갱신하지 않는다.** Helm 의 CRD 처리 방식 때문에 버전 업그레이드 시
CRD 를 수동 apply 해야 한다.
- Kubernetes 1.25+ 필요. 검증 환경은 RKE2 v1.34.1 이다.
### 2) 배포
```sh
git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog/manifests/helm/cloudnative-pg/0.29.0
helm upgrade cnpg ./ -f custom-values.yaml --install -n cnpg-system --create-namespace --wait
```
### 3) 확인
```sh
kubectl -n cnpg-system get pods
kubectl get crd | grep cnpg # 11개
```
> `kubectl get cluster` 는 쓰지 않는다. Rancher(`clusters.management.cattle.io`) 와
> CAPI(`clusters.cluster.x-k8s.io`) 가 같은 단축명을 쓰기 때문에 엉뚱한 리소스가 조회된다.
> 반드시 `kubectl get clusters.postgresql.cnpg.io` 로 FQN 을 쓴다.
## 2. custom-values.yaml 설명
### 1) 이미지 설정
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `image.repository` | 오퍼레이터 이미지. 오프라인 환경에서는 사내 미러 경로로 변경 | `ghcr.io/cloudnative-pg/cloudnative-pg` |
| `image.tag` | 미설정 시 차트 `appVersion`(1.30.0) 사용. 버전 변경은 차트 교체를 우선한다 | `""` |
| `imagePullSecrets` | 사설 레지스트리 인증 시크릿 | `[]` |
### 2) 감시 범위 (RBAC 영향)
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `config.clusterWide` | `true` = ClusterRole 로 전체 네임스페이스 감시. `false` = 설치 네임스페이스만 감시하고 RBAC 이 Role 로 축소됨 | `true` |
| `config.data.WATCH_NAMESPACE` | `clusterWide: true` 상태에서 감시 대상을 특정 네임스페이스로 한정 (쉼표 구분) | 미설정 |
| `config.maxConcurrentReconciles` | 동시 reconcile 수 | `10` |
#### 보안 관점 — 실측 RBAC 비교 (operator 1.30.0)
`clusterWide` 는 감시 범위와 **RBAC 범위를 함께** 바꾼다. 실제로 렌더링해 측정한 결과다.
| | `clusterWide: true` | `clusterWide: false` |
| --- | --- | --- |
| ClusterRole 규칙 수 | 다수 (전 리소스) | **3개** |
| ClusterRole 이 다루는 리소스 | `secrets`, `pods`, `pods/exec`, `serviceaccounts`, `roles`, `rolebindings`, `deployments`, `configmaps`, PVC, webhook 설정, `nodes` … | `nodes`(RO), `clusterimagecatalogs`(RO), webhook 설정(get/patch) |
| 네임스페이스 Role | 없음 | 생성됨 (20 규칙, 설치 NS 한정) |
| ClusterRoleBinding | 생성됨 | 생성됨 (축소된 ClusterRole 에 바인딩) |
**`clusterWide: true` 의 실제 위험도:** 오퍼레이터 ServiceAccount 가 클러스터 전체에 대해
다음을 갖는다.
- `secrets` 전체 CRUD → 모든 네임스페이스의 모든 시크릿 열람 (Harbor·Keycloak·Infisical 토큰 포함)
- `pods/exec` → 임의 네임스페이스의 임의 Pod 에 exec
- `roles` / `rolebindings` 생성 → **권한 상승 경로**
- `serviceaccounts`, `deployments` 조작
- `mutatingwebhookconfigurations` / `validatingwebhookconfigurations` patch → 어드미션 제어 변경
이 조합은 실질적으로 **cluster-admin 에 준한다.** 오퍼레이터 Pod 가 침해되면 클러스터 전체가
침해된다고 봐야 한다.
**`WATCH_NAMESPACE` 는 보안 경계가 아니다.** 이 값은 오퍼레이터가 *reconcile 할 대상*만
좁힌다. RBAC 은 그대로 cluster-wide 로 남으므로 토큰의 권한은 줄어들지 않는다.
심층 방어(defense-in-depth) 수단일 뿐, 권한 축소로 오해하면 안 된다.
**권고**
- **보안이 우선이면 `clusterWide: false`** — 테넌트 네임스페이스마다 오퍼레이터를 따로 설치한다.
다만 CRD 와 webhook 설정은 cluster-scoped 싱글턴이므로 **모든 오퍼레이터의 버전이 같아야 하고**,
각 설치가 동일한 webhook 설정을 patch 하려고 경쟁한다. 운영 복잡도가 크게 올라간다.
- **운영 편의가 우선이면 `clusterWide: true`** — 단, 위 권한을 감수하는 결정임을 명시하고
다음 보완책을 함께 적용한다.
- 오퍼레이터 네임스페이스에 접근 가능한 주체를 최소화 (`cnpg-system` 을 별도 관리)
- 오퍼레이터 Pod 의 exec/attach 를 Kyverno 등으로 차단
- 감사 로그에서 오퍼레이터 SA 의 `secrets` 접근을 모니터링
- `clusterimagecatalogs` 로 허용 이미지를 고정해 임의 이미지 기동을 막는다
배포 테스트에서는 격리를 위해 `clusterWide: false` 를 사용했다.
`custom-values.yaml` 기본값은 업스트림과 동일한 `true` 이므로, 도입 시 이 결정을 반드시
검토해야 한다.
### 3) Webhook
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `webhook.port` | webhook 서비스 포트 | `9443` |
| `webhook.mutating.create` | mutating webhook 생성 여부 | `true` |
| `webhook.validating.create` | validating webhook 생성 여부 | `true` |
| `webhook.*.failurePolicy` | `Fail` 유지 권장. `Ignore` 로 바꾸면 검증 없이 잘못된 Cluster 스펙이 통과된다 | `Fail` |
### 4) 모니터링
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `monitoring.podMonitorEnabled` | Prometheus Operator CRD 필요. 없으면 배포 실패 | `false` |
| `monitoring.grafanaDashboard.create` | Grafana 대시보드 ConfigMap 생성 | `false` |
### 5) 리소스
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `resources` | 오퍼레이터 Pod 의 cpu/memory. 티어별 값은 `dip-resources-quotas.yaml` 참고 | 업스트림은 `{}` (무제한) |
| `replicaCount` | leader election 기반이라 2 이상은 가용성 목적 (reconcile 은 리더 1개가 수행) | `1` |
## 3. 업그레이드
```sh
# 1) CRD 를 먼저 수동 갱신 (helm upgrade 는 CRD 를 건드리지 않음)
kubectl apply --server-side -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/v1.30.0/releases/cnpg-1.30.0.yaml \
--dry-run=server # 먼저 dry-run 으로 영향 확인
# 2) 차트 업그레이드
helm upgrade cnpg ./ -f custom-values.yaml -n cnpg-system --wait
```
오퍼레이터 업그레이드는 실행 중인 `Cluster` 의 인스턴스를 롤링 재시작시킨다.
`primaryUpdateStrategy` 설정에 따라 primary 전환이 발생하므로 서비스 영향 시간을 고려해야 한다.
## 4. 제거
**순서가 중요하다.** webhook 이 `failurePolicy: Fail` 이므로 오퍼레이터를 먼저 지우면
`Cluster` 리소스를 삭제할 수 없게 된다.
```sh
# 1) 먼저 모든 Cluster 리소스 삭제
kubectl get clusters.postgresql.cnpg.io -A
kubectl -n <ns> delete clusters.postgresql.cnpg.io <name>
# 2) 오퍼레이터 제거
helm uninstall cnpg -n cnpg-system
# 3) cluster-scoped 잔여물 제거 (helm uninstall 로 남는다)
kubectl delete validatingwebhookconfiguration cnpg-validating-webhook-configuration --ignore-not-found
kubectl delete mutatingwebhookconfiguration cnpg-mutating-webhook-configuration --ignore-not-found
kubectl get crd -o name | grep cnpg.io | xargs -r kubectl delete
```
> CRD 삭제는 해당 CRD 의 모든 리소스를 삭제한다. PVC 는 남지만 `Cluster` 정의는 사라지므로
> 운영 클러스터에서는 3단계를 실행하기 전에 반드시 백업을 확인한다.
## 5. 검증 이력
`doc/charts/cnpg/deploy-test.md` 참고. RKE2 v1.34.1 / Longhorn 환경에서 3-instance 구성,
failover 3초, 데이터 정합성 유지를 확인했다.
@@ -0,0 +1,6 @@
dependencies:
- name: cluster
repository: https://cloudnative-pg.github.io/grafana-dashboards
version: 0.0.5
digest: sha256:92acaa7742cad61339d69da604eda609e3d5e02f05efa224f5a58f2b845cd2b4
generated: "2026-01-19T21:05:18.955160552+02:00"
@@ -0,0 +1,26 @@
apiVersion: v2
appVersion: 1.30.0
dependencies:
- alias: monitoring
condition: monitoring.grafanaDashboard.create
name: cluster
repository: https://cloudnative-pg.github.io/grafana-dashboards
version: "0.0"
description: CloudNativePG Operator Helm Chart
home: https://cloudnative-pg.io
icon: https://raw.githubusercontent.com/cloudnative-pg/artwork/main/cloudnativepg-logo.svg
keywords:
- operator
- controller
- postgresql
- postgres
- database
kubeVersion: '>=1.29.0-0'
maintainers:
- email: p.scorsolini@gmail.com
name: phisco
name: cloudnative-pg
sources:
- https://github.com/cloudnative-pg/charts
type: application
version: 0.29.0
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,139 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
About this chart
----------------
Helm chart to install the [CloudNativePG operator](https://cloudnative-pg.io), originally created and sponsored
by [EDB](https://www.enterprisedb.com/) to manage PostgreSQL workloads on any supported Kubernetes cluster
running in private, public, or hybrid cloud environments.
**NOTE**: this chart supports only the latest point release of the CloudNativePG operator.
The chart installs only the operator (controller manager, webhooks, RBAC and CRDs). To provision a PostgreSQL
`Cluster` resource, use the companion [`cluster`](https://github.com/cloudnative-pg/charts/tree/main/charts/cluster) chart
(see the [Cluster chart README](https://github.com/cloudnative-pg/charts/blob/main/charts/cluster/README.md) for details)
or apply your own `Cluster` manifest.
Getting Started
---------------
### Add the chart repository
```console
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
```
### Install the operator
```console
helm upgrade --install cnpg \
--namespace cnpg-system \
--create-namespace \
cnpg/cloudnative-pg
```
### Install with custom parameters
You can override individual chart values from the command line with `--set`.
For example, to enable the Prometheus `PodMonitor`:
```console
helm upgrade --install cnpg \
--namespace cnpg-system \
--create-namespace \
--set monitoring.podMonitorEnabled=true \
cnpg/cloudnative-pg
```
> **Note**
> Enabling the `PodMonitor` requires the Prometheus Operator CRDs to be installed in the cluster.
> Without them the install fails with `no matches for kind "PodMonitor"`.
See the [Values](#values) section below for the full list of configurable parameters.
### Verify the installation
```console
kubectl -n cnpg-system get deploy
kubectl -n cnpg-system rollout status deploy/cnpg-cloudnative-pg
```
Single namespace installation
-----------------------------
It is possible to limit the operator's capabilities to solely the namespace in which it has been installed.
With this restriction, the cluster-level permissions required by the operator will be substantially reduced,
and the security profile of the installation will be enhanced.
You can install the operator in single-namespace mode by setting the `config.clusterWide` flag to `false`,
as in the following example:
```console
helm upgrade --install cnpg \
--namespace cnpg-system \
--create-namespace \
--set config.clusterWide=false \
cnpg/cloudnative-pg
```
**IMPORTANT**: the single-namespace installation mode can't coexist with the cluster-wide operator. Otherwise
there would be collisions when managing the resources in the namespace watched by the single-namespace
operator. It is up to the user to ensure there is no collision between operators.
Uninstalling
------------
```console
helm uninstall cnpg --namespace cnpg-system
```
> **Warning**
> Uninstalling the chart does not remove the CRDs. Deleting them cascade-deletes every `Cluster` (and other
> CloudNativePG) resource across the whole cluster, together with the PostgreSQL data stored in their PVCs.
> This is irreversible, so only delete the CRDs if you intend to permanently remove all managed databases.
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
{{ template "chart.maintainersSection" . }}
Contributing
------------
Please read the [code of conduct](https://github.com/cloudnative-pg/charts/blob/main/CODE-OF-CONDUCT.md) and the
[guidelines](https://github.com/cloudnative-pg/charts/blob/main/CONTRIBUTING.md) to contribute to the project.
Copyright
---------
Helm charts for CloudNativePG are distributed under [Apache License 2.0](./LICENSE).
{{- if not .SkipVersionFooter }}
{{ template "helm-docs.versionFooter" . }}
{{- end }}
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
@@ -0,0 +1,6 @@
apiVersion: v2
appVersion: 1.16.0
description: CloudNativePG Grafana Cluster Dashboard.
name: cluster
type: application
version: 0.0.5
@@ -0,0 +1,59 @@
<!-- THIS FILE IS AUTOMATICALLY GENERATED. Make changes to README.md.gotmpl instead. -->
# cluster
![Version: 0.0.5](https://img.shields.io/badge/Version-0.0.5-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: 1.16.0](https://img.shields.io/badge/AppVersion-1.16.0-informational?style=flat-square)
![Grafana CloudNativePG Cluster Overview](../../images/overview.png)
Getting Started
---------------
_**Note,** this dashboard is already included in the [CloudNativePG Operator Helm Chart][operator]._
There are 4 ways to use the CloudNativePG Grafana Cluster Dashboard:
0. Install the [CloudNativePG Operator Helm Chart][operator]
1. Install manually via [Grafana.com](https://grafana.com/grafana/dashboards/20417-cloudnativepg/).
2. Install manually via the [Grafana JSON](https://github.com/cloudnative-pg/grafana-dashboards/blob/main/charts/cluster/grafana-dashboard.json):
```
https://raw.githubusercontent.com/cloudnative-pg/grafana-dashboards/main/charts/cluster/grafana-dashboard.json
```
3. Install directly in your cluster as a Helm Chart:
```bash
helm repo add cnpg-grafana https://cloudnative-pg.github.io/grafana-dashboards
helm upgrade
--install \
--namespace monitoring \
cnpg-grafana-cluster cnpg-grafana/cluster
```
2. As as a dependency to an existing chart:
```yaml
dependencies:
- name: cluster
alias: cnpg-grafana-cluster-dashboard
version: "0.0"
repository: https://cloudnative-pg.github.io/grafana-dashboards
```
## Values
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| fullnameOverride | string | `""` | |
| grafanaDashboard.annotations | object | `{}` | Annotations that ConfigMaps can have to get configured in Grafana. |
| grafanaDashboard.configMapName | string | `"cnpg-grafana-dashboard"` | The name of the ConfigMap containing the dashboard. |
| grafanaDashboard.labels | object | `{}` | Labels that ConfigMaps should have to get configured in Grafana. |
| grafanaDashboard.namespace | string | `""` | Allows overriding the namespace where the ConfigMap will be created, defaulting to the same one as the Release. |
| grafanaDashboard.sidecarLabel | string | `"grafana_dashboard"` | Label that ConfigMaps should have to be loaded as dashboards. DEPRECATED: Use labels instead. |
| grafanaDashboard.sidecarLabelValue | string | `"1"` | Label value that ConfigMaps should have to be loaded as dashboards. DEPRECATED: Use labels instead. |
| nameOverride | string | `""` | |
[operator]: https://github.com/cloudnative-pg/charts/tree/main/charts/cloudnative-pg
@@ -0,0 +1,59 @@
<!-- THIS FILE IS AUTOMATICALLY GENERATED. Make changes to README.md.gotmpl instead. -->
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
![Grafana CloudNativePG Cluster Overview](../../images/overview.png)
Getting Started
---------------
_**Note,** this dashboard is already included in the [CloudNativePG Operator Helm Chart][operator]._
There are 4 ways to use the CloudNativePG Grafana Cluster Dashboard:
0. Install the [CloudNativePG Operator Helm Chart][operator]
1. Install manually via [Grafana.com](https://grafana.com/grafana/dashboards/20417-cloudnativepg/).
2. Install manually via the [Grafana JSON](https://github.com/cloudnative-pg/grafana-dashboards/blob/main/charts/cluster/grafana-dashboard.json):
```
https://raw.githubusercontent.com/cloudnative-pg/grafana-dashboards/main/charts/cluster/grafana-dashboard.json
```
3. Install directly in your cluster as a Helm Chart:
```bash
helm repo add cnpg-grafana https://cloudnative-pg.github.io/grafana-dashboards
helm upgrade
--install \
--namespace monitoring \
cnpg-grafana-cluster cnpg-grafana/cluster
```
2. As as a dependency to an existing chart:
```yaml
dependencies:
- name: cluster
alias: cnpg-grafana-cluster-dashboard
version: "0.0"
repository: https://cloudnative-pg.github.io/grafana-dashboards
```
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
{{ template "chart.maintainersSection" . }}
{{ template "helm-docs.versionFooter" . }}
[operator]: https://github.com/cloudnative-pg/charts/tree/main/charts/cloudnative-pg
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
CloudNativePG Grafana Dashboard installed successfully.
{{- if (or .Values.grafanaDashboard.sidecarLabel .Values.grafanaDashboard.sidecarLabelValue) }}
DEPRECATION NOTICE: The grafanaDashboard.sidecarLabel is deprecated and will be removed in a future release. Use the grafanaDashboard.labels instead.
{{- end }}
@@ -0,0 +1,21 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.grafanaDashboard.configMapName }}
namespace: {{ default .Release.Namespace .Values.grafanaDashboard.namespace }}
{{- if (or .Values.grafanaDashboard.labels .Values.grafanaDashboard.sidecarLabel) }}
labels:
{{- if .Values.grafanaDashboard.sidecarLabel }}
{{ .Values.grafanaDashboard.sidecarLabel }}: {{ .Values.grafanaDashboard.sidecarLabelValue | quote }}
{{- end }}
{{- with .Values.grafanaDashboard.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- with .Values.grafanaDashboard.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
data:
cnp.json: |-
{{ .Files.Get "grafana-dashboard.json" | indent 6 }}
@@ -0,0 +1,35 @@
{
"$schema": "http://json-schema.org/schema#",
"type": "object",
"properties": {
"fullnameOverride": {
"type": "string"
},
"grafanaDashboard": {
"type": "object",
"properties": {
"annotations": {
"type": "object"
},
"configMapName": {
"type": "string"
},
"labels": {
"type": "object"
},
"namespace": {
"type": "string"
},
"sidecarLabel": {
"type": "string"
},
"sidecarLabelValue": {
"type": "string"
}
}
},
"nameOverride": {
"type": "string"
}
}
}
@@ -0,0 +1,20 @@
# Default values for cluster.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
nameOverride: ""
fullnameOverride: ""
grafanaDashboard:
# -- Allows overriding the namespace where the ConfigMap will be created, defaulting to the same one as the Release.
namespace: ""
# -- The name of the ConfigMap containing the dashboard.
configMapName: "cnpg-grafana-dashboard"
# -- Label that ConfigMaps should have to be loaded as dashboards. DEPRECATED: Use labels instead.
sidecarLabel: "grafana_dashboard"
# -- Label value that ConfigMaps should have to be loaded as dashboards. DEPRECATED: Use labels instead.
sidecarLabelValue: "1"
# -- Labels that ConfigMaps should have to get configured in Grafana.
labels: {}
# -- Annotations that ConfigMaps can have to get configured in Grafana.
annotations: {}
@@ -0,0 +1,66 @@
# CloudNativePG Operator — PaaSup 오버라이드
# 업스트림 기본값은 values.yaml 참고. 여기에는 변경이 필요한 항목만 둔다.
image:
# 자체 빌드(대응 우선순위 c) — 업스트림 1.30.0 이 게이트 차단 HIGH 3건(stdlib·x/text·grpc,
# Go 모듈 정적 링크라 베이스 OS 교체로 해소 불가)으로 막혀 release-1.30 브랜치를 직접
# 컴파일했다. 근거: doc/analysis/cloudnative-pg-operator-cve.md, 결정: doc/decisions/0005.
# 빌드 정의: images/cloudnative-pg/. 상위 태그가 나오면(대응 우선순위 a) 되돌리는 것이 우선.
registry: "docker.io/wbsong111"
repository: "cloudnative-pg"
tag: "1.30.0-security-hardened-20260730c"
# 오프라인/사설 레지스트리 환경에서 미러 사용 시 지정.
# imagePullSecrets:
# - name: paasup-registry
replicaCount: 1
crds:
# operator 차트가 CRD 를 함께 설치한다. helm upgrade 로는 CRD 가 갱신되지 않으므로
# 버전 업그레이드 시 CRD 를 수동 apply 해야 한다 (CUSTOM-README.md 참고).
create: true
config:
# true(업스트림 기본): 전체 네임스페이스 감시.
# → ClusterRole 에 secrets 전체 CRUD, pods/exec, roles/rolebindings 생성 권한이
# 클러스터 범위로 부여된다. 실질적으로 cluster-admin 급이다.
# false: 설치 네임스페이스만 감시. ClusterRole 이 3개 규칙(nodes RO,
# clusterimagecatalogs RO, webhook 설정 get/patch)으로 축소되고 나머지 권한은
# 설치 네임스페이스 한정 Role 로 내려간다.
# 상세 비교는 CUSTOM-README.md 의 "보안 관점 — 실측 RBAC 비교" 참고.
clusterWide: true
data: {}
# 감시 대상 네임스페이스 한정 (clusterWide: true 와 함께 사용).
# 주의: reconcile 범위만 좁힌다. RBAC 은 여전히 cluster-wide 이므로
# 보안 경계가 아니다 — 심층 방어 수단으로만 취급한다.
# WATCH_NAMESPACE: "platform,defense-llm"
webhook:
port: 9443
# failurePolicy: Fail — operator 가 죽으면 Cluster 리소스 조작이 막힌다.
# operator 를 제거할 때 webhook 설정도 반드시 함께 삭제해야 한다.
mutating:
create: true
failurePolicy: Fail
validating:
create: true
failurePolicy: Fail
monitoring:
# rancher-monitoring(Prometheus Operator) 설치 환경에서만 true 로 둔다.
podMonitorEnabled: false
grafanaDashboard:
create: false
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 500m
memory: 500Mi
tolerations: []
nodeSelector: {}
@@ -0,0 +1,35 @@
# CloudNativePG Operator 리소스 티어.
# operator 는 컨트롤 플레인 컴포넌트로, 관리하는 Cluster 수에 따라 부하가 늘어난다.
# 실제 DB 리소스는 cnpg-cluster 차트의 dip-resources-quotas.yaml 에서 정의한다.
small:
replicaCount: 1
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 500m
memory: 500Mi
medium:
replicaCount: 1
resources:
requests:
cpu: 200m
memory: 500Mi
limits:
cpu: 1000m
memory: 1Gi
large:
# 다수 Cluster 를 관리하는 환경. leader election 기반이라 replica 를 늘려도
# 실제 reconcile 은 리더 1개가 수행한다(가용성 목적).
replicaCount: 3
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 2Gi
@@ -0,0 +1,51 @@
# CloudNativePG Operator — DIP 플랫폼 기본 배포값
# custom-values.yaml 과 달리 플랫폼 운영 환경(멀티 테넌트) 기준값이다.
image:
# 자체 빌드(대응 우선순위 c) — custom-values.yaml 상단 주석·doc/decisions/0005 참고.
registry: "docker.io/wbsong111"
repository: "cloudnative-pg"
tag: "1.30.0-security-hardened-20260730c"
replicaCount: 1
crds:
create: true
config:
# 플랫폼 운영은 테넌트 네임스페이스를 모두 감시해야 하므로 cluster-wide 로 둔다.
# 이는 오퍼레이터 SA 에 cluster-admin 급 권한(전 네임스페이스 secrets CRUD,
# pods/exec, roles/rolebindings 생성)을 부여하는 결정이다. CUSTOM-README.md 의
# "보안 관점 — 실측 RBAC 비교" 에 정리된 보완책을 반드시 함께 적용한다.
clusterWide: true
# reconcile 대상을 테넌트 네임스페이스로 한정한다(RBAC 축소 효과는 없음).
data: {}
# WATCH_NAMESPACE: "platform,defense-llm"
webhook:
mutating:
create: true
failurePolicy: Fail
validating:
create: true
failurePolicy: Fail
monitoring:
# rancher-monitoring 이 설치된 플랫폼에서는 활성화한다.
podMonitorEnabled: true
grafanaDashboard:
create: true
sidecarLabel: grafana_dashboard
sidecarLabelValue: "1"
resources:
requests:
cpu: 100m
memory: 200Mi
limits:
cpu: 500m
memory: 500Mi
tolerations: []
nodeSelector: {}
@@ -0,0 +1,3 @@
The JSON file has been moved to a dedicated repository for CloudNativePG dashboards located at:
https://github.com/cloudnative-pg/grafana-dashboards/blob/main/charts/cluster/grafana-dashboard.json
@@ -0,0 +1,21 @@
CloudNativePG operator should be installed in namespace "{{ include "cloudnative-pg.namespace" . }}".
You can now create a PostgreSQL cluster with 3 nodes as follows:
cat <<EOF | kubectl apply -f -
# Example of PostgreSQL cluster
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-example
{{if not .Values.config.clusterWide -}}
namespace: {{ include "cloudnative-pg.namespace" . }}
{{- end }}
spec:
instances: 3
storage:
size: 1Gi
EOF
kubectl get -A cluster
@@ -0,0 +1,325 @@
{{/*
Allow the release namespace to be overridden for multi-namespace deployments in combined charts
*/}}
{{- define "cloudnative-pg.namespace" -}}
{{- if .Values.namespaceOverride -}}
{{- .Values.namespaceOverride -}}
{{- else -}}
{{- .Release.Namespace -}}
{{- end -}}
{{- end -}}
{{/*
Expand the name of the chart.
*/}}
{{- define "cloudnative-pg.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "cloudnative-pg.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "cloudnative-pg.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "cloudnative-pg.labels" -}}
helm.sh/chart: {{ include "cloudnative-pg.chart" . }}
{{ include "cloudnative-pg.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "cloudnative-pg.selectorLabels" -}}
app.kubernetes.io/name: {{ include "cloudnative-pg.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "cloudnative-pg.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "cloudnative-pg.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Define the common set of rules that can be applied either with
namespace scope or clusterwide
*/}}
{{- define "cloudnative-pg.commonRules" }}
- apiGroups:
- ""
resources:
- configmaps
- secrets
- services
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- ""
resources:
- configmaps/status
- secrets/status
verbs:
- get
- patch
- update
- apiGroups:
- ""
resources:
- events
verbs:
- create
- patch
- apiGroups:
- ""
resources:
- persistentvolumeclaims
- pods
- pods/exec
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- ""
resources:
- pods/status
verbs:
- get
- apiGroups:
- ""
resources:
- serviceaccounts
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
- deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- batch
resources:
- jobs
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- coordination.k8s.io
resources:
- leases
verbs:
- create
- get
- list
- update
- watch
- apiGroups:
- discovery.k8s.io
resources:
- endpointslices
verbs:
- get
- list
- watch
- apiGroups:
- monitoring.coreos.com
resources:
- podmonitors
verbs:
- create
- delete
- get
- list
- patch
- watch
- apiGroups:
- policy
resources:
- poddisruptionbudgets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- postgresql.cnpg.io
resources:
- backups
- clusters
- databaseroles
- databases
- poolers
- publications
- scheduledbackups
- subscriptions
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- postgresql.cnpg.io
resources:
- failoverquorums
verbs:
- create
- delete
- get
- list
- watch
- apiGroups:
- postgresql.cnpg.io
resources:
- backups/status
- databases/status
- publications/status
- scheduledbackups/status
- subscriptions/status
verbs:
- get
- patch
- update
- apiGroups:
- postgresql.cnpg.io
resources:
- imagecatalogs
verbs:
- get
- list
- watch
- apiGroups:
- postgresql.cnpg.io
resources:
- clusters/finalizers
- databaseroles/finalizers
- poolers/finalizers
verbs:
- update
- apiGroups:
- postgresql.cnpg.io
resources:
- clusters/status
- databaseroles/status
- poolers/status
- failoverquorums/status
verbs:
- get
- patch
- update
- watch
- apiGroups:
- rbac.authorization.k8s.io
resources:
- rolebindings
- roles
verbs:
- create
- get
- list
- patch
- update
- watch
- apiGroups:
- snapshot.storage.k8s.io
resources:
- volumesnapshots
verbs:
- create
- get
- list
- patch
- watch
{{- end }}
{{/*
Define the set of rules that must be applied clusterwide
*/}}
{{- define "cloudnative-pg.clusterwideRules" }}
- apiGroups:
- ""
resources:
- nodes
verbs:
- get
- list
- watch
- apiGroups:
- admissionregistration.k8s.io
resources:
- mutatingwebhookconfigurations
- validatingwebhookconfigurations
verbs:
- get
- patch
- apiGroups:
- postgresql.cnpg.io
resources:
- clusterimagecatalogs
verbs:
- get
- list
- watch
{{- end }}
@@ -0,0 +1,61 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
{{- if .Values.config.create }}
{{- if not .Values.config.secret }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.config.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
data:
{{- if .Values.config.clusterWide -}}
{{- toYaml .Values.config.data | nindent 2 }}
{{- else -}}
{{- $watchNamespaceMap := dict "WATCH_NAMESPACE" .Release.Namespace -}}
{{- $fullConfiguration := merge .Values.config.data $watchNamespaceMap -}}
{{- toYaml $fullConfiguration | nindent 2 }}
{{- end -}}
{{- else }}
apiVersion: v1
kind: Secret
type: Opaque
metadata:
name: {{ .Values.config.name }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
stringData:
{{- if .Values.config.clusterWide -}}
{{- toYaml .Values.config.data | nindent 2 }}
{{- else -}}
{{- $watchNamespaceMap := dict "WATCH_NAMESPACE" .Release.Namespace -}}
{{- $fullConfiguration := merge .Values.config.data $watchNamespaceMap -}}
{{- toYaml $fullConfiguration | nindent 2 }}
{{- end -}}
{{- end }}
{{- end }}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,175 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "cloudnative-pg.fullname" . }}
namespace: {{ include "cloudnative-pg.namespace" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "cloudnative-pg.selectorLabels" . | nindent 6 }}
{{- if .Values.updateStrategy }}
strategy:
{{- toYaml .Values.updateStrategy | nindent 4 }}
{{- end }}
template:
metadata:
annotations:
checksum/rbac: {{ include (print $.Template.BasePath "/rbac.yaml") . | sha256sum }}
checksum/config: {{ include (print $.Template.BasePath "/config.yaml") . | sha256sum }}
checksum/monitoring-config: {{ include (print $.Template.BasePath "/monitoring-configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "cloudnative-pg.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if .Values.hostNetwork }}
hostNetwork: {{ .Values.hostNetwork }}
{{- end }}
{{- if .Values.dnsPolicy }}
dnsPolicy: {{ .Values.dnsPolicy }}
{{- end }}
containers:
- args:
- controller
- --leader-elect
- --max-concurrent-reconciles={{ .Values.config.maxConcurrentReconciles }}
{{- if .Values.config.name }}
{{- if not .Values.config.secret }}
- --config-map-name={{ .Values.config.name }}
{{- else }}
- --secret-name={{ .Values.config.name }}
{{- end }}
{{- end }}
- --webhook-port={{ .Values.webhook.port }}
{{- range .Values.additionalArgs }}
- {{ . }}
{{- end }}
command:
- /manager
env:
- name: OPERATOR_IMAGE_NAME
value: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
- name: OPERATOR_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: MONITORING_QUERIES_CONFIGMAP
value: "{{ .Values.monitoringQueriesConfigMap.name }}"
{{- if not .Values.config.clusterWide }}
- name: WATCH_NAMESPACE
value: "{{ include "cloudnative-pg.namespace" . }}"
{{- end }}
{{- if .Values.additionalEnv }}
{{- tpl (.Values.additionalEnv | toYaml) . | nindent 8 }}
{{- end }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
livenessProbe:
httpGet:
path: /readyz
port: webhook-server
scheme: HTTPS
{{- if .Values.webhook.livenessProbe.initialDelaySeconds }}
initialDelaySeconds: {{ .Values.webhook.livenessProbe.initialDelaySeconds }}
{{- end }}
name: manager
ports:
- containerPort: 8080
name: metrics
protocol: TCP
- containerPort: {{ .Values.webhook.port }}
name: webhook-server
protocol: TCP
readinessProbe:
httpGet:
path: /readyz
port: webhook-server
scheme: HTTPS
{{- if .Values.webhook.readinessProbe.initialDelaySeconds }}
initialDelaySeconds: {{ .Values.webhook.readinessProbe.initialDelaySeconds }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
securityContext:
{{- toYaml .Values.containerSecurityContext | nindent 10 }}
startupProbe:
{{- if .Values.webhook.startupProbe.failureThreshold }}
failureThreshold: {{ .Values.webhook.startupProbe.failureThreshold }}
{{- end }}
httpGet:
path: /readyz
port: webhook-server
scheme: HTTPS
{{- if .Values.webhook.startupProbe.periodSeconds }}
periodSeconds: {{ .Values.webhook.startupProbe.periodSeconds }}
{{- end }}
volumeMounts:
- mountPath: /controller
name: scratch-data
- mountPath: /run/secrets/cnpg.io/webhook
name: webhook-certificates
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName }}
{{- end }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
serviceAccountName: {{ include "cloudnative-pg.serviceAccountName" . }}
terminationGracePeriodSeconds: 10
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- emptyDir: {}
name: scratch-data
- name: webhook-certificates
secret:
defaultMode: 420
optional: true
secretName: cnpg-webhook-cert
@@ -0,0 +1,33 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.monitoringQueriesConfigMap.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
cnpg.io/reload: ""
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
data:
queries: {{- toYaml .Values.monitoringQueriesConfigMap.queries | nindent 4 }}
@@ -0,0 +1,116 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
{{- if .Values.webhook.mutating.create }}
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: cnpg-mutating-webhook-configuration
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /mutate-postgresql-cnpg-io-v1-backup
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.mutating.failurePolicy }}
name: mbackup.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- backups
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /mutate-postgresql-cnpg-io-v1-cluster
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.mutating.failurePolicy }}
name: mcluster.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- clusters
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /mutate-postgresql-cnpg-io-v1-database
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.mutating.failurePolicy }}
name: mdatabase.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- databases
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /mutate-postgresql-cnpg-io-v1-scheduledbackup
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.mutating.failurePolicy }}
name: mscheduledbackup.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- scheduledbackups
sideEffects: None
{{- end }}
@@ -0,0 +1,48 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
{{- if .Values.monitoring.podMonitorEnabled }}
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: {{ include "cloudnative-pg.fullname" . }}
namespace: {{ include "cloudnative-pg.namespace" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.monitoring.podMonitorAdditionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end}}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
selector:
matchLabels:
{{- include "cloudnative-pg.selectorLabels" . | nindent 6 }}
podMetricsEndpoints:
- port: metrics
{{- with .Values.monitoring.podMonitorMetricRelabelings }}
metricRelabelings:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with .Values.monitoring.podMonitorRelabelings }}
relabelings:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- end }}
@@ -0,0 +1,182 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
{{- if .Values.serviceAccount.create }}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "cloudnative-pg.serviceAccountName" . }}
namespace: {{ include "cloudnative-pg.namespace" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
{{- if .Values.rbac.create }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "cloudnative-pg.fullname" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- include "cloudnative-pg.clusterwideRules" . }}
{{/*
If we're doing a clusterWide installation (default)
we add ALL the necessary rules for the operator to the ClusterRole
*/}}
{{- if .Values.config.clusterWide }}
{{- include "cloudnative-pg.commonRules" . }}
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "cloudnative-pg.fullname" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "cloudnative-pg.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "cloudnative-pg.serviceAccountName" . }}
namespace: {{ include "cloudnative-pg.namespace" . }}
{{/*
If we're doing a single-namespace installation
we create a Role with the common rules for the operator,
and a RoleBinding. We already created the ClusterRole above with the
required cluster-wide rules
*/}}
{{- if eq .Values.config.clusterWide false }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "cloudnative-pg.fullname" . }}
namespace: {{ include "cloudnative-pg.namespace" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
rules:
{{- include "cloudnative-pg.commonRules" . }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ include "cloudnative-pg.fullname" . }}
namespace: {{ include "cloudnative-pg.namespace" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ include "cloudnative-pg.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "cloudnative-pg.serviceAccountName" . }}
namespace: {{ include "cloudnative-pg.namespace" . }}
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "cloudnative-pg.fullname" . }}-view
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- if .Values.rbac.aggregateClusterRoles }}
rbac.authorization.k8s.io/aggregate-to-view: "true"
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-admin: "true"
{{- end }}
rules:
- apiGroups:
- postgresql.cnpg.io
resources:
- backups
- clusters
- clusters/status
- databaseroles
- databases
- failoverquorums
- poolers
- publications
- scheduledbackups
- imagecatalogs
- clusterimagecatalogs
- subscriptions
verbs:
- get
- list
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "cloudnative-pg.fullname" . }}-edit
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- if .Values.rbac.aggregateClusterRoles }}
rbac.authorization.k8s.io/aggregate-to-edit: "true"
rbac.authorization.k8s.io/aggregate-to-admin: "true"
{{- end }}
rules:
- apiGroups:
- postgresql.cnpg.io
resources:
- backups
- clusters
- clusters/status
- databaseroles
- databases
- failoverquorums
- poolers
- publications
- scheduledbackups
- imagecatalogs
- clusterimagecatalogs
- subscriptions
verbs:
- create
- delete
- deletecollection
- patch
- update
---
{{- end }}
@@ -0,0 +1,44 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.commonAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if .Values.service.ipFamilyPolicy }}
ipFamilyPolicy: {{ .Values.service.ipFamilyPolicy }}
{{- end }}
{{- if .Values.service.ipFamilies }}
ipFamilies: {{ .Values.service.ipFamilies | toYaml | nindent 2 }}
{{- end }}
ports:
- port: {{ .Values.service.port }}
targetPort: webhook-server
name: webhook-server
selector:
{{- include "cloudnative-pg.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,137 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
{{- if .Values.webhook.validating.create }}
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: cnpg-validating-webhook-configuration
labels:
{{- include "cloudnative-pg.labels" . | nindent 4 }}
{{- with .Values.rbac.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
webhooks:
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /validate-postgresql-cnpg-io-v1-backup
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.validating.failurePolicy }}
name: vbackup.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- backups
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /validate-postgresql-cnpg-io-v1-cluster
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.validating.failurePolicy }}
name: vcluster.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- clusters
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /validate-postgresql-cnpg-io-v1-scheduledbackup
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.validating.failurePolicy }}
name: vscheduledbackup.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- scheduledbackups
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /validate-postgresql-cnpg-io-v1-database
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.validating.failurePolicy }}
name: vdatabase.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- databases
sideEffects: None
- admissionReviewVersions:
- v1
clientConfig:
service:
name: {{ .Values.service.name }}
namespace: {{ include "cloudnative-pg.namespace" . }}
path: /validate-postgresql-cnpg-io-v1-pooler
port: {{ .Values.service.port }}
failurePolicy: {{ .Values.webhook.validating.failurePolicy }}
name: vpooler.cnpg.io
rules:
- apiGroups:
- postgresql.cnpg.io
apiVersions:
- v1
operations:
- CREATE
- UPDATE
resources:
- poolers
sideEffects: None
{{- end }}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,696 @@
#
# Copyright © contributors to CloudNativePG, established as
# CloudNativePG a Series of LF Projects, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
# Default values for CloudNativePG.
# This is a YAML-formatted file.
# Please declare variables to be passed to your templates.
replicaCount: 1
image:
repository: ghcr.io/cloudnative-pg/cloudnative-pg
pullPolicy: IfNotPresent
# -- Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
namespaceOverride: ""
hostNetwork: false
dnsPolicy: ""
# -- Update strategy for the operator.
# ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy
updateStrategy: {}
# For example:
# type: RollingUpdate
# rollingUpdate:
# maxSurge: 25%
# maxUnavailable: 25%
crds:
# -- Specifies whether the CRDs should be created when installing the chart.
create: true
# -- The webhook configuration.
webhook:
port: 9443
mutating:
create: true
failurePolicy: Fail
validating:
create: true
failurePolicy: Fail
livenessProbe:
initialDelaySeconds: 3
readinessProbe:
initialDelaySeconds: 3
startupProbe:
failureThreshold: 6
periodSeconds: 5
# Operator configuration.
config:
# -- Specifies whether the secret should be created.
create: true
# -- The name of the configmap/secret to use.
name: cnpg-controller-manager-config
# -- Specifies whether it should be stored in a secret, instead of a configmap.
secret: false
# -- This option determines if the operator is responsible for observing
# events across the entire Kubernetes cluster or if its focus should be
# narrowed down to the specific namespace within which it has been deployed.
clusterWide: true
# -- The content of the configmap/secret, see
# https://cloudnative-pg.io/documentation/current/operator_conf/#available-options
# for all the available options.
data: {}
# INHERITED_ANNOTATIONS: categories
# INHERITED_LABELS: environment, workload, app
# WATCH_NAMESPACE: namespace-a,namespace-b
# -- The maximum number of concurrent reconciles. Defaults to 10.
maxConcurrentReconciles: 10
# -- Additional arguments to be added to the operator's args list.
additionalArgs: []
# -- Array containing extra environment variables which can be templated.
additionalEnv: []
# For example:
# - name: RELEASE_NAME
# value: "{{ .Release.Name }}"
# - name: MY_VAR
# value: "mySpecialKey"
serviceAccount:
# -- Specifies whether the service account should be created.
create: true
# -- The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template.
name: ""
rbac:
# -- Specifies whether ClusterRole and ClusterRoleBinding should be created.
create: true
# -- Aggregate ClusterRoles to Kubernetes default user-facing roles.
# Ref: https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles
aggregateClusterRoles: false
# -- Annotations to be added to all other resources.
commonAnnotations: {}
# -- Annotations to be added to the pod.
podAnnotations: {}
# -- Labels to be added to the pod.
podLabels: {}
# -- Container Security Context.
containerSecurityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsUser: 10001
runAsGroup: 10001
seccompProfile:
type: RuntimeDefault
capabilities:
drop:
- "ALL"
# -- Security Context for the whole pod.
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
# fsGroup: 2000
# -- Priority indicates the importance of a Pod relative to other Pods.
priorityClassName: ""
service:
type: ClusterIP
# -- The name of the Webhook Service.
name: cnpg-webhook-service
# DO NOT CHANGE THE SERVICE NAME as it is currently used to generate the certificate
# and can not be configured
port: 443
# -- Set the ip family policy to configure dual-stack see [Configure dual-stack](https://kubernetes.io/docs/concepts/services-networking/dual-stack/#services)
ipFamilyPolicy: ""
# -- Sets the families that should be supported and the order in which they should be applied to ClusterIP as well. Can be IPv4 and/or IPv6.
ipFamilies: []
resources: {}
# If you want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
#
# limits:
# cpu: 100m
# memory: 200Mi
# requests:
# cpu: 100m
# memory: 100Mi
# -- Nodeselector for the operator to be installed.
nodeSelector: {}
# -- Topology Spread Constraints for the operator to be installed.
topologySpreadConstraints: []
# -- Tolerations for the operator to be installed.
tolerations: []
# -- Affinity for the operator to be installed.
affinity: {}
monitoring:
# -- Specifies whether the monitoring should be enabled. Requires Prometheus Operator CRDs.
podMonitorEnabled: false
# -- Metrics relabel configurations to apply to samples before ingestion.
podMonitorMetricRelabelings: []
# -- Relabel configurations to apply to samples before scraping.
podMonitorRelabelings: []
# -- Additional labels for the podMonitor
podMonitorAdditionalLabels: {}
grafanaDashboard:
create: false
# -- Allows overriding the namespace where the ConfigMap will be created, defaulting to the same one as the Release.
namespace: ""
# -- The name of the ConfigMap containing the dashboard.
configMapName: "cnpg-grafana-dashboard"
# -- Label that ConfigMaps should have to be loaded as dashboards. DEPRECATED: Use labels instead.
sidecarLabel: "grafana_dashboard"
# -- Label value that ConfigMaps should have to be loaded as dashboards. DEPRECATED: Use labels instead.
sidecarLabelValue: "1"
# -- Labels that ConfigMaps should have to get configured in Grafana.
labels: {}
# -- Annotations that ConfigMaps can have to get configured in Grafana.
annotations: {}
# Default monitoring queries
monitoringQueriesConfigMap:
# -- The name of the default monitoring configmap.
name: cnpg-default-monitoring
# -- A string representation of a YAML defining monitoring queries.
queries: |
backends:
query: |
SELECT sa.datname
, sa.usename
, sa.application_name
, states.state
, COALESCE(sa.count, 0) AS total
, COALESCE(sa.max_tx_secs, 0) AS max_tx_duration_seconds
FROM ( VALUES ('active')
, ('idle')
, ('idle in transaction')
, ('idle in transaction (aborted)')
, ('fastpath function call')
, ('disabled')
) AS states(state)
LEFT JOIN (
SELECT datname
, state
, usename
, COALESCE(application_name, '') AS application_name
, pg_catalog.count(*)
, COALESCE(EXTRACT (EPOCH FROM (pg_catalog.max(pg_catalog.now() OPERATOR(pg_catalog.-) xact_start))), 0) AS max_tx_secs
FROM pg_catalog.pg_stat_activity
GROUP BY datname, state, usename, application_name
) sa ON states.state OPERATOR(pg_catalog.=) sa.state
WHERE sa.usename IS NOT NULL
metrics:
- datname:
usage: "LABEL"
description: "Name of the database"
- usename:
usage: "LABEL"
description: "Name of the user"
- application_name:
usage: "LABEL"
description: "Name of the application"
- state:
usage: "LABEL"
description: "State of the backend"
- total:
usage: "GAUGE"
description: "Number of backends"
- max_tx_duration_seconds:
usage: "GAUGE"
description: "Maximum duration of a transaction in seconds"
backends_waiting:
query: |
SELECT pg_catalog.count(*) AS total
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype OPERATOR(pg_catalog.=) blocked_locks.locktype
AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database
AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation
AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page
AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple
AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid
AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid
AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid
AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid
AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid
AND blocking_locks.pid OPERATOR(pg_catalog.<>) blocked_locks.pid
JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid OPERATOR(pg_catalog.=) blocking_locks.pid
WHERE NOT blocked_locks.granted
metrics:
- total:
usage: "GAUGE"
description: "Total number of backends that are currently waiting on other queries"
pg_database:
query: |
SELECT datname
, pg_catalog.pg_database_size(datname) AS size_bytes
, pg_catalog.age(datfrozenxid) AS xid_age
, pg_catalog.mxid_age(datminmxid) AS mxid_age
FROM pg_catalog.pg_database
WHERE datallowconn
metrics:
- datname:
usage: "LABEL"
description: "Name of the database"
- size_bytes:
usage: "GAUGE"
description: "Disk space used by the database"
- xid_age:
usage: "GAUGE"
description: "Number of transactions from the frozen XID to the current one"
- mxid_age:
usage: "GAUGE"
description: "Number of multiple transactions (Multixact) from the frozen XID to the current one"
pg_postmaster:
query: |
SELECT EXTRACT(EPOCH FROM pg_postmaster_start_time) AS start_time
FROM pg_catalog.pg_postmaster_start_time()
metrics:
- start_time:
usage: "GAUGE"
description: "Time at which postgres started (based on epoch)"
pg_replication:
query: |
SELECT CASE WHEN (
NOT pg_catalog.pg_is_in_recovery()
OR pg_catalog.pg_last_wal_receive_lsn() OPERATOR(pg_catalog.=) pg_catalog.pg_last_wal_replay_lsn())
THEN 0
ELSE GREATEST (0,
EXTRACT(EPOCH FROM (pg_catalog.now() OPERATOR(pg_catalog.-) pg_catalog.pg_last_xact_replay_timestamp())))
END AS lag,
pg_catalog.pg_is_in_recovery() AS in_recovery,
EXISTS (TABLE pg_catalog.pg_stat_wal_receiver) AS is_wal_receiver_up,
(SELECT pg_catalog.count(*) FROM pg_catalog.pg_stat_replication) AS streaming_replicas
metrics:
- lag:
usage: "GAUGE"
description: "Replication lag behind primary in seconds"
- in_recovery:
usage: "GAUGE"
description: "Whether the instance is in recovery"
- is_wal_receiver_up:
usage: "GAUGE"
description: "Whether the instance wal_receiver is up"
- streaming_replicas:
usage: "GAUGE"
description: "Number of streaming replicas connected to the instance"
pg_replication_slots:
query: |
SELECT slot_name,
slot_type,
database,
active,
(CASE pg_catalog.pg_is_in_recovery()
WHEN TRUE THEN pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_last_wal_receive_lsn(), restart_lsn)
ELSE pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_current_wal_lsn(), restart_lsn)
END) as pg_wal_lsn_diff
FROM pg_catalog.pg_replication_slots
WHERE NOT temporary
metrics:
- slot_name:
usage: "LABEL"
description: "Name of the replication slot"
- slot_type:
usage: "LABEL"
description: "Type of the replication slot"
- database:
usage: "LABEL"
description: "Name of the database"
- active:
usage: "GAUGE"
description: "Flag indicating whether the slot is active"
- pg_wal_lsn_diff:
usage: "GAUGE"
description: "Replication lag in bytes"
pg_stat_archiver:
query: |
SELECT archived_count
, failed_count
, COALESCE(EXTRACT(EPOCH FROM (pg_catalog.now() OPERATOR(pg_catalog.-) last_archived_time)), -1) AS seconds_since_last_archival
, COALESCE(EXTRACT(EPOCH FROM (pg_catalog.now() OPERATOR(pg_catalog.-) last_failed_time)), -1) AS seconds_since_last_failure
, COALESCE(EXTRACT(EPOCH FROM last_archived_time), -1) AS last_archived_time
, COALESCE(EXTRACT(EPOCH FROM last_failed_time), -1) AS last_failed_time
, COALESCE(CAST(CAST('x' OPERATOR(pg_catalog.||) pg_catalog.right(pg_catalog.split_part(last_archived_wal, '.', 1), 16) AS pg_catalog.bit(64)) AS pg_catalog.int8), -1) AS last_archived_wal_start_lsn
, COALESCE(CAST(CAST('x' OPERATOR(pg_catalog.||) pg_catalog.right(pg_catalog.split_part(last_failed_wal, '.', 1), 16) AS pg_catalog.bit(64)) AS pg_catalog.int8), -1) AS last_failed_wal_start_lsn
, EXTRACT(EPOCH FROM stats_reset) AS stats_reset_time
FROM pg_catalog.pg_stat_archiver
predicate_query: |
SELECT NOT pg_catalog.pg_is_in_recovery()
OR pg_catalog.current_setting('archive_mode') OPERATOR(pg_catalog.=) 'always'
metrics:
- archived_count:
usage: "COUNTER"
description: "Number of WAL files that have been successfully archived"
- failed_count:
usage: "COUNTER"
description: "Number of failed attempts for archiving WAL files"
- seconds_since_last_archival:
usage: "GAUGE"
description: "Seconds since the last successful archival operation"
- seconds_since_last_failure:
usage: "GAUGE"
description: "Seconds since the last failed archival operation"
- last_archived_time:
usage: "GAUGE"
description: "Epoch of the last time WAL archiving succeeded"
- last_failed_time:
usage: "GAUGE"
description: "Epoch of the last time WAL archiving failed"
- last_archived_wal_start_lsn:
usage: "GAUGE"
description: "Archived WAL start LSN"
- last_failed_wal_start_lsn:
usage: "GAUGE"
description: "Last failed WAL LSN"
- stats_reset_time:
usage: "GAUGE"
description: "Time at which these statistics were last reset"
pg_stat_bgwriter:
runonserver: "<17.0.0"
query: |
SELECT checkpoints_timed
, checkpoints_req
, checkpoint_write_time
, checkpoint_sync_time
, buffers_checkpoint
, buffers_clean
, maxwritten_clean
, buffers_backend
, buffers_backend_fsync
, buffers_alloc
FROM pg_catalog.pg_stat_bgwriter
metrics:
- checkpoints_timed:
usage: "COUNTER"
description: "Number of scheduled checkpoints that have been performed"
- checkpoints_req:
usage: "COUNTER"
description: "Number of requested checkpoints that have been performed"
- checkpoint_write_time:
usage: "COUNTER"
description: "Total amount of time that has been spent in the portion of checkpoint processing where files are written to disk, in milliseconds"
- checkpoint_sync_time:
usage: "COUNTER"
description: "Total amount of time that has been spent in the portion of checkpoint processing where files are synchronized to disk, in milliseconds"
- buffers_checkpoint:
usage: "COUNTER"
description: "Number of buffers written during checkpoints"
- buffers_clean:
usage: "COUNTER"
description: "Number of buffers written by the background writer"
- maxwritten_clean:
usage: "COUNTER"
description: "Number of times the background writer stopped a cleaning scan because it had written too many buffers"
- buffers_backend:
usage: "COUNTER"
description: "Number of buffers written directly by a backend"
- buffers_backend_fsync:
usage: "COUNTER"
description: "Number of times a backend had to execute its own fsync call (normally the background writer handles those even when the backend does its own write)"
- buffers_alloc:
usage: "COUNTER"
description: "Number of buffers allocated"
pg_stat_bgwriter_17:
runonserver: ">=17.0.0"
name: pg_stat_bgwriter
query: |
SELECT buffers_clean
, maxwritten_clean
, buffers_alloc
, EXTRACT(EPOCH FROM stats_reset) AS stats_reset_time
FROM pg_catalog.pg_stat_bgwriter
metrics:
- buffers_clean:
usage: "COUNTER"
description: "Number of buffers written by the background writer"
- maxwritten_clean:
usage: "COUNTER"
description: "Number of times the background writer stopped a cleaning scan because it had written too many buffers"
- buffers_alloc:
usage: "COUNTER"
description: "Number of buffers allocated"
- stats_reset_time:
usage: "GAUGE"
description: "Time at which these statistics were last reset"
pg_stat_checkpointer:
runonserver: ">=17.0.0"
query: |
SELECT num_timed AS checkpoints_timed
, num_requested AS checkpoints_req
, restartpoints_timed
, restartpoints_req
, restartpoints_done
, write_time
, sync_time
, buffers_written
, EXTRACT(EPOCH FROM stats_reset) AS stats_reset_time
FROM pg_catalog.pg_stat_checkpointer
metrics:
- checkpoints_timed:
usage: "COUNTER"
description: "Number of scheduled checkpoints that have been performed"
- checkpoints_req:
usage: "COUNTER"
description: "Number of requested checkpoints that have been performed"
- restartpoints_timed:
usage: "COUNTER"
description: "Number of scheduled restartpoints due to timeout or after a failed attempt to perform it"
- restartpoints_req:
usage: "COUNTER"
description: "Number of requested restartpoints that have been performed"
- restartpoints_done:
usage: "COUNTER"
description: "Number of restartpoints that have been performed"
- write_time:
usage: "COUNTER"
description: "Total amount of time that has been spent in the portion of processing checkpoints and restartpoints where files are written to disk, in milliseconds"
- sync_time:
usage: "COUNTER"
description: "Total amount of time that has been spent in the portion of processing checkpoints and restartpoints where files are synchronized to disk, in milliseconds"
- buffers_written:
usage: "COUNTER"
description: "Number of buffers written during checkpoints and restartpoints"
- stats_reset_time:
usage: "GAUGE"
description: "Time at which these statistics were last reset"
pg_stat_database:
query: |
SELECT datname
, xact_commit
, xact_rollback
, blks_read
, blks_hit
, tup_returned
, tup_fetched
, tup_inserted
, tup_updated
, tup_deleted
, conflicts
, temp_files
, temp_bytes
, deadlocks
, blk_read_time
, blk_write_time
FROM pg_catalog.pg_stat_database
metrics:
- datname:
usage: "LABEL"
description: "Name of this database"
- xact_commit:
usage: "COUNTER"
description: "Number of transactions in this database that have been committed"
- xact_rollback:
usage: "COUNTER"
description: "Number of transactions in this database that have been rolled back"
- blks_read:
usage: "COUNTER"
description: "Number of disk blocks read in this database"
- blks_hit:
usage: "COUNTER"
description: "Number of times disk blocks were found already in the buffer cache, so that a read was not necessary (this only includes hits in the PostgreSQL buffer cache, not the operating system's file system cache)"
- tup_returned:
usage: "COUNTER"
description: "Number of rows returned by queries in this database"
- tup_fetched:
usage: "COUNTER"
description: "Number of rows fetched by queries in this database"
- tup_inserted:
usage: "COUNTER"
description: "Number of rows inserted by queries in this database"
- tup_updated:
usage: "COUNTER"
description: "Number of rows updated by queries in this database"
- tup_deleted:
usage: "COUNTER"
description: "Number of rows deleted by queries in this database"
- conflicts:
usage: "COUNTER"
description: "Number of queries canceled due to conflicts with recovery in this database"
- temp_files:
usage: "COUNTER"
description: "Number of temporary files created by queries in this database"
- temp_bytes:
usage: "COUNTER"
description: "Total amount of data written to temporary files by queries in this database"
- deadlocks:
usage: "COUNTER"
description: "Number of deadlocks detected in this database"
- blk_read_time:
usage: "COUNTER"
description: "Time spent reading data file blocks by backends in this database, in milliseconds"
- blk_write_time:
usage: "COUNTER"
description: "Time spent writing data file blocks by backends in this database, in milliseconds"
pg_stat_replication:
primary: true
query: |
SELECT usename
, COALESCE(application_name, '') AS application_name
, COALESCE(client_addr::text, '') AS client_addr
, COALESCE(client_port::text, '') AS client_port
, EXTRACT(EPOCH FROM backend_start) AS backend_start
, COALESCE(pg_catalog.age(backend_xmin), 0) AS backend_xmin_age
, pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_current_wal_lsn(), sent_lsn) AS sent_diff_bytes
, pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_current_wal_lsn(), write_lsn) AS write_diff_bytes
, pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_current_wal_lsn(), flush_lsn) AS flush_diff_bytes
, COALESCE(pg_catalog.pg_wal_lsn_diff(pg_catalog.pg_current_wal_lsn(), replay_lsn),0) AS replay_diff_bytes
, COALESCE((EXTRACT(EPOCH FROM write_lag)),0)::float AS write_lag_seconds
, COALESCE((EXTRACT(EPOCH FROM flush_lag)),0)::float AS flush_lag_seconds
, COALESCE((EXTRACT(EPOCH FROM replay_lag)),0)::float AS replay_lag_seconds
FROM pg_catalog.pg_stat_replication
metrics:
- usename:
usage: "LABEL"
description: "Name of the replication user"
- application_name:
usage: "LABEL"
description: "Name of the application"
- client_addr:
usage: "LABEL"
description: "Client IP address"
- client_port:
usage: "LABEL"
description: "Client TCP port"
- backend_start:
usage: "COUNTER"
description: "Time when this process was started"
- backend_xmin_age:
usage: "COUNTER"
description: "The age of this standby's xmin horizon"
- sent_diff_bytes:
usage: "GAUGE"
description: "Difference in bytes from the last write-ahead log location sent on this connection"
- write_diff_bytes:
usage: "GAUGE"
description: "Difference in bytes from the last write-ahead log location written to disk by this standby server"
- flush_diff_bytes:
usage: "GAUGE"
description: "Difference in bytes from the last write-ahead log location flushed to disk by this standby server"
- replay_diff_bytes:
usage: "GAUGE"
description: "Difference in bytes from the last write-ahead log location replayed into the database on this standby server"
- write_lag_seconds:
usage: "GAUGE"
description: "Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written it"
- flush_lag_seconds:
usage: "GAUGE"
description: "Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written and flushed it"
- replay_lag_seconds:
usage: "GAUGE"
description: "Time elapsed between flushing recent WAL locally and receiving notification that this standby server has written, flushed and applied it"
pg_settings:
query: |
SELECT name,
CASE setting WHEN 'on' THEN '1' WHEN 'off' THEN '0' ELSE setting END AS setting
FROM pg_catalog.pg_settings
WHERE vartype IN ('integer', 'real', 'bool')
ORDER BY 1
metrics:
- name:
usage: "LABEL"
description: "Name of the setting"
- setting:
usage: "GAUGE"
description: "Setting value"
pg_extensions:
query: |
SELECT
pg_catalog.current_database() as datname,
name as extname,
default_version,
installed_version,
CASE
WHEN default_version OPERATOR(pg_catalog.=) installed_version THEN 0
ELSE 1
END AS update_available
FROM pg_catalog.pg_available_extensions
WHERE installed_version IS NOT NULL
metrics:
- datname:
usage: "LABEL"
description: "Name of the database"
- extname:
usage: "LABEL"
description: "Extension name"
- default_version:
usage: "LABEL"
description: "Default version"
- installed_version:
usage: "LABEL"
description: "Installed version"
- update_available:
usage: "GAUGE"
description: "An update is available"
target_databases:
- '*'
@@ -0,0 +1,128 @@
# cnpg-cluster 버전 갱신 가이드
이 차트는 **PaaSup 자체 제작**이다. 업스트림 차트를 내려받는 `cloudnative-pg` 와 달리
`helm pull` 로 갱신하지 않는다. 갱신 사유는 두 가지다.
1. PostgreSQL major 버전 상향 (예: 18 → 19)
2. CNPG operator 버전 상향으로 CRD 필드가 바뀐 경우
## 1. git 작업 환경 구성
```sh
git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog
git checkout -b update-cnpg-cluster/<신규버전>
```
## 2. 신규 버전 디렉토리 생성
```sh
NEW=1.1.0
OLD=1.0.0
cd manifests/helm/cnpg-cluster
cp -R "$OLD" "$NEW"
# Chart.yaml 의 version / appVersion 갱신
```
| 필드 | 의미 |
| --- | --- |
| `version` | 이 차트의 버전 (디렉토리명과 일치시킨다) |
| `appVersion` | 배포되는 PostgreSQL 버전 |
## 3. CRD 스키마 대조
operator 를 올렸다면 `Cluster` / `Database` / `Pooler` / `ScheduledBackup` CRD 스키마가
바뀌었을 수 있다. 템플릿이 쓰는 필드가 아직 존재하는지 확인한다.
```sh
# 템플릿이 참조하는 spec 필드 목록 확인
kubectl get crd clusters.postgresql.cnpg.io -o json | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(sorted(d['spec']['versions'][0]['schema']['openAPIV3Schema']
['properties']['spec']['properties'].keys()))
"
kubectl get crd databases.postgresql.cnpg.io -o json | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(sorted(d['spec']['versions'][0]['schema']['openAPIV3Schema']
['properties']['spec']['properties'].keys()))
"
```
## 4. 렌더링·검증
```sh
NEW=1.1.0
cd manifests/helm/cnpg-cluster/$NEW
# 1) lint
helm lint . -f custom-values.yaml
# 2) 기본 경로 렌더링
helm template pg-cnpg . -f custom-values.yaml -n test >/dev/null && echo OK
# 3) 옵션 경로(backup/pooler/scheduledBackup) 렌더링 — 기본값이 false 이므로 별도 확인 필요
helm template pg-cnpg . -f custom-values.yaml -n test \
--set backup.enabled=true \
--set backup.barmanObjectStore.destinationPath=s3://x/y \
--set backup.barmanObjectStore.s3Credentials.accessKeyId.name=s \
--set backup.barmanObjectStore.s3Credentials.secretAccessKey.name=s \
--set scheduledBackup.enabled=true \
--set pooler.enabled=true >/dev/null && echo "옵션 경로 OK"
# 4) 실제 API 서버 + CNPG webhook 검증 (스키마 위반을 여기서 잡는다)
helm template pg-dryrun . -f custom-values.yaml -n <기존ns> \
| kubectl apply --dry-run=server -f -
```
4단계가 가장 중요하다. `helm template` 은 CRD 스키마를 검사하지 않으므로 렌더링이 통과해도
실제 apply 에서 거부될 수 있다.
## 5. 렌더링 결과 비교
```sh
helm template pg . "$OLD" -f "$OLD/custom-values.yaml" -n test > /tmp/old.yaml 2>/dev/null || \
helm template pg "$OLD" -f "$OLD/custom-values.yaml" -n test > /tmp/old.yaml
helm template pg "$NEW" -f "$NEW/custom-values.yaml" -n test > /tmp/new.yaml
diff -u /tmp/old.yaml /tmp/new.yaml
```
## 6. 배포 검증
개발 클러스터에 실제 배포해 `doc/charts/cnpg/deploy-test.md` 의 검증 항목을 재실행한다.
최소 통과 기준:
1. `readyInstances = instances`, `phase = Cluster in healthy state`
2. `-rw` / `-ro` 서비스 라우팅 분리 (`pg_is_in_recovery()``f` / `t`)
3. `-ro` 로 쓰기 시도 시 `read-only transaction` 오류
4. `databases[].extensions` 가 primary·전체 replica 에 모두 생성됨
5. primary Pod 삭제 → failover 후 쓰기 복구, failover 전후 데이터 보존
6. `postgresql.parameters``SHOW` 로 실제 반영 확인
### PostgreSQL major 버전 상향 시 추가 확인
- 확장 호환성: `pgaudit`, `pg_stat_statements` 의 신규 major 대응 버전 존재 여부
- `postgresql.parameters` 중 제거·개명된 GUC 가 있는지
- major 업그레이드는 in-place 가 아니다. CNPG 는 논리 복제(`import`) 또는
새 클러스터 생성 후 데이터 이관 방식을 쓴다. `imageName` 만 바꾸면 기동에 실패한다.
## 7. 문서 갱신
| 파일 | 갱신 내용 |
| --- | --- |
| `CUSTOM-README.md` | 차트/PostgreSQL 버전, 변경된 values 키, operator 호환 버전 |
| `BUILD-README.md` | 이 문서의 절차에 변경이 있으면 반영 |
| `dip-values.yaml` / `dip-*-quotas.yaml` | 파라미터 이름이 바뀌었으면 반영 |
| `doc/charts/cnpg/deploy-test.md` | 신규 버전 검증 결과 추가 |
## 8. PR
```sh
git add manifests/helm/cnpg-cluster/<신규버전> doc/
git commit -m "cnpg-cluster <신규버전> 추가 (PostgreSQL <버전>)"
git push -u origin update-cnpg-cluster/<신규버전>
```
`helm-catalog-sbom` 워크플로가 PR 에서 변경 차트의 SBOM·취약점 스캔을 수행한다.
@@ -0,0 +1,269 @@
# cnpg-cluster 배포
차트 버전 `1.0.0` / PostgreSQL `18`
CloudNativePG 의 `Cluster` / `Database` / `Pooler` / `ScheduledBackup` 커스텀 리소스를
Helm 으로 감싼 PaaSup 자체 제작 차트다. 업스트림 차트가 아니다.
**사전 조건:** `cloudnative-pg` 오퍼레이터(차트 `0.29.0` / operator `1.30.0`)가 먼저 설치되어
있어야 한다. 오퍼레이터가 없으면 webhook 부재로 `Cluster` 생성 자체가 거부된다.
## 1. 배포 방법
### 1) 배포 시 주의 사항
- **`instances` 와 노드 수를 맞춘다.** `affinity.podAntiAffinityType: required` 상태에서
노드 수가 `instances` 보다 적으면 Pod 가 `Pending` 에 머문다. 단일 노드 개발 환경은
`preferred` 로 내려야 한다.
- **`backup.enabled: false` 면 PITR 이 불가능하다.** 또한 WAL 이 오브젝트 스토리지로
아카이브되지 않아 `walStorage` 볼륨에 계속 쌓인다. 운영 배포는 반드시 백업을 켠다.
- **확장(extension)은 `databases` 로 선언한다.** `bootstrap.initdb.postInitApplicationSQL`
`CREATE EXTENSION` 을 넣으면 **오류 없이 무시된다** (operator 1.30.0 에서 확인,
`doc/charts/cnpg/deploy-test.md` 검증 기록 참고).
- **`bootstrap` 은 최초 1회만 적용된다.** 이미 생성된 클러스터의 `bootstrap.initdb.database`
를 바꿔도 아무 일도 일어나지 않는다. DB 추가는 `databases` 로 한다.
### 2) 배포
```sh
git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog/manifests/helm/cnpg-cluster/1.0.0
helm upgrade pg-cnpg ./ -f custom-values.yaml --install -n <namespace> --create-namespace
```
### 3) 확인
```sh
# FQN 필수 — kubectl get cluster 는 Rancher/CAPI 리소스와 충돌한다
kubectl -n <ns> get clusters.postgresql.cnpg.io pg-cnpg
kubectl -n <ns> get pods -l cnpg.io/cluster=pg-cnpg -L cnpg.io/instanceRole
kubectl -n <ns> get databases.postgresql.cnpg.io
```
## 2. custom-values.yaml 설명
### 1) 클러스터 규모
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `instances` | PostgreSQL 인스턴스 수. 1 = 단독(failover 불가), 3 = primary 1 + replica 2 | `3` |
| `primaryUpdateStrategy` | `unsupervised` = operator 가 자동 switchover 후 업데이트. `supervised` = 운영자 수동 승격 | `unsupervised` |
| `primaryUpdateMethod` | `switchover` = 정상 전환. `restart` = 제자리 재시작(다운타임 발생) | `switchover` |
### 2) 이미지
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `postgresql.image.repository` | PostgreSQL 이미지 저장소 | `ghcr.io/cloudnative-pg/postgresql` |
| `postgresql.image.tag` | **베이스 OS 를 포함한 태그를 써야 한다** (아래 경고 참고) | `18.4-system-trixie` |
| `postgresql.imageName` | 전체 이미지 경로 직접 지정 (오프라인 미러). 지정 시 위 두 값은 무시된다 | `""` |
#### 이미지 타입 — `system` 은 deprecated 다
CNPG 는 세 가지 타입을 발행한다.
([업스트림 README](https://github.com/cloudnative-pg/postgres-containers#image-types))
| 타입 | 내용 | 상태 |
| --- | --- | --- |
| `minimal` | PostgreSQL 본체. **PG18+ 는 JIT 없음, pgaudit·pgvector 없음** | 현행 |
| `standard` | + pgaudit, pgvector, pg-failover-slots, JIT, 전 로케일 | **현행 · 권장** |
| `system` | `standard` + barman-cloud 바이너리 | **deprecated** |
업스트림은 `standard``system` 과 **기능 동등**하며, barman-cloud 는
[Barman Cloud Plugin](https://github.com/cloudnative-pg/plugin-barman-cloud) 으로 대체하라고
명시한다. `minimal`/`standard` 는 애초에 백업 플러그인과 함께 쓰도록 설계된 이미지다.
**현재 `custom-values.yaml``18.4-system-trixie`(deprecated)를 쓰고 있다.**
`standard-trixie` 로 전환하면 차단 CVE 가 32 → 23 건으로 줄어든다(barman 의 Python 스택 제거).
전환 전 백업 경로를 정해야 한다 — 아래 [8. 백업](#8-백업) 참고.
**경고 — 맨 major 태그(`:18`)를 쓰지 말 것.** 실측 결과다.
| 태그 | 베이스 OS | 지원 종료 |
| --- | --- | --- |
| `ghcr.io/cloudnative-pg/postgresql:18` | Debian 11 (bullseye) | **2026-08-31** |
| `ghcr.io/cloudnative-pg/postgresql:18.4-system-trixie` | Debian 13 (trixie) | 2030-06-30 |
두 태그 모두 PostgreSQL 18.4 지만 베이스 OS 가 다르다. `:18` 로 배포하면 instance-manager 가
`OS distribution is deprecated` 를 로그로 남긴다. 보안 카탈로그 관점에서 EOL 베이스 이미지는
패치되지 않는 OS 패키지 CVE 를 그대로 안고 가는 것이므로 사용하지 않는다.
`18.4-system-trixie` 에 포함된 확장(실측): `pgaudit 18.0`, `pg_stat_statements 1.12`,
`pgcrypto 1.4`, `pg_trgm 1.6`, `vector 0.8.5`.
참고로 `imageName` 을 아예 비우면 operator 1.30.0 이 `18.4-system-trixie` 를 기본값으로
채워준다. 다만 SBOM·재현성을 위해 카탈로그에서는 태그를 명시적으로 고정한다.
### 3) PostgreSQL 설정
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `postgresql.parameters` | postgresql.conf 파라미터 map. 값은 전부 문자열로 렌더링된다 | custom-values.yaml 참고 |
| `postgresql.sharedPreloadLibraries` | preload 라이브러리 목록. operator 가 자체 항목과 병합한다 | `[pgaudit, pg_stat_statements]` |
| `postgresql.pg_hba` | pg_hba.conf 추가 규칙. CNPG 기본값은 TLS + scram-sha-256 | `[]` |
| `postgresql.synchronous` | 동기 복제. 미설정 시 비동기(async). 운영은 `{method: any, number: 1}` 권장 | 미설정 |
`sharedPreloadLibraries` 에 올리는 것과 `databases[].extensions``CREATE EXTENSION` 하는 것은
**별개**다. `pg_stat_statements` 는 둘 다 필요하다 — preload 만 하면 뷰가 없고,
extension 만 만들면 데이터가 수집되지 않는다.
### 4) 스토리지
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `storage.size` / `storage.storageClass` | 데이터 볼륨 | `10Gi` / `longhorn` |
| `walStorage.enabled` | WAL 을 별도 볼륨으로 분리. I/O 경합 감소 + WAL 폭증이 데이터 볼륨을 채우는 것을 방지 | `true` |
| `walStorage.size` | WAL 볼륨. 대략 데이터의 50%. 백업 미설정 시 더 크게 잡는다 | `5Gi` |
볼륨 크기는 축소할 수 없다. 티어별 값은 `dip-volumes-quotas.yaml` 참고.
### 5) 데이터베이스·확장 (Database CRD)
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `databases[].name` | DB 이름. `bootstrap.initdb.database` 와 같은 이름을 쓰면 그 DB 를 관리 대상으로 잡는다 | - |
| `databases[].owner` | 소유자. 생략 시 `bootstrap.initdb.owner` | - |
| `databases[].ensure` | `present` / `absent` | `present` |
| `databases[].reclaimPolicy` | `retain` = Database 리소스를 지워도 실제 DB 보존. `delete` = 함께 삭제 | `retain` |
| `databases[].extensions` | `[{name, ensure, version, schema}]` | `[]` |
| `databases[].schemas` | `[{name, ensure, owner}]` | `[]` |
#### 중요 — 확장이 조용히 사라지는 문제 (operator 1.30.0 실측)
**증상.** primary switchover(failover, 롤링 이미지 업데이트) 이후 `Database` 로 선언한 확장이
전체 인스턴스에서 사라진다. 그런데 `Database.status` 는 계속 `applied: true` 로 남는다.
**조용한 실패이므로 status 만 보면 정상으로 보인다.**
배포 테스트에서 2회 모두 재현되었다 (failover 후 1회, 롤링 이미지 업데이트 후 1회).
데이터(테이블·레코드)는 정상 보존되며 확장만 유실된다.
**원인.** `Database` reconciler 는 **spec generation 이 바뀔 때만** 동작한다. 지속적으로
수렴(converge)시키지 않는다. 검증 내용:
- DB 에서 수동으로 `DROP EXTENSION` → 2분간 관찰, operator 는 복구하지 않음.
`status.extensions[].applied` 는 계속 `true`.
- `helm upgrade` 로 동일한 Database 매니페스트 재적용 → spec 이 같으므로 generation 불변
**reconcile 이 돌지 않아 복구되지 않음**.
- `kubectl annotate` → generation 을 바꾸지 않으므로 효과 없음.
**점검 방법.** status 를 믿지 말고 DB 에 직접 질의한다.
```sh
kubectl -n <ns> exec <primary-pod> -c postgres -- \
psql -U postgres -d appdb -Atc "SELECT extname, extversion FROM pg_extension ORDER BY 1;"
```
**복구 방법 (검증됨).** `Database` 리소스를 삭제하고 재생성해 generation 을 초기화한다.
`reclaimPolicy: retain` 이면 실제 DB 와 데이터는 보존된다(테스트에서 레코드 수 유지 확인).
```sh
kubectl -n <ns> delete databases.postgresql.cnpg.io <release>-<dbname>
helm upgrade <release> ./ -f custom-values.yaml -n <ns> # 재생성 → reconcile 실행
```
`reclaimPolicy: delete` 로 설정한 상태에서 이 절차를 쓰면 **실제 DB 가 삭제된다.**
반드시 `retain` 인지 먼저 확인한다.
**운영 권고**
- switchover·업그레이드 후에는 확장 존재 여부를 점검 항목에 넣는다.
- 확장 유무에 기능이 의존하는 서비스(감사 로깅 등)는 확장 존재를 애플리케이션 레벨에서
헬스체크하거나, 위 점검을 모니터링으로 자동화한다.
- `pgaudit` 처럼 보안 요건에 해당하는 확장이 조용히 사라지면 **감사 로그가 중단된다.**
`shared_preload_libraries``Cluster` spec 이라 유지되지만, `CREATE EXTENSION`
풀리면 pgaudit 의 세션 감사 기능이 동작하지 않는다.
### 6) 인증·보안
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `enableSuperuserAccess` | `false` 면 postgres superuser 시크릿이 아예 생성되지 않는다. 보안상 `false` 권장 | `false` |
| `bootstrap.initdb.secretName` | 앱 계정 비밀번호를 담은 기존 시크릿. 미지정 시 operator 가 `<release>-app` 에 무작위 생성 | `""` |
operator 가 클러스터 CA(`<release>-ca`), 서버 인증서(`<release>-server`),
복제 인증서(`<release>-replication`)를 자동 발급한다. 인스턴스 간 통신은 mTLS 다.
컨테이너는 항상 non-root(uid 26)로 실행되며 차트에서 재정의할 값이 없다.
### 7) 배치
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `affinity.podAntiAffinityType` | `required` = 노드당 1개 강제(운영). `preferred` = 부족해도 스케줄(단일 노드 개발) | `preferred` |
| `affinity.topologyKey` | 분산 기준. 멀티 AZ 는 `topology.kubernetes.io/zone` | `kubernetes.io/hostname` |
### 8) 백업
CNPG 1.30 은 세 가지 백업 경로를 제공한다. 이 차트는 현재 첫 번째만 구현하고 있다.
| 경로 | CRD 필드 | 이미지 내장 barman | 이 차트 지원 |
| --- | --- | --- | --- |
| 오브젝트 스토리지 (in-core) | `spec.backup.barmanObjectStore` | **필요** (`system` 전용) | ✅ (`backup.*`) |
| CSI 볼륨 스냅샷 | `spec.backup.volumeSnapshot` | 불필요 | ❌ 미구현 |
| Barman Cloud Plugin | `spec.plugins[]` + `isWALArchiver` | 불필요 | ❌ 미구현 |
**in-core barman 은 phase out 예정이다.** `standard`/`minimal` 이미지로 전환하려면 아래 둘 중
하나를 구현해야 한다.
- **플러그인**: barman 이 사이드카 이미지에 있어 PostgreSQL 이미지와 분리된다. 업스트림 권장
- **CSI 스냅샷**: `VolumeSnapshotClass` 가 선행 필요하다. 이 dev 클러스터는 VolumeSnapshot CRD 와
Longhorn CSI 드라이버는 있으나 **클래스가 정의되어 있지 않다.** 또한 스냅샷은 베이스 백업이므로
스냅샷 시점 사이로 복구(PITR)하려면 WAL 아카이빙이 별도로 필요하다
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `backup.enabled` | S3 호환 스토리지로 WAL 아카이브 + base backup (`system` 이미지 필요) | `false` |
| `backup.retentionPolicy` | 보존 기간 | `30d` |
| `backup.barmanObjectStore.destinationPath` | 예: `s3://pg-backup/cnpg` | `""` |
| `backup.barmanObjectStore.endpointURL` | MinIO/RustFS 등 사설 S3 엔드포인트 | `""` |
| `scheduledBackup.enabled` | 정기 백업 활성화 (`backup.enabled: true` 필요) | `false` |
| `scheduledBackup.schedule` | **6필드 cron** (초 분 시 일 월 요일). 표준 5필드가 아니다 | `"0 0 2 * * *"` |
### 9) 커넥션 풀러
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `pooler.enabled` | PgBouncer 배포 | `false` |
| `pooler.type` | `rw` (primary) / `ro` (replica) | `rw` |
| `pooler.poolMode` | `transaction` 권장. `session` 은 풀링 효과가 낮다 | `transaction` |
## 3. 접속
| 서비스 | 대상 |
| --- | --- |
| `<release>-rw` | primary — 읽기/쓰기 |
| `<release>-ro` | replica 만 — 읽기 전용 |
| `<release>-r` | 전체 인스턴스 — 읽기 라운드로빈 |
| `<release>-pooler-rw` | PgBouncer 경유 (pooler 활성화 시) |
```sh
kubectl -n <ns> get secret <release>-app -o jsonpath='{.data.password}' | base64 -d
```
## 4. 운영
### failover
primary Pod 손실 시 operator 가 자동으로 replica 를 승격한다. 실측 2~3초.
구 primary 는 재기동 후 replica 로 자동 재합류한다.
수동 switchover 는 `Cluster``status.targetPrimary` 를 직접 바꾸지 않고
`kubectl cnpg promote` 플러그인을 쓴다. 플러그인이 없으면 primary Pod 를 삭제하는
방식으로 대체할 수 있다(계획된 전환에는 권장하지 않음).
### 시퀀스 주의
failover 후 `serial`/`identity` 시퀀스 값이 점프한다(실측 1 → 34). WAL 에 기록되지 않은
시퀀스 캐시 블록이 유실되는 PostgreSQL 표준 동작이다. 시퀀스 연속성을 가정하는 애플리케이션은
영향을 받는다.
### 제거
```sh
helm uninstall <release> -n <ns>
# PVC 는 남는다. 데이터까지 지우려면 명시적으로 삭제한다.
kubectl -n <ns> get pvc -l cnpg.io/cluster=<release>
```
## 5. 검증 이력
`doc/charts/cnpg/deploy-test.md` 참고.
@@ -0,0 +1,6 @@
apiVersion: v2
name: cnpg-cluster
description: A Helm chart for PostgreSQL cluster with CloudNativePG operator
type: application
version: 1.0.0
appVersion: "18.4"
@@ -0,0 +1,38 @@
# cnpg-cluster
CloudNativePG 커스텀 리소스를 Helm 으로 감싼 PostgreSQL 클러스터 차트.
- 차트 버전: `1.0.0`
- PostgreSQL: `18`
- 필요 operator: `cloudnative-pg` 차트 `0.29.0` (operator `1.30.0`) 이상
## 생성되는 리소스
| 리소스 | 조건 | 파일 |
| --- | --- | --- |
| `Cluster` | 항상 | `templates/cluster.yaml` |
| `Database` | `databases` 가 비어있지 않을 때 | `templates/database.yaml` |
| `Pooler` | `pooler.enabled: true` | `templates/pooler.yaml` |
| `ScheduledBackup` | `backup.enabled` + `scheduledBackup.enabled` | `templates/scheduled-backup.yaml` |
operator 가 위 리소스를 받아 StatefulSet 없이 개별 Pod, PVC, Service(`-rw`/`-ro`/`-r`),
TLS 시크릿을 생성한다.
## 빠른 시작
```sh
helm upgrade pg-cnpg ./ -f custom-values.yaml --install -n <namespace> --create-namespace
```
배포 옵션과 주의사항은 [CUSTOM-README.md](CUSTOM-README.md), 버전 갱신 절차는
[BUILD-README.md](BUILD-README.md) 를 참고한다.
## 값 파일
| 파일 | 용도 |
| --- | --- |
| `values.yaml` | 차트 기본값 (전체 키 문서화) |
| `custom-values.yaml` | PaaSup 표준 오버라이드 |
| `dip-values.yaml` | DIP 플랫폼 운영 배포값 (백업·pooler·동기복제 활성) |
| `dip-resources-quotas.yaml` | Small/Medium/Large CPU·메모리 티어 |
| `dip-volumes-quotas.yaml` | Small/Medium/Large 볼륨 티어 |
@@ -0,0 +1,123 @@
# cnpg-cluster — PaaSup 오버라이드
# 사전 조건: cloudnative-pg operator(차트 0.29.0 / operator 1.30.0)가 설치되어 있어야 한다.
instances: 3
postgresql:
# SUSE BCI 15.7 기반 자체 하드닝 빌드로 교체했다 (2026-07-28).
# 결정 근거·받아들인 비용 → doc/decisions/0001-cnpg-postgresql-image.md
# 빌드 정의 → images/cnpg-postgresql/suse.Dockerfile + suse.build.env
#
# 태그에 빌드일을 포함한다. 같은 앱 버전이라도 베이스 업데이트 결과가 시점마다 다르므로
# 롤링 태그를 쓰지 않는다 (doc/image-selection.md 2번).
imageName: "docker.io/wbsong111/cnpg-postgresql:18.4-bci15.7-hardened-20260729"
#
# trivy 는 SLES 15.7 을 정상 커버한다(2026-07-29 재측정, 양성 대조로 13건 실측 —
# doc/analysis/sles-oval-measurement.md). 2026-07-28 시점에는 "trivy 가 SLES 15.7
# 데이터를 커버하지 않아 0건이 측정 불가다"로 판단해 OVAL 직접 평가로 우회했으나
# 이는 오판이었던 것으로 정정됐다 — 실효 C/H 0/0 은 실제 결과다. 게이트 PASS.
#
# ⚠️ pg-failover-slots 확장이 없다 — PGDG zypp 저장소에 패키지가 없다.
# 이 차트는 쓰지 않지만 상위 구성에서 요구하면 확인해야 한다.
#
# 이전 값: ghcr.io/cloudnative-pg/postgresql:18.4-system-trixie
# 업스트림 deprecated 타입이었고 실효 CRITICAL/HIGH 6/26 (차단 32건) 이었다.
# 그 32건은 전부 수정 버전이 없어 어떤 조치로도 해소되지 않는다.
# image:
# repository: ghcr.io/cloudnative-pg/postgresql
# tag: "18.4-standard-trixie"
parameters:
max_connections: "200"
shared_buffers: 256MB
work_mem: 8MB
maintenance_work_mem: 128MB
effective_cache_size: 1GB
log_timezone: Asia/Seoul
timezone: Asia/Seoul
# 감사 로깅. CNPG 기본 이미지에 pgaudit 가 포함되어 있다.
sharedPreloadLibraries:
- pgaudit
- pg_stat_statements
primaryUpdateStrategy: unsupervised
primaryUpdateMethod: switchover
storage:
size: 20Gi
storageClass: longhorn
walStorage:
enabled: true
size: 10Gi
storageClass: longhorn
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
bootstrap:
initdb:
database: appdb
owner: appuser
# 비밀번호를 직접 관리할 때 지정. 미지정 시 operator 가 <release>-app 시크릿에 자동 생성한다.
# secretName: "$INFISICAL_SECRET"
encoding: UTF8
# CREATE EXTENSION 은 여기에 넣지 않는다 (CNPG 1.30.0 에서 무시됨). 아래 databases 로 선언한다.
postInitApplicationSQL: []
# 확장은 Database CRD 로 선언한다. sharedPreloadLibraries 에 올린 것과 짝을 맞춘다.
databases:
- name: appdb
owner: appuser
ensure: present
reclaimPolicy: retain
extensions:
- name: pg_stat_statements
- name: pgaudit
# postgres superuser 직접 접속 차단. 시크릿 자체가 생성되지 않는다.
enableSuperuserAccess: false
affinity:
enablePodAntiAffinity: true
# 단일 노드 dev 환경에서는 preferred 여야 3 인스턴스가 스케줄된다.
# 노드 수 >= instances 인 운영 환경에서는 required 로 변경한다.
podAntiAffinityType: preferred
topologyKey: kubernetes.io/hostname
nodeSelector: {}
tolerations: []
monitoring:
# rancher-monitoring(Prometheus Operator) 설치 환경에서만 true
enablePodMonitor: false
# 백업 — 운영 배포 시 반드시 활성화한다. 미설정이면 PITR 불가.
backup:
enabled: false
retentionPolicy: 30d
barmanObjectStore:
destinationPath: "" # 예: s3://pg-backup/cnpg
endpointURL: "" # 예: http://rustfs.defense-llm.svc.cluster.local:9000
s3Credentials:
accessKeyId:
name: "" # 예: pg-backup-s3
key: ACCESS_KEY_ID
secretAccessKey:
name: ""
key: ACCESS_SECRET_KEY
scheduledBackup:
enabled: false
schedule: "0 0 2 * * *" # 6필드 cron — 매일 02:00
pooler:
enabled: false
instances: 2
type: rw
poolMode: transaction
@@ -0,0 +1,52 @@
small:
instances: 1
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 1000m
memory: 2Gi
postgresql:
parameters:
max_connections: "100"
shared_buffers: 256MB
effective_cache_size: 1GB
pooler:
enabled: false
medium:
instances: 3
resources:
requests:
cpu: "1"
memory: 4Gi
limits:
cpu: "2"
memory: 8Gi
postgresql:
parameters:
max_connections: "200"
shared_buffers: 2GB
effective_cache_size: 6GB
pooler:
enabled: true
instances: 2
large:
instances: 3
resources:
requests:
cpu: "4"
memory: 16Gi
limits:
cpu: "8"
memory: 32Gi
postgresql:
parameters:
max_connections: "500"
shared_buffers: 8GB
effective_cache_size: 24GB
pooler:
enabled: true
instances: 3
@@ -0,0 +1,110 @@
# cnpg-cluster — DIP 플랫폼 기본 배포값 (멀티 테넌트 운영 기준)
instances: 3
postgresql:
# custom-values.yaml 과 동일하게 SUSE BCI 15.7 자체 빌드를 쓴다.
# 근거·비용 → doc/decisions/0001-cnpg-postgresql-image.md
# trivy 는 SLES 15.7 을 정상 커버한다(2026-07-29 재측정 — doc/analysis/sles-oval-measurement.md).
# 실효 C/H 0/0, 게이트 PASS.
imageName: "docker.io/wbsong111/cnpg-postgresql:18.4-bci15.7-hardened-20260729"
parameters:
max_connections: "200"
shared_buffers: 512MB
work_mem: 16MB
maintenance_work_mem: 256MB
effective_cache_size: 3GB
log_timezone: Asia/Seoul
timezone: Asia/Seoul
# 감사 로깅 — pgaudit 대상 지정
pgaudit.log: "ddl, role, write"
pgaudit.log_catalog: "off"
pgaudit.log_parameter: "on"
sharedPreloadLibraries:
- pgaudit
- pg_stat_statements
# 운영 환경은 최소 1개 동기 복제본을 요구한다.
synchronous:
method: any
number: 1
primaryUpdateStrategy: unsupervised
primaryUpdateMethod: switchover
storage:
size: 100Gi
storageClass: longhorn
walStorage:
enabled: true
size: 50Gi
storageClass: longhorn
resources:
requests:
cpu: "1"
memory: 4Gi
limits:
cpu: "4"
memory: 8Gi
bootstrap:
initdb:
database: appdb
owner: appuser
# 플랫폼은 Infisical 로 자격증명을 주입한다.
secretName: "$INFISICAL_SECRET"
encoding: UTF8
postInitApplicationSQL: []
databases:
- name: appdb
owner: appuser
ensure: present
reclaimPolicy: retain
extensions:
- name: pg_stat_statements
- name: pgaudit
enableSuperuserAccess: false
affinity:
enablePodAntiAffinity: true
# 운영 클러스터는 노드 수 >= instances 이므로 required 로 노드 분산을 강제한다.
podAntiAffinityType: required
topologyKey: kubernetes.io/hostname
nodeSelector: {}
tolerations: []
monitoring:
enablePodMonitor: true
backup:
enabled: true
retentionPolicy: 30d
barmanObjectStore:
destinationPath: "s3://dip-pg-backup/cnpg"
endpointURL: ""
s3Credentials:
accessKeyId:
name: pg-backup-s3
key: ACCESS_KEY_ID
secretAccessKey:
name: pg-backup-s3
key: ACCESS_SECRET_KEY
scheduledBackup:
enabled: true
schedule: "0 0 2 * * *"
pooler:
enabled: true
instances: 2
type: rw
poolMode: transaction
parameters:
max_client_conn: "1000"
default_pool_size: "25"
@@ -0,0 +1,30 @@
# data 와 WAL 을 별도 볼륨으로 분리한다.
# WAL 볼륨은 대략 data 의 50% 를 잡는다. 백업(barmanObjectStore) 미설정 시 WAL 이
# 정리되지 않고 쌓이므로 WAL 볼륨을 더 크게 잡아야 한다.
small:
storage:
size: 20Gi
storageClass: "longhorn"
walStorage:
enabled: true
size: 10Gi
storageClass: "longhorn"
medium:
storage:
size: 100Gi
storageClass: "longhorn"
walStorage:
enabled: true
size: 50Gi
storageClass: "longhorn"
large:
storage:
size: 500Gi
storageClass: "longhorn"
walStorage:
enabled: true
size: 200Gi
storageClass: "longhorn"
@@ -0,0 +1,55 @@
PostgreSQL 클러스터 "{{ include "cnpg-cluster.fullname" . }}" 를 배포했습니다.
인스턴스 {{ .Values.instances }}개 / 이미지 {{ include "cnpg-cluster.image" . }}
1. 상태 확인 (반드시 FQN 을 사용하세요 — kubectl get cluster 는 Rancher/CAPI 리소스와 충돌합니다)
kubectl -n {{ include "cnpg-cluster.namespace" . }} get clusters.postgresql.cnpg.io {{ include "cnpg-cluster.fullname" . }}
kubectl -n {{ include "cnpg-cluster.namespace" . }} get pods -l cnpg.io/cluster={{ include "cnpg-cluster.fullname" . }} -L cnpg.io/instanceRole
2. 접속 엔드포인트
{{ include "cnpg-cluster.fullname" . }}-rw : primary (읽기/쓰기)
{{ include "cnpg-cluster.fullname" . }}-ro : replica 만 (읽기 전용)
{{ include "cnpg-cluster.fullname" . }}-r : 전체 인스턴스 (읽기 라운드로빈)
{{- if .Values.pooler.enabled }}
{{ include "cnpg-cluster.fullname" . }}-pooler-{{ .Values.pooler.type }} : PgBouncer ({{ .Values.pooler.poolMode }} 모드)
{{- end }}
3. 자격증명
DB : {{ .Values.bootstrap.initdb.database }}
User : {{ .Values.bootstrap.initdb.owner }}
{{- if .Values.bootstrap.initdb.secretName }}
Secret : {{ .Values.bootstrap.initdb.secretName }} (사용자 지정)
{{- else }}
Secret : {{ include "cnpg-cluster.fullname" . }}-app (operator 자동 생성)
kubectl -n {{ include "cnpg-cluster.namespace" . }} get secret {{ include "cnpg-cluster.fullname" . }}-app \
-o jsonpath='{.data.password}' | base64 -d
{{- end }}
{{- if not .Values.enableSuperuserAccess }}
superuser(postgres) 직접 접속은 비활성화되어 있습니다 (enableSuperuserAccess: false).
{{- end }}
4. psql 접속 예시
kubectl -n {{ include "cnpg-cluster.namespace" . }} exec -it {{ include "cnpg-cluster.fullname" . }}-1 -c postgres -- \
psql -U postgres -d {{ .Values.bootstrap.initdb.database }}
{{- if not .Values.backup.enabled }}
경고: 백업이 비활성화되어 있습니다(backup.enabled: false). 운영 환경에서는
barmanObjectStore 를 설정하고 scheduledBackup 을 활성화하세요.
백업 없이는 PITR(Point-In-Time Recovery)이 불가능합니다.
{{- end }}
{{- if eq (int .Values.instances) 1 }}
경고: instances=1 입니다. replica 가 없어 failover 가 불가능합니다.
{{- end }}
{{- if eq .Values.affinity.podAntiAffinityType "preferred" }}
참고: podAntiAffinityType 이 "preferred" 입니다. 여러 인스턴스가 같은 노드에
배치될 수 있어 노드 장애 시 동시 손실 위험이 있습니다.
노드 수가 instances 이상인 운영 환경에서는 "required" 를 사용하세요.
{{- end }}
@@ -0,0 +1,70 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "cnpg-cluster.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "cnpg-cluster.fullname" -}}
{{- if .Values.nameOverride }}
{{- .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- .Release.Name | default "cnpg-cluster" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "cnpg-cluster.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "cnpg-cluster.labels" -}}
helm.sh/chart: {{ include "cnpg-cluster.chart" . }}
{{ include "cnpg-cluster.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "cnpg-cluster.selectorLabels" -}}
app.kubernetes.io/name: {{ include "cnpg-cluster.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the namespace to use
*/}}
{{- define "cnpg-cluster.namespace" -}}
{{- if .Values.namespaceOverride }}
{{- .Values.namespaceOverride }}
{{- else }}
{{- .Release.Namespace }}
{{- end }}
{{- end }}
{{/*
PostgreSQL 이미지. imageName 이 지정되면 그것을 그대로 쓰고, 아니면 repository:tag 조합.
주의: 맨 major 태그(`:18`)를 쓰면 안 된다. 그 태그는 Debian 11(bullseye) 기반으로
2026-08-31 에 지원이 종료된다. 반드시 베이스 OS 를 포함한 태그를 지정한다
(예: 18.4-system-trixie → Debian 13, 2030-06-30 까지 지원).
*/}}
{{- define "cnpg-cluster.image" -}}
{{- if .Values.postgresql.imageName }}
{{- .Values.postgresql.imageName }}
{{- else }}
{{- printf "%s:%s" .Values.postgresql.image.repository (.Values.postgresql.image.tag | toString) }}
{{- end }}
{{- end }}
@@ -0,0 +1,122 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: {{ include "cnpg-cluster.fullname" . }}
namespace: {{ include "cnpg-cluster.namespace" . }}
labels:
{{- include "cnpg-cluster.labels" . | nindent 4 }}
{{- with .Values.extraLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.extraAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
instances: {{ .Values.instances }}
imageName: {{ include "cnpg-cluster.image" . }}
primaryUpdateStrategy: {{ .Values.primaryUpdateStrategy }}
primaryUpdateMethod: {{ .Values.primaryUpdateMethod }}
enableSuperuserAccess: {{ .Values.enableSuperuserAccess }}
postgresql:
{{- with .Values.postgresql.parameters }}
parameters:
{{- range $k, $v := . }}
{{ $k }}: {{ $v | quote }}
{{- end }}
{{- end }}
{{- with .Values.postgresql.sharedPreloadLibraries }}
shared_preload_libraries:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with .Values.postgresql.pg_hba }}
pg_hba:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with .Values.postgresql.synchronous }}
synchronous:
{{- toYaml . | nindent 6 }}
{{- end }}
bootstrap:
initdb:
database: {{ .Values.bootstrap.initdb.database }}
owner: {{ .Values.bootstrap.initdb.owner }}
{{- with .Values.bootstrap.initdb.secretName }}
secret:
name: {{ . }}
{{- end }}
{{- with .Values.bootstrap.initdb.encoding }}
encoding: {{ . }}
{{- end }}
{{- with .Values.bootstrap.initdb.localeCollate }}
localeCollate: {{ . }}
{{- end }}
{{- with .Values.bootstrap.initdb.localeCType }}
localeCType: {{ . }}
{{- end }}
{{- with .Values.bootstrap.initdb.postInitApplicationSQL }}
postInitApplicationSQL:
{{- toYaml . | nindent 8 }}
{{- end }}
storage:
size: {{ .Values.storage.size }}
{{- with .Values.storage.storageClass }}
storageClass: {{ . }}
{{- end }}
{{- if .Values.walStorage.enabled }}
walStorage:
size: {{ .Values.walStorage.size }}
{{- with .Values.walStorage.storageClass }}
storageClass: {{ . }}
{{- end }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 4 }}
affinity:
enablePodAntiAffinity: {{ .Values.affinity.enablePodAntiAffinity }}
podAntiAffinityType: {{ .Values.affinity.podAntiAffinityType }}
topologyKey: {{ .Values.affinity.topologyKey }}
{{- with .Values.affinity.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with .Values.affinity.tolerations }}
tolerations:
{{- toYaml . | nindent 6 }}
{{- end }}
monitoring:
enablePodMonitor: {{ .Values.monitoring.enablePodMonitor }}
{{- with .Values.monitoring.customQueriesConfigMap }}
customQueriesConfigMap:
{{- toYaml . | nindent 6 }}
{{- end }}
{{- if .Values.backup.enabled }}
backup:
retentionPolicy: {{ .Values.backup.retentionPolicy }}
barmanObjectStore:
destinationPath: {{ required "backup.enabled=true 이면 backup.barmanObjectStore.destinationPath 가 필요하다" .Values.backup.barmanObjectStore.destinationPath }}
{{- with .Values.backup.barmanObjectStore.endpointURL }}
endpointURL: {{ . }}
{{- end }}
s3Credentials:
accessKeyId:
name: {{ required "backup.enabled=true 이면 s3Credentials.accessKeyId.name 이 필요하다" .Values.backup.barmanObjectStore.s3Credentials.accessKeyId.name }}
key: {{ .Values.backup.barmanObjectStore.s3Credentials.accessKeyId.key }}
secretAccessKey:
name: {{ required "backup.enabled=true 이면 s3Credentials.secretAccessKey.name 이 필요하다" .Values.backup.barmanObjectStore.s3Credentials.secretAccessKey.name }}
key: {{ .Values.backup.barmanObjectStore.s3Credentials.secretAccessKey.key }}
wal:
{{- toYaml .Values.backup.barmanObjectStore.wal | nindent 8 }}
data:
{{- toYaml .Values.backup.barmanObjectStore.data | nindent 8 }}
{{- end }}
@@ -0,0 +1,48 @@
{{- /*
선언적 데이터베이스/확장 관리 (CNPG 1.26+ Database CRD).
중요: bootstrap.initdb.postInitApplicationSQL 안의 CREATE EXTENSION 은 CNPG 1.30.0 에서
반영되지 않는다(오류도 나지 않고 조용히 무시됨 — doc/deploy-test-cnpg.md 검증 기록 참고).
확장은 반드시 이 Database CRD 로 선언해야 한다.
*/}}
{{- range .Values.databases }}
---
apiVersion: postgresql.cnpg.io/v1
kind: Database
metadata:
name: {{ include "cnpg-cluster.fullname" $ }}-{{ .name }}
namespace: {{ include "cnpg-cluster.namespace" $ }}
labels:
{{- include "cnpg-cluster.labels" $ | nindent 4 }}
spec:
cluster:
name: {{ include "cnpg-cluster.fullname" $ }}
name: {{ .name }}
owner: {{ .owner | default $.Values.bootstrap.initdb.owner }}
ensure: {{ .ensure | default "present" }}
# retain = Database 리소스를 지워도 실제 DB 는 남는다 (운영 기본값)
databaseReclaimPolicy: {{ .reclaimPolicy | default "retain" }}
{{- with .extensions }}
extensions:
{{- range . }}
- name: {{ .name }}
ensure: {{ .ensure | default "present" }}
{{- with .version }}
version: {{ . | quote }}
{{- end }}
{{- with .schema }}
schema: {{ . }}
{{- end }}
{{- end }}
{{- end }}
{{- with .schemas }}
schemas:
{{- range . }}
- name: {{ .name }}
ensure: {{ .ensure | default "present" }}
{{- with .owner }}
owner: {{ . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -0,0 +1,28 @@
{{- if .Values.pooler.enabled }}
apiVersion: postgresql.cnpg.io/v1
kind: Pooler
metadata:
name: {{ include "cnpg-cluster.fullname" . }}-pooler-{{ .Values.pooler.type }}
namespace: {{ include "cnpg-cluster.namespace" . }}
labels:
{{- include "cnpg-cluster.labels" . | nindent 4 }}
spec:
cluster:
name: {{ include "cnpg-cluster.fullname" . }}
instances: {{ .Values.pooler.instances }}
type: {{ .Values.pooler.type }}
pgbouncer:
poolMode: {{ .Values.pooler.poolMode }}
{{- with .Values.pooler.parameters }}
parameters:
{{- range $k, $v := . }}
{{ $k }}: {{ $v | quote }}
{{- end }}
{{- end }}
template:
spec:
containers:
- name: pgbouncer
resources:
{{- toYaml .Values.pooler.resources | nindent 12 }}
{{- end }}
@@ -0,0 +1,16 @@
{{- if and .Values.backup.enabled .Values.scheduledBackup.enabled }}
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: {{ include "cnpg-cluster.fullname" . }}
namespace: {{ include "cnpg-cluster.namespace" . }}
labels:
{{- include "cnpg-cluster.labels" . | nindent 4 }}
spec:
# CNPG 의 schedule 은 6필드다 (초 분 시 일 월 요일) — 표준 5필드 cron 이 아니다.
schedule: {{ .Values.scheduledBackup.schedule | quote }}
backupOwnerReference: {{ .Values.scheduledBackup.backupOwnerReference }}
immediate: {{ .Values.scheduledBackup.immediate }}
cluster:
name: {{ include "cnpg-cluster.fullname" . }}
{{- end }}
@@ -0,0 +1,167 @@
# Default values for cnpg-cluster
# CloudNativePG operator(cloudnative-pg 차트)가 먼저 설치되어 있어야 한다.
nameOverride: ""
namespaceOverride: ""
# PostgreSQL 인스턴스 수. 1 = 단독, 3 이상 = primary 1 + replica N-1
instances: 3
postgresql:
image:
repository: ghcr.io/cloudnative-pg/postgresql
# 반드시 베이스 OS 를 포함한 태그를 쓴다.
# 금지: "18" — Debian 11(bullseye) 기반이며 2026-08-31 지원 종료
# 권장: "18.4-system-trixie" — Debian 13, 2030-06-30 까지 지원
# (operator 1.30.0 이 imageName 미지정 시 선택하는 기본값과 동일)
# 포함 확장: pgaudit 18.0, pg_stat_statements 1.12, pgcrypto 1.4, pg_trgm 1.6, vector 0.8.5
tag: "18.4-system-trixie"
# 전체 이미지 경로를 직접 지정할 때 사용 (오프라인 미러 등). 지정 시 image.* 는 무시된다.
imageName: ""
# postgresql.conf 파라미터
parameters:
max_connections: "100"
shared_buffers: 128MB
# 감사 로깅용. pgaudit 는 CNPG 기본 이미지에 포함되어 있다.
# shared_preload_libraries 는 CNPG 가 자동 관리하므로 직접 넣지 않는다.
# 확장 기능 (pgaudit 등). shared_preload_libraries 는 operator 가 계산한다.
sharedPreloadLibraries: []
# pg_hba.conf 추가 규칙. CNPG 기본값은 TLS 강제 + scram-sha-256 이다.
pg_hba: []
# - hostssl appdb appuser 10.42.0.0/16 scram-sha-256
# 동기 복제 설정. 미설정 시 비동기(async) 복제다.
# synchronous:
# method: any
# number: 1
# primary 업데이트 전략
# unsupervised = operator 가 자동으로 switchover 후 업데이트 (기본)
# supervised = 운영자가 수동 승격해야 진행
primaryUpdateStrategy: unsupervised
primaryUpdateMethod: switchover
storage:
size: 10Gi
storageClass: longhorn
# WAL 을 별도 볼륨으로 분리한다. I/O 경합을 줄이고 WAL 폭증이 데이터 볼륨을 채우는 것을 막는다.
walStorage:
enabled: true
size: 5Gi
storageClass: longhorn
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
# 초기 DB/사용자 생성
bootstrap:
initdb:
database: appdb
owner: appuser
# 비밀번호를 담은 기존 시크릿 이름. 미지정 시 operator 가 무작위 생성해
# <release>-app 시크릿에 저장한다.
secretName: ""
encoding: UTF8
localeCollate: ""
localeCType: ""
# 초기화 SQL. 최초 bootstrap 시 1회만 superuser 로 실행된다.
# 주의: CREATE EXTENSION 은 여기에 넣으면 안 된다. CNPG 1.30.0 에서 오류 없이
# 무시된다(검증 기록: doc/deploy-test-cnpg.md). 확장은 아래 databases 로 선언한다.
postInitApplicationSQL: []
# - ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;
# 선언적 데이터베이스·확장·스키마 관리 (CNPG 1.26+ Database CRD).
# 확장 설치의 정식 경로다. bootstrap 으로 만든 DB 도 여기서 관리 대상으로 선언할 수 있다.
databases: []
# - name: appdb # bootstrap.initdb.database 와 같은 이름을 쓰면 그 DB 를 관리한다
# owner: appuser # 생략 시 bootstrap.initdb.owner
# ensure: present # present | absent
# reclaimPolicy: retain # retain = Database 리소스를 지워도 실제 DB 는 보존
# extensions:
# - name: pg_stat_statements
# - name: pgaudit
# schemas:
# - name: app
# owner: appuser
# postgres(superuser) 계정으로의 직접 접속 허용 여부.
# false 면 superuser 시크릿이 아예 생성되지 않는다. 보안상 false 를 권장한다.
enableSuperuserAccess: false
# Pod 분산 배치
affinity:
enablePodAntiAffinity: true
# preferred = 노드가 부족해도 스케줄됨 (단일 노드 dev 환경 필수)
# required = 노드당 1개 강제 (운영 권장, 노드 수 >= instances 필요)
podAntiAffinityType: preferred
topologyKey: kubernetes.io/hostname
nodeSelector: {}
tolerations: []
# 참고: 컨테이너 보안 컨텍스트는 operator 가 관리한다. CNPG 는 항상 non-root(uid 26)
# runAsNonRoot / readOnlyRootFilesystem 로 Pod 를 만들며, 차트에서 재정의할 값이 없다.
monitoring:
# Prometheus Operator CRD 필요
enablePodMonitor: false
# 커스텀 메트릭 쿼리 ConfigMap
customQueriesConfigMap: []
# 백업 — S3 호환 오브젝트 스토리지 (Barman Cloud)
backup:
enabled: false
# 보존 기간
retentionPolicy: 30d
barmanObjectStore:
destinationPath: "" # 예: s3://pg-backup/cnpg
endpointURL: "" # 예: http://rustfs.defense-llm.svc:9000
s3Credentials:
accessKeyId:
name: "" # 시크릿 이름
key: ACCESS_KEY_ID
secretAccessKey:
name: ""
key: ACCESS_SECRET_KEY
wal:
compression: gzip
maxParallel: 2
data:
compression: gzip
jobs: 2
# 정기 백업 스케줄 (backup.enabled: true 일 때만 생성)
scheduledBackup:
enabled: false
# CNPG 의 cron 은 6필드다 (초 분 시 일 월 요일)
schedule: "0 0 2 * * *"
backupOwnerReference: self
immediate: false
# PgBouncer 커넥션 풀러
pooler:
enabled: false
instances: 2
type: rw # rw | ro
poolMode: transaction # session | transaction | statement
parameters:
max_client_conn: "1000"
default_pool_size: "25"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
# 추가 라벨/어노테이션
extraLabels: {}
extraAnnotations: {}
+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+109
View File
@@ -0,0 +1,109 @@
# etcd 버전 갱신 가이드
## 1. git 작업 환경 구성
```sh
git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog
git checkout -b update-etcd/<신규버전>
```
## 2. helm chart 업데이트
### 1) 신규 버전 확인
```sh
helm repo add groundhog2k https://groundhog2k.github.io/helm-charts/
helm repo update groundhog2k
helm search repo groundhog2k/etcd --versions | head
```
차트 버전과 etcd 버전(appVersion)은 다르다. 대응 관계를 반드시 확인한다.
| 차트 버전 | etcd(appVersion) |
| --- | --- |
| 1.1.12 | v3.7.1 |
### 2) 이미지 레지스트리 재확인 (매 버전 갱신 시 필수)
`CUSTOM-README.md` 에 적어둔 대로 etcd 프로젝트는 `gcr.io/etcd-development`,
`quay.io/coreos` 를 **3.8부터 폐지**하고 `registry.k8s.io/etcd` 로 이전한다
([etcd-io/etcd#20928](https://github.com/etcd-io/etcd/issues/20928)). 버전을 올릴 때마다
아래를 확인해 어느 레지스트리로 고정할지 다시 판단한다.
```sh
NEW_VER=v3.8.0 # 예시
# registry.k8s.io 에 신규 버전이 이미 올라와 있는지 확인
curl -sL "https://registry.k8s.io/v2/etcd/tags/list" | grep "\"$NEW_VER\""
# 아직 없다면 과도기 레지스트리(quay.io/coreos, gcr.io/etcd-development)로 폴백
```
### 3) 신규 버전 디렉토리 생성
버전별 독립 디렉토리다. 기존 디렉토리를 수정하지 않고 새로 만든다.
```sh
NEW=1.2.0
OLD=1.1.12
cd manifests/helm/etcd
helm pull groundhog2k/etcd --version "$NEW" --untar --untardir /tmp/etcd-pull
mkdir -p "$NEW"
cp -R /tmp/etcd-pull/etcd/. "$NEW"/
# PaaSup 관리 파일을 이전 버전에서 승계한다
for f in custom-values.yaml dip-values.yaml dip-resources-quotas.yaml \
dip-volumes-quotas.yaml CUSTOM-README.md BUILD-README.md; do
cp "$OLD/$f" "$NEW/$f"
done
```
### 4) diff 확인
```sh
diff -u "$OLD/values.yaml" "$NEW/values.yaml" | less
helm template test "$NEW" -f "$NEW/custom-values.yaml" >/dev/null && echo "렌더링 OK"
```
`custom-values.yaml``image.tag`/`initImage.tag` 를 신규 appVersion·busybox 최신
고정 태그로 갱신한다(위 2번 결과 반영).
### 5) 렌더링 결과 비교
```sh
helm template etcd "$OLD" -f "$OLD/custom-values.yaml" -n etcd-system > /tmp/old.yaml
helm template etcd "$NEW" -f "$NEW/custom-values.yaml" -n etcd-system > /tmp/new.yaml
diff -u /tmp/old.yaml /tmp/new.yaml
```
## 3. 문서 갱신
| 파일 | 갱신 내용 |
| --- | --- |
| `CUSTOM-README.md` | 차트/etcd 버전 번호, 이미지 레지스트리 판단 결과 |
| `BUILD-README.md` | 위 버전 대응 표에 신규 행 추가 |
| `doc/charts/etcd/deploy-test.md` | 신규 버전으로 배포 테스트 재실행 후 결과 갱신 |
| `doc/image-selection.md` | 현재 카탈로그 상태 표의 etcd 행 갱신 |
## 4. 배포 검증
```sh
IMAGE_NAME=<registry>/<repository>:<tag> bash scripts/deploy-test/deploy-test-etcd.sh /tmp/etcd-deploy-test-out
```
최소한 다음을 통과해야 한다.
1. 3-replica StatefulSet 이 모두 `Running`
2. `etcdctl endpoint health --cluster` 전 멤버 healthy
3. `etcdctl put`/`get` 쓰기·읽기 왕복 확인
## 5. PR
```sh
git add manifests/helm/etcd/<신규버전> doc/
git commit -m "etcd <신규버전> 추가"
git push -u origin update-etcd/<신규버전>
```
PR 생성 시 `helm-catalog-sbom` 워크플로가 변경 차트에 대해 SBOM·취약점 스캔을 수행한다.
CRITICAL 취약점이 있으면 내용을 확인하고 PR 본문에 판단 근거를 남긴다.
+106
View File
@@ -0,0 +1,106 @@
# etcd 배포
차트 버전 `1.1.12` (`groundhog2k/etcd`) / etcd 버전 `v3.7.1`
etcd 는 분산 key-value 저장소다. 이 차트는 오퍼레이터 없이 StatefulSet 으로 직접
멀티노드 클러스터를 구성한다(cnpg 처럼 operator + CR 구조가 아니다).
## 1. 배포 방법
### 1) 배포 시 주의 사항
- **`replicas` 는 최초 배포 후 변경할 수 없다.** 업스트림 차트가 명시하는 제약이다
(`values.yaml` 주석 — "Automatic scaling or manually scaling the etcd cluster after
first deployment is not supported"). 클러스터 크기를 바꾸려면 재배포(새 클러스터 생성
+ 데이터 마이그레이션)가 필요하다. 배포 전에 티어(`dip-resources-quotas.yaml`)를 반드시
확정한다.
- **자동 백업이 없다.** cnpg 의 `barmanObjectStore`/`scheduledBackup` 같은 내장 기능이
없다. 스냅샷은 운영자가 수동으로 실행한다.
```sh
kubectl -n <ns> exec <pod-0> -- etcdctl snapshot save /tmp/snapshot.db
kubectl -n <ns> cp <pod-0>:/tmp/snapshot.db ./snapshot.db
```
정기 백업이 필요하면 별도 CronJob 을 구성해야 한다 — 이 차트 범위 밖이다.
- **TLS 는 기본 비활성이다.** `custom-values.yaml` 은 업스트림 기본값(평문 클라이언트/피어
통신)을 그대로 쓴다. 운영 배포는 `dip-values.yaml``settings.https.autoTls: true`
켠다(자체 서명 인증서 자동 생성). CA 발급 인증서로 교체하려면 `extraSecrets` 로 마운트하고
`settings.https.autoTls``false` 로 내린 뒤 인증서 경로를 직접 지정해야 한다
(차트 `values.yaml``settings.https` 주석 참고).
- init 컨테이너(`busybox`)가 데이터 디렉토리(`/data/etcd`)를 `chmod 700` 으로 생성한다.
`securityContext`(uid/gid 999, non-root, RO rootfs)와 함께 이 권한이 맞물려야 정상
기동한다 — 배포 테스트에서 이미지가 실제로 uid 999 로 뜨는지 확인 완료
(`doc/charts/etcd/deploy-test.md`).
### 2) 배포
```sh
git clone https://github.com/paasup/dip-catalog.git
cd dip-catalog/manifests/helm/etcd/1.1.12
helm upgrade etcd ./ -f custom-values.yaml --install -n etcd-system --create-namespace --wait
```
### 3) 확인
```sh
kubectl -n etcd-system get pods -l app.kubernetes.io/name=etcd
kubectl -n etcd-system exec etcd-0 -- etcdctl endpoint health --cluster
```
## 2. custom-values.yaml 설명
### 1) 이미지 설정
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `image.registry` / `image.repository` / `image.tag` | etcd 본체 이미지. **자체 빌드**(`docker.io/wbsong111/etcd:3.7.1-security-hardened-20260731`) — 업스트림 `quay.io/coreos/etcd:v3.7.1` 이 게이트 차단 HIGH 1건(`CVE-2026-56852`, `golang.org/x/text`)으로 막혀 대체했다. 근거·빌드 정의는 [images/etcd/README.md](../../../images/etcd/README.md), 채택 결정 배경은 security-catalog 프로젝트 decisions/0007(dip-catalog 미이관). 상위 태그가 나오거나 `release-3.7` 에 백포트되면 업스트림으로 되돌리는 것이 우선(대응 우선순위 a) | 업스트림 `quay.io/coreos/etcd` (태그 미지정) |
| `initImage.registry` / `initImage.repository` / `initImage.tag` | 데이터 디렉토리 초기화용 init 컨테이너. 업스트림 기본값(`busybox:stable`)은 롤링 태그라 `busybox:1.38.0-uclibc` 로 고정 | 업스트림 `busybox:stable` |
### 2) 클러스터 크기
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `replicas` | 최초 배포 후 변경 불가(위 주의사항). 이 카탈로그 항목은 단일 APISIX 인스턴스의 설정 저장소 용도라 쿼럼 HA 대신 단일 노드로 둔다(결정 배경: security-catalog 프로젝트 decisions/0006, dip-catalog 미이관) — 노드 장애 시 단일 장애점이 되는 트레이드오프를 받아들인 것. 쿼럼 HA가 필요하면 `dip-values.yaml`(3, 홀수 권장) 사용 | 1 |
### 3) 스토리지
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `storage.requestedSize` | PVC 크기. 티어별 값은 `dip-volumes-quotas.yaml` 참고 | `5Gi` |
| `storage.className` | `longhorn` | `longhorn` |
etcd 는 권장 데이터 크기 상한이 있다(기본 `--quota-backend-bytes` 2GB, 업스트림도 8GB 초과를
권장하지 않음). 볼륨을 과하게 키우는 것이 능사가 아니다.
### 4) TLS
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `settings.https.enabled` | 클라이언트/피어 통신 TLS 사용 여부 | `false`(custom-values) / `true`(dip-values) |
| `settings.https.autoTls` | etcd 자체 서명 인증서 자동 생성 | `false`(custom-values) / `true`(dip-values) |
### 5) 리소스
| Name | 설명 | 기본값 |
| --- | --- | --- |
| `resources` | 티어별 값은 `dip-resources-quotas.yaml` 참고 | 업스트림은 `{}`(무제한) |
## 3. 업그레이드
버전 갱신 절차는 `BUILD-README.md` 참고. etcd 는 마이너 버전 간 데이터 포맷 호환성을
공식 문서에서 보장하는 범위 내에서만 순차 업그레이드해야 한다(예: 3.6→3.7). 여러 마이너
버전을 건너뛰지 않는다.
## 4. 제거
```sh
helm uninstall etcd -n etcd-system
kubectl -n etcd-system delete pvc -l app.kubernetes.io/name=etcd
```
PVC 는 helm uninstall 로 지워지지 않는다(StatefulSet 볼륨클레임 기본 동작). 데이터를
보존할 필요가 없다면 위처럼 명시적으로 삭제한다. `longhorn``reclaimPolicy: Retain` 이므로
PV 자체가 남을 수 있다 — `.claude/pitfalls.md` 참고.
## 5. 검증 이력
`doc/charts/etcd/deploy-test.md` 참고.
+13
View File
@@ -0,0 +1,13 @@
apiVersion: v2
appVersion: v3.7.1
description: A Helm chart for etcd on Kubernetes
icon: https://etcd.io/etcd-horizontal-white.png
keywords:
- database
- etcd
maintainers:
- name: groundhog2k
url: https://github.com/groundhog2k/helm-charts
name: etcd
type: application
version: 1.1.12
+176
View File
@@ -0,0 +1,176 @@
# Etcd
![Version: 1.1.12](https://img.shields.io/badge/Version-1.1.12-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.7.1](https://img.shields.io/badge/AppVersion-v3.7.1-informational?style=flat-square)
## Changelog
see [RELEASENOTES.md](RELEASENOTES.md)
A Helm chart for a Etcd HA-cluster on Kubernetes
## TL;DR
```bash
helm repo add groundhog2k https://groundhog2k.github.io/helm-charts/
helm install my-release groundhog2k/etcd
```
## Introduction
This chart uses the original [Etcd image from Quay.io](https://quay.io/repository/coreos/etcd) to deploy a stateful Etcd cluster in Kubernetes.
It fully supports deployment of the multi-architecture docker image.
## Prerequisites
- Kubernetes 1.12+
- Helm 3.x
- PV provisioner support in the underlying infrastructure
## Installing the Chart
To install the chart with the release name `my-release`:
```bash
helm install my-release groundhog2k/etcd
```
## Uninstalling the Chart
To uninstall/delete the `my-release` deployment:
```bash
helm uninstall my-release
```
## Common parameters
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| fullnameOverride | string | `""` | Fully override the deployment name |
| nameOverride | string | `""` | Partially override the deployment name |
## Deployment parameters
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| image.pullPolicy | string | `"IfNotPresent"` | Image pull policy |
| image.registry | string | `"quay.io/coreos"` | Image registry |
| image.repository | string | `"etcd"` | Image name |
| image.tag | string | `""` | Image tag |
| initImage.pullPolicy | string | `"IfNotPresent"` | Init image pull policy |
| initImage.registry | string | `"docker.io"` | Image registry |
| initImage.repository | string | `"busybox"` | Init image name |
| initImage.tag | string | `"stable"` | Init image tag |
| imagePullSecrets | list | `[]` | Image pull secrets |
| extraInitContainers | list | `[]` | Extra init containers |
| extaContainers | list | `[]` | Extra containers for usage as sidecars |
| startupProbe | object | `see values.yaml` | Startup probe configuration |
| livenessProbe | object | `see values.yaml` | Liveness probe configuration |
| readinessProbe | object | `see values.yaml` | Readiness probe configuration |
| customStartupProbe | object | `{}` | Custom startup probe (overwrites default startup probe configuration) |
| customLivenessProbe | object | `{}` | Custom liveness probe (overwrites default liveness probe configuration) |
| customReadinessProbe | object | `{}` | Custom readiness probe (overwrites default readiness probe configuration) |
| resources | object | `{}` | Resource limits and requests |
| priorityClassName | string | `""` | Deployment priority class name |
| nodeSelector | object | `{}` | Deployment node selector |
| customLabels | object | `{}` | Additional labels for Deployment or StatefulSet |
| customAnnotations | object | `{}` | Additional annotations for Deployment or StatefulSet |
| podAnnotations | object | `{}` | Additional pod annotations |
| podLabels | object | `{}` | Additional pod labels |
| podSecurityContext | object | `see values.yaml` | Pod security context |
| securityContext | object | `see values.yaml` | Container security context |
| env | list | `[]` | Additional container environmment variables |
| args | list | `[]` | Additional container command arguments |
| rbac.create | bool | `true` | Enable creation of RBAC |
| serviceAccount.annotations | object | `{}` | Additional service account annotations |
| serviceAccount.create | bool | `true` | Enable service account creation |
| serviceAccount.name | string | `""` | Optional name of the service account |
| serviceAccount.automountServiceAccountToken | bool | `true` | Specifies whether a service account token should be automatically mounted |
| affinity | object | `{}` | Affinity for pod assignment |
| tolerations | list | `[]` | Tolerations for pod assignment |
| topologySpreadConstraints | object | `{}` | Topology spread constraints for pods |
| podManagementPolicy | string | `"Parallel"` | Pod management policy |
| updateStrategyType | string | `"RollingUpdate"` | Pod update strategy |
| replicas | int | `1` | Number of replicas (Due to the nature of etcd cluster initialization this value must be set before deploying the cluster) |
| revisionHistoryLimit | int | `nil` | Maximum number of revisions maintained in revision history |
| podDisruptionBudget | object | `{}` | Pod disruption budget |
| podDisruptionBudget.minAvailable | int | `nil` | Minimum number of pods that must be available after eviction |
| podDisruptionBudget.maxUnavailable | int | `nil` | Maximum number of pods that can be unavailable after eviction |
| clusterDomain | string | `"cluster.local"` | Kubernetes cluster domain (DNS) suffix |
## Service parameters
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| service.type | string | `"ClusterIP"` | Service type |
| service.clusterIP | string | `nil` | The cluster ip address (only relevant for type LoadBalancer or NodePort) |
| service.loadBalancerIP | string | `nil` | The load balancer ip address (only relevant for type LoadBalancer) |
| service.loadBalancerSourceRanges | list | `[]` | The list of IP CIDR ranges that are allowed to access the load balancer (only relevent for type LoadBalancer) |
| service.client.port | int | `2379` | Client service port |
| service.client.nodePort | int | `nil` | Service node port (only relevant for type LoadBalancer or NodePort) |
| service.peer.port | int | `2380` | Peer service port |
| service.peer.nodePort | int | `nil` | Service node port (only relevant for type LoadBalancer or NodePort) |
| service.annotations | object | `{}` | Additional service annotations |
| service.labels | object | `{}` | Additional service labels |
## Service monitor parameters
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| serviceMonitor.enabled | bool | `false` | Enable service monitor |
| serviceMonitor.additionalLabels | object | `{}` | Additional labels for the service monitor object |
| serviceMonitor.annotations | object | `{}` | Annotations for the service monitor object |
| serviceMonitor.interval | Duration | `nil` | Scrape interval for prometheus |
| serviceMonitor.scrapeTimeout | Duration | `nil` | Scrape timeout value |
| serviceMonitor.extraEndpointParameters | object | `nil` | Extra parameters rendered to the [service monitor endpoint](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#endpoint) |
| serviceMonitor.extraParameters | object | `nil` | Extra parameters rendered to the [service monitor object](https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/api.md#servicemonitorspec) |
## Network policies
Allows to define optional network policies for [ingress and egress](https://kubernetes.io/docs/concepts/services-networking/network-policies/)
The policyTypes will be automatically set
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| networkPolicy.ingress | object | `{}` | Ingress network policies |
| networkPolicy.egress | object | `{}` | Egress network policies |
## Storage parameters
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| storage.accessModes[0] | string | `"ReadWriteOnce"` | Storage access mode |
| storage.volumeName | string | `"etcd-data"` | Internal volume name and prefix of a created PVC |
| storage.persistentVolumeClaimName | string | `nil` | PVC name when existing storage volume should be used |
| storage.requestedSize | string | `nil` | Size for new PVC, when no existing PVC is used |
| storage.className | string | `nil` | Storage class name |
| storage.annotations | object | `{}` | Additional storage annotations |
| storage.labels | object | `{}` | Additional storage labels |
| extraStorage | list | `[]` | A list of additional existing PVC that will be mounted into the container |
| extraStorage[].name | string | `nil` | Internal name of the volume |
| extraStorage[].pvcName | string | `nil` | Name of the existing PVC |
| extraStorage[].mountPath | string | `nil` | Mount path where the PVC should be mounted into the container |
## Etcd settings
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| settings.clusterToken | bool | `"etcd-cluster-0"` | Unique cluser token |
| settings.https.enabled | bool | `false` | Enable HTTPS |
| settings.https.autoTls | bool | `false` | Automatic TLS mode of etcd (TLS certs. created automaically) |
| settings.shutdownDelay | int | `3` | Delay after termination request to give etcd process time for graceful shutdown |
## Etcd secrets and configuration
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| extraSecrets | list | `[]` | A list of additional existing secrets that will be mounted into the container |
| extraSecrets[].name | string | `nil` | Name of the existing K8s secret |
| extraSecrets[].defaultMode | int | `0440` | Mount default access mode |
| extraSecrets[].mountPath | string | `nil` | Mount path where the secret should be mounted into the container (f.e. /mysecretfolder) |
| extraConfigs | list | `[]` | A list of additional existing configMaps that will be mounted into the container |
| extraConfigs[].name | string | `nil` | Name of the existing K8s configMap |
| extraConfigs[].defaultMode | int | `0440` | Mount default access mode |
| extraConfigs[].mountPath | string | `nil` | Mount path where the configMap should be mounted into the container (f.e. /myconfigfolder) |
| extraEnvSecrets | list | `[]` | A list of existing secrets that will be mounted into the container as environment variables |
@@ -0,0 +1,38 @@
# Changelog
| Chart version | App version | Change description |
| :------------ | :---------- | :----------------- |
| 0.1.0 | v3.5.6 | Initial version |
| 0.1.1 | v3.5.7 | Upgraded etcd to v3.5.7 |
| 0.1.2 | v3.5.7 | Updated default security context |
| 0.1.3 | v3.5.7 | Fixed "nil" syntax error for health checks (thx @omegazeng) |
| 0.1.4 | v3.5.8 | Upgraded etcd to v3.5.8 |
| 0.1.5 | v3.5.9 | Upgraded etcd to v3.5.9 |
| 0.1.6 | v3.5.9 | Added support for network policies and additional labels and annotations |
| 0.1.7 | v3.5.10 | Upgraded etcd to v3.5.10 |
| 0.1.8 | v3.5.11 | Upgraded etcd to v3.5.11 |
| 0.1.9 | v3.5.12 | Upgraded etcd to v3.5.12 |
| 0.1.10 | v3.5.13 | Upgraded etcd to v3.5.13 |
| 0.1.11 | v3.5.13 | Fixed build pipeline issue |
| 1.0.0 | v3.5.13 | Final version with configuration secret, extra config and extra volume support |
| 1.0.1 | v3.5.15 | Upgraded etcd to v3.5.15 |
| 1.0.2 | v3.5.16 | Upgraded etcd to v3.5.16 |
| 1.0.3 | v3.5.17 | Upgraded etcd to v3.5.17 |
| 1.0.4 | v3.5.18 | Upgraded etcd to v3.5.18 |
| 1.0.6 | v3.5.20 | Upgraded etcd to v3.5.20 |
| 1.0.7 | v3.5.21 | Upgraded etcd to v3.5.21 |
| 1.0.8 | v3.5.21 | Added support for loadBalancerSourceRanges |
| 1.1.0 | v3.6.4 | Upgraded etcd to v3.6.4 |
| 1.1.1 | v3.6.4 | Changed to busybox:stable containter image |
| 1.1.2 | v3.6.4 | Added ability to set automountServiceAccountToken - thx @krizzpiBiGdirekt |
| 1.1.3 | v3.6.4 | Added priorityClassName - thx @JimCronqvist |
| 1.1.4 | v3.6.6 | Upgraded etcd to v3.6.6 |
| 1.1.5 | v3.6.7 | Upgraded etcd to v3.6.7 |
| 1.1.6 | v3.6.9 | Upgraded etcd to v3.6.9 |
| 1.1.7 | v3.6.9 | Fixed insecure parameters - thx @oleksiialeksieiev |
| 1.1.8 | v3.6.11 | Upgraded etcd to v3.6.11 |
| 1.1.9 | v3.6.13 | Upgraded etcd to v3.6.13, fixed PVC definition - thx @trandbert37 |
| 1.1.10 | v3.7.0 | Upgraded etcd to v3.7.0 |
| 1.1.11 | v3.7.0 | Fixed README.md markdown formatting |
| 1.1.12 | v3.7.1 | Upgraded etcd to v3.7.1 |
| | | |
@@ -0,0 +1,50 @@
# etcd — PaaSup 오버라이드
# 업스트림 기본값은 values.yaml 참고. 여기에는 변경이 필요한 항목만 둔다.
image:
# 자체 빌드(대응 우선순위 c) — 업스트림 quay.io/coreos/etcd:v3.7.1 이 게이트 차단
# HIGH 1건(golang.org/x/text, CVE-2026-56852, 바이너리 정적 링크라 베이스 OS 교체로
# 해소 불가)으로 막혀 v3.7.1 태그가 가리키는 commit 을 그대로 컴파일했다. x/text 만
# go.work 워크스페이스 전역 replace 로 0.39.0 이상으로 강제. 근거: doc/analysis/etcd-cve.md,
# 결정: doc/decisions/0007-etcd-image-self-build.md. 빌드 정의: images/etcd/.
# 상위 태그가 나오거나 release-3.7 에 백포트되면(대응 우선순위 a) 되돌리는 것이 우선.
#
# 이전 값: quay.io/coreos/etcd:v3.7.1 (업스트림). etcd 프로젝트는 3.8부터
# gcr.io/etcd-development·quay.io/coreos 를 폐지하고 registry.k8s.io/etcd 로 이전
# 예정이다(etcd-io/etcd#20928) — 상위 태그로 돌아갈 때 이것도 함께 재검토한다.
registry: "docker.io/wbsong111"
repository: "etcd"
tag: "3.7.1-security-hardened-20260731"
initImage:
# 업스트림 기본값 "stable" 은 롤링 태그다 (doc/image-selection.md 2번 규칙 위반).
# 이 init 컨테이너도 extract-helm-images.sh 에 잡혀 게이트 대상이 되므로 고정한다.
registry: "docker.io"
repository: "busybox"
tag: "1.38.0-uclibc"
# 클러스터 초기화 특성상 최초 배포 전에 정해야 하며, 이후 자동/수동 스케일링을
# 지원하지 않는다(업스트림 values.yaml 주석).
#
# 이 카탈로그 항목의 용도는 단일 APISIX 인스턴스의 설정 저장소다 — APISIX 자체가
# 단일 인스턴스라 etcd 쪽에서 쿼럼 HA를 가져갈 이유가 없고, 오퍼레이터 없이 가벼운
# 차트 하나로 충분하다는 것이 이 결정의 핵심이다(doc/decisions/0006-etcd-chart-selection.md).
# 노드 장애 시 단일 장애점이 된다는 것을 받아들인 트레이드오프다. 쿼럼 HA가 필요한
# 다른 소비자가 생기면 dip-values.yaml(replicas: 3)을 쓰거나 이 값을 재검토한다.
replicas: 1
storage:
requestedSize: 5Gi
className: "longhorn"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
# TLS — 업스트림 기본값(평문 클라이언트/피어 통신)을 그대로 둔다. 운영 배포는
# dip-values.yaml 에서 autoTls: true 로 켠다. 여기서는 CVE 스캔 기준값 + 최소 동작
# 확인용이라 단순하게 유지한다.
@@ -0,0 +1,33 @@
# etcd 는 데이터셋이 작고(대개 수백 MB~수 GB) CPU/메모리 요구량도 낮다.
# 티어는 주로 replica 수(가용성)로 구분한다 — replica 수는 최초 배포 후 변경 불가
# (업스트림 values.yaml 주석)하므로 반드시 배포 전에 티어를 정해야 한다.
small:
replicas: 1
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
medium:
replicas: 3
resources:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
large:
replicas: 5
resources:
requests:
cpu: "2"
memory: 2Gi
limits:
cpu: "4"
memory: 4Gi
@@ -0,0 +1,32 @@
# etcd — DIP 플랫폼 기본 배포값 (운영 기준)
replicas: 3
settings:
clusterToken: "etcd-cluster-0"
https:
# 평문 클라이언트/피어 통신은 운영 기본값으로 부적절하다.
# autoTls 는 etcd 가 자체 서명 인증서를 생성한다 — CA 발급 인증서로
# 교체하려면 extraSecrets 로 마운트하고 이 값을 다시 false 로 내려야 한다.
enabled: true
autoTls: true
serviceMonitor:
# rancher-monitoring(Prometheus Operator) 설치 환경에서만 true
enabled: true
storage:
requestedSize: 20Gi
className: "longhorn"
resources:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
# 이 차트는 자동 백업이 없다. 스냅샷은 운영자가 수동으로 실행한다
# kubectl exec <pod> -- etcdctl snapshot save /tmp/snapshot.db
# 정기 백업이 필요하면 별도 CronJob 을 구성해야 한다(이번 범위 밖).
@@ -0,0 +1,17 @@
# etcd 는 권장 데이터 크기 상한이 있다(기본 quota-backend-bytes 2GB, 상향 시에도
# 8GB 를 넘기지 않는 것을 업스트림이 권장). 볼륨은 여유를 두되 과하게 키우지 않는다.
small:
storage:
requestedSize: 5Gi
className: "longhorn"
medium:
storage:
requestedSize: 20Gi
className: "longhorn"
large:
storage:
requestedSize: 50Gi
className: "longhorn"
@@ -0,0 +1,62 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "etcd.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "etcd.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "etcd.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "etcd.labels" -}}
helm.sh/chart: {{ include "etcd.chart" . }}
{{ include "etcd.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "etcd.selectorLabels" -}}
app.kubernetes.io/name: {{ include "etcd.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "etcd.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "etcd.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
@@ -0,0 +1,29 @@
kind: ConfigMap
apiVersion: v1
metadata:
name: {{ include "etcd.fullname" . }}
labels:
{{- include "etcd.labels" . | nindent 4 }}
data:
{{- $replicaCount := int .Values.replicas }}
{{- $initialCluster := list }}
{{- $etcdFullname := include "etcd.fullname" . }}
{{- $etcdInternalServiceName := printf "%s-internal" $etcdFullname }}
{{- $protocol := (or .Values.settings.https.enabled .Values.settings.https.autoTls) | ternary "https" "http" }}
{{- $servicefqdn := printf "%s.%s.svc.%s" $etcdInternalServiceName .Release.Namespace .Values.clusterDomain }}
ETCD_DATA_DIR: "/data/etcd"
ETCD_INITIAL_CLUSTER_TOKEN: "{{ .Values.settings.clusterToken }}"
ETCD_INITIAL_CLUSTER_STATE: "new"
ETCD_LISTEN_CLIENT_URLS: "{{ $protocol }}://0.0.0.0:2379"
ETCD_LISTEN_PEER_URLS: "{{ $protocol }}://0.0.0.0:2380"
{{- if .Values.serviceMonitor.enabled }}
ETCD_LISTEN_METRICS_URLS: "http://0.0.0.0:12379"
{{- end }}
{{- range $e, $i := until $replicaCount }}
{{- $initialCluster = append $initialCluster (printf "%s-%d=%s://%s-%d.%s:%d" $etcdFullname $i $protocol $etcdFullname $i $servicefqdn 2380) }}
{{- end }}
ETCD_INITIAL_CLUSTER: {{ join "," $initialCluster | quote }}
{{- if .Values.settings.https.autoTls }}
ETCD_AUTO_TLS: "true"
ETCD_PEER_AUTO_TLS: "true"
{{- end }}
@@ -0,0 +1,25 @@
{{- with .Values.networkPolicy }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "etcd.fullname" $ }}
spec:
podSelector:
matchLabels:
{{- include "etcd.selectorLabels" $ | nindent 6 }}
policyTypes:
{{- if .ingress }}
- Ingress
{{- end }}
{{- if .egress }}
- Egress
{{- end }}
{{- with .ingress }}
ingress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .egress }}
egress:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
@@ -0,0 +1,18 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "etcd.fullname" . }}-internal
labels:
{{- include "etcd.labels" . | nindent 4 }}
spec:
clusterIP: None
publishNotReadyAddresses: true
ports:
- port: {{ .Values.service.client.port }}
targetPort: client
name: client
- port: {{ .Values.service.peer.port }}
targetPort: peer
name: peer
selector:
{{- include "etcd.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,47 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "etcd.fullname" . }}
labels:
{{- include "etcd.labels" . | nindent 4 }}
{{- with .Values.service.labels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.client.port }}
targetPort: client
name: client
{{- if and ( or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") ) (.Values.service.client.nodePort) }}
nodePort: {{ .Values.service.client.nodePort }}
{{- end }}
- port: {{ .Values.service.peer.port }}
targetPort: peer
name: peer
{{- if and ( or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") ) (.Values.service.peer.nodePort) }}
nodePort: {{ .Values.service.peer.nodePort }}
{{- end }}
{{- if .Values.serviceMonitor.enabled }}
- port: {{ .Values.service.prometheus.port }}
targetPort: prometheus
name: prometheus
{{- if and ( or (eq .Values.service.type "LoadBalancer") (eq .Values.service.type "NodePort") ) (.Values.service.prometheus.nodePort) }}
nodePort: {{ .Values.service.prometheus.nodePort }}
{{- end }}
{{- end }}
{{- if and (eq .Values.service.type "LoadBalancer") (.Values.service.loadBalancerIP) }}
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
{{- end }}
{{- if and (eq .Values.service.type "LoadBalancer") (.Values.service.loadBalancerSourceRanges) }}
loadBalancerSourceRanges: {{- toYaml .Values.service.loadBalancerSourceRanges | nindent 4 }}
{{- end }}
{{- if .Values.service.clusterIP }}
clusterIP: {{ .Values.service.clusterIP }}
{{- end }}
selector:
{{- include "etcd.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,13 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "etcd.serviceAccountName" . }}
labels:
{{- include "etcd.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
{{- end }}
@@ -0,0 +1,34 @@
{{- if .Values.serviceMonitor.enabled }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: {{ include "etcd.fullname" . }}
labels:
{{- include "etcd.labels" . | nindent 4 }}
{{- with .Values.serviceMonitor.additionalLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.serviceMonitor.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
endpoints:
- port: "prometheus"
path: "/metrics"
{{- if .Values.serviceMonitor.interval }}
interval: {{ .Values.serviceMonitor.interval }}
{{- end }}
{{- if .Values.serviceMonitor.scrapeTimeout }}
scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }}
{{- end }}
{{- with .Values.serviceMonitor.extraEndpointParameters }}
{{- toYaml . | nindent 6 }}
{{- end }}
{{- with .Values.serviceMonitor.extraParameters }}
{{- toYaml . | nindent 2 }}
{{- end }}
selector:
matchLabels:
{{- include "etcd.selectorLabels" . | nindent 6 }}
{{- end }}
@@ -0,0 +1,280 @@
{{- $fullname := include "etcd.fullname" . }}
{{- $etcdInternalServiceName := printf "%s-internal" $fullname }}
{{- $protocol := (or .Values.settings.https.enabled .Values.settings.https.autoTls) | ternary "https" "http" }}
{{- $servicefqdn := printf "%s.%s.svc.%s" $etcdInternalServiceName .Release.Namespace .Values.clusterDomain }}
{{- $createPvc := and (empty .Values.storage.persistentVolumeClaimName) (.Values.storage.requestedSize) }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ $fullname }}
labels:
{{- include "etcd.labels" . | nindent 4 }}
{{- with .Values.customLabels }}
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.customAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
replicas: {{ .Values.replicas }}
{{- if .Values.revisionHistoryLimit }}
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
{{- end }}
serviceName: {{ $fullname }}-internal
podManagementPolicy: {{ .Values.podManagementPolicy }}
updateStrategy:
type: {{ .Values.updateStrategyType }}
selector:
matchLabels:
{{- include "etcd.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/etcdconfig: {{ include (print $.Template.BasePath "/etcdconfig.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "etcd.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "etcd.serviceAccountName" . }}
automountServiceAccountToken: {{ .Values.serviceAccount.automountServiceAccountToken }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
initContainers:
- name: {{ .Chart.Name }}-init
{{- with .Values.securityContext }}
securityContext:
{{- toYaml . | nindent 12 }}
{{- end }}
image: "{{ .Values.initImage.registry }}/{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}"
imagePullPolicy: {{ .Values.initImage.pullPolicy }}
volumeMounts:
- name: {{ .Values.storage.volumeName }}
mountPath: /data
command: ["/bin/sh"]
args: ["-c", "mkdir -p /data/etcd && chmod 700 /data/etcd"]
{{- with .Values.extraInitContainers }}
{{- toYaml . | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: client
containerPort: 2379
- name: peer
containerPort: 2380
{{- if .Values.serviceMonitor.enabled }}
- name: prometheus
containerPort: 12379
{{- end }}
{{- if .Values.customStartupProbe }}
startupProbe:
{{- toYaml .Values.customStartupProbe | nindent 12 }}
{{- else }}
{{- if .Values.startupProbe.enabled }}
startupProbe:
exec:
command:
- /usr/local/bin/etcdctl
- endpoint
- health
{{- if (or .Values.settings.https.enabled .Values.settings.https.autoTls) }}
- --insecure-skip-tls-verify=true
- --insecure-transport=false
{{- end }}
{{- with .Values.startupProbe }}
initialDelaySeconds: {{ .initialDelaySeconds }}
timeoutSeconds: {{ .timeoutSeconds }}
failureThreshold: {{ .failureThreshold }}
successThreshold: {{ .successThreshold }}
periodSeconds: {{ .periodSeconds }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.customLivenessProbe }}
livenessProbe:
{{- toYaml .Values.customLivenessProbe | nindent 12 }}
{{- else }}
{{- if .Values.livenessProbe.enabled }}
livenessProbe:
exec:
command:
- /usr/local/bin/etcdctl
- endpoint
- health
{{- if (or .Values.settings.https.enabled .Values.settings.https.autoTls) }}
- --insecure-skip-tls-verify=true
- --insecure-transport=false
{{- end }}
{{- with .Values.livenessProbe }}
initialDelaySeconds: {{ .initialDelaySeconds }}
timeoutSeconds: {{ .timeoutSeconds }}
failureThreshold: {{ .failureThreshold }}
successThreshold: {{ .successThreshold }}
periodSeconds: {{ .periodSeconds }}
{{- end }}
{{- end }}
{{- end }}
{{- if .Values.customReadinessProbe }}
readinessProbe:
{{- toYaml .Values.customReadinessProbe | nindent 12 }}
{{- else }}
{{- if .Values.readinessProbe.enabled }}
readinessProbe:
exec:
command:
- /usr/local/bin/etcdctl
- endpoint
- health
{{- if (or .Values.settings.https.enabled .Values.settings.https.autoTls) }}
- --insecure-skip-tls-verify=true
- --insecure-transport=false
{{- end }}
{{- with .Values.readinessProbe }}
initialDelaySeconds: {{ .initialDelaySeconds }}
timeoutSeconds: {{ .timeoutSeconds }}
failureThreshold: {{ .failureThreshold }}
successThreshold: {{ .successThreshold }}
periodSeconds: {{ .periodSeconds }}
{{- end }}
{{- end }}
{{- end }}
{{- with .Values.resources }}
resources:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- if .Values.args }}
args:
{{- range .Values.args }}
- {{ . }}
{{- end }}
{{- end }}
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: ETCD_NAME
value: $(NODE_NAME)
- name: ETCD_ADVERTISE_CLIENT_URLS
value: "{{ $protocol }}://$(NODE_NAME).{{ $servicefqdn }}:2379"
- name: ETCD_INITIAL_ADVERTISE_PEER_URLS
value: "{{ $protocol }}://$(NODE_NAME).{{ $servicefqdn }}:2380"
{{- with .Values.env }}
{{- toYaml . | nindent 12 }}
{{- end }}
envFrom:
- configMapRef:
name: {{ $fullname }}
{{- range .Values.extraEnvSecrets }}
- secretRef:
name: {{ . }}
{{- end }}
volumeMounts:
- name: {{ .Values.storage.volumeName }}
mountPath: /data
- name: tmp
mountPath: /tmp
{{- range $secret := .Values.extraSecrets }}
- name: {{ $secret.name }}
mountPath: {{ $secret.mountPath }}
{{- end }}
{{- range $config := .Values.extraConfigs }}
- name: {{ $config.name }}
mountPath: {{ $config.mountPath }}
{{- end }}
{{- range $storage := .Values.extraStorage }}
- name: {{ $storage.name }}
mountPath: {{ $storage.mountPath }}
{{- end }}
{{- with .Values.extraContainers }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.topologySpreadConstraints }}
topologySpreadConstraints:
{{- toYaml . | nindent 8 }}
{{- end }}
volumes:
- name: tmp
emptyDir: {}
{{- range $secret := .Values.extraSecrets }}
- name: {{ $secret.name }}
secret:
secretName: {{ $secret.name }}
defaultMode: {{ $secret.defaultMode | default 0440 }}
{{- end }}
{{- range $config := .Values.extraConfigs }}
- name: {{ $config.name }}
configMap:
name: {{ $config.name }}
defaultMode: {{ $config.defaultMode | default 0440 }}
{{- end }}
{{- range $storage := .Values.extraStorage }}
- name: {{ $storage.name }}
persistentVolumeClaim:
claimName: {{ $storage.pvcName }}
{{- end }}
{{- with .Values.storage }}
{{- if not $createPvc }}
- name: {{ .volumeName }}
{{- if .persistentVolumeClaimName }}
persistentVolumeClaim:
claimName: {{ .persistentVolumeClaimName }}
{{- else }}
emptyDir: {}
{{- end }}
{{- else }}
volumeClaimTemplates:
- apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .volumeName }}
{{- with .labels }}
labels:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- with .annotations }}
annotations:
{{- toYaml . | nindent 10 }}
{{- end }}
spec:
{{- with .accessModes }}
accessModes:
{{- toYaml . | nindent 10 }}
{{- end }}
{{- if .className }}
storageClassName: {{ .className }}
{{- end }}
resources:
requests:
storage: {{ .requestedSize }}
{{- end }}
{{- end }}
+273
View File
@@ -0,0 +1,273 @@
# Default values for Etcd deployment
## Etcd container image
image:
registry: "quay.io/coreos"
repository: "etcd"
pullPolicy: IfNotPresent
tag: ""
# Default Init container image
initImage:
registry: "docker.io"
repository: "busybox"
pullPolicy: IfNotPresent
tag: "stable"
## Pull secrets and name override options
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
## Additional labels for Deployment or StatefulSet
customLabels: {}
## Additional annotations for Deployment or StatefulSet
customAnnotations: {}
## Number of etcd replicas in the cluster
## Due to the nature of etcd cluster initialization this value must be set before deploying the cluster
## Automatic scaling or manually scaling the etcd cluster after first deployment is not supported
replicas: 1
## Optional service account
serviceAccount:
# Specifies whether a service account should be created
create: false
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# Specifies whether a service account token should be automatically mounted
automountServiceAccountToken: true
## Additional pod annotations
podAnnotations: {}
## Additional pod labels
podLabels: {}
## Pod management policy
podManagementPolicy: Parallel
## Pod update strategy
updateStrategyType: RollingUpdate
## Pod security context uses file system group 999 (postgres)
podSecurityContext:
fsGroup: 999
supplementalGroups:
- 999
## Default security options to run PostgreSQL as non-root (postgres user), read only container without privilege escalation
securityContext:
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsGroup: 999
runAsUser: 999
capabilities:
drop:
- ALL
## Etcd service ports (default: Client port 2379, Peer port 2380)
service:
type: ClusterIP
## Client service port
client:
port: 2379
## The node port (only relevant for type LoadBalancer or NodePort)
nodePort:
## Peer service port
peer:
port: 2380
## The node port (only relevant for type LoadBalancer or NodePort)
nodePort:
## Prometheus service port
prometheus:
port: 12379
## The node port (only relevant for type LoadBalancer or NodePort)
nodePort:
## The cluster ip address (only relevant for type LoadBalancer or NodePort)
clusterIP:
## The loadbalancer ip address (only relevant for type LoadBalancer)
loadBalancerIP:
## The list of IP CIDR ranges that are allowed to access the load balancer (only relevent for type LoadBalancer)
loadBalancerSourceRanges: []
## Annotations to add to the service
annotations: {}
## Labels to add to the service
labels: {}
## Service monitor configuration for Prometheus metrics
serviceMonitor:
## Enable service monitor
enabled: false
## Additional labels for the service monitor object
additionalLabels: {}
## Annotations for the service monitor object
annotations: {}
## The scrape interval for prometheus
# interval:
## The scrape timeout value
# scrapeTimeout:
## Extra parameters rendered to the service monitor endpoint
extraEndpointParameters: {}
## Extra parameters rendered to the service monitor
extraParameters: {}
resources: {}
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi
## Pod priority class name
priorityClassName: ""
## Additional node selector
nodeSelector: {}
## Tolerations for pod assignment
tolerations: []
## Affinity for pod assignment
affinity: {}
## Topology spread constraints for pods
topologySpreadConstraints: {}
## Maximum number of revisions maintained in revision history
revisionHistoryLimit:
## Custom startup probe (overwrites default startup probe)
customStartupProbe: {}
## Default startup check
startupProbe:
enabled: true
initialDelaySeconds: 10
timeoutSeconds: 5
failureThreshold: 30
successThreshold: 1
periodSeconds: 10
## Custom liveness probe (overwrites default liveness probe)
customLivenessProbe: {}
## Default health check
livenessProbe:
enabled: true
initialDelaySeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
periodSeconds: 10
## Custom readiness probe (overwrites default readiness probe)
customReadinessProbe: {}
## Default readiness probe
readinessProbe:
enabled: true
initialDelaySeconds: 10
timeoutSeconds: 5
failureThreshold: 3
successThreshold: 1
periodSeconds: 10
## Extra init containers
extraInitContainers: []
## Extra containers for usage as sidecars
extraContainers: []
## Additional environment variables
env: []
## Arguments for the container entrypoint process
args: []
## A list of existing secrets that will be mounted into the container as environment variables
extraEnvSecrets: []
## A list of additional existing secrets that will be mounted into the container
## The mounted files of the secrets can be used for advanced configuration (see settings.https.enabled)
extraSecrets: []
## Name of the existing K8s secret
# - name:
## Mount default mode (0440 if parameter is omitted)
# defaultMode: 0440
## Mount path where the secret should be mounted into the container (f.e. /mysecretfolder)
# mountPath:
## A list of additional existing configMaps that will be mounted into the container
extraConfigs: []
## Name of the existing K8s configMap
# - name:
## Mount default mode (0440 if parameter is omitted)
# defaultMode: 0440
## Mount path where the configMap should be mounted into the container (f.e. /mysecretfolder)
# mountPath:
## Default Kubernetes cluster domain
clusterDomain: cluster.local
## Etcd specific settings
settings:
## Unique cluser token
clusterToken: "etcd-cluster-0"
## Configure secure transport
## Certificates must be mounted into the container using `extraSecrets:` or generated automatically using autoTls: true
## Other tls options have to be added manually using environment variables or args: (see https://etcd.io/docs/v3.5/op-guide/clustering/#tls and https://etcd.io/docs/v3.5/op-guide/configuration/)
https:
## Enable HTTPS
enabled: false
## Automatic TLS mode of etcd (TLS certs. created automaically)
autoTls: false
## Delay after termination request to give etcd process time for graceful shutdown
shutdownDelay: 3
## Storage parameters
storage:
## Set persistentVolumenClaimName to reference an existing PVC
persistentVolumeClaimName:
## Internal volume name and prefix of a created PVC
volumeName: "etcd-data"
## Alternative set requestedSize to define a size for a dynmaically created PVC
requestedSize:
## the storage class name
className:
## Default access mode (ReadWriteOnce)
accessModes:
- ReadWriteOnce
## Additional storage annotations
annotations: {}
## Additional storage labels
labels: {}
## Mount existing extra PVC
extraStorage: {}
## Internal volume name
# - name:
## Container mount path
# mountPath:
## Name of existing PVC
# pvcName:
## Network policies
networkPolicy: {}
## Ingress and Egress policies
# ingress: {}
# egress: {}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
# =============================================================================
# deploy-test-cnpg-cluster.sh
# cnpg-postgresql 자체 빌드 이미지가 실제로 CNPG operator 위에서 동작하는지
# Kubernetes 배포 수준에서 확인한다. build-image.yml 이 카탈로그 PR 을 연 뒤,
# 병합 전에 사람이 로컬 kubeconfig 로 이 스크립트를 돌려 확인한다.
#
# CVE 게이트(scripts/pipeline/cve-gate.py)는 "취약점이 없는가" 만 본다. 이 스크립트는
# "0건이어도 실제로 뜨는가" 를 본다 — .claude/deploy-test-procedure.md 의 원칙
# ("Zero CVE 로는 부족하다. 동작하지 않는 이미지는 카탈로그에 올릴 수 없다").
#
# Operator(cloudnative-pg)는 상시 컴포넌트로 취급한다 — 없으면 1회 설치하고
# 있으면 그대로 재사용한다. 매번 새로 설치하지 않는 이유:
# cloudnative-pg 차트의 ValidatingWebhookConfiguration/MutatingWebhookConfiguration
# 이름이 릴리스와 무관하게 클러스터 전역으로 하드코딩돼 있어(cnpg-*-webhook-configuration,
# manifests/helm/cloudnative-pg/0.29.0/templates/validatingwebhookconfiguration.yaml)
# 같은 클러스터에 operator 를 두 번 설치할 수 없다.
#
# Postgres 앱(cnpg-cluster)은 매 실행 고정 이름으로 지우고 새로 만든다 — 반복 실행
# 가능해야 "배포 검증"이 의미가 있다. 정리는 성공/실패 무관 항상 수행한다(trap).
#
# 배포는 각 차트의 **기본값(values.yaml)** 만 쓴다. custom-values.yaml 은 쓰지 않는다 —
# 운영 설정(백업·확장·사이징)까지 검증 대상에 섞이면 "이 이미지가 도는가" 라는 질문과
# 무관한 변수가 늘어난다. 유일한 override 는 검증 대상인 이미지 자체뿐이다.
#
# 사용:
# IMAGE_NAME=docker.io/wbsong111/cnpg-postgresql:<tag> bash scripts/deploy-test/deploy-test-cnpg-cluster.sh <OUT_DIR>
#
# 환경변수:
# IMAGE_NAME (필수) 검증할 postgresql 이미지 전체 경로:태그
# OPERATOR_CHART_DIR 기본 manifests/helm/cloudnative-pg/0.29.0
# APP_CHART_DIR 기본 manifests/helm/cnpg-cluster/1.0.0
# OPERATOR_NAMESPACE 기본 cnpg-system (상시 — 이 스크립트가 지우지 않는다)
# OPERATOR_RELEASE 기본 cnpg
# TEST_NAMESPACE 기본 pg-test-build (매 실행 재생성)
# APP_RELEASE 기본 pg-build
# WAIT_TIMEOUT 기본 600 (초) — Cluster healthy 대기 한도
#
# 산출물 (OUT_DIR):
# deploy-test.log 전체 실행 로그
#
# 종료 코드: 0 = 기능 확인(psql SELECT 1) 통과, 그 외 = 실패.
# 정리는 종료 코드와 무관하게 항상 수행된다.
# =============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
OUT_DIR="${1:?사용법: deploy-test-cnpg-cluster.sh <OUT_DIR>}"
mkdir -p "$OUT_DIR"
LOG="$OUT_DIR/deploy-test.log"
: > "$LOG"
IMAGE_NAME="${IMAGE_NAME:?IMAGE_NAME 환경변수가 필요하다 (검증할 postgresql 이미지 전체 경로:태그)}"
OPERATOR_CHART_DIR="${OPERATOR_CHART_DIR:-$REPO_ROOT/manifests/helm/cloudnative-pg/0.29.0}"
APP_CHART_DIR="${APP_CHART_DIR:-$REPO_ROOT/manifests/helm/cnpg-cluster/1.0.0}"
OPERATOR_NAMESPACE="${OPERATOR_NAMESPACE:-cnpg-system}"
OPERATOR_RELEASE="${OPERATOR_RELEASE:-cnpg}"
TEST_NAMESPACE="${TEST_NAMESPACE:-pg-test-build}"
APP_RELEASE="${APP_RELEASE:-pg-build}"
WAIT_TIMEOUT="${WAIT_TIMEOUT:-600}"
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 차트 없음: $APP_CHART_DIR"; exit 2; }
if ! kubectl get ns >/dev/null 2>>"$LOG"; then
log "::error:: 클러스터 연결 실패 — kubeconfig/네트워크 확인"
log "::error:: (dev 클러스터는 간헐적 타임아웃이 있을 수 있다 — 재시도해볼 것)"
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
# 우리가 모르는 operator 가 이미 떠 있으면(웹훅 이름이 클러스터 전역 고정이라) 충돌한다.
# 자동으로 지우지 않고 사람이 정리하게 한다.
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)."
log "::error:: cloudnative-pg 웹훅 설정은 클러스터 전역 고정 이름이라 두 번째 설치가 불가능하다."
log "::error:: 그 operator 를 먼저 정리하거나 OPERATOR_NAMESPACE/OPERATOR_RELEASE 를 그 값에 맞춰라."
exit 3
fi
fi
log "== operator: 신규 설치 ($OPERATOR_RELEASE/$OPERATOR_NAMESPACE, 차트 기본값) =="
helm install "$OPERATOR_RELEASE" "$OPERATOR_CHART_DIR" \
-n "$OPERATOR_NAMESPACE" --create-namespace --wait --timeout "${WAIT_TIMEOUT}s" \
>>"$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
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 삭제 실패 — 수동 확인 필요"
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 "== app: 설치 ($APP_RELEASE/$TEST_NAMESPACE, 기본값 + imageName override) =="
kubectl create ns "$TEST_NAMESPACE" >>"$LOG" 2>&1
if ! helm install "$APP_RELEASE" "$APP_CHART_DIR" -n "$TEST_NAMESPACE" \
--set postgresql.imageName="$IMAGE_NAME" --wait --timeout "${WAIT_TIMEOUT}s" \
>>"$LOG" 2>&1; then
log "::error:: app 차트 설치 실패 — $LOG 확인"
exit 1
fi
log "== Cluster healthy 대기 (최대 ${WAIT_TIMEOUT}s) =="
instances="$(kubectl -n "$TEST_NAMESPACE" get clusters.postgresql.cnpg.io "$APP_RELEASE" \
-o jsonpath='{.spec.instances}' 2>/dev/null)"
deadline=$((SECONDS + WAIT_TIMEOUT))
phase=""
ready=0
while [ "$SECONDS" -lt "$deadline" ]; do
phase="$(kubectl -n "$TEST_NAMESPACE" get clusters.postgresql.cnpg.io "$APP_RELEASE" \
-o jsonpath='{.status.phase}' 2>/dev/null)"
ready="$(kubectl -n "$TEST_NAMESPACE" get clusters.postgresql.cnpg.io "$APP_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:: 타임아웃 — Cluster 가 healthy 상태에 도달하지 못함"
kubectl -n "$TEST_NAMESPACE" get pods -l "cnpg.io/cluster=$APP_RELEASE" >>"$LOG" 2>&1
exit 1
fi
log "== 기능 확인: psql SELECT 1 (appdb) =="
# 항상 clusters.postgresql.cnpg.io FQN 을 쓴다 — bare `cluster` 는 Rancher/CAPI 의
# 동명 CRD 와 충돌한다(.claude/pitfalls.md).
primary_pod="$(kubectl -n "$TEST_NAMESPACE" get pods \
-l "cnpg.io/cluster=$APP_RELEASE,cnpg.io/instanceRole=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)"
if [ -z "$primary_pod" ]; then
log "::error:: primary pod 를 찾지 못함"
exit 1
fi
log " primary=$primary_pod"
version="$(kubectl -n "$TEST_NAMESPACE" exec "$primary_pod" -c postgres -- \
psql -U postgres -d appdb -Atc "SELECT version();" 2>>"$LOG")"
log " version: $version"
answer="$(kubectl -n "$TEST_NAMESPACE" exec "$primary_pod" -c postgres -- \
psql -U postgres -d appdb -Atc "SELECT 1;" 2>>"$LOG")"
if [ "$answer" = "1" ]; then
log "== 결과: PASS =="
RESULT=0
else
log "::error:: psql 응답 이상 (got: '$answer')"
RESULT=1
fi
exit "$RESULT"
+187
View File
@@ -0,0 +1,187 @@
#!/usr/bin/env bash
# =============================================================================
# deploy-test-etcd.sh
# etcd 차트가 지정한 이미지로 실제 Kubernetes 배포 수준에서 정상 동작하는지
# 확인한다. CVE 게이트(scripts/pipeline/cve-gate.py)는 "취약점이 없는가"만
# 본다. 이 스크립트는 "0건이어도 실제로 뜨는가"를 본다 —
# .claude/deploy-test-procedure.md 의 원칙("Zero CVE 로는 부족하다. 동작하지
# 않는 이미지는 카탈로그에 올릴 수 없다").
#
# cnpg 배포 테스트(deploy-test-cnpg-cluster.sh)와 달리 etcd 차트는 오퍼레이터가
# 없는 단일 차트다 — 상시 컴포넌트 설치 단계가 없고, 매 실행 테스트 네임스페이스에
# 차트를 설치하고 끝나면 지운다. 정리는 성공/실패 무관 항상 수행한다(trap).
#
# 배포는 차트 **기본값(values.yaml)** 만 쓴다. custom-values.yaml 은 쓰지 않는다 —
# 유일한 override 는 검증 대상 이미지와 replica 수뿐이다(cnpg 원칙과 동일).
#
# 사용:
# IMAGE_NAME=quay.io/coreos/etcd:v3.7.1 bash scripts/deploy-test/deploy-test-etcd.sh <OUT_DIR>
#
# 환경변수:
# IMAGE_NAME (필수) 검증할 etcd 이미지 전체 경로:태그 (registry/repository:tag)
# INIT_IMAGE init 컨테이너 이미지 전체 경로:태그 기본 docker.io/busybox:1.38.0-uclibc
# CHART_DIR 기본 manifests/helm/etcd/1.1.12
# TEST_NAMESPACE 기본 etcd-test-build (매 실행 재생성)
# RELEASE 기본 etcd-build
# REPLICAS 기본 3 (쿼럼 확인을 위해 홀수 권장)
# WAIT_TIMEOUT 기본 300 (초) — StatefulSet 롤아웃 대기 한도
#
# 산출물 (OUT_DIR):
# deploy-test.log 전체 실행 로그
#
# 종료 코드: 0 = 기능 확인(quorum health + put/get) 통과, 그 외 = 실패.
# 정리는 종료 코드와 무관하게 항상 수행된다.
# =============================================================================
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
OUT_DIR="${1:?사용법: deploy-test-etcd.sh <OUT_DIR>}"
mkdir -p "$OUT_DIR"
LOG="$OUT_DIR/deploy-test.log"
: > "$LOG"
IMAGE_NAME="${IMAGE_NAME:?IMAGE_NAME 환경변수가 필요하다 (검증할 etcd 이미지 전체 경로:태그)}"
INIT_IMAGE="${INIT_IMAGE:-docker.io/busybox:1.38.0-uclibc}"
CHART_DIR="${CHART_DIR:-$REPO_ROOT/manifests/helm/etcd/1.1.12}"
TEST_NAMESPACE="${TEST_NAMESPACE:-etcd-test-build}"
RELEASE="${RELEASE:-etcd-build}"
REPLICAS="${REPLICAS:-3}"
WAIT_TIMEOUT="${WAIT_TIMEOUT:-300}"
log() { echo "$(date -u +%H:%M:%S) $*" | tee -a "$LOG"; }
# IMAGE_NAME="registry[/path]/repository:tag" → registry / repository / tag 분해.
# 차트가 imageName 단일 필드가 아니라 3분할 필드(image.registry/repository/tag)를
# 쓰기 때문에 필요하다.
split_image() {
local full="$1" tag repo_full registry repository
tag="${full##*:}"
repo_full="${full%:*}"
registry="${repo_full%/*}"
repository="${repo_full##*/}"
echo "$registry" "$repository" "$tag"
}
read -r IMAGE_REGISTRY IMAGE_REPOSITORY IMAGE_TAG <<< "$(split_image "$IMAGE_NAME")"
read -r INIT_REGISTRY INIT_REPOSITORY INIT_TAG <<< "$(split_image "$INIT_IMAGE")"
log "== 사전 확인 =="
for t in kubectl helm python3; do
command -v "$t" >/dev/null || { log "::error:: $t 없음"; exit 2; }
done
[ -d "$CHART_DIR" ] || { log "::error:: 차트 없음: $CHART_DIR"; 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))"
log " image=$IMAGE_REGISTRY/$IMAGE_REPOSITORY:$IMAGE_TAG"
log " initImage=$INIT_REGISTRY/$INIT_REPOSITORY:$INIT_TAG"
log " replicas=$REPLICAS"
# --- 테스트 네임스페이스 정리 (선-정리·후-정리 공용) --------------------------
cleanup_test_ns() {
kubectl get ns "$TEST_NAMESPACE" >/dev/null 2>&1 || return 0
log "== 정리: $TEST_NAMESPACE =="
if helm status "$RELEASE" -n "$TEST_NAMESPACE" >/dev/null 2>&1; then
helm uninstall "$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 삭제 실패 — 수동 확인 필요"
done
kubectl delete ns "$TEST_NAMESPACE" --wait --timeout=120s >>"$LOG" 2>&1
log " 완료"
}
RESULT=1
trap 'cleanup_test_ns' EXIT
cleanup_test_ns # 직전 실행이 비정상 종료했을 경우를 대비한 선-정리
log "== etcd: 설치 ($RELEASE/$TEST_NAMESPACE, 차트 기본값 + 이미지·replicas override) =="
kubectl create ns "$TEST_NAMESPACE" >>"$LOG" 2>&1
if ! helm install "$RELEASE" "$CHART_DIR" -n "$TEST_NAMESPACE" \
--set image.registry="$IMAGE_REGISTRY" \
--set image.repository="$IMAGE_REPOSITORY" \
--set image.tag="$IMAGE_TAG" \
--set initImage.registry="$INIT_REGISTRY" \
--set initImage.repository="$INIT_REPOSITORY" \
--set initImage.tag="$INIT_TAG" \
--set replicas="$REPLICAS" \
--wait --timeout "${WAIT_TIMEOUT}s" \
>>"$LOG" 2>&1; then
log "::error:: 차트 설치 실패 — $LOG 확인"
kubectl -n "$TEST_NAMESPACE" get pods >>"$LOG" 2>&1
exit 1
fi
STS_NAME="$(kubectl -n "$TEST_NAMESPACE" get statefulset -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)"
if [ -z "$STS_NAME" ]; then
log "::error:: StatefulSet 을 찾지 못함"
exit 1
fi
log "== StatefulSet 롤아웃 대기 (최대 ${WAIT_TIMEOUT}s): $STS_NAME =="
if ! kubectl -n "$TEST_NAMESPACE" rollout status "statefulset/$STS_NAME" --timeout="${WAIT_TIMEOUT}s" >>"$LOG" 2>&1; then
log "::error:: 타임아웃 — StatefulSet 이 준비되지 못함"
kubectl -n "$TEST_NAMESPACE" get pods -o wide >>"$LOG" 2>&1
kubectl -n "$TEST_NAMESPACE" describe pods >>"$LOG" 2>&1
exit 1
fi
pod0="${STS_NAME}-0"
log "== 컨테이너 실행 계정 확인 (non-root uid 999 기대) =="
# quay.io/coreos/etcd 는 etcd/etcdctl 바이너리만 든 최소 이미지라 id/cat/sh 조차
# 없다(실측: `id` exec 시 "executable file not found in \$PATH"). uid 는 직접 확인할
# 수 없으므로, 아래 quorum health·put/get 이 성공한다는 것 자체를 간접 증거로 삼는다
# — securityContext(runAsUser 999, RO rootfs)가 거부됐다면 kubelet 이 애초에
# 컨테이너를 못 띄웠거나 /data 쓰기가 실패했을 것이다.
if uid="$(kubectl -n "$TEST_NAMESPACE" exec "$pod0" -- id -u 2>>"$LOG")" && [ -n "$uid" ]; then
log " pod0 uid=$uid"
[ "$uid" = "999" ] || log "::warning:: 기대한 uid(999) 와 다름 — CUSTOM-README.md 의 securityContext 가정 재검토 필요"
else
log " id 바이너리 없음(최소 이미지) — 직접 확인 불가. 아래 quorum health/put-get 성공 여부로 간접 확인"
fi
log "== 기능 확인: quorum health =="
health_out="$(kubectl -n "$TEST_NAMESPACE" exec "$pod0" -- etcdctl endpoint health --cluster 2>>"$LOG")"
log " $health_out"
healthy_count="$(echo "$health_out" | grep -c "is healthy" || true)"
if [ "$healthy_count" != "$REPLICAS" ]; then
log "::error:: quorum health 이상 (healthy=$healthy_count / replicas=$REPLICAS)"
exit 1
fi
log "== 기능 확인: put/get 왕복 =="
if ! kubectl -n "$TEST_NAMESPACE" exec "$pod0" -- etcdctl put smoke-test ok >>"$LOG" 2>&1; then
log "::error:: etcdctl put 실패"
exit 1
fi
answer="$(kubectl -n "$TEST_NAMESPACE" exec "$pod0" -- etcdctl get smoke-test --print-value-only 2>>"$LOG")"
log " get smoke-test => '$answer'"
if [ "$answer" = "ok" ]; then
log "== 결과: PASS =="
RESULT=0
else
log "::error:: get 응답 이상 (got: '$answer')"
RESULT=1
fi
exit "$RESULT"