운영 서버가 이미지를 다시 만들고 있었다
CI를 통과한 원본을 OCI에서 다시 checkout하고 Docker 이미지로 만들면 CI가 검증한 이미지를 배포하는 것이 아니다. 같은 원본으로 새 이미지를 만드는 것이다.
운영 서버에서 직접 build하는 구조에는 다음 문제가 있었다.
- 작은 OCI VM의 CPU와 메모리, 디스크 I/O를 배포에 사용한다.
- Checkout 상태와 호스트 도구 체인이 결과에 개입한다.
- 같은 commit을 다시 만들어도 이전 이미지를 가리킬 고정 식별자가 없다.
- 롤백할 때도 알려진 이미지 대신 이전 원본을 다시 빌드할 수 있다.
CI가 검증한 리비전과 운영 서버가 실제로 만든 결과가 같은지도 별도로 증명해야 했다. 그래서 GitHub에서 운영 아키텍처에 맞는 이미지를 한 번 만들고 GHCR에 보관했다. OCI는 원본 대신 정확한 digest를 가져온다.
Backend CI 통과
-> 검증된 commit SHA
-> Native ARM64 runner에서 build
-> GHCR에 publish
-> 변경 불가능한 digest 생성
-> OCI는 digest로 pull성공한 CI 실행만 이미지를 만든다
Backend Image Publish는 push 자체가 아니라 Backend CI의 workflow_run 완료 이벤트를 받는다. 다음
조건을 모두 만족해야 게시한다.
- CI conclusion이
success인가 - CI를 시작한 event가
push인가 - 대상 branch가
main인가 - CI run의 repository가 현재 repository와 같은가
Pull request와 다른 branch, fork, 실패한 CI는 운영 후보 이미지를 만들지 않는다. Git push가 곧바로 이미지가 되는 것이 아니라 CI가 검증 관문이 된다.
최신 main 대신 검증된 SHA를 사용했다
CI가 끝난 뒤 게시가 시작되기 전에 main에 새 commit이 추가될 수 있다. 단순히 main을 checkout하면
검증 대상과 빌드 대상이 달라진다.
Publish workflow는 workflow_run.head_sha를 TARGET_SHA로 사용한다. 해당 full SHA를 checkout하고
git rev-parse HEAD도 같은 값인지 확인한다. 이 SHA는 이미지 tag와 OCI label에도 들어가 GitHub run과
원본, 컨테이너를 연결한다.
운영과 같은 ARM64에서 만들었다
운영 OCI는 ARM64이므로 image도 linux/arm64여야 한다. OCI에서 직접 build하거나 x86 runner에서 QEMU를
사용할 수도 있었지만, 당시에는 x86와 ARM을 함께 지원할 필요가 없었다.
GitHub-hosted ubuntu-24.04-arm을 선택해 빌드와 실행 아키텍처를 맞췄다. QEMU 설정과 self-hosted
실행기 운영을 피하고, 운영 VM의 자원도 사용하지 않았다.
네이티브 실행기라고 아키텍처가 자동으로 보장되지는 않는다. 게시한 이미지를 다시 가져와 OS가 linux,
아키텍처가 arm64인지 검사한다.
SHA tag와 digest의 역할을 나눴다
이미지에는 sha-<full commit SHA> tag를 붙인다. 사람이 commit에서 registry의 이미지를 찾기 위한
추적 키다.
실제 배포 identity는 다음 digest reference다.
ghcr.io/<owner>/<image>@sha256:<digest>| 값 | 역할 |
|---|---|
| Full SHA tag | 원본 리비전에서 이미지를 찾기 위한 별칭 |
| SHA-256 digest | 실제 이미지 내용을 고정하는 배포 식별자 |
Tag가 다른 manifest를 가리키도록 바뀔 수 있는 것과 달리 같은 digest는 같은 이미지 내용을 뜻한다.
current와 previous 릴리스를 기록하거나 롤백 대상을 고를 때는 digest가 필요하다.
게시한 이미지를 다시 검사했다
빌드 성공만 믿지 않고 SHA tag로 이미지를 다시 가져와 다음 조건을 확인한다.
- RepoDigest가
sha256:뒤 64자리 hex인가 - OS와 architecture가
linux/arm64인가 org.opencontainers.image.revision이TARGET_SHA인가org.opencontainers.image.source가 기대한 저장소인가- 새 build의 digest가 BuildKit output digest와 같은가
GitHub Actions cache는 layer 재사용을 위한 최적화일 뿐, SHA checkout과 digest 검증을 대신하지 않는다.
GHCR에는 저장소 콘텐츠 읽기와 패키지 쓰기 권한으로 게시했다. 당시 패키지는 public으로 운영했고 OCI에서 별도 자격 증명 없이 digest를 가져올 수 있는지도 확인했다. 이미지를 공개하되 배포 trigger와 호스트 권한은 별도로 보호한 선택이며, 모든 registry를 공개해야 한다는 일반 원칙은 아니다.
최초 이미지의 식별자를 확인했다
최초 성공 run 31614836296은 server commit
894b5b134556cdfb83406e075827945e6d753935로 다음 digest를 만들었다.
sha256:c679a3eed7989711932cd2cec32847afec5c916e89bb2db3a4c6bc5fe36a256f전체 리비전 tag와 linux/arm64, 리비전·원본 label, 정확한 digest, 인증 없는 public pull을 확인했다.
이후 run 31769258098은 2026년 8월 14일 04:14:37 UTC에 시작해 04:15:05 UTC에 끝났고 28초가 걸렸다.
직전 CI 시작부터 publish 완료까지는 약 3분 27초였다. 최초 실행 약 3분과 cache가 적용된 28초는 각각의
실행기와 네트워크 조건에서 나온 관찰값이며 성능 보장이나 일반적인 개선률이 아니다.
운영 빌드를 없앤 대신 의존성이 생겼다
이미지 게시는 GitHub-hosted ARM64 실행기와 GHCR 가용성에 의존한다. 패키지 권한과 공개 범위, 보존 정책을 관리해야 하고 ARM64 전용 이미지는 x86 호스트에서 실행할 수 없다. CI 검증용 빌드와 운영 이미지 빌드가 나뉘어 비용도 중복된다.
또한 digest가 생겼다고 안전한 배포까지 끝나는 것은 아니다. current와 previous 릴리스 상태, 이미지
롤백과 데이터베이스 마이그레이션 호환성은 별도 문제다. 이미지 롤백은 스키마 롤백을 뜻하지 않는다.
이 단계에서 얻은 결과는 운영에 올릴 수 있는 후보 이미지였다. 검증한 원본과 연결되고 아키텍처가 맞으며, 이후 어느 시점에도 같은 내용을 가리키는 digest를 만든 것이 핵심이었다.