자체 빌드 이미지 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
@@ -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 }}