--- title: CI와 Supply Chain을 Skeleton 계약으로 묶기 source_type: blog status: draft confidence: unknown tags: [blog, ca-tmpl, ci-cd, gradle, supply-chain, reproducible-builds] related_projects: [ca-tmpl] last_reviewed: canonical_sources: [] audience: backend-engineer target_publish: status_label: draft --- # CI와 Supply Chain을 Skeleton 계약으로 묶기 > CI에 도구를 붙이는 건 쉽습니다. 어려운 건 "이게 실패하면 누가 책임지는가"를 정하는 일입니다. ## TL;DR - CI 파이프라인에 formatter, linter, scanner, SBOM, signing을 순서대로 추가하는 건 어렵지 않지만, **어떤 gate가 release를 막는지, 실패하면 누가 고치는지**를 정해두지 않으면 CI는 금방 장식이 됩니다. - ca-tmpl은 이걸 `.github/ci-gate-matrix.yml`이라는 **gate ownership matrix**로 정리하고, 스크립트로 문서와 실제 workflow가 어긋나지 않는지 검사합니다. - Supply chain(SBOM, Cosign, SLSA)도 마찬가지로 workflow와 검증 스크립트가 **repo-level에서** 존재합니다. 다만 **실제 hosted CI에서 release를 발행하고 Rekor/GHCR로 검증한 경험은 아직 없습니다** — 이 경계를 이 글에서 분명히 하려 합니다. --- ## 1. CI는 도구 목록이 아니라 release 계약이다 CI를 설계할 때 흔한 실수는 "무엇을 실행할지"만 정하는 것입니다. formatter, linter, unit test, integration test, vulnerability scanner, SBOM, signing, provenance — 순서대로 추가하면 화면은 그럴듯해 보입니다. 하지만 실제로 더 중요한 질문은 따로 있습니다. - 이 검사가 실패하면 **release가 막히는가?** - **누가** 이 정책을 소유하는가? - 문서에 적힌 gate가 **실제 workflow에도 남아 있는가?** 이 세 질문에 답하지 못하면, CI는 시간이 지날수록 "돌아는 가는데 아무도 그 의미를 모르는" 상태가 됩니다. --- ## 2. Gate Matrix — 문서가 아니라 검사 대상 ca-tmpl의 `.github/ci-gate-matrix.yml`은 바로 이 질문에 답하기 위한 파일입니다. 각 gate는 다음 정보를 가집니다. ```yaml gates: - id: architecture-test release_blocking: true owner_branch: feature-architecture-enforcement-rules mechanism: contract-test ref: app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java runs_in: ci-quality-gates ``` - **release_blocking**: 이 검사가 실패하면 release가 막히는가 - **owner_branch**: 실패했을 때 누가 고쳐야 하는가 - **mechanism / ref**: 실제로 무엇으로 구현되어 있는가 여기서 중요한 지점은, **이 matrix가 그냥 참고 문서로 끝나지 않는다는 것**입니다. `verify-gate-matrix.sh`가 matrix의 각 row를 읽고, `mechanism`별로 실제 존재 여부를 확인합니다. ```bash # Cross-checks every row of .github/ci-gate-matrix.yml against reality: # gradle-custom-task -> a tasks.register('') exists # contract-test -> the test-class file exists under src/ # workflow-job -> the job id exists in .github/workflows/.yml ``` `gradle-custom-task`라고 적혀 있으면 실제로 `tasks.register('')`가 존재해야 하고, `contract-test`라고 적혀 있으면 그 test 클래스 파일이 실제로 있어야 하고, `workflow-job`이라고 적혀 있으면 workflow 안에 그 job id가 실제로 존재해야 합니다. 이렇게 하면 **"문서에는 gate가 있는데 실제 CI에서는 빠진 상태"**를 줄일 수 있습니다. 흔히 일어나는 일이죠 — 누군가 workflow를 리팩터링하면서 job 이름을 바꿨는데, 문서는 그대로 남아있는 경우요. --- ## 3. Gradle Baseline — Entropy를 줄이는 것과 증명하는 것은 다르다 Gradle 쪽 baseline도 여러 층으로 나뉩니다. `src/build.gradle`은 Spotless, Checkstyle, SpotBugs, ErrorProne을 subproject에 적용하고, dependency locking을 strict mode로 켭니다. ```groovy dependencyLocking { lockAllConfigurations() lockMode = LockMode.STRICT } tasks.withType(AbstractArchiveTask).configureEach { preserveFileTimestamps = false reproducibleFileOrder = true } ``` archive task는 timestamp, file order, permission을 고정해서 build artifact가 host 환경에 따라 덜 흔들리게 만듭니다. 여기서 짚어야 할 경계가 있습니다. **이건 production artifact reproducibility를 완전히 증명한다는 뜻이 아닙니다.** skeleton 단계에서 entropy source(빌드할 때마다 달라질 수 있는 요인)를 줄이는 baseline일 뿐이에요. "재현 가능한 빌드를 만들었다"와 "재현 가능한 빌드의 조건 몇 가지를 미리 고정해뒀다"는 다른 문장입니다. --- ## 4. Supply Chain — 증거를 digest 중심으로 엮기 Supply chain 쪽은 release evidence를 digest 중심으로 묶으려는 방향입니다. `.github/workflows/build-release-supply-chain.yml`에는 SBOM generation, Trivy image scan, Cosign sign/attest, SLSA provenance, verification, promotion step이 순서대로 들어 있습니다. `.github/scripts/verify-supply-chain-contract.sh`는 workflow와 policy 파일에 필요한 문자열과 job wiring이 실제로 남아 있는지 확인합니다. ```bash require_fixed "${workflow}" 'cosign sign --yes' 'D6 keyless image signature' require_fixed "${workflow}" 'cosign attest --yes' 'D4 digest-bound SBOM attestation' require_fixed "${workflow}" '--certificate-identity' 'D12 signer identity verification' require_fixed "${workflow}" '--certificate-oidc-issuer' 'D12 OIDC issuer verification' require_fixed "${workflow}" 'generator_container_slsa3.yml@v2.1.0' 'D7 isolated SLSA generator' ``` Cosign의 keyless 서명, digest에 바인딩된 SBOM attestation, signer identity 검증, OIDC issuer 검증, 그리고 격리된 SLSA generator 사용까지 — 이런 조건들이 workflow에서 실제로 지켜지고 있는지를 스크립트가 확인합니다. ### 여기서 가장 중요한 경계선 **ca-tmpl에 workflow와 검증 스크립트가 존재한다는 사실**과, **실제 release artifact가 public registry에 올라갔고 Rekor/GHCR evidence로 검증됐다는 사실**은 다릅니다. project canonical은 후자를 확인하지 않았다고 명시적으로 밝힙니다. 그래서 이 글은 **"supply-chain release를 운영했다"가 아니라 "supply-chain release contract를 repo-level workflow와 script로 고정했다"**까지만 말할 수 있습니다. 이 구분이 왜 중요하냐면, "Cosign이랑 SLSA를 붙였어요"라는 말만 들으면 이미 실제 release에서 검증까지 끝난 것처럼 들리기 쉽거든요. 하지만 workflow 파일이 존재하는 것과, 그 workflow가 실제로 몇 번 돌아서 서명된 아티팩트가 검증된 것은 완전히 다른 단계의 증거입니다. --- ## 5. DX — 진입점을 하나로 줄이기 DX(Developer Experience)도 같은 관점으로 다룹니다. `./gradlew bootstrap` 명령 하나가 compile, Docker preflight, dependency/migration/startup, sample contract, HTTP smoke test까지를 하나의 진입점으로 묶습니다. 이 명령이 **모든 OS와 CI 환경을 보장한다는 뜻은 아닙니다.** 대신 새 프로젝트를 받은 사람이 "무엇부터 실행해야 하나"를 덜 고민하게 만들고, 실패 지점을 단계별로 나눠서 보여주려는 목적입니다. --- ## 6. 정리 — "도구를 썼다"가 아니라 "어떤 증거가 release를 통과시키는가" 결국 ca-tmpl의 DevOps baseline은 "이 도구를 썼다"보다 **"어떤 증거가 release를 통과시키는가"**에 가깝습니다. - gate matrix가 실제 workflow와 어긋나지 않아야 하고 - dependency lock이 조용히 풀리면 안 되며 - vulnerability suppression은 사유와 만료일 없이 남아있으면 안 됩니다 이 정도가 **local/repo-level에서 검증된 범위**입니다. 실제 hosted CI run, release publication, live Cosign/Rekor 검증은 별도의 근거가 쌓인 뒤에만 말할 수 있는 다음 단계로 남겨둡니다.