- Published on
GitLab CI+Kaniko로 Rootless 멀티아치 이미지 빌드
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 CI 러너에서 Docker-in-Docker를 켜기 어렵거나, 보안 정책상 privileged 컨테이너를 금지하는 환경에서는 이미지 빌드 자체가 장애물이 됩니다. 이때 Kaniko는 Docker 데몬 없이도 Dockerfile을 해석해 이미지를 만들고 레지스트리로 푸시할 수 있어 대안으로 자주 선택됩니다.
이번 글에서는 GitLab CI에서 Kaniko를 사용해 rootless(비특권) 방식으로 이미지를 빌드하고, amd64와 arm64를 함께 제공하는 멀티아치 이미지까지 만들어 배포하는 구성을 다룹니다. 단순 예제가 아니라 실제로 막히는 지점(매니페스트 생성, 캐시, 인증, 빌드 재현성)을 중심으로 설명합니다.
왜 Kaniko인가: rootless와 멀티아치의 현실적인 타협
Docker-in-Docker의 문제
privileged필요: 대개 DinD는privileged컨테이너를 요구합니다. 보안팀이 싫어합니다.- 데몬 의존성: 러너 노드에 Docker 데몬이 필요하거나, DinD 서비스가 필요합니다.
- 네트워크·스토리지 이슈: 레이어 캐시와 네트워크가 꼬이면 빌드가 불안정해집니다.
Kaniko의 장점
- Docker 데몬 없이 동작: 컨테이너 내부에서 filesystem snapshot을 만들어 레이어를 구성합니다.
- rootless 운영에 유리: GitLab Runner가 Kubernetes executor를 쓰거나, shared runner에서 권한이 제한된 경우에도 비교적 잘 동작합니다.
- 레지스트리 캐시 지원:
--cache=true및--cache-repo로 빌드 속도를 올릴 수 있습니다.
단, 멀티아치 빌드는 “한 번에” 해결되기보다, 아키텍처별 이미지를 각각 빌드한 뒤 매니페스트 리스트를 조합하는 방식으로 접근하는 게 안정적입니다.
목표 아키텍처: amd64 + arm64 멀티아치
우리가 최종적으로 얻고 싶은 것은 다음과 같습니다.
my-registry/myapp:1.2.3하나의 태그를 풀하면- 클라이언트가 자신의 아키텍처(amd64 또는 arm64)에 맞는 이미지를 자동으로 받음
이를 위해서는 레지스트리에 “매니페스트 리스트”가 존재해야 합니다. Kaniko는 기본적으로 단일 아키텍처 이미지를 빌드하므로, 다음의 2단계를 구성합니다.
- Kaniko로 아키텍처별 이미지 빌드 및 푸시
- 예:
myapp:1.2.3-amd64,myapp:1.2.3-arm64
- 매니페스트 툴로 멀티아치 매니페스트 생성 및 푸시
- 예:
myapp:1.2.3가 위 두 이미지를 가리키도록 조합
사전 조건: GitLab Runner와 레지스트리 인증
Runner 조건
- Kubernetes executor 또는 Docker executor 모두 가능
- 핵심은
privileged없이 컨테이너 실행이 가능해야 함
레지스트리 인증 주입
GitLab CI에서는 보통 다음 변수가 제공됩니다.
CI_REGISTRYCI_REGISTRY_IMAGECI_REGISTRY_USERCI_REGISTRY_PASSWORD
Kaniko는 Docker config 형식의 인증 파일을 사용하므로, 잡에서 /kaniko/.docker/config.json을 만들어 주입하는 패턴이 일반적입니다.
Dockerfile 작성 팁: 멀티아치에서 깨지기 쉬운 지점
멀티아치에서 가장 흔한 실패는 “빌드는 되는데 실행이 안 됨”입니다. 주된 원인은 다음입니다.
- 베이스 이미지가 특정 아키텍처만 지원
- 빌드 단계에서 다운로드한 바이너리가 amd64 전용
uname -m분기 처리 미흡
가능하면 공식 이미지나 멀티아치 지원이 명확한 베이스를 선택하고, 아키텍처별 다운로드가 필요한 경우 TARGETARCH를 활용합니다.
예시 Dockerfile:
# syntax=docker/dockerfile:1
FROM alpine:3.20 AS runtime
ARG TARGETARCH
RUN echo "building for arch=$TARGETARCH" \
&& apk add --no-cache ca-certificates
WORKDIR /app
COPY ./bin/myapp /app/myapp
RUN chmod +x /app/myapp
ENTRYPOINT ["/app/myapp"]
주의할 점은 --platform=$TARGETPLATFORM 같은 구문이 실제 빌드 엔진에 따라 다르게 해석될 수 있다는 점입니다. Kaniko도 Dockerfile을 해석하지만, 모든 BuildKit 기능을 동일하게 지원하는 것은 아닙니다. 따라서 복잡한 BuildKit 전용 기능은 최소화하는 편이 안전합니다.
GitLab CI 구성: 아키텍처별 Kaniko 빌드
아래 예시는 GitLab Container Registry로 푸시하는 구성입니다. 핵심은 다음입니다.
- Kaniko executor 이미지를 사용
- 인증 파일 생성
--customPlatform으로 대상 플랫폼 지정- 아키텍처별 태그로 push
- 레지스트리 캐시 사용
stages:
- build
- manifest
variables:
IMAGE: "$CI_REGISTRY_IMAGE"
VERSION_TAG: "$CI_COMMIT_TAG"
# 태그가 없을 때를 대비한 fallback
FALLBACK_TAG: "$CI_COMMIT_SHORT_SHA"
.build_template:
stage: build
image:
name: gcr.io/kaniko-project/executor:v1.23.2-debug
entrypoint: [""]
script:
- mkdir -p /kaniko/.docker
- |
cat > /kaniko/.docker/config.json << 'EOF'
{
"auths": {
"${CI_REGISTRY}": {
"username": "${CI_REGISTRY_USER}",
"password": "${CI_REGISTRY_PASSWORD}"
}
}
}
EOF
- |
if [ -n "$VERSION_TAG" ]; then
export TAG="$VERSION_TAG"
else
export TAG="$FALLBACK_TAG"
fi
- |
/kaniko/executor \
--context "$CI_PROJECT_DIR" \
--dockerfile "$CI_PROJECT_DIR/Dockerfile" \
--destination "$IMAGE:${TAG}-${ARCH}" \
--cache=true \
--cache-repo "$IMAGE/cache" \
--snapshotMode=redo \
--customPlatform "linux/${ARCH}" \
--build-arg "TARGETARCH=${ARCH}" \
--verbosity=info
build:amd64:
extends: .build_template
variables:
ARCH: amd64
build:arm64:
extends: .build_template
variables:
ARCH: arm64
포인트 해설
entrypoint: [""]는 Kaniko 이미지의 기본 entrypoint를 비활성화해 스크립트 실행을 단순화합니다.--customPlatform은 Kaniko가 생성할 이미지 메타데이터(플랫폼)를 지정합니다.--cache-repo를 별도 레포로 두면 캐시 레이어가 본 이미지 태그와 섞이지 않아 관리가 쉽습니다.--snapshotMode=redo는 파일 변경 감지 방식에 따른 예기치 않은 캐시 미스를 줄이는 데 도움이 되는 경우가 많습니다.
멀티아치 매니페스트 생성: Crane로 조합
이제 :tag-amd64, :tag-arm64 두 이미지가 레지스트리에 올라갔습니다. 다음은 :tag 하나로 묶는 단계입니다.
여기서는 Google의 crane을 사용합니다. docker manifest는 Docker CLI가 필요하고, 환경에 따라 데몬이 필요해지는 경우가 있어 rootless CI에서는 번거롭습니다. crane은 레지스트리 API를 통해 매니페스트를 다루므로 CI에 잘 맞습니다.
manifest:
stage: manifest
image:
name: gcr.io/go-containerregistry/crane:debug
entrypoint: [""]
needs:
- job: build:amd64
- job: build:arm64
script:
- |
if [ -n "$CI_COMMIT_TAG" ]; then
export TAG="$CI_COMMIT_TAG"
else
export TAG="$CI_COMMIT_SHORT_SHA"
fi
- crane auth login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD"
# 멀티아치 인덱스 생성
- crane manifest create \
"$CI_REGISTRY_IMAGE:${TAG}-amd64" \
"$CI_REGISTRY_IMAGE:${TAG}-arm64" \
-t "$CI_REGISTRY_IMAGE:${TAG}"
# latest 같은 추가 태그가 필요하면 복사
- |
if [ "$CI_COMMIT_BRANCH" = "$CI_DEFAULT_BRANCH" ]; then
crane tag "$CI_REGISTRY_IMAGE:${TAG}" latest
fi
이제 클라이언트는 docker pull myapp:TAG만으로 자신의 아키텍처에 맞는 이미지를 받게 됩니다.
Rootless 관점에서의 보안 체크리스트
rootless라고 해서 자동으로 안전해지는 것은 아닙니다. 다음을 점검하세요.
- 레지스트리 자격증명 최소화: 프로젝트 범위 토큰, 만료되는 토큰 사용
- 빌드 컨텍스트 최소화:
.dockerignore로 불필요한 파일(특히 키,.env) 제외 - 이미지 내부 권한:
USER지정, 불필요한 패키지 제거 - SBOM 및 취약점 스캔: GitLab Ultimate 기능 또는 외부 스캐너 연동
참고로 CI에서 인증이나 토큰 처리 같은 “권한 문제”는 GitHub Actions에서도 빈번합니다. OIDC 기반 권한 설계/트러블슈팅 감각은 다른 CI로도 전이됩니다. 관련해서는 GitHub Actions OIDC에서 AWS AssumeRoleAccessDenied 해결 글의 접근 방식이 도움이 됩니다.
캐시 전략: 빠르게, 하지만 재현 가능하게
Kaniko 캐시는 체감 성능에 큰 영향을 줍니다. 다만 캐시를 공격적으로 쓰면 “내 로컬에서는 되는데 CI에서만 깨짐” 같은 재현성 문제가 생길 수 있습니다.
권장 전략:
--cache=true는 기본 활성화--cache-repo를 고정된 경로로 운영- Dockerfile에서 변동이 큰 레이어를 뒤로 미루기
- 예:
COPY . .는 가능한 뒤쪽 - 패키지 설치는 앞쪽
- 예:
예시:
FROM alpine:3.20
WORKDIR /app
# 의존성 설치를 먼저
RUN apk add --no-cache ca-certificates
# 변동이 큰 소스는 나중에
COPY ./bin/myapp /app/myapp
RUN chmod +x /app/myapp
ENTRYPOINT ["/app/myapp"]
흔한 장애와 해결책
1) arm64 이미지가 빌드되지만 실행 시 exec format error
원인:
- arm64인데 amd64 바이너리를 넣었거나, 반대로 넣음
대응:
- 빌드 산출물을 아키텍처별로 분리
ARG TARGETARCH기반으로 다운로드 URL을 분기
2) 매니페스트는 생성됐는데 특정 아키텍처에서 pull이 실패
원인:
- 아키텍처별 이미지에 platform 메타데이터가 잘못 들어감
대응:
- Kaniko에
--customPlatform을 명시 - 이미지 inspect로 확인
crane으로 확인:
crane manifest my-registry/myapp:1.2.3
3) GitLab CI에서 인증은 했는데 push가 401
원인:
CI_REGISTRY가 레지스트리 호스트와 다르거나- group/project 레벨 권한이 부족
대응:
/kaniko/.docker/config.json의 key가 실제 레지스트리 도메인과 일치하는지 확인- GitLab Deploy Token 사용 검토
4) 멀티아치 빌드 시간이 너무 길다
대응:
- 캐시 레포 분리 및 유지
- 빌드 컨텍스트 축소
- 불필요한 레이어 제거
성능 튜닝은 결국 “병목을 수치로 확인하고 줄이는” 문제로 귀결됩니다. 운영 서비스 튜닝 관점이 필요하다면 GCP Cloud Run 503·콜드스타트 줄이는 튜닝처럼 병목을 단계적으로 제거하는 방식의 글도 같이 참고하면 좋습니다.
운영 팁: 태그 정책과 롤백
멀티아치 이미지는 태그 정책이 특히 중요합니다.
- 불변 태그:
1.2.3같은 릴리스 태그는 재푸시 금지 - 가변 태그:
latest는 기본 브랜치에만 부여 - 롤백:
1.2.2로 즉시 되돌릴 수 있도록 릴리스 태그를 유지
GitLab CI에서는 CI_COMMIT_TAG 기반으로 릴리스 파이프라인을 분리하는 것이 좋습니다.
전체 예시: 최소 동작 파이프라인 정리
build:amd64가:tag-amd64푸시build:arm64가:tag-arm64푸시manifest가:tag멀티아치 태그 생성- 기본 브랜치면
latest도 태깅
이 구성은 다음 조건에서 특히 유용합니다.
- shared runner를 쓰고
privileged를 못 켜는 경우 - Kubernetes 기반 GitLab Runner에서 보안 정책이 엄격한 경우
- amd64, arm64 혼재 클러스터(예: Graviton 포함)로 배포하는 경우
마무리
GitLab CI에서 Kaniko를 사용하면 Docker 데몬 없이도 rootless에 가까운 형태로 이미지 빌드를 자동화할 수 있고, 여기에 crane 같은 레지스트리 기반 도구를 조합하면 멀티아치 매니페스트까지 안정적으로 운영할 수 있습니다.
핵심은 “Kaniko로 아키텍처별 이미지를 확실히 만든 뒤, 매니페스트는 별도 단계에서 조합한다”는 분리 전략입니다. 이 방식이 디버깅도 쉽고, 실패 지점을 명확히 나눌 수 있어 운영 파이프라인에서 특히 강합니다.