Kaniko가 archived된 뒤, 우리는 어떻게 컨테이너 빌드 도구를 골랐나
작년쯤만 해도 우리 팀 CI 파이프라인의 절반은 Kaniko로 굴러갔다. EKS 안에서 in-cluster 빌드를 돌리는 게 너무 깔끔했기 때문에. 근데 2025년 6월 3일에 GoogleContainerTools/kaniko 리포가 read-only로 archived 돼 버렸다. 그 다음 주에 사내 보안팀에서 "유지보수 안 되는 OSS는 점진적으로 걷어내자"는 공지가 떴고, 우리도 슬슬 다른 길을 찾기 시작했다.
이 글은 그 과정에서 Buildx, Kaniko(Chainguard fork 포함), ko 세 가지를 다 돌려보고 결정한 이야기다. "이게 정답이다" 같은 결론은 없다. 워크로드에 따라 답이 갈렸다.
우리가 쓰던 환경
먼저 맥락을 좀 깔자. 우리 팀 빌드 워크로드는 대략 60개의 서비스 이미지인데, 분포가 좀 특이하다.
- Go 백엔드: 40개 (대부분 단일 바이너리, distroless 기반)
- Node.js: 12개 (Next.js, 일반 API)
- Python: 6개 (배치, ML 추론)
- 기타 (Java, Rust): 2개
CI는 GitHub Actions 위에 ARC(Actions Runner Controller)로 self-hosted runner를 EKS에 띄워 쓴다. 이전에는 모든 빌드가 Kaniko Pod에서 돌았다. 노드에 Docker daemon 없이도 root 권한 없이 이미지를 만들 수 있다는 게 매력적이었으니까.
그런데 Kaniko archived 이후 우리가 가진 선택지는 결국 셋이었다. 첫째, Chainguard fork(chainguard-dev/kaniko)로 갈아타기. 둘째, BuildKit 기반의 Buildx로 옮기되 rootless로 운영하기. 셋째, Go가 압도적으로 많으니 그 부분만이라도 ko로 빼기.
세 도구를 다 PoC로 굴려봤다. 같은 이미지(Go gRPC 서버, 약 12MB 바이너리 + distroless)를 빌드해서 비교했더니 결과가 이렇게 나왔다.
ko (Go) : 18초 (캐시 적중 시 6초)
Buildx + cache-to=registry : 1분 12초 (캐시 적중 시 11초)
Chainguard Kaniko : 2분 40초 (캐시 적중 시 1분 5초)
캐시 미적중 기준으로 ko가 압도적이다. 근데 ko는 Go만 된다. 그래서 단순 속도 비교만으로 결정할 수 없었다.
Buildx, 결국 대부분의 경우 정답
솔직히 Buildx는 옛날 Docker build의 인상이 너무 강해서 처음엔 좀 꺼렸다. "EKS 안에서 Docker daemon을 어떻게 돌리지?"라는 게 첫 반응이었다. 근데 요즘 Buildx는 그런 고민이 없다. docker buildx create --driver kubernetes로 빌더를 띄우면 BuildKit Pod 자체가 클러스터 안에 돌아가고, CI 잡은 그냥 그 빌더에 빌드 요청만 던진다. rootless 모드도 있고, 멀티 플랫폼 빌드(arm64/amd64)도 한 번에 된다.
가장 좋았던 부분은 캐시였다. Kaniko도 registry 기반 캐시를 지원하긴 하는데, BuildKit의 inline cache + cache-to=type=registry,mode=max가 훨씬 똑똑하게 동작한다. 특히 모노레포에서 변경된 디렉토리만 다시 빌드되는 케이스가 많아졌는데, BuildKit의 dependency analysis가 그걸 잘 잡아낸다.
Buildx의 약점은 한 가지였다. BuildKit Pod가 빌더 풀처럼 동작하기 때문에 여러 잡이 동시에 들어오면 큰 컨테이너 빌드(예: 우리 ML 이미지 4.2GB)가 다른 잡을 느리게 만든다. 우리는 이걸 ML/배치용 별도 BuildKit 빌더 풀을 따로 띄워서 해결했다. 이 구분이 좀 귀찮긴 한데, Kaniko처럼 매번 Pod를 새로 띄우는 것보다는 자원 효율이 훨씬 좋았다.
Kaniko, Chainguard fork에 남길 만한 가치가 있나
Chainguard가 chainguard-dev/kaniko를 fork해서 유지보수를 이어가는 건 다행이지만, Chainguard 측 발표에서도 "major feature work은 없을 것"이라고 못 박았다. 의존성 업데이트와 CVE 패치 중심으로만 가겠다는 얘기다.
이게 무슨 의미냐면, Kaniko의 가장 큰 가치였던 "Dockerfile을 root 권한 없이, Docker daemon 없이 빌드한다"는 점은 그대로 살아남지만, BuildKit이 이미 가지고 있는 새로운 기능들(예: --mount=type=secret의 더 안전한 처리, BuildKit frontend 확장, 더 빠른 캐시 알고리즘)은 절대 따라잡지 못한다는 뜻이다.
그럼에도 Kaniko를 완전히 버리지 못한 이유가 있다. 우리 회사 일부 자회사 환경은 정책상 Docker daemon이나 BuildKit Pod 같은 "장기 실행 빌더"를 못 띄운다. CI 잡이 끝나면 모든 리소스가 정리돼야 하는 단발성 환경이다. 그런 곳에서는 Kaniko처럼 Pod 하나에서 빌드를 완결 짓는 방식이 여전히 깔끔하다.
결론적으로 우리는 Kaniko를 60개 중 6개 워크로드에만 남겼다. 그것도 Chainguard fork 이미지를 쓴다. 나머지는 다 걷어냈다.
ko를 한 번 써보면 못 돌아간다
ko는 Go에만 쓸 수 있다. 그게 단점이 아니라 장점이다. Dockerfile이 아예 없다. KO_DOCKER_REPO=...; ko build ./cmd/api만 치면 끝이다. 내부적으로는 그냥 go build로 바이너리를 만들고 distroless base 위에 한 레이어만 얹어서 이미지를 만든다.
처음 Go 서비스 40개 중 5개로 시범 적용했을 때 가장 놀란 부분은 빌드 시간이 아니라 "신경 쓸 게 사라졌다"는 점이었다. Dockerfile 리뷰가 없다. 멀티스테이지 빌드 캐시 깨질 일도 없다. SBOM은 기본 생성되고, 이미지 sign(cosign 연동)도 한 줄로 끝난다. 멀티 아키텍처 빌드는 --platform=linux/amd64,linux/arm64만 붙이면 된다.
물론 한계도 있다. cgo 쓰는 코드는 곤란하고, OS 패키지가 필요하면 base 이미지를 따로 만들어 줘야 한다(ko.local 같은 매핑으로 해결 가능하지만 손이 좀 간다). 그래서 우리 Go 서비스 40개 중에서도 cgo로 sqlite 쓰는 5개 정도는 ko에서 빼고 Buildx로 돌린다.
근데 이 5개를 제외한 나머지 35개가 ko로 옮겨가면서 CI 비용이 눈에 띄게 줄었다. Buildx 빌더 풀이 ML 워크로드 전용으로 한가해진 덕분에 BuildKit Pod도 한 사이즈 줄였다. 월간 빌드 시간 합계로 보면 약 60% 감소했다.
그래서 우리는 어떻게 갈랐나
지금 우리 팀이 정착시킨 구도는 이렇다.
- Go 서비스 35개 → ko (cgo 없는 것들)
- Go 서비스 5개 → Buildx (cgo 있음)
- Node.js / Python / Java / Rust 14개 → Buildx
- 자회사 단발성 빌드 환경 6개 → Chainguard Kaniko
Buildx가 메인이고, ko는 Go 전용 가속기, Kaniko는 정책 제약 때문에 남아 있는 잔재에 가깝다. 만약 우리가 Go 비중이 적었다면 ko는 안 들였을 거고, 자회사 정책이 풀린다면 Kaniko도 결국 사라질 것 같다.
도구를 고를 때 가장 도움 됐던 질문은 "Dockerfile을 직접 쓰는 게 우리 팀에 가치가 있는가?"였다. Go처럼 빌드 결과가 단순한 언어는 Dockerfile이 그냥 보일러플레이트다. 그런 데는 ko 같은 도구가 어울린다. 반면 멀티스테이지 빌드로 빌드 의존성을 격리해야 하는 언어는 Buildx의 캐시 메커니즘이 빛난다.
혹시 다른 팀에서 비슷한 마이그레이션 하시는 분 있으면, ko 도입 전후로 CI 빌드 큐 지표를 꼭 비교해 보세요. 우리도 처음엔 단순히 이미지 빌드 시간만 봤는데, 실제로는 빌더 큐 대기 시간이 더 크게 줄었습니다.