hardened-containers가 이미 rescan.yml로 매일 자율 재스캔·재빌드하고, sbom.yml이 custom-values.yaml 기준으로 자체 빌드 이미지를 다른 카탈로그 이미지와 동일하게 스캔하고 있어 self-build-drift-check.yml의 트리거·전용 스캔이 순수 중복이었다 (SECURITY_IMAGES_DISPATCH_TOKEN도 등록된 적 없어 트리거 스텝은 항상 실패하던 죽은 코드). check-rebuild-needed.py가 더하던 fixable/no-fix 구분도 cve-gate.py 리포트에 이미 있어 흡수할 필요 없이 삭제했다. 근거는 ADR 0005. 곁들여 CI 위생 문제(trivy DB 캐시 없음, concurrency 없음, catalog-tag-update.yml의 브랜치 누적)를 함께 고치고, hardened-containers의 docs/image-authoring.md가 docs/image-authoring/ 로 분할된 것을 반영해 관련 링크를 정정했다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
9.0 KiB
CI 파이프라인 아키텍처
이 문서는 .github/workflows/에 있는 3개 워크플로가 무엇을 트리거로 도는지, 잡 흐름이
어떻게 이어지는지를 한눈에 보여준다. 각 워크플로 내부 메커니즘(스캔 방식, 게이트 판정
로직 등)의 단일 출처는 별도 문서다 — 여기서 복제하지 않는다. 스타일은 별도 레포
hardened-containers의 docs/architecture.md를 참고했다.
1. 두 축, 세 워크플로
카탈로그는 "무엇을 배포 중인가"(차트 카탈로그 축)와 "그 이미지를 어떻게 만드는가"(자체 빌드 축)를 분리해서 다룬다 — 축·소유 레포·게이트 강제력 비교는 CLAUDE.md의 "CVE/SBOM 게이트 작업" 표 참고. 이 레포에 있는 워크플로 3개는 그 두 축 위에 이렇게 나뉜다.
| 워크플로 | 축 | 트리거 | 게이트 |
|---|---|---|---|
sbom.yml |
차트 카탈로그(자체 빌드 이미지 포함) | pull_request(manifests/helm/**) · workflow_dispatch · 매주 일 18:00 UTC |
warn-only |
cve-edge-post.yml |
차트 카탈로그 | workflow_dispatch만 (스케줄 주석 처리됨) |
없음 — 원시 집계 POST |
catalog-tag-update.yml |
자체 빌드 | workflow_dispatch · 매일 03:00 UTC |
없음 — 반영만 |
PR 이벤트에 반응하는 워크플로는 sbom.yml 하나뿐이다. 나머지 두 개는 사람이 Actions
탭에서 수동 실행하거나 스케줄로만 돈다.
자체 빌드 이미지의 CVE 드리프트 스캔·재빌드 트리거를 위한 전용 워크플로 (
self-build-drift-check.yml)는 없다 —sbom.yml이 자체 빌드 이미지도 다른 카탈로그 이미지와 동일하게 스캔·보고하고, 재빌드는 전적으로hardened-containers의rescan.yml이 자율 수행한다. 근거는 ADR 0005.
2. 트리거 조건 → Job 흐름
flowchart TD
PR["pull_request<br/>manifests/helm/** 변경"]
WD1["workflow_dispatch<br/>chart, limit"]
SCH1["schedule<br/>매주 일 18:00 UTC"]
subgraph SBOM["sbom.yml — 차트 카탈로그 SBOM/CVE 스캔 (자체 빌드 이미지 포함)"]
S1[이미지 인벤토리 추출]
S2["PR: diff 증분만 대상<br/>스케줄·수동: 전체 또는 지정 차트"]
S3[SBOM 생성 CycloneDX]
S4[취약점 스캔]
S5["CVE 게이트 판정<br/>warn-only — 실패해도 PR 안 막음"]
S6[아티팩트 업로드 + Job Summary]
S1 --> S2 --> S3 --> S4 --> S5 --> S6
end
PR --> S1
WD1 --> S1
SCH1 --> S1
WD2["workflow_dispatch<br/>chart, limit<br/>(스케줄 없음 — 주석 처리)"]
subgraph EDGE["cve-edge-post.yml — 외부 집계 전송"]
V1[이미지 인벤토리 추출]
V2[SBOM 생성 + 취약점 스캔]
V3[CVE JSON 집계]
V4["POST edge.gke.paasup.io<br/>승인 예외·실효 등급 미적용"]
V1 --> V2 --> V3 --> V4
end
WD2 --> V1
subgraph HC["hardened-containers — 다른 레포, 독립 루프 (이 레포는 트리거하지 않는다)"]
HR1["rescan.yml<br/>매일 자율 재스캔"]
HR2{드리프트 있음?}
HB1[build-image.yml<br/>빌드→검증→SBOM→스캔→게이트]
HB2["PASS → 레지스트리 push + attest<br/>+ published.json 갱신"]
HR1 --> HR2
HR2 -->|예| HB1
HB1 --> HB2
end
WD4["workflow_dispatch"]
SCH4["schedule<br/>매일 03:00 UTC"]
subgraph SYNC["catalog-tag-update.yml — 발행 태그 카탈로그 반영"]
C1[published.json 조회]
C2["반영 대상 계산<br/>apply-published-tags.py"]
C3{변경 있음?}
C4["고정 브랜치로 push<br/>+ compare 링크 Job Summary"]
C5[변경 없음 — 종료]
C1 --> C2 --> C3
C3 -->|예| C4
C3 -->|아니오| C5
end
WD4 --> C1
SCH4 --> C1
HB2 -.->|"public raw URL 폴링<br/>인증 없음, pull 방식"| C1
C4 -.->|"CI는 GITHUB_TOKEN으로 PR 생성 불가<br/>(조직 정책, 실측 확정)"| HumanPR["사람이 compare 링크로<br/>PR 직접 오픈"]
세 갈래를 눈여겨볼 것:
sbom.yml만 PR에 반응한다.cve-edge-post.yml·catalog-tag-update.yml은 스케줄·수동뿐이다.- 레포 경계를 넘는 방향은 하나뿐이다 — pull.
catalog-tag-update.yml이published.json을 폴링해서 가져올 뿐, 자체 빌드가 언제 도는지는 이 레포가 전혀 모른다 —hardened-containers의rescan.yml이 전적으로 자율 결정하고, 이 레포는 어느 쪽으로도 그 레포를 트리거하지 않는다. - 끝은 항상 사람이다.
catalog-tag-update.yml이 브랜치까지는 push하지만 PR은 열지 않는다 — CI가GITHUB_TOKEN으로 PR을 못 여는 게 조직 정책으로 실측 확정된 제약이라서다 (완화 방법은 MEMORY.md, .claude/skills/cve-remediation/SKILL.md 참고).
3. 워크플로별 상세
sbom.yml — 차트 카탈로그 SBOM/CVE 스캔 (warn-only)
메커니즘 상세(스캔 대상 결정, cve-gate.py 판정 로직, 아티팩트 구조, trivy DB 캐싱·
concurrency)의 단일 출처는 doc/sbom-pipeline.md다. 여기서는 트리거·잡
경계만 짚는다.
- PR에서는
git diff로 변경된 차트만 골라 스캔 범위를 줄인다(전체 스캔은 무겁다). - 스케줄·수동 실행은 전체(또는
chart입력으로 지정한 하나)를 스캔한다. - 자체 빌드 이미지(
docker.io/paasup/*)도 같은 대상이다 —extract-helm-images.sh가custom-values.yaml을 적용해 렌더하므로 별도 취급 없이 그대로 스캔된다. 자체 빌드 이미지 CVE 드리프트를 따로 보는 워크플로는 없다 — 이 리포트가 유일한 경로다. - 실행 컨테이너(
vars.SBOM_PIPELINE_IMAGE)에 helm/trivy/python3/bash/git이 없으면 preflight 스텝에서 즉시 실패한다. - 게이트가 실패해도(
--warn-only) 워크플로 자체는 성공으로 끝난다 — Job Summary와 아티팩트로만 결과를 남긴다.
cve-edge-post.yml — 외부 집계 전송
sbom.yml과 이미지 인벤토리·SBOM·스캔 스텝을 그대로 재사용하지만, 게이트 판정 (cve-gate.py)을 부르지 않는다 — 승인 예외·실효 등급(max(벤더,NVD))이 적용 안 된 원시 집계라는 뜻이다.sbom.yml의 결과와 CRITICAL 건수가 다르게 보일 수 있다.- 결과는
(catalog, version, image)조합 단위 JSON으로 만들어져https://edge.gke.paasup.io/api/v1/cve-scans로 POST된다(CVE_API_KEY헤더 인증, TLS 검증은--insecure로 스킵). - 스케줄이 주석 처리돼 있어 현재는 수동 실행만 된다.
catalog-tag-update.yml — 발행 태그 카탈로그 반영
hardened-containers는 이 카탈로그의 존재를 모른다 — 게이트 PASS + push가 실제로 일어났을 때만 자기 레포의published.json을 갱신해둘 뿐이다. 이 워크플로가 그 파일을 public raw URL로 폴링해서 가져온다(인증 불필요, 상세 계약은 doc/migrations/self-build-images-to-hardened-containers.md). 역방향 알림(repository_dispatch등)은 의도적으로 안 쓴다 — 그러려면hardened-containers쪽에 이 카탈로그 쓰기 권한 PAT을 둬야 하는데, 그 레포의 "외부 의존성 없이 단독 동작" 원칙과 충돌하기 때문이다.scripts/build/apply-published-tags.py가gate == "pass"인 이미지만 골라custom-values.yaml/dip-values.yaml을 패치한다(patch-catalog-tag.py를 내부에서 호출) — 실패한 빌드의 태그가 반영되는 일은 없다.- 변경이 있으면 고정 브랜치(
build/catalog-tag-update)로 force push까지만 하고 멈춘다 — 매 실행 새 브랜치를 만들지 않으므로 방치된 동일 내용 브랜치가 쌓이지 않는다. PR은 자동으로 열리지 않는다 — Job Summary의 compare 링크를 보고 사람이 직접 연다.
4. 참고
- 두 축 개념·소유 레포·문서 단일 출처 매핑: CLAUDE.md "CVE/SBOM 게이트 작업"
- 차트 카탈로그 축 스캔 메커니즘·CI 위생(캐싱·concurrency) 상세: doc/sbom-pipeline.md
- 자체 빌드 축 이관 배경·계약: doc/migrations/
- 자체 빌드 드리프트 전용 워크플로를 없앤 근거: ADR 0005
- 차단 CVE 대응 시 레버 판단: .claude/skills/cve-remediation/SKILL.md
hardened-containers자체 파이프라인(빌드→검증→SBOM→스캔→게이트→push, 매일 자율 재스캔rescan.yml) 상세는 그 레포의docs/architecture.md가 단일 출처다 — 이 문서는 그 파이프라인의 트리거·출력 지점만 다룬다.