파이프라인을 두 축으로 갈라 소유 문서를 확정한다

"파이프라인이 chart CVE 조치와 커스텀 이미지 빌드 2개로 나뉘어 있는가" 를 확인하다가 실제
구성이 그 모델과 다른 것이 드러났다.

  1. 워크플로는 3개다. cve-edge-post.yml 이 CLAUDE.md 에서 디렉토리 트리와 괄호 안에만
     등장해 파이프라인으로 읽히지 않았다 — "2개" 인식의 근원이다.
  2. doc/sbom-pipeline.md 가 두 축을 한 파일에 담고 있었다(자체 빌드 절 43줄).
     커스텀 이미지가 별도 레포로 분리될 예정인데 이 상태로는 분리 때 파일을 찢어야 한다.
  3. 그 문서가 이미 뒤집힌 결정을 담고 있었다 — "schedule 트리거는 없다. 블라인드 정기
     재빌드는 제거했다" 인데 PR #40 이 schedule 을 추가했다.

축을 이렇게 갈랐다
-----------------
  차트 카탈로그 축   sbom.yml · cve-edge-post.yml   →  doc/sbom-pipeline.md
  자체 빌드 축       build-image.yml                →  .claude/image-authoring.md

doc/sbom-pipeline.md — 차트 축만 남긴다
  - 상단에 "이 문서가 다루는 축" 을 두고 자체 빌드는 링크로 넘긴다
  - 자체 빌드 절(43줄)을 image-authoring.md 로 이관
  - sbom.yml ↔ cve-edge-post.yml 비교 표 신설. **판정기가 두 벌**이라는 사실을 명시했다 —
    cve-edge-post.yml 은 집계를 워크플로 YAML 안의 인라인 python 으로 갖고 있어 승인 예외도
    실효 등급도 적용하지 않는다. 같은 스캔 데이터에서 다른 숫자가 나올 수 있다
  - PR 을 실제로 막는 게이트는 images/** PR 뿐이고 manifests/applicationset/** 는 아무
    워크플로도 보지 않는다는 사각지대를 적었다

.claude/image-authoring.md — 자체 빌드 축의 단일 출처가 된다
  - 이관받은 워크플로 서술 + "이 워크플로의 게이트는 강제다"(차트 축 warn-only 와 다르다는
    사실이 지금까지 한 곳에만 있었다)
  - schedule 결정 정정 — 지금 것은 블라인드가 아니라 CVE 트리거다. 수정 버전이 있는 차단
    CVE 가 있을 때만 빌드하고 없으면 아무것도 하지 않는다. 거부된 것과 조건이 다르다
  - "레포 분리 후 무엇이 끊기는가" 결합점 7개 표. 3번(탐지가 카탈로그를 읽는다)이 가장 크고,
    게이트 공유는 workflow_call 이 아니라 composite action 이어야 한다는 것도 적었다
    (workflow_call 은 별도 job 이라 $OUT_DIR 를 공유하지 못한다)
  - 파일 상단에 "레포 분리 시 images/·scripts/build/·build-image.yml 과 함께 이동한다"
  - #35 에서 실측한 매핑 함정 추가 — CHART_DIRS 에 없는 파일은 patch-catalog-tag.py 가
    검사조차 하지 않아 cnpg-cluster/1.1.0 이 조용히 빠졌다

CLAUDE.md — 지도만 남긴다
  두 축 비교 표(질문·워크플로·게이트 강도·소유 문서)로 바꾸고 메커니즘 서술을 걷어냈다.
  cve-edge-post.yml 을 파이프라인으로 처음 등재했다.

검증
----
  표의 사실 대조   각 워크플로의 cve-gate 호출·warn-only·활성 schedule 을 파일에서 확인
  축 분리          sbom-pipeline.md 에 남은 build-image.yml 언급은 전부 링크·대조·시크릿
                   공유 문장(의도된 것)
  뒤집힌 결정      "schedule 트리거는 없다" 잔존 0건
  링크             3개 문서의 로컬 링크 전부 실재

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
wbsong111
2026-08-20 16:26:14 +09:00
parent 981d36daa4
commit 79555215a0
3 changed files with 150 additions and 71 deletions
+86 -2
View File
@@ -1,7 +1,15 @@
# 자체 빌드 이미지 작업 규칙
[CLAUDE.md](../CLAUDE.md) 에서 분리했다. 새 자체 빌드 이미지를 추가하거나(CVE 게이트
대응 우선순위 중 "자체 빌드") 기존 이미지의 빌드 정의를 바꿀 때만 참고한다.
**자체 빌드 축의 단일 출처다.** 새 자체 빌드 이미지를 추가하거나 기존 빌드 정의를 바꿀 때,
그리고 `build-image.yml` 이 무엇을 하는지 알아야 할 때 여기를 본다.
카탈로그의 CVE 파이프라인은 두 축이고 이 문서는 **"이미지를 어떻게 만드는가"** 를 갖는다.
**"우리가 배포하는 이미지에 무엇이 있는가"**(`sbom.yml`·`cve-edge-post.yml`, 스캔·게이트
메커니즘)는 [doc/sbom-pipeline.md](../doc/sbom-pipeline.md) 가 단일 출처다 — 여기 복제하지 않는다.
> **레포 분리 시 이 파일은 `images/` · `scripts/build/` · `build-image.yml` 과 함께 이동한다.**
> 커스텀 이미지가 별도 레포로 나가는 것이 계획이고, 그때 끊기는 결합점은 아래
> "레포 분리 후 무엇이 끊기는가" 에 정리돼 있다.
security-catalog 레포에서 검증한 자체 빌드 프레임워크를 포팅했다. 자체 빌드 이미지는
`images/<image>/` 에 있고(목록은 그 디렉토리가 단일 출처) `docker.io/paasup` 에 push 되어
@@ -354,6 +362,82 @@ python3 scripts/build/check-rebuild-needed.py --reports ... --image etcd --apply
docstring 이 어느 함수가 어느 쪽인지 적어둔다 — **탐지는 카탈로그 쪽에 남고, 핀 판단은 이미지
쪽으로 간다.** 분리 작업 때 그 주석을 먼저 읽는다.
## CI — `build-image.yml`
이 축의 워크플로다. 실행기는 `scripts/build/build-hardened-image.sh` 하나이고 빌드 → `verify.sh`
→ SBOM → `scan-sbom.sh``cve-gate.py` 를 순서대로 부른다 — 스캔·게이트는 차트 축의 스크립트를
그대로 재사용한다([doc/sbom-pipeline.md](../doc/sbom-pipeline.md)).
**이 워크플로의 게이트는 강제다.** 차트 축의 `sbom.yml``--warn-only` 인 것과 다르다 —
게이트가 실패하면 push 도 카탈로그 반영도 일어나지 않는다.
| 트리거 | 대상 결정 | 빌드·검증·게이트 | 레지스트리 push | 카탈로그 태그 갱신 |
|--------|----------|------------------|-----------------|-------------------|
| `workflow_dispatch` | `image` 입력(필수) · `base_os`(비우면 `catalog.env``DEFAULT_BASE_OS`) · `push`(기본 `true`) | ✅ | `push=true` 일 때만 | 게이트 PASS + 태그 변경 시 |
| `pull_request` (`images/**`) | 변경된 `images/<image>/` 를 diff 로 자동 탐지(복수면 매트릭스 병렬) | ✅ | ❌ | ❌ |
| `schedule` (`0 2 * * 1`, 매주 월요일 02:00 UTC) · `mode=drift` | 배포 중인 이미지를 스캔해 재빌드가 필요한 것만 | ✅ | ❌ | ❌ |
PR 트리거가 push 하지 않는 것은 `REGISTRY` 를 넘기지 않기 때문이다 — 태그가 `localhost/...` 로 남아
push 를 시도할 수조차 없다(검증 전용 안전장치).
- **`schedule` 이 있지만 블라인드 정기 재빌드는 아니다.** 예전에는 트리거가 없었고 그 이유가
"블라인드 재빌드는 정말 개선인지를 매번 되묻게 만든다" 였다. 지금 것은 **수정 버전이 있는 차단
CVE 가 있을 때만** 빌드하고 없으면 아무것도 하지 않는다 — 거부된 것과 조건이 다르다.
판정은 아래 "핀은 가만히 있어도 뒤처진다" 절이 갖는다.
- **`sbom.yml` 의 게이트가 이 워크플로를 자동 호출하지 않는다.** 차트 축이 차단 CVE 를 찾아도
자체 빌드로 갈지는 사람이 판단한다 — 두 축 사이에 자동 연결은 없다. 이 워크플로의 `schedule`
**자기 이미지만** 본다(배포 중인 자체 빌드 이미지).
- 레지스트리는 `REGISTRY_HOST: docker.io/paasup` 고정. 인증은 `DOCKERHUB_USER`/`DOCKERHUB_TOKEN`.
### 카탈로그 반영 — PR 은 자동 생성되지 않는다
게이트 PASS 이고 새 태그가 현재 카탈로그 태그와 다르면, 워크플로가 `catalog.env``CHART_DIRS` 아래
`custom-values.yaml`/`dip-values.yaml``scripts/build/patch-catalog-tag.py` 로 갱신하고
`build/<image>-<타임스탬프>` 브랜치를 push 한다. **여기까지가 자동이다.**
PR 오픈은 사람이 한다 — `GITHUB_TOKEN` 으로 PR 을 만드는 것 자체를 조직 정책이 막는다("Allow GitHub
Actions to create and approve pull requests" 미허용, 리포 설정으로 변경 불가). 2026-08-04 실측으로
확인했고(run 30882785612, GraphQL: `GitHub Actions is not permitted to create or approve pull requests`),
`gh pr create` 호출은 워크플로에서 제거했다. 대신 Job Summary 에 compare 링크가 남는다.
병합 전에 사람이 해야 하는 것:
1. compare 링크로 PR 오픈.
2. `gh workflow run helm-catalog-sbom --ref <브랜치>` 로 카탈로그 게이트 확인.
3. **배포 검증** — 게이트 PASS 는 "동작한다"를 증명하지 않는다(스캐너는 런타임 요구사항을 보지 못한다).
절차: [deploy-test-procedure.md](deploy-test-procedure.md).
> **매핑이 불완전하면 조용히 지나간다.** `patch-catalog-tag.py` 는 "예상 패턴을 못 찾으면 실패"
> 하도록 만들어져 있지만 **애초에 `CHART_DIRS` 에 없는 파일은 검사조차 하지 않는다.** 실측
> (2026-08-20): `cnpg-postgresql` 의 `CHART_DIRS` 가 `cnpg-cluster/1.0.0` 만 담고 있어 같은
> 이미지를 쓰는 `1.1.0` 이 낡은 태그로 남았고, 게이트만 계속 차단으로 잡았다. 카탈로그는 여러
> 버전을 동시에 보관하므로 **이미지 하나가 여러 버전 디렉토리에 걸리는 것이 정상**이다 —
> 새 버전 디렉토리를 만들 때 `catalog.env` 를 함께 본다.
### 레포 분리 후 무엇이 끊기는가
커스텀 이미지가 별도 레포로 나가면 이 축은 **"어떻게 만드는가"** 만 갖고, **"무엇을 배포 중인가"**
는 카탈로그 레포에 남는다. 지금 한 레포 안이라 보이지 않는 결합이 그때 드러난다.
| # | 결합점 | 분리 후 필요한 것 |
|---|---|---|
| 1 | `patch-catalog-tag.py` 가 카탈로그 values 를 **쓴다** | 카탈로그 쪽이 자기 파일을 쓰게 한다(반영 워크플로) |
| 2 | `catalog.env``CHART_DIRS`·`TAG_STYLE`·`TAG_BLOCK` | **카탈로그 레이아웃 정보다** — 카탈로그 쪽으로 옮긴다 |
| 3 | `check-rebuild-needed.py` 가 카탈로그 values 를 **읽어** 배포 중 ref 를 해석 | **탐지는 카탈로그 쪽에 남는다** (그 파일 docstring 의 절단면 참고) |
| 4 | `build-hardened-image.sh``scan-sbom.sh`·`cve-gate.py` 를 부른다 | 게이트를 composite action 으로 공개해 양쪽이 `uses:` |
| 5 | `doc/cve-exceptions.json` 공유 | 카탈로그가 소유하고 action 입력으로 받는다(예외는 "무엇을 감수하는가") |
| 6 | `build/<image>-<ts>` 브랜치를 카탈로그 레포에 push | 카탈로그 쪽이 자기 브랜치를 만든다 |
| 7 | `images/` 가 이미지 목록의 단일 출처 (여러 문서가 이 경로 참조) | 참조 갱신 |
**3번이 가장 크다** — 드리프트 탐지는 "카탈로그가 가리키는 이미지" 를 기준으로 재는 것이 설계의
핵심인데 이미지 레포는 카탈로그를 갖고 있지 않다. 그래서 **탐지는 카탈로그, 빌드는 이미지 레포**
로 갈라야 한다.
**게이트를 공유할 때 `workflow_call` 은 쓸 수 없다** — 별도 job 으로 돌아 파일시스템을 공유하지
않는데 게이트는 스캔과 **같은 job 에서 `$OUT_DIR` 를 읽어야** 한다. composite action 이어야 한다.
참고로 `SBOM_PIPELINE_IMAGE` 는 도구만 담고 있어(`Dockerfile``COPY` 가 없다) 그것만으로는
`cve-gate.py` 가 따라오지 않는다.
## 두 가지 유형 (둘 다 같은 스크립트를 쓴다)
| 유형 | Dockerfile 이 하는 일 | 셸 유무 |