MEMORY.md 는 3번째 줄에서 "지금 시점의 상태와 다음에 할 일만 담는다" 고 스스로 선언하는데
실제로는 완료 기록이 쌓여 264줄이 됐고, 이슈 참조가 한 건도 없었다. 그 결과:
- SBOM_PIPELINE_IMAGE 재빌드가 파일 안에서 3번 반복된다 (215·234·262줄)
- 게이트 강제력 전환이 2번 반복되고, 그 내용은 이미 이슈 #7 체크리스트와 #19 에 있다
- argo-cd 절 55줄은 커밋 33e91c0 메시지의 요약본이다
- 베이스 OS 정책·"벤더 릴리스 노트 믿지 말 것" 은 본문이 스스로 "이미 image-authoring.md /
skill 에 있다" 고 밝히면서 중복 서술한다
같은 사실이 여러 곳에 있으면 나중에 어느 쪽이 맞는지가 새 문제가 된다.
성격별로 목적지를 정하고 내보냈다
---------------------------------
추적이 필요한 미결 → GitHub 이슈 (#29~#33 발행, MEMORY.md 엔 한 줄 링크)
재발 방지 교훈 → .claude/ 문서·skill
단순 완료 기록 → 삭제 (커밋·PR·images/<image>/README.md 가 권위 있는 기록)
교훈 이관 — 아직 어디에도 없던 것만 옮겼다:
- image-authoring.md: 베이스 OS 를 CVE 수치만으로 고르면 배포에서 죽는다.
cp --update=none(coreutils 9.3+) 실측과, 차트가 실행하는 명령을 verify.sh 에서
재현하라는 규칙. 원칙 2 의 "BCI 버전은 실측해서 정한다" 바로 뒤에 붙였다.
- self-build-image skill: 같은 날 재빌드하면 태그가 겹쳐 노드 캐시가 반영을 막는다.
근본 해결은 #31, 여기엔 우회법만.
- cve-remediation skill: 레버 표를 보기 전에 "이 컴포넌트가 필요하긴 한가" 를 먼저 묻는다.
dex 가 한 줄로 차단 57건(58%)이었다. 가장 값싼 레버인데 표에 없어서 새 단계로 넣었다.
유지 규칙을 명시했다
--------------------
트리거는 시간이 아니라 상태다 — "1달 지났으니 정리" 는 아무도 시작하지 않고, 날짜로 자르면
오래됐지만 유효한 미결까지 잘린다. 항목이 "다음에 할 일" 이 아니게 되는 순간 내보낸다.
다만 놓친 것을 걷어내기 위해 월 1회 점검하고 파일 상단의 점검 날짜를 갱신한다.
규칙은 MEMORY.md 자체와 CLAUDE.md("작업 기록을 어디에 남기는가") 양쪽에 뒀다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
argo-cd 절의 수치·상태는 전부 재확인해 그대로 뒀다(게이트 574→337→280→239, 10.4.0 0건,
BCI 16.0 빌드 PASS/CoverageProbe ok, 7.7.0 삭제·7.8.11 잔존).
- airflow 비결정적 렌더: 재렌더 조건을 "ArgoCD 재시작·hard refresh" 로 적어뒀는데, 이후
실측에서 **일반 refresh 로도** 새 ReplicaSet 이 생겼다. 조건을 정정하고, 정해진 조치
방향(dip-values placeholder + dip-console-api DeployPreInstall 치환)과 아직 커밋 전이라는
것, 기존 테넌트는 재배포해야 적용된다는 것을 함께 적는다.
- define-chart-resources: "신규 차트 3종의 리소스 프로파일이 없다 / 규칙을 못 지켰다" 는
사실이 아니다. 세 차트 모두 dip-resources-quotas.yaml 에 small/medium/large 를 갖고 있고
차트 도입 커밋(6878b61)에 함께 들어갔다. 남은 건 문서에 전용 절이 없다는 것뿐이라
그렇게 고쳐 적는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 apisix 작업에서 만들어 실제로 적용한 skill 인데 커밋되지 않은 채 로컬에만
있었다. MEMORY.md 와 CLAUDE.md 가 .claude/skills/cve-remediation/SKILL.md 를 가리키므로
레포만 보는 사람에게는 깨진 링크였다.
CLAUDE.md 의 skill 표에도 같은 이유로 빠져 있던 줄을 넣는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
업스트림 quay.io/argoproj/argocd 의 차단 CVE 는 대부분 OS 패키지가 아니라 바이너리에
정적 링크된 Go 모듈이라, 상위 태그 교체(이미 최신)로도 베이스 OS 교체로도 잡히지 않는다.
부분 조치는 효과가 없었다 — 같은 stdlib·x/crypto 취약점이 다섯 바이너리(argocd·helm·
kustomize·git-lfs·pebble)에 각각 들어있어 본체만 고치면 거의 줄지 않는다. 업스트림
Dockerfile 이 helm/kustomize/git-lfs 를 hack/install.sh 로 **미리 빌드된 릴리스 바이너리**
로 내려받기 때문에 그 바이너리의 Go 버전을 우리가 통제할 수도 없다. 그래서 넷 다 소스에서
다시 컴파일한다.
- images/argocd/ 신규 (Go 4개 + C 2개 + Node UI — 기존 자체 빌드 중 가장 크다)
- 최종 베이스 ubuntu → SUSE BCI(원칙 2). ubuntu 가 딸려오던 pebble 도 함께 사라진다
- tini·connect-proxy 는 SLE_BCI 에 없어 소스 빌드 — 기능을 빼지 않기 위해
- UI 는 업스트림과 동일하게 node 로 빌드(Go embed 라 생략 불가)
- manifests/helm/argo-cd/10.4.0/custom-values.yaml 이 이 이미지를 가리킨다
global.image 하나로 argocd 바이너리를 쓰는 5개 컴포넌트가 공유한다
재사용 설계 — Dockerfile 에 버전을 박지 않는다
---------------------------------------------
이 이미지는 앞으로도 CVE 조치를 반복해서 받는다. 모듈 목록·패키지 목록을 전부 값으로 빼서
다음 조치에서 바뀌는 것이 source.build.env 한 곳뿐이게 했다. 이전 방식이라면 모듈 하나
추가에 build.env·ARG 선언·go get 목록·BUILD_ARGS 네 곳을 고쳐야 했다.
- scripts/build/suggest-go-upgrades.py 신규 — 스캔 리포트의 FixedVersion 에서
GO_MODULE_UPGRADES / GO_BUILDER_TAG 를 산출한다. 사람이 CVE 를 훑어 최대값을 고르지
않는다. 빌드 시점에 최신을 당기는 방식(go get -u)은 재현성을 버리므로 택하지 않았다 —
값은 제안만 하고 채택은 사람이 커밋한다.
- images/argocd/go-mod-upgrade.sh 신규 — 목록 하나를 네 프로젝트에 재사용한다.
go get 은 의존성에 없는 모듈도 go.mod 에 추가하므로 그래프에 있는 것만 골라 적용한다.
실측 함정 — verify.sh 에 반영했다
--------------------------------
차트의 repo-server init 컨테이너가 `cp --update=none` 을 쓴다. 이 형식은 GNU coreutils
9.3+ 에서만 되는데, BCI 15.7 로 빌드한 이미지가 게이트도 verify.sh 도 통과하고 **배포
시점에** Init:CrashLoopBackOff 로 죽었다. BCI 16.0(coreutils 9.6)으로 올려 해소했고,
verify.sh 가 이 명령과 copyutil 흐름을 직접 재현하도록 해서 다음엔 빌드 단계에서 걸린다.
16.0 의 패키지 가용성과 trivy 커버리지(CoverageProbe=ok, EOSL 아님)도 확인했다.
검증
----
게이트 PASS — 실효 CRITICAL/HIGH 0건, CoverageProbe ok
기능 VERIFY-OK (심볼릭 링크 9개 · 번들 도구 3종 · LFS 필터 · copyutil 흐름)
배포 docker.io/paasup 에 push 후 운영 argocd 교체 — 파드 8종 Running,
admin 로그인, Application 2건 Synced/Healthy, repo-server 렌더링 확인
(서버가 go1.26.6 · helm v4.2.4 로 보고 — 우리 빌드가 맞다)
카탈로그 차단은 574 → 239 건이 됐고(7.7.0 삭제 · dex 비활성 · 이 커밋), 남은 239 건은
전부 동결된 7.8.11 몫이다. 운영에 쓰는 10.4.0 은 0 건이다. 경과·미결은 MEMORY.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CVE 게이트 실측에서 ghcr.io/dexidp/dex:v2.45.1 이 혼자 차단 57 건을 만들고 있었다.
argo-cd 10.4.0 전체 98 건의 58% 다. 그런데 이 dex 는 인증 경로에 전혀 관여하지
않는 상태였다.
차트 기본값 dex.enabled = true
운영 argocd-cm oidc.config 있음(Keycloak 직접) / dex.config 없음
dipup tpl 항상 configs.cm.oidc.config — dex 참조 0건
ArgoCD 는 oidc.config 가 있으면 dex 를 우회하고 dex.config 를 쓸 때만 dex 가
관여한다. 즉 쓰이지도 않는 파드가 떠서 차단 CVE 의 절반 이상을 만들고 있었다.
대응 레버를 순서대로 검토한 결과 이 방법이 먼저였다.
태그 교체 소진 — dex v2.45.1(2026-03-03) · argocd v3.5.1(2026-08-12) 모두 최신.
번들 도구도 git-lfs 3.7.1 · kustomize 5.8.1 로 이미 최신이고,
낡은 Go 는 각 업스트림 프로젝트의 선택이라 버전으로 해결되지 않는다.
베이스 OS 무의미 — 차단 98 건 중 9 건(9%)만 OS 패키지다. 89 건이 Go 모듈이라
바이너리에 컴파일돼 들어가 있어 베이스를 바꿔도 남는다.
미사용 제거 ← 이것. 한 줄로 57 건(58%).
자체 빌드 남은 41 건 대상. 별건으로 검토한다.
검증:
게이트 재실행 차단 337 → 280 건 (10.4.0 몫 98 → 41)
배포 테스트 PASS — 워크로드 7종 → 6종, dex-server 사라짐, /api/version v3.5.1
dex 기반 SSO 로 전환하려면 dex.enabled 를 true 로 되돌리고 configs.cm.dex.config 를
채운다. 그때 dex 이미지의 CVE 가 다시 잡히므로 대응 계획이 함께 필요하다 —
CUSTOM-README 에 근거와 함께 적어뒀다.
7.8.11(구버전)의 dex 는 건드리지 않았다. released 된 버전이라 동결한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
벤더 C/H · NVD C/H · 실효 C/H 세 열이 나란히 놓여 어느 것이 판정값인지 헷갈렸다.
실제로 "벤더 5 / NVD 11 인데 실효가 15" 를 어떻게 읽어야 하는지 질문이 나왔다 —
세 열이 같은 CVE 집합을 세지 않기 때문이다. 실효는 CVE 마다 max(벤더, NVD) 를
적용한 결과라 합집합이 된다(5 + 11 − 교집합 1 = 15). 가로로 더하거나 빼서
검산되지 않는 숫자들을 나란히 두는 것이 혼란의 원인이었다.
판정 기준은 실효 C/H 하나뿐이므로 그것만 남기고, 벤더·NVD 의 불일치는
`하향` 열(벤더가 NVD 보다 낮게 매긴 CVE 수)로 대신 드러낸다. 개별 CVE 목록은
이미 `⚠️ 벤더 하향 등급` 절에 있어 집계 열은 중복이었다.
이전: | 벤더 C/H | NVD C/H | 실효 C/H | 차단 | 예외 |
이후: | 실효 C/H | 차단 | 예외 | 하향 |
- 범례(SUMMARY_LEGEND) 를 상수로 빼서 render_md 와 render_brief_md 양쪽에 붙였다.
Job Summary(render_brief_md)에는 범례가 아예 없어서, 새 `하향` 열이 설명 없이
노출될 뻔했다.
하향 신호를 열로 남긴 이유 — 실측:
argocd:v2.14.5 (ubuntu 24.04) 하향 43건
argocd:v3.5.1 (ubuntu 26.04) 하향 1건
dex:v2.45.1 (alpine) 하향 4건 ← 전부 SeveritySource=ghsa (Go 모듈)
redis 2개 (alpine) 하향 0건 ← Alpine 은 자체 등급을 제공하지 않는다
os-pkgs 의 등급 출처를 세어보면 ubuntu 332건은 SeveritySource=ubuntu 인데
alpine 150건은 nvd 이거나 출처 없음이다. 즉 Alpine 이미지에서 "벤더" 열은
NVD 를 그대로 되비추는 값이라 비교 자체가 성립하지 않았다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
카탈로그가 한 차트의 여러 버전을 들고 있어서, 게이트 요약표에 버전 정렬이 없으면
최신 버전과 구버전이 위험도 순으로 섞여 "지금 쓸 버전이 어느 정도인가" 를 한눈에
볼 수 없었다. 실측 사례 — argo-cd 10.4.0 이 구버전 7.8.11 사이에 끼어 표시됐다.
변경 전 (chart asc → 실효 C/H desc)
argo-cd@7.8.11 argocd:v2.14.5 14/101
argo-cd@7.8.11 redis:7.4.2 5/60
argo-cd@10.4.0 dex:v2.45.1 5/52 ← 최신이 구버전 사이에
argo-cd@7.8.11 dex:v2.42.0 4/55
argo-cd@10.4.0 argocd:v3.5.1 1/40
변경 후 (chart asc → 버전 desc → 실효 C/H desc)
argo-cd@10.4.0 dex:v2.45.1 5/52
argo-cd@10.4.0 argocd:v3.5.1 1/40
argo-cd@10.4.0 redis:8.6.4 0/0
argo-cd@7.8.11 argocd:v2.14.5 14/101
argo-cd@7.8.11 redis:7.4.2 5/60
argo-cd@7.8.11 dex:v2.42.0 4/55
- scripts/pipeline/cve-gate.py
- version_sort_key() 추가. 문자열 비교로는 "10.4.0" < "7.8.11" 이 되어 최신이
아래로 내려간다. 숫자 구간을 정수로 파싱한다.
프리릴리스는 코어와 분리해 다룬다 — 단순히 구간을 이어붙이면 리스트가 긴 쪽이
커져 "1.2.0-rc1" 이 "1.2.0" 보다 최신으로 정렬된다(단위 테스트로 실측).
semver 규칙대로 프리릴리스 없는 정식 릴리스를 더 크게 본다.
- 정렬은 안정 정렬을 3번 겹쳐 만든다. 버전만 내림차순이라 reverse=True 가
필요해 한 번에 튜플 하나로 묶을 수 없다.
- manifests/helm/argo-cd/7.7.0 제거
게이트 차단 574건 중 237건이 이 버전의 이미지 3개였다
(argocd:v2.13.0 117 / dex:v2.41.1 61 / redis:7.2.4-alpine 59 — 뒤 둘은 EOSL).
제거 후 차단 337건.
doc/catalog-stack-classification.md 의 버전 목록도 갱신했다(10.4.0 이 빠져 있어
이미 stale 이었다). doc/chart-restructure-plan.md 는 과거 이관 기록이라 두었다.
로컬 파이프라인으로 검증했다(extract → generate → scan → gate, argo-cd 범위).
이미지 6개 / SBOM 6개 성공 / 커버리지 전부 ok.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
k8s 1.33 에서 Deployment/ReplicaSet 에 추가된 .status.terminatingReplicas 를
ArgoCD v2.14.5(k8s 라이브러리 0.31/0.32)가 몰라, 클러스터가 1.33 이상이면
ServerSideApply=true 인 Application 이 전부 아래 오류로 죽는다.
ComparisonError: failed to calculate diff: error calculating structured
merge diff: .status.terminatingReplicas: field not declared in schema
v3.5.1 은 k8s 라이브러리 0.36.1 을 써서 해소된다. dev 클러스터(1.35.7+rke2r1)
에서 실제 발생·해소를 확인했다.
- manifests/helm/argo-cd/10.4.0/ 추가 (chart_updater 로 생성)
- custom-values.yaml 을 실제 배포 기준(dipup argo-cd-values.yaml.tpl)에 맞춤:
kong → apisix, server.extraArgs(--insecure), configs.cm/rbac 추가
- configs.params.controller.resource.health.persist=true 지정 —
ArgoCD v3.0 부터 리소스 health 를 CR 에 저장하지 않는 것이 기본값인데,
dip-console-api 가 .status.resources[].health 를 읽는다
- CUSTOM-README.md 는 현재 차트 기준 배포 가이드로 재작성.
승계된 cert-manager 예시가 10.4.0 에 없는 구버전 스키마(hosts/tls 리스트)라
Ingress 가 생성되지 않는 상태였던 것도 함께 수정
- breaking_change_check: breaking=false (제거 26건이 전부 미사용 key)
- scripts/deploy-test/deploy-test-argo-cd.sh 신규
같은 클러스터에 두 번째 argo-cd 를 띄우려면 crds.install=false 가 필수다 —
CRD 3종이 클러스터 전역이고 운영 릴리스가 소유해 Helm 이 ownership 충돌로
거부한다. Ingress 도 항상 끈다(운영과 host 충돌).
검증: 워크로드 롤아웃 / health.persist / api 버전 / admin 로그인
- argo-cd/7.8.11/BUILD-README.md 에 `helm repo add argo ...` 추가
chart_version_detector 가 이 한 줄을 정규식으로 뽑아 업스트림 레포를 정하는데,
argo-cd 에는 없어서 신규 버전 자동 감지가 조용히 실패하고 있었다
(repo: null → latest_version: null). CLAUDE.md 의 "변경 금지" 규정도
실제 파싱 계약에 맞게 정정했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
-ingress-unauth 는 kubectl get ingress 목록에서 "인증 없는 입구"를
그대로 광고한다. 리소스가 무엇인지가 아니라 무엇이 없는지로 이름을
지은 것이기도 해서, 경로가 /static 외로 늘어나면 이름이 뜻을 잃는다.
파일명(webserver-ingress-static.yaml)과 일치시키고 용도를 그대로
드러내는 -ingress-static 으로 바꾼다. 값 키 dip.unauthenticatedPaths
는 values 파일 안에만 있고 의도를 정확히 서술하므로 그대로 둔다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
세션이 없는 상태로 접속하면 Airflow UI 한 페이지가 /static 자원을 수십 개
동시에 요청하고, 각 요청이 저마다 APISIX openid-connect 로그인 플로우를
시작한다. 세션은 state를 하나만 보관하므로 콜백이 동시에 돌아오면 마지막
하나를 뺀 전부가 state 검증에 실패해 500이 난다.
templates/webserver/webserver-ingress-static.yaml 은 업스트림 Apache Airflow
차트에 없는 PaaSup 추가 템플릿이다. dip.unauthenticatedPaths 가 있을 때만
애노테이션 없는 Ingress를 하나 더 렌더해 해당 경로를 인증 없이 통과시킨다.
애노테이션을 전부 비우는 것은 의도된 것으로, plugin-config-name 을 빼는 게
목적이고 cert-manager.io/* 까지 빼는 이유는 웹 Ingress와 같은 TLS Secret 을
두고 Certificate 를 중복 생성하지 않게 하기 위해서다.
값을 ingress.web 아래가 아니라 dip 아래에 두는 이유는 values.schema.json 의
ingress.web 이 additionalProperties: false 라 그 아래 새 키를 넣으면 스키마
검증에서 배포가 실패하기 때문이다. qdrant·litellm 의 dip.mainPath 와 같은
자리를 쓴다.
webserver-ingress.yaml 과 동일한 조건으로 렌더하므로 Airflow 3.0
(webserver → api-server) 전환 시 함께 재작업이 필요하다. 업스트림 파일은
수정하지 않았고 fork 델타는 신규 템플릿 1개뿐이다.
이슈 #24 는 dip-console 이 차트 밖에서 생성하는 방안(B안)을 권고했으나,
실제로 증상이 재현되는 서비스가 airflow 하나로 좁혀져 차트 템플릿(A안)으로
갔다. dip-console 로 옮길 때는 dip.unauthenticatedPaths 를 비우면 된다.
redirect_uri 고정은 여전히 dip-console 몫으로 남는다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- CLAUDE.md 정적 카탈로그 규모를 실측치로 교체(차트 59 / 버전 디렉토리 70).
차트 디렉토리 5종 파일이 전부 갖춰지지 않은 곳이 있다는 사실과 dip-* 계열이
선택적이라는 점을 명시해 "5개 필수"로 오해하지 않게 했다.
- 디렉토리 트리에 scripts/deploy-test/·images/·MEMORY.md·doc/ 하위 문서를 추가.
scripts/build/ 의 "아직 실사용 이미지 없음" 문구는 실제 빌드 이미지가 생겨
더 이상 사실이 아니라 제거했다.
- chart-to-cnpg: 남은 전환 대상에서 flowise 제외(2026-08 완료), infisical-standalone 추가.
- self-build-image: 이미지 3종 → 7종. 목록이 또 어긋나지 않도록 images/ 디렉토리가
단일 출처임을 설명에 박아뒀다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
flowise/6.0.0 의 dip-values.yaml + dip-questions.yaml 스키마를 airflow 에 적용했다.
doc1 은 cnpg-cluster(airflow-db), doc2 는 airflow 본체다.
dip-values.yaml
- bootstrap.initdb 의 database/owner 는 airflow-db-values.yaml 과 맞춘 airflow.
- databases[] 도 airflow/airflow. 차트 기본값인 appdb/appuser 를 쓰면 appuser
롤이 생성되지 않아 Database CR reconcile 이 실패한다.
- bootstrap.initdb 에는 ensure/reclaimPolicy 를 넣지 않았다. cnpg-cluster 의
cluster.yaml 이 읽지 않는 databases[] 전용 키다.
- postgresql.enabled: false. 내장 bitnami 대신 syncWave 0 의 cnpg 를 쓴다.
- metadataConnection.host 는 "{{ .Name }}-airflow-db-rw". dip 배포는 cnpg
릴리스명에 테넌트 접두사를 붙여 나간다(관측: instance=demo01-air-airflow-db).
flowise 의 externalPostgresql.host 와 같은 규칙이다.
dip-questions.yaml
- git-sync 는 GIT_SYNC_*/GITSYNC_* 네 키를 모두 선언한다. 차트가 네 개를 각각
secretKeyRef 로 잡아서 하나라도 없으면 파드가 기동되지 않는다.
- connection 기본값 호스트는 $CATALOG_NAME-airflow-db-rw.$CATALOG_NAME.
차트가 data.metadataSecretName 의 connection 키를
AIRFLOW__DATABASE__SQL_ALCHEMY_CONN 에 그대로 주입하므로 이 값이 유일한 실효 값이다.
두 파일의 토큰 형식 차이는 의도한 것이다 — dip-questions 는 $CATALOG_NAME,
dip-values 는 {{ .Name }} 가 각 파일의 기존 관례다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LLM·데이터분석 스택(2,3)에 남아있던 P0(litellm, open-webui, langfuse,
strimzi-kafka-operator, kafka-cluster, airflow, lakekeeper, superset)를 P1/P2로
재분류해, P0는 dipup 기본 배포(argocd·harbor·gitea·infisical·keycloakx)와 그
직접 의존 인프라(cert-manager, apisix, etcd, cloudnative-pg, cnpg-cluster,
rancher, dnsup)에만 남도록 정리했다. 데이터분석 스택 차트 수 헤더(16→18)도
실제 개수에 맞춰 수정.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
limit(개수 상한)만으로는 특정 차트를 겨냥해 스캔할 수 없었다(images_final.tsv가
차트 알파벳 순으로 쌓여 앞쪽 무관한 차트까지 같이 스캔됨). chart 입력을 비우면
기존과 동일하게 전체 스캔.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
etcd(bitnamilegacy 동결 미러) → etcd.enabled=false + 카탈로그 자체 etcd 차트를
externalEtcd 기본값으로 연결. adc·apisix-ingress-controller·apisix(paasup/apisix)
세 이미지는 SUSE BCI 자체 빌드로 교체 — 전부 벤더 등급만으로는 안 보이던
벤더 하향 등급 CVE(NVD 재평가 시 드러남)가 원인이었다.
- images/apisix-ingress-controller: 정적 링크 Go 모듈 취약 버전만 강제 업그레이드
- images/apisix: APISIX-Runtime(WASM·dubbo 등 커스텀 모듈 포함) 전체를 SUSE BCI
위에서 소스로 재현, keycloak-authz 플러그인 오버레이
- images/adc: 업스트림 빌더 스테이지는 그대로 두고 distroless 최종 베이스만
SUSE BCI+nodejs24 로 교체
scripts/build/patch-catalog-tag.py 의 TAG_BLOCK 이 점 구분 중첩 경로를 지원하도록
확장(apisix 서브차트 alias 때문에 필요).
세 이미지 모두 게이트 PASS(실효 CRITICAL/HIGH 0/0)와 배포 검증(테스트 클러스터)을
마쳤다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CVE 자체 빌드 이미지가 앱 버전까지 함께 올리며 두 카탈로그 버전이 서로 다른
컴포넌트 버전을 요구하는 복잡도가 생겨, 구버전을 유지하는 대신 최신 버전
하나로 정리한다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
manifests/helm 차트를 DIP/LLM/데이터분석 3개 스택과 우선순위(P0~P2)로 분류한다.
차트별 CVE 트리아지 상세는 issue #19로 별도 관리한다.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
## doc/sbom-pipeline.md — 중복·모순 정리 (328 → 286줄)
같은 사실이 여러 절에 흩어져 있었고, 일부는 문서가 아니라 변경 이력이었다.
- `SEVERITY` 를 전 심각도로 덮어써야 하는 이유가 환경변수 절·CI 절·요약 절 3곳에
있었다. 스크립트 절의 blockquote 하나로 합쳤다 — "게이트를 돌릴 거라면 전 심각도로
스캔해야 한다"가 핵심이고 나머지는 그 결과다.
- Job Summary 1MB 제한이 CI 절과 결과 확인 절에 중복됐다. CI 절 하나로 합쳤다.
- `--warn-only` 서술이 mermaid 라벨·CI 절·게이트 절·트리아지 절 4곳에 있었다.
게이트 절 하나로 합치고, `build-image.yml` 쪽은 이미 강제라는 대비를 함께 적었다.
- 자체 빌드 트리거 표가 "PR 은 push 안 함"을 말하는데 바로 아래 불릿이 같은 말을
반복했다. 표는 그대로 두고 불릿은 **왜** 그런지(REGISTRY 미전달 → localhost 태그라
push 를 시도할 수조차 없다)만 남겼다.
- "오해를 주던 단일 '총 소요'는 제거" 같은 변경 이력 서술을 걷어냈다. 문서는 현재
상태를 적는 곳이다.
- "첫 전체 실행 결과(2026-07-08)" 절은 수치를 싣고 바로 아래에서 "현재 수치가
아니다"로 무효화하는 구조였다. 절 자체를 없애고, 거기서 유일하게 쓸모 있던 사실
(SBOM 생성 실패는 대부분 사설/미인증 레지스트리이거나 대용량 timeout)만 스크립트
절로 옮겼다.
- "실행 이력(2026-08-04)" 절은 MEMORY.md 와 중복이라 제거했다. 거기서만 알 수 있던
사실(Actions 시크릿의 push 권한 확인)은 GitHub 설정 표에 반영했다.
- `CVE_API_KEY` 가 본문에만 있고 GitHub 설정 표에 빠져 있어 추가했다.
- 아키텍처 절 불릿이 mermaid 서브그래프 라벨과 같은 말을 하고 있어, "pull 을 ② 한
곳에 몰아둔 것이 핵심"이라는 결론 한 문장으로 줄였다.
## 이미지 목록을 문서에 박아두지 않는다
이미지는 계속 추가되므로 열거하면 추가할 때마다 낡는다. `images/` 디렉토리를 단일
출처로 삼고 CLAUDE.md·image-authoring.md·build-image.yml·sbom-pipeline.md 의 열거를
걷어냈다. keycloak README 의 베이스 OS 결정 근거도 "기존 3종" 대신 "먼저 들어온
이미지들"로 바꿨다 — 근거의 내용은 그대로다.
## 현황 서술 정정
- build-image.yml 주석이 "아직 도입된 자체 빌드 이미지가 없다(images/ 가 비어 있음)"
로 남아 있었다. 이 레포 CI 에서 빌드→검증→게이트→push→카탈로그 브랜치 push 까지
실제로 검증된 상태다.
- MEMORY.md: cve-exceptions.json 첫 예외 등록, 베이스 OS 정책 확정, PR 자동 생성이
조직 정책으로 불가하다는 실측(run 30882785612)을 반영했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI(31148639764)의 trivy 취약점 DB 가 로컬보다 최신이라 로컬 스캔에는 없던
CVE-2026-40983·CVE-2026-40984(io.micrometer:micrometer-core)가 차단으로 잡혔다.
netty·jackson 과 동일한 성격의 번들 jar CVE 라 같은 오버레이 메커니즘으로 처리한다.
micrometer 1.16.3 → 1.16.6. netty 와 같은 이유로 CVE 가 붙은 micrometer-core 만이
아니라 패밀리 전체 4종(commons·core·observation·registry-prometheus-simpleclient)을
함께 올린다 — 아티팩트 간 버전 혼용이 비지원이다.
Dockerfile 의 ARG 선언을 처음에 빠뜨렸는데 overlay-jars.sh 의 `: "${VAR:?}"` 가드가
설계대로 빌드를 세웠다("parameter null or not set"). 조용히 통과하지 않는다.
재빌드 결과(로컬 trivy DB 캐시를 지우고 CI 와 같은 최신 DB 로 재판정):
차단 0건 — 게이트 PASS
docker.io/paasup/keycloak:26.7.1-bci15.7-hardened-20260807 push 완료
태그는 빌드일 기준이라 같은 날 재빌드가 같은 태그를 덮는다(프레임워크 설계대로).
직전에 push 된 것은 micrometer 취약점이 남아 있던 빌드이고 어디에도 소비되지 않았다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
기존 3종의 verify.sh 가 전부 100755 인데 100644 로 들어갔다. 둘 다 `bash <파일>`
로 호출되어 동작에는 영향이 없지만 관례를 맞춘다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #18 이 카탈로그에 넣는 quay.io/keycloak/keycloak:26.6.4 가 게이트에서 차단
17건(실효 HIGH 17 / CRITICAL 0)이었다. sbom.yml 이 warn-only 라 PR 은 통과했지만
실제로는 게이트 실패 상태로 카탈로그에 들어간다.
## 상위 태그·베이스 OS 교체를 먼저 검토한 결과
차단 17건 중 12건이 배포본에 함께 실린 jar 다. keycloak 26.6.4 와 최신 26.7.1 의
quarkus.version 이 둘 다 3.33.2.1 이고 그 BOM 이 netty 4.1.135.Final /
jackson-bom 2.21.2 를 고정한다(keycloak pom.xml 두 태그 + quarkus BOM 실측).
필요한 수정 버전은 netty 4.1.136.Final, jackson 2.21.4 라 **상위 태그로도 풀리지
않고**, CVE 가 OS 패키지가 아니라 jar 자체라 **베이스 OS 교체도 통하지 않는다.**
jar 를 직접 교체하는 자체 빌드가 유일한 수단이다 — etcd 이미지의
`go.work replace golang.org/x/text` 와 같은 성격의 의존성 override.
## images/keycloak/
업스트림 quarkus/container/Dockerfile 을 기준으로 하되 셋이 다르다.
1. 런타임 rootfs 가 SUSE BCI. bci-micro 파일시스템을 **씨앗으로 깔고** 그 위에
zypper --installroot 로 설치한다. 업스트림 ubi-null.sh 처럼 별도 installroot 를
micro 위에 덮으면 micro 의 rpmdb 가 가려져 micro 자체 패키지가 SBOM 에서 통째로
사라진다 — CVE 가 주는 게 아니라 스캔 사각지대가 생긴다. 씨앗 방식으로 OS 패키지
65종이 정상적으로 잡히는 것을 SBOM 으로 확인했다.
2. 취약 jar 오버레이(overlay-jars.sh). netty 17종 → 4.1.136.Final, jackson
core/databind → 2.21.4, pgjdbc → 42.7.12. Quarkus fast-jar 의 클래스패스가
파일명을 그대로 참조하므로 **파일명은 유지하고 내용만** 바꾸고 sha1 로 검증한다.
trivy 는 jar 내부 메타데이터를 읽으므로 SBOM 에 새 버전이 정확히 잡힌다.
3. bin/client 제거. keycloak-admin-cli 가 jackson 을 shade 로 품은 uber-jar 라
교체가 불가능하다. 서버 JVM 이 로드하지 않는 독립 CLI 라 제거했다 — 업스트림
대비 유일한 기능적 차이이며 CUSTOM-README 에 대안을 적었다.
버전은 26.7.1 로 올렸다. 26.6.4 는 26.7.1(및 26.6.5)에서만 패치된 keycloak-services
HIGH 5건(CVE-2026-16102/16442/16443/15572/15573)에 취약하다. 차트(keycloakx 7.2.2)는
최신이고 그대로 둔다 — appVersion 26.6.4 는 codecentric 의 릴리스 캐던스 지연이다.
## 베이스 OS 정책 확정 (image-authoring.md 원칙 2 미결 해소)
SUSE BCI 로 통일하되 **버전은 이미지마다 실측해서 고른다.** BCI 16.0 이 나와 있지만
SLE_BCI 의 java-21-openjdk-headless 가 15.7 은 21.0.12, 16.0 은 21.0.11 이라 최신
베이스로 가면 CVE-2026-41254·CVE-2026-47063 이 오히려 남는다. bci-micro 에
sed·grep·find 가 셋 다 없다는 것과 SLE 패키지명 차이(tzdata→timezone 등)도 함께
기록했다.
## 실측 결과
로컬 빌드(linux/amd64) → verify.sh → SBOM → 전 심각도 스캔 → 게이트:
차단 17건 → **0건** (커버리지 자가진단 ok, OS=sles 15.7)
남은 1건 CVE-2025-59250 은 예외 등록했다 — 트리비가 같은 mssql-jdbc jar 하나로
컴포넌트를 둘 만들어(pom.properties 의 13.2.1.jre11 / 파일명의 13.2.1) 접미사가
잘린 쪽이 매칭된 파싱 오탐이다. 설치본은 FixedVersion 목록에 있는 13.2.1.jre11 이다.
dev 클러스터 격리 네임스페이스(kc-test-build)에 cnpg-cluster + keycloakx 로 실배포
검증: Pod Running, jdbc-postgresql 연결, liquibase 스키마 생성, admin 부트스트랩
(KC-SERVICES0077), apisix ingress 경유 OIDC discovery 200 / admin 토큰 발급 /
realm·client 생성(201) 까지 확인. 정리 시 Longhorn Volume 까지 삭제했다.
## custom-values.yaml ingress 수정
path 가 exact "/" 였다. apisix 에서는 루트만 매치되어 /realms/*, /admin/* 이 전부
404 가 난다 — airflow·superset·mlflow·lakekeeper 에서 이미 실측된 문제로 카탈로그가
regex 방식으로 통일돼 있다. path: /.* + k8s.apisix.apache.org/use-regex 로 맞췄고,
배포 검증에서 이 경로들이 실제로 뜨는 것을 확인했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1.0.0 에서 복사한 뒤 안 고친 차트 버전 표기(1.0.0)와 배포 경로를 1.1.0 으로
맞추고, 저장소에 실제로 존재하지 않는 doc/decisions·doc/analysis·doc/charts
경로 인용을 제거했다(리뷰 중 확인 — PR #17).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dev 클러스터 격리 네임스페이스에 실제 배포해 검증하는 과정에서 두 가지
누락을 발견했다:
1. command/args 기본값이 둘 다 빈 배열이라, 지정하지 않으면 컨테이너가
인자 없는 kc.sh(도움말 출력, exit 0)로 끝나 CrashLoopBackOff가 된다.
2. hostname-strict 기본값이 true라 KC_HOSTNAME 을 지정하지 않으면
"hostname is not configured" 로 기동이 실패한다.
두 값 모두 custom-values.yaml에 추가하고, BUILD-README/CUSTOM-README에
실측 근거를 남겼다. 이후 admin 부트스트랩(KC-SERVICES0077), DB 마이그레이션,
admin REST API로 realm/client 생성까지 전부 정상 동작 확인.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
codecentric/keycloak(18.4.0, WildFly 기반, appVersion 17.0.1-legacy)이
bitnami/postgresql 서브차트를 조건부 의존성으로 포함해 bitnami 무료 배포
정책 변경 문제가 그대로 전이됐다. codecentric은 이 WildFly 차트를 더 이상
갱신하지 않고 Quarkus 기반 Keycloak(17+)용 별도 차트 keycloakx를 제공하며,
keycloakx는 서브차트 의존성이 전혀 없어(Chart.yaml에 dependencies 없음)
문제가 근본적으로 해소된다.
실제 소비자가 없어(문서 예시 표 한 줄 외 참조 없음) phased 전환 없이
keycloak/18.4.0을 같은 커밋에서 제거했다.
custom-values.yaml 작성 시 확인한 핵심 사항 — 이 차트의 http.relativePath
기본값이 구버전 WildFly Keycloak 호환용 "/auth"라, 명시적으로 "/"로
오버라이드해야 한다(안 하면 OIDC issuer/admin API 경로가 소비 앱들의
경로 접미사 없음 가정과 어긋난다). database.existingSecret/existingSecretKey
로 kubernetes.io/basic-auth 시크릿을 그대로 참조 가능함도 helm template로
확인했다.
Closes#1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
앱별 부트스트랩 SQL 을 ConfigMap/Secret 참조로 실행할 수 있게 한다. CNPG CRD 에는
이미 있는 필드(operator 0.29.0/CRD 확인됨)인데 차트가 렌더하지 않아 values 로 쓸 수
없었다.
용도: 스키마 덤프처럼 큰 SQL 을 values 에 인라인하지 않고 ConfigMap 으로 넘기는 경우.
postInitApplicationSQL(인라인)과 시점·권한이 동일(클러스터 생성 직후 1회, 앱 DB 안에서
superuser) 하고 SQL 출처만 다르다. 1회성이라 비멱등 SQL(CREATE TABLE 등)을 그대로
넣어도 재실행되지 않는다.
- values.yaml: bootstrap.initdb.postInitApplicationSQLRefs 추가(configMapRefs/secretRefs,
기본 빈 배열)
- templates/cluster.yaml: 값이 있을 때만 렌더(있으면 렌더, 없으면 필드 자체가 안 나옴 —
기존 클러스터에 영향 없음을 렌더 테스트로 확인)
- CUSTOM-README.md: postInitApplicationSQL 과의 차이·참조 처리 순서(Secret 전체 →
ConfigMap 전체) 문서화
카탈로그 관례 파일(BUILD-README·CUSTOM-README 본문 대부분·dip-*.yaml·custom-values.yaml)은
1.0.0 에서 그대로 승계했다(diff 로 확인).
검증: 빈 값일 때 필드 미노출, configMapRefs 지정 시 정상 렌더, 기존 1.0.0
custom-values.yaml 로 1.1.0 렌더해도 문제없음(하위 호환) 확인.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12.4.0 → 12.6.0 승계 시 BUILD-README/CUSTOM-README/custom-values 3개만 옮기고
카탈로그 관례 파일 5개를 빠뜨렸다. 12.4.0 에서 그대로 승계한다:
- argo-values.yaml
- dip-questions.yaml
- dip-resources-quotas.yaml
- dip-values.yaml
- dip-volumes-quotas.yaml
dip-*.yaml 은 카탈로그 전반의 관례다(25개 이상 차트, 총 100개 파일). 갱신 대상
7개 차트 중 이 파일들을 가진 것은 gitea 뿐이라 다른 차트는 영향이 없다.
내용은 버전 의존적이지 않아(리소스/볼륨 티어, questions, ApplicationSet 용 values)
수정 없이 그대로 옮겼다.
검증: 5개 파일 모두 12.4.0 과 12.6.0 에서 helm 렌더 결과가 동일하다. dip-values.yaml
(`{{ .Name }}`·`{{ .Domain }}` 미치환)과 argo-values.yaml(postgresql/postgresql-ha
동시 활성)은 양쪽 버전에서 똑같이 렌더 실패하는데, ApplicationSet 계층이 먼저
치환·조정하는 것을 전제로 한 파일이라 원래 helm 단독 렌더 대상이 아니다 —
이번 승계로 생긴 회귀가 아님을 12.4.0 대조로 확인했다.
차트 12개 렌더 재검증 12/12 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
rancher 2.14.3
- custom-values 의 `preinstallHook: true` 제거. 2.10.1 에 있던
templates/preinstallHook/ (tls-ca secret 생성 Job)이 2.14.3 본문에는 없어
이 값이 아무 동작도 하지 않는다. dipup 은 설치 전 단계에서 직접 만든다
(pkg/kube/secret.go CreateRancherCASecret).
- privateCA: true 는 deployment 가 tls-ca secret 을 non-optional 로 마운트하게
하므로, 카탈로그 차트만으로 배포할 때 secret 이 없으면 파드가
ContainerCreating 에서 멈춘다. 해당 주의를 custom-values·CUSTOM-README 에 명시.
- BUILD-README 상단에 2.14.3 이 dipup tgz 전개본이라 이 문서의 차트 수정 절차가
적용되지 않았음을 명시하고, preinstallHook 단계를 무효 표시.
infisical-standalone 1.9.0
- custom-values 에 `ingress.nginx.enabled: false` 추가. 차트 기본값이 활성이라
스캐너(extract-helm-images.sh 가 custom-values 로 effective image 산출)가
dipup 이 배포하지 않는 k8s.gcr.io/ingress-nginx/controller:v1.1.0 ·
kube-webhook-certgen:v1.1.1 을 잡아 CVE 트리아지 잡음이 됐다.
dipup env/values/infisical-values.yaml 과 값을 맞춘다.
검증: 12/12 차트 helm template --kube-version 1.34.1 렌더 성공.
infisical effective image 가 dipup 배포분 3종으로 축소됨을 확인.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dipup 이 go:embed 로 직접 보관·관리하던 Helm 차트를 카탈로그로 옮기는 첫 단계다.
두 저장소가 각자 CVE/SBOM 파이프라인을 운영하는 이중화를 해소하려면, 먼저 카탈로그가
dipup 과 같은 차트·같은 이미지를 보게 만들어야 한다.
배경: CVE 파이프라인 구성 이전에 두 곳에서 같은 차트를 유지하기 어려워 dipup 이 별도로
차트를 관리해 왔고, 그 결과 버전이 갈라졌다. 겹치는 10개 중 버전까지 일치하는 것은
postgresql-ha·dnsup 2개뿐이었다.
## 버전 갱신 (7개) — 신규 버전 디렉토리 추가, 구버전은 보존
| 차트 | 기존 | 신규 | appVersion |
|---|---|---|---|
| apisix | 2.14.0 | 2.16.0 | 3.16.0 → 3.17.0 |
| argo-cd | 7.7.0 | 7.8.11 | v2.13.0 → v2.14.5 |
| cert-manager | v1.16.1 | v1.21.0 | 동일 |
| gitea | 12.4.0 | 12.6.0 | 1.24.6 → 1.26.1 |
| harbor | 1.16.2 | 1.19.1 | 2.12.2 → 2.15.1 |
| kyverno | 3.4.1 | 3.8.2 | v1.14.1 → v1.18.2 |
| rancher | 2.10.1 | 2.14.3 | v2.10.1 → v2.14.3 |
차트 본문은 dipup 이 임베딩한 .tgz 를 그대로 전개했다(네트워크 pull 이 아니라 dipup 이
실제 배포하는 바이트와 동일함을 보장하기 위함). BUILD-README/CUSTOM-README/custom-values
3개 파일은 구버전에서 승계했다.
## 신규 추가 (5개)
infisical-standalone 1.9.0, longhorn 109.3.1+up1.11.2, longhorn-crd 109.3.1+up1.11.2,
metallb 0.16.1, secrets-operator v0.10.33.
longhorn/longhorn-crd 는 업스트림이 아니라 Rancher 패키징 차트(109.x 라인, Rancher 2.14
계열과 짝)다. BUILD-README 의 `helm repo add` 라인은 chart_version_detector 가 파싱하는
계약이라 실제 업스트림 repo 를 검증해 기재했고, 감지기로 현재/최신 버전이 정상 조회되는
것을 확인했다.
## custom-values — 버전과 결합된 이미지 핀 정리
카탈로그 스캐너가 dipup 의 effective image 를 보게 하려면 이미지 핀이 맞아야 한다.
- **kyverno: 승계본이 3.8.2 에서 깨져 재작성.** 3.4.1 은 정리 훅이
`registry: ~ / repository: bitnami/kubectl` 이라 bitnamilegacy 오버라이드가 맞았지만,
3.8.2 는 `registry: ghcr.io / repository: kyverno/readiness-checker` 로 바뀌었다.
그대로 옮기면 ghcr.io/bitnamilegacy/kubectl 이라는 없는 좌표가 된다. 해당 오버라이드를
제거하고, 3.8.2 에서 삭제된 policyReportsCleanup 키도 함께 뺐다. 남는 조치는 tag 고정뿐
(기본 tag 가 비어 latest 로 떨어짐 → v1.18.2 로 고정).
- apisix: 3.16.0-keycloak-authz → 3.17.0-keycloak-authz (차트 appVersion 과 함께 이동)
- gitea: image.tag 1.26.4 핀 추가 — 차트 기본 1.26.1 대비 CRITICAL 2→0, HIGH 44→12
- infisical: image.tag v0.162.7 핀 — 기본 v0.158.x 는 stale Debian base 로 OS 기인 CVE
다수(fixable CRITICAL 53→5, HIGH 491→55). redis/postgresql 은 bitnamilegacy 좌표로.
- longhorn: 실측 기반 리소스 튜닝(manager request, guaranteedInstanceManagerCPU,
systemManagedCSIComponentsResourceLimits). replica 수처럼 노드 수에 의존하는 값은
넣지 않았다 — 소비 측에서 주입한다.
## 검증
12개 차트 전부 `helm template --kube-version 1.34.1` 렌더 성공. 렌더 결과 이미지가
dipup 배포 이미지와 일치함을 확인(paasup/apisix:3.17.0-keycloak-authz,
gitea:1.26.4-rootless, readiness-checker:v1.18.2, infisical:v0.162.7).
## 범위에서 뺀 것
- **keycloak**: 카탈로그는 codecentric(app 17.0.1-legacy), dipup 은 bitnami(app 26.2.4)로
계보가 다르다. 이슈 #1(bitnami 대체 방안 검토)의 결론이 나온 뒤 처리한다.
- **rancher-monitoring(-crd)**: 14c05f1 에서 불필요 판단으로 제거된 차트이고
victoria-metrics 스택으로 대체 예정이라 추가하지 않는다.
- **dip-api/dip-console**: 자체 개발 차트로 각 앱 저장소가 출처다. 대조 결과 앱 저장소와
dipup 사본이 일치해 카탈로그가 개입할 이유가 없다.
- **postgresql-ha/dnsup**: 이미 버전이 일치해 작업 대상이 아니었다.
## 후속 과제
dnsup 은 카탈로그·dipup 사본(1.0.1)이 원본(dip-console-api helm/dnsup 1.0.0)보다 앞서
있다. 1.0.1 에만 있는 service.LoadBalancerIP·service.annotations 지원을 원본으로 백포트한
뒤, 카탈로그에서 dnsup 을 제거하는 것이 자체 개발 차트 출처 원칙에 맞다.
세 파이프라인의 실행법이 문서에만 있어 "그 문서를 읽어야만" 알 수 있었다.
관련 작업 시 자동 로드되도록 Skill 로 등록한다.
- catalog-update-pipeline: agent/update_catalog 파이프라인. CATALOG_ROOT 가
개인 로컬 경로로 하드코딩돼 있어 오버라이드 필수라는 점, create_pr 단계가
주석 처리돼 PR 이 생성되지 않는다는 점을 명시했다.
- sbom-cve-gate: SBOM·스캔·게이트. CoverageProbe(ok/none/n-a) 해석과 게이트가
현재 warn-only 라는 점을 명시했다.
- self-build-image: 자체 빌드. 오케스트레이터는 하나뿐이라는 원칙과 전역 ARG
선언, 게이트 PASS 가 동작을 보장하지 않는다는 점을 명시했다.
Skill 은 절차 본문을 복제하지 않고 권위 문서를 가리킨다 — 문서가 단일 출처이고
Skill 은 실행 계약과 함정·현재 상태만 담는다.
함께 고친 stale 문서(image-authoring.md):
- "images/ 디렉토리 자체가 없다" → 실제로는 이미지 3종이 있고 push 까지 됐다
- "자동화된 배포 테스트 절차는 아직 없다" → deploy-test-procedure.md 와
전용 스크립트가 있다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mlflow 도 ingressClassName 이 비어 있어 이 클러스터(apisix 전용)에서 Ingress 가
CLASS: <none> 으로 뜨고 어떤 컨트롤러도 처리하지 않았다 — ingress 경유 접근이
아예 불가능한 상태였다. path: / 도 exact 매치라 앱 리다이렉트 이후 404 가 난다.
전환 대상 4개 중 3개(airflow/superset/mlflow)가 같은 문제를 갖고 있었다.
실측 검증: /health OK, UI 200, experiments API 가 cnpg DB 에서 Default 실험을
반환 — ingress → 앱 → cnpg 전 경로 확인.
관련: #14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
이슈 #9에서 결정된 CloudNativePG 채택을 카탈로그 앱에 확장 적용한다.
airflow/lakekeeper/mlflow/superset/flowise 5개 차트가 내장하던 bitnami
postgresql 서브차트를 끄고 앱 전용 cnpg-cluster 인스턴스를 외부 DB로 쓰도록
전환했다. 5개 모두 dev 클러스터 격리 네임스페이스에서 배포 테스트로 실측 검증했다.
gitea/keycloak/dnsup 는 서브차트가 아니라 공유 postgresql-ha 를 외부 참조하며
paasup/dipup 레포 관리 대상이라 제외했다 — 인수인계 문서만 추가했다.
배포 구조:
- ArgoCD ApplicationSet 으로 DB(syncWave 0) → 앱(syncWave 1) 순서를 보장한다.
기존 openmetadata/victoria-metrics 관례를 따랐다. cnpg-cluster 차트는 범용
상태로 유지하고 앱별 값은 manifests/applicationset/<app>/ 에 둔다.
검증 중 발견해 함께 고친 문제:
- lakekeeper: cnpg 의 -ro 는 replica 전용이라 instances:1 에서 엔드포인트가
0개다. 읽기 연결을 -r(전체 라운드로빈)로 교체했다.
- airflow/superset: ingressClassName 누락 + kong 애노테이션 잔존으로 이
클러스터(apisix 전용)에서 ingress 접근이 아예 불가능했다. apisix + regex
path 로 교체했다.
- 배포 테스트가 PV 만 지우고 Longhorn Volume CR 을 남겨 storageScheduled 가
누적됐다(orphan 112개 ~1TB 로 배포 차단). 두 스크립트의 정리 로직을 고쳤다.
재사용 구조화:
- .claude/skills/chart-to-cnpg/ 신규. 남은 4개 차트(langflow-ide, langfuse,
litellm, nemo)에 같은 절차를 재사용한다. flowise 에 실제 적용해 검증했다.
- 배포 테스트 공통 절차는 .claude/deploy-test-procedure.md, 환경 함정은
.claude/pitfalls.md 로 단일화하고 앱별 README 는 참조만 남겼다.
관련: #9, #14
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>