자체 빌드 이미지 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:
+199
@@ -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"
|
||||
Reference in New Issue
Block a user