레지스트리 없이 쿠버네티스 부트스트랩 – containerd 이미지 사전 태깅

발행: (2026년 6월 1일 AM 04:07 GMT+9)
8 분 소요
원문: Dev.to

출처: Dev.to

프라이빗 프로젝트—내부 도구, 격리된 네트워크, 사내 스택—용으로 Kubernetes를 설정하고 있다면 어느 순간 이미지가 어디서 오는가에 대한 질문에 직면하게 됩니다.

모든 튜토리얼은 docker.io, ghcr.io, quay.io 같은 공개 레지스트리에 접근할 수 있다고 가정합니다. 그런데 접근할 수 없을 때는 닭‑달걀 문제가 발생합니다. 레지스트리 이미지를 레지스트리에서 끌어올 수 없습니다. IdP가 올라오기 전에는 IdP에 인증할 수도 없습니다. 모든 기반 서비스가 같은 형태를 가집니다.

모든 기반 서비스가 같은 문제를 갖는다

다음과 같은 패턴이 어디서든 나타납니다:

  • 레지스트리: kubelet이 레지스트리 이미지를 어디선가 끌어와야 하는데, 레지스트리 자체가 그 이미지를 제공해야 합니다.
  • Identity Provider: OIDC를 사용하는 모든 것은 IdP가 가동 중이어야 합니다—그리고 IdP 파드도 이미지 풀 없이 시작되지 않습니다.

각각을 특수 케이스로 다루면 “첫 번째 실행 전용” 스크립트가 쌓이고, 이는 일반 배포 경로와 동기화가 맞지 않게 됩니다.

실용적인 접근법

메커니즘 자체는 매우 단순합니다:

  1. docker build 로 이미지를 만든다.
  2. docker save 로 tarball 로 저장한다.
  3. tarball 을 모든 Kubernetes 노드에 복사한다.
  4. ctr -n k8s.io images import <파일> 로 containerd에 가져온다.

Pod 스펙에 imagePullPolicy: IfNotPresent 를 설정하면, kubelet은 캐시된 이미지를 사용하고 풀을 시도하지 않습니다. 레지스트리가 가동될 필요도 없고, 외부에 접근할 필요도 없습니다.

이름이 일치해야 한다

containerd에 이미지를 가져오면, tarball에 적힌 이름 그대로 캐시에 저장됩니다. 그 이름은 단순 문자열일 뿐이며, repo/registry:latestregistry.example.com/repo/registry:latest 사이에 특별한 차이는 없습니다—두 형태 모두 유효하고 레지스트리 호스트명이 해석되지 않아도 캐시에 존재할 수 있습니다.

베어 네임 (Day 1)

# values.yaml
image:
  repository: repo/registry
  tag: latest
  pullPolicy: IfNotPresent
docker build -t repo/registry:latest .
docker save repo/registry:latest > registry.tar
scp registry.tar node:/tmp/
ssh node 'sudo ctr -n k8s.io images import /tmp/registry.tar'

Day 1 에는 잘 동작합니다. 하지만 레지스트리가 가동되고 registry.example.com/repo/registry:v0.4.2 와 같은 실제 빌드를 푸시하기 시작하면, 모든 차트에서 image.repository 를 완전한 경로로 바꿔야 합니다. Day‑1 과 Day‑2 배포가 서로 다른 코드 경로가 됩니다.

Fully‑qualified name (일관된 경로)

docker build -t registry.example.com/repo/registry:v0.4.2 .
docker save registry.example.com/repo/registry:v0.4.2 > registry.tar
scp registry.tar node:/tmp/
ssh node 'sudo ctr -n k8s.io images import /tmp/registry.tar'
image:
  repository: registry.example.com/repo/registry
  tag: v0.4.2
  pullPolicy: IfNotPresent

이제 containerd의 캐시 키, 차트의 image.repository, 그리고 캐시 미스 시 kubelet이 조회할 호스트명이 모두 같은 문자열이 됩니다. 클러스터는 이미지가 어제 사이드로드된 것인지, 오늘 아침 레지스트리에서 풀된 것인지 구분할 수 없습니다.

태그에 대한 주의점: latest 대신 v0.4.2 와 같이 불변(immutable) 태그를 사용하세요. IfNotPresent 를 쓰면 kubelet은 이미 캐시된 이미지를 그대로 유지하고 다시 풀어오지 않으므로, 사이드로드된 latest 가 레지스트리에서 새로운 latest 로 바뀌어도 그대로 고정됩니다. 불변 태그를 사용하면 부트스트랩 노드는 v0.4.2 를 영원히 보유하고, 다음 릴리즈는 v0.4.3 으로 배포되므로 레지스트리가 올라왔을 때 정상적으로 풀됩니다.

이 접근법이 제공하는 이점

  • 모든 기반 서비스가 동일한 방식으로 부트스트랩된다. 레지스트리든 IdP든 같은 메커니즘, 같은 네이밍 패턴, 특수 케이스가 없습니다. 부트스트랩 스크립트는 이미지 리스트를 순회하는 루프가 됩니다.
  • Helm values 를 한 번만 작성한다. 부트스트랩 모드 오버레이와 프로덕션 모드 오버레이를 구분할 필요가 없습니다. image.repository 마이그레이션을 추적할 일도 없습니다. 배포하는 values 파일이 그대로 올바른 값입니다.
  • Argo CD 가 클러스터를 깔끔하게 상속한다. GitOps 로 전환하면 Argo CD 역시 같은 Helm 차트를 읽습니다. 이미지 문자열이 변하지 않으므로, 사이드로드된 태그는 이미 캐시돼 있고, 새로운 릴리즈는 노드가 한 번도 보지 못한 태그이므로 레지스트리에서 정상적으로 풀됩니다. 마이그레이션도, 첫‑동기화 모드도, 부트스트랩 경로를 알아야 하는 Application 도 없습니다. Day‑1 과 Day‑2 가 같은 코드 경로를 공유하게 됩니다.

결론

핵심은 메커니즘이 아니라 정렬입니다. docker savectr import 가 똑똑한 것이 아니라, containerd 안의 이름, 차트 안의 이름, 그리고 kubelet 이 호출할 이름이 모두 첫 번째 가져오기부터 동일한 문자열이어야 합니다.

이것이 맞춰지면, 닭‑달걀 문제는 더 이상 문제처럼 느껴지지 않습니다.

0 조회
Back to Blog

관련 글

더 보기 »