Files
service-catalog/doc/architecture.md
T
wbsong111 f5b8e4b91f 자체 빌드 드리프트 스캔·재빌드 트리거를 제거하고 CI 파이프라인을 정리한다
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>
2026-08-26 09:48:48 +09:00

9.0 KiB

CI 파이프라인 아키텍처

이 문서는 .github/workflows/에 있는 3개 워크플로가 무엇을 트리거로 도는지, 잡 흐름이 어떻게 이어지는지를 한눈에 보여준다. 각 워크플로 내부 메커니즘(스캔 방식, 게이트 판정 로직 등)의 단일 출처는 별도 문서다 — 여기서 복제하지 않는다. 스타일은 별도 레포 hardened-containersdocs/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-containersrescan.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 직접 오픈"]

세 갈래를 눈여겨볼 것:

  1. sbom.yml만 PR에 반응한다. cve-edge-post.yml·catalog-tag-update.yml은 스케줄·수동뿐이다.
  2. 레포 경계를 넘는 방향은 하나뿐이다 — pull. catalog-tag-update.ymlpublished.json을 폴링해서 가져올 뿐, 자체 빌드가 언제 도는지는 이 레포가 전혀 모른다 — hardened-containersrescan.yml이 전적으로 자율 결정하고, 이 레포는 어느 쪽으로도 그 레포를 트리거하지 않는다.
  3. 끝은 항상 사람이다. 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.shcustom-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.pygate == "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가 단일 출처다 — 이 문서는 그 파이프라인의 트리거·출력 지점만 다룬다.