- Published on
Kaniko로 루트리스 멀티스테이지 빌드 60% 단축
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
CI에서 컨테이너 이미지를 빌드할 때 가장 흔한 병목은 docker build 자체가 아니라, 그 빌드를 가능하게 만들기 위한 런타임 권한과 캐시 전략입니다. Docker-in-Docker(dind)는 편하지만 보안상 특권 권한이 필요하거나, 러너 환경에 따라 네트워크/스토리지 오버헤드가 커져 빌드 시간이 들쭉날쭉해집니다.
Kaniko는 이런 상황에서 특히 강력합니다. 쿠버네티스나 일반 CI 러너에서 도커 데몬 없이 Dockerfile을 해석해 이미지를 만들고 레지스트리에 푸시합니다. 또한 캐시를 레지스트리에 저장할 수 있어, 멀티스테이지 빌드에서도 “의존성 단계”를 안정적으로 재사용할 수 있습니다.
이 글에서는 다음을 목표로 합니다.
- 루트 권한 없이(
rootless에 가까운 운영 형태) Kaniko로 이미지를 빌드/푸시하기 - 멀티스테이지 Dockerfile을 캐시 친화적으로 재구성하기
- 레지스트리 캐시를 활용해 CI 빌드 시간을 체감 60% 수준까지 단축하기
관련해서 CI 캐시가 예상대로 동작하지 않을 때의 디버깅 관점은 이 글도 함께 참고하면 좋습니다: GitHub Actions 캐시가 안 먹을 때 - key·dir 충돌 디버깅
왜 Kaniko가 “루트리스”에 유리한가
엄밀히 말해 Kaniko 자체가 완전한 의미의 rootless 빌더(예: BuildKit rootless 모드)와 동일하진 않습니다. 하지만 실무에서 우리가 원하는 건 대개 다음입니다.
- CI 러너에서
--privileged없이 빌드하기 dockerd를 띄우지 않기- 쿠버네티스에서 Pod Security 정책을 만족하기
Kaniko는 데몬이 없고, 컨테이너 내부 파일시스템에서 레이어를 구성해 푸시합니다. 즉, “도커 데몬을 위해 특권 권한을 주는 구조”를 제거하는 것만으로도 보안/운영 복잡도가 크게 줄고, 그 과정에서 빌드 시간 변동폭도 줄어듭니다.
60% 단축의 핵심: 멀티스테이지를 캐시 친화적으로 쪼개기
빌드가 느린 이유는 보통 다음 중 하나입니다.
- 의존성 설치 단계가 매번 다시 돈다(예:
npm ci,pip install,poetry install) - 소스 변경이 잦아 캐시가 쉽게 무효화된다
- 빌드 산출물이 큰데 불필요한 파일이 런타임 이미지에 포함된다
멀티스테이지는 3번을 해결하지만, 1번과 2번은 Dockerfile 작성 순서에 따라 오히려 더 악화될 수 있습니다. Kaniko에서 60%까지 줄이려면 “의존성 단계 캐시 적중률”을 올리는 게 핵심입니다.
예시 Dockerfile: Node.js(Next.js) 멀티스테이지
아래는 캐시를 최대한 살리는 구조입니다. 포인트는 package.json과 lockfile만 먼저 복사해서 의존성 레이어를 고정하는 것입니다.
# syntax=docker/dockerfile:1
FROM node:20-bookworm AS deps
WORKDIR /app
# 의존성 설치는 소스 전체가 아니라 lockfile 기반으로만 캐시되게
COPY package.json package-lock.json ./
RUN npm ci
FROM node:20-bookworm AS builder
WORKDIR /app
# deps 레이어를 재사용
COPY /app/node_modules ./node_modules
# 그 다음에 소스를 복사해야 캐시가 덜 깨짐
COPY . .
# 빌드 산출물 생성
RUN npm run build
FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
# 런타임에 필요한 것만 복사
COPY /app/package.json ./
COPY /app/.next ./.next
COPY /app/public ./public
COPY /app/node_modules ./node_modules
EXPOSE 3000
CMD ["npm", "run", "start"]
이 구조에서 소스 변경이 잦아도 deps 단계는 lockfile이 바뀌지 않는 한 재사용됩니다. Kaniko 캐시가 제대로 붙으면 npm ci가 통째로 스킵되는 날이 많아지고, 여기서 시간이 크게 절약됩니다.
Kaniko 실행 옵션: 캐시를 “레지스트리”에 저장하라
Kaniko는 로컬 디스크 캐시도 가능하지만, CI 러너는 대개 매번 새로 뜨므로 로컬 캐시의 효용이 제한적입니다. 대신 레지스트리 캐시를 쓰면 러너가 바뀌어도 캐시가 유지됩니다.
핵심 플래그는 다음입니다.
--cache=true--cache-repo=...(캐시 레이어를 저장할 리포지토리)--cache-copy-layers=true(복사 단계 캐시에도 도움)--snapshot-mode=redo또는time(환경에 따라 성능/정확성 트레이드오프)
예시 커맨드:
/kaniko/executor \
--context "$PWD" \
--dockerfile Dockerfile \
--destination "ghcr.io/acme/myapp:${GITHUB_SHA}" \
--cache=true \
--cache-repo "ghcr.io/acme/myapp-cache" \
--cache-copy-layers=true \
--snapshot-mode=redo
여기서 --cache-repo를 앱 이미지와 분리하면 운영이 편합니다.
- 앱 이미지는 태그 정책이 엄격할 수 있음
- 캐시는 태그/GC 정책이 달라도 됨
GitHub Actions 예시: 루트 권한 없이 Kaniko로 빌드/푸시
GitHub Actions에서 Kaniko를 쓰는 대표 패턴은 “Kaniko 컨테이너를 액션 런너에서 실행”하는 방식입니다.
주의할 점은 config.json(Docker auth)을 Kaniko가 읽을 수 있는 경로에 놓는 것입니다. Kaniko는 기본적으로 /kaniko/.docker/config.json을 봅니다.
name: build
on:
push:
branches: ["main"]
jobs:
kaniko:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login for Kaniko (write config.json)
run: |
mkdir -p kaniko-docker
cat > kaniko-docker/config.json <<'EOF'
{
"auths": {
"ghcr.io": {
"auth": "${{ secrets.GHCR_AUTH_B64 }}"
}
}
}
EOF
- name: Build & Push with Kaniko
run: |
docker run --rm \
-v "$PWD":/workspace \
-v "$PWD/kaniko-docker":/kaniko/.docker \
gcr.io/kaniko-project/executor:v1.23.2 \
--context /workspace \
--dockerfile /workspace/Dockerfile \
--destination "ghcr.io/${{ github.repository }}:${{ github.sha }}" \
--cache=true \
--cache-repo "ghcr.io/${{ github.repository }}-cache" \
--cache-copy-layers=true \
--snapshot-mode=redo
GHCR_AUTH_B64는 username:token을 base64로 인코딩한 값입니다. 예:
echo -n "USERNAME:TOKEN" | base64
캐시가 “먹는지” 확인하는 방법
Kaniko 로그에서 다음 키워드를 확인합니다.
Using cached layer또는Found cached layer- 특정
RUN npm ci단계가 캐시로 대체되는지
캐시가 계속 미스난다면, 보통은 Dockerfile 레이어가 불안정한 게 원인입니다. 예를 들어 아래 패턴은 캐시를 쉽게 깨뜨립니다.
COPY . .가 의존성 설치보다 먼저 나옴- 빌드 과정에서 타임스탬프/환경에 따라 파일이 매번 달라짐
.dockerignore가 없어 불필요한 파일이 컨텍스트에 매번 포함됨
.dockerignore는 빌드 시간 단축의 숨은 1등 공신
Kaniko든 Docker든, 컨텍스트가 커지면 전송/스냅샷 비용이 증가합니다. 특히 모노레포나 프론트엔드 프로젝트에서 node_modules, .next/cache, dist 등이 컨텍스트에 섞이면 캐시 적중률이 떨어지고 빌드가 느려집니다.
# .dockerignore
node_modules
.next/cache
dist
build
coverage
.git
.github
*.log
.DS_Store
컨텍스트 최적화는 “캐시가 잘 붙는데도 느린” 케이스를 많이 줄여줍니다.
멀티스테이지에서 자주 하는 실수: 런타임 이미지에 빌드 도구 포함
빌드 시간 단축과 별개로, 결과 이미지가 커지면 Pull 시간이 늘어 배포 시간이 길어집니다. 멀티스테이지의 목적은 런타임에 불필요한 도구를 제거하는 것입니다.
- 빌드 스테이지: 컴파일러, dev dependencies, 테스트 도구
- 런타임 스테이지: 실행에 필요한 바이너리/정적 파일만
이렇게 하면 CI 빌드 시간이 줄어든 만큼, 배포 리드타임도 함께 줄어드는 효과가 있습니다.
쿠버네티스에서 Kaniko를 쓸 때의 보안 포인트
쿠버네티스 Job으로 Kaniko를 돌리는 경우, 다음을 점검하세요.
- 가능한 한
runAsNonRoot설정 - 쓰기 가능한 볼륨(워크스페이스) 제공
- 레지스트리 크리덴셜은 Secret로 마운트
Pod가 자꾸 재시작하거나 상태가 불안정하면, 단순히 빌드 문제가 아니라 probe/리소스 설정 문제일 수 있습니다. 운영 관점에서 CrashLoop을 잡는 방법은 이 글이 도움이 됩니다: K8s CrashLoopBackOff - liveness probe 오탐 해결
실제로 60%를 줄이는 체크리스트
아래 항목을 순서대로 적용하면 체감 성능이 크게 좋아집니다.
- Dockerfile에서 의존성 설치 레이어를 lockfile 기반으로 분리했는가
.dockerignore로 컨텍스트를 최소화했는가- Kaniko에
--cache=true와--cache-repo를 설정했는가 - 캐시 저장소에 대한 권한/인증이 안정적인가
snapshot-mode를 환경에 맞게 선택했는가(대부분redo부터 시도)
추가로, GitHub Actions에서 “캐시가 분명 있어야 하는데 계속 미스”라면 키/경로 충돌 같은 CI 캐시 이슈도 함께 의심해야 합니다. 이때는 GitHub Actions 캐시가 안 먹을 때 - key·dir 충돌 디버깅에서 소개한 방식처럼, 캐시가 저장되는 경로와 키가 실제로 일치하는지부터 확인하는 게 빠릅니다.
마무리
Kaniko는 단순히 docker build를 대체하는 도구가 아니라, CI에서 이미지 빌드를 “보안적으로 덜 위험하고, 캐시를 레지스트리로 표준화할 수 있는” 구조로 바꾸는 선택지입니다. 멀티스테이지 Dockerfile을 캐시 친화적으로 재구성하고, 레지스트리 캐시를 붙이면 의존성 설치 단계가 반복되는 시간을 크게 줄일 수 있습니다.
정리하면, 빌드 60% 단축의 대부분은 Kaniko 자체의 마법이 아니라 다음 2가지에서 나옵니다.
- 레이어가 잘 쪼개진 멀티스테이지 Dockerfile
- 러너가 바뀌어도 유지되는 레지스트리 캐시
이 두 가지를 먼저 고정하고, 그 다음에 러너 스펙/병렬화/아키텍처(예: 멀티플랫폼)로 확장하는 것이 가장 비용 대비 효과가 좋습니다.