# CI 파이프라인 아키텍처 이 문서는 `.github/workflows/`에 있는 3개 워크플로가 **무엇을 트리거로 도는지**, **잡 흐름이 어떻게 이어지는지**를 한눈에 보여준다. 각 워크플로 내부 메커니즘(스캔 방식, 게이트 판정 로직 등)의 단일 출처는 별도 문서다 — 여기서 복제하지 않는다. 스타일은 별도 레포 `hardened-containers`의 `docs/architecture.md`를 참고했다. ## 1. 두 축, 세 워크플로 카탈로그는 "무엇을 배포 중인가"(차트 카탈로그 축)와 "그 이미지를 어떻게 만드는가"(자체 빌드 축)를 분리해서 다룬다 — 축·소유 레포·게이트 강제력 비교는 [CLAUDE.md](../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](decisions/0005-self-build-drift-check-removed.md). ## 2. 트리거 조건 → Job 흐름 ```mermaid flowchart TD PR["pull_request
manifests/helm/** 변경"] WD1["workflow_dispatch
chart, limit"] SCH1["schedule
매주 일 18:00 UTC"] subgraph SBOM["sbom.yml — 차트 카탈로그 SBOM/CVE 스캔 (자체 빌드 이미지 포함)"] S1[이미지 인벤토리 추출] S2["PR: diff 증분만 대상
스케줄·수동: 전체 또는 지정 차트"] S3[SBOM 생성 CycloneDX] S4[취약점 스캔] S5["CVE 게이트 판정
warn-only — 실패해도 PR 안 막음"] S6[아티팩트 업로드 + Job Summary] S1 --> S2 --> S3 --> S4 --> S5 --> S6 end PR --> S1 WD1 --> S1 SCH1 --> S1 WD2["workflow_dispatch
chart, limit
(스케줄 없음 — 주석 처리)"] subgraph EDGE["cve-edge-post.yml — 외부 집계 전송"] V1[이미지 인벤토리 추출] V2[SBOM 생성 + 취약점 스캔] V3[CVE JSON 집계] V4["POST edge.gke.paasup.io
승인 예외·실효 등급 미적용"] V1 --> V2 --> V3 --> V4 end WD2 --> V1 subgraph HC["hardened-containers — 다른 레포, 독립 루프 (이 레포는 트리거하지 않는다)"] HR1["rescan.yml
매일 자율 재스캔"] HR2{드리프트 있음?} HB1[build-image.yml
빌드→검증→SBOM→스캔→게이트] HB2["PASS → 레지스트리 push + attest
+ published.json 갱신"] HR1 --> HR2 HR2 -->|예| HB1 HB1 --> HB2 end WD4["workflow_dispatch"] SCH4["schedule
매일 03:00 UTC"] subgraph SYNC["catalog-tag-update.yml — 발행 태그 카탈로그 반영"] C1[published.json 조회] C2["반영 대상 계산
apply-published-tags.py"] C3{변경 있음?} C4["고정 브랜치로 push
+ compare 링크 Job Summary"] C5[변경 없음 — 종료] C1 --> C2 --> C3 C3 -->|예| C4 C3 -->|아니오| C5 end WD4 --> C1 SCH4 --> C1 HB2 -.->|"public raw URL 폴링
인증 없음, pull 방식"| C1 C4 -.->|"CI는 GITHUB_TOKEN으로 PR 생성 불가
(조직 정책, 실측 확정)"| HumanPR["사람이 compare 링크로
PR 직접 오픈"] ``` 세 갈래를 눈여겨볼 것: 1. **`sbom.yml`만 PR에 반응한다.** `cve-edge-post.yml`·`catalog-tag-update.yml`은 스케줄·수동뿐이다. 2. **레포 경계를 넘는 방향은 하나뿐이다 — pull.** `catalog-tag-update.yml`이 `published.json`을 폴링해서 가져올 뿐, 자체 빌드가 언제 도는지는 이 레포가 전혀 모른다 — `hardened-containers`의 `rescan.yml`이 전적으로 자율 결정하고, 이 레포는 어느 쪽으로도 그 레포를 트리거하지 않는다. 3. **끝은 항상 사람이다.** `catalog-tag-update.yml`이 브랜치까지는 push하지만 PR은 열지 않는다 — CI가 `GITHUB_TOKEN`으로 PR을 못 여는 게 조직 정책으로 실측 확정된 제약이라서다 (완화 방법은 [MEMORY.md](../MEMORY.md), [.claude/skills/cve-remediation/SKILL.md](../.claude/skills/cve-remediation/SKILL.md) 참고). ## 3. 워크플로별 상세 ### `sbom.yml` — 차트 카탈로그 SBOM/CVE 스캔 (warn-only) 메커니즘 상세(스캔 대상 결정, `cve-gate.py` 판정 로직, 아티팩트 구조, trivy DB 캐싱· concurrency)의 단일 출처는 [doc/sbom-pipeline.md](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](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](../CLAUDE.md) "CVE/SBOM 게이트 작업" - 차트 카탈로그 축 스캔 메커니즘·CI 위생(캐싱·concurrency) 상세: [doc/sbom-pipeline.md](sbom-pipeline.md) - 자체 빌드 축 이관 배경·계약: [doc/migrations/](migrations/) - 자체 빌드 드리프트 전용 워크플로를 없앤 근거: ADR [0005](decisions/0005-self-build-drift-check-removed.md) - 차단 CVE 대응 시 레버 판단: [.claude/skills/cve-remediation/SKILL.md](../.claude/skills/cve-remediation/SKILL.md) - `hardened-containers` 자체 파이프라인(빌드→검증→SBOM→스캔→게이트→push, 매일 자율 재스캔 `rescan.yml`) 상세는 그 레포의 `docs/architecture.md`가 단일 출처다 — 이 문서는 그 파이프라인의 트리거·출력 지점만 다룬다.