이름은 그대로, 받는 곳만 바꾼다
한 줄 요약
폐쇄망 쿠버네티스에 이미지를 넣는 방법은 둘이다. 노드마다 아카이브를 직접 넣거나, 사내 레지스트리를 세우고 런타임에 미러를 알려 주거나. 미러는 매니페스트의 이미지 이름을 하나도 바꾸지 않는다. 대신 런타임마다 설정 파일이 다르고(podman 은 registries.conf, containerd 는 hosts.toml, k3s 는 registries.yaml), 모두 사설 CA 를 따로 알려 줘야 한다.
왜 이게 필요했나
노드가 열 대인 폐쇄망 클러스터에 새 버전을 올릴 때마다 모든 노드에 tar 를 복사해 넣을 수는 없다. 그래서 사내 레지스트리를 둔다. 그런데 흔한 첫 시도는 매니페스트와 차트의 이미지 이름을 registry.corp.internal/... 로 바꾸는 것이다. 그러면 바깥 저장소의 차트와 갈라지고, 업스트림이 새 버전을 낼 때마다 이름 바꾸기를 다시 해야 하며, 다이제스트로 고정한 매니페스트는 옮기는 과정에서 다이제스트가 바뀌면 깨진다. 미러는 "registry.k8s.io 는 여기서 받는다" 만 런타임에 알려 주어 이 문제를 피한다.
어떻게 동작하나
사내 레지스트리. CNCF distribution(옛 docker registry)은 설정 파일의 storage.filesystem.rootdirectory 에 이미지를 두고, http.addr 로 주소를, http.tls 의 certificate·key 로 HTTPS 를 정한다. proxy.remoteurl 을 주면 pull-through 캐시가 되지만 그 모드에서는 push 가 안 된다 — 폐쇄망에서는 위가 없으니 일반 레지스트리로 두고 반입물을 push 한다. 우분투 noble 에는 이것이 docker-registry 패키지(설정 /etc/docker/registry/config.yml, 서비스 docker-registry.service)로 들어 있다.
다이제스트를 지키며 옮기기. skopeo copy 는 레지스트리에서 레지스트리로 바로 옮긴다. --all 은 여러 아키텍처 목록을 통째로 옮기고, --preserve-digests 는 다이제스트를 지킬 수 없으면 실패하게 한다. 한 아키텍처만 옮기면 목록의 다이제스트가 바뀌어, image@sha256:... 로 고정한 매니페스트가 미러에서 이미지를 찾지 못한다. skopeo sync 는 YAML 목록으로 여러 이미지를 한 번에 옮긴다.
podman: registries.conf. containers-registries.conf(5) 의 [[registry]] 는 prefix 로 어떤 이름에 적용할지, location 으로 원래 위치를 적고, [[registry.mirror]] 의 location 이 먼저 시도할 곳이다. pull-from-mirror 로 태그·다이제스트 가운데 어떤 것을 미러에서 받을지 고를 수 있다. 조각 파일은 /etc/containers/registries.conf.d/ 에 두면 알파벳 순으로 이어 읽힌다. 사설 CA 는 /etc/containers/certs.d/<호스트:포트>/ca.crt 다.
containerd: hosts.toml. containerd 문서에 따르면 /etc/containerd/certs.d/<레지스트리>/hosts.toml 의 디렉터리 이름이 원래 레지스트리, server 가 원래 주소, [host."..."] 가 먼저 시도할 미러이고 거기에 capabilities(pull·resolve·push)와 ca 를 적는다. 나열한 host 를 차례로 시도한 뒤 모두 실패하면 server 로 물러선다. 폐쇄망에서는 그 물러섬이 막힌 바깥으로 가므로 오류 메시지가 헷갈릴 수 있다. CRI 가 이 디렉터리를 보게 하는 설정(config_path)의 위치는 containerd 1.x 와 2.x 가 다르고, ctr images pull --hosts-dir 은 같은 형식을 명령 하나에서 시험하게 해 준다.
k3s: registries.yaml. k3s 문서에 따르면 /etc/rancher/k3s/registries.yaml 의 mirrors 에 원래 레지스트리와 endpoint 를, configs 에 미러 주소의 tls(ca_file 등)와 인증을 적고, 바꾼 뒤에는 노드마다 k3s 를 다시 띄워야 한다. 문서는 기본 엔드포인트가 늘 마지막 수단으로 시도된다고도 적는다. 이 실습의 k3s(v1.33.3+k3s1)는 containerd 2.0 을 쓴다.
현장에서 만나는 모습
이 실습 VM 에서 재 보았다. registry.k8s.io/e2e-test-images/busybox:1.36.1-1 의 목록에는 리눅스 다섯 아키텍처와 윈도 이미지 둘이 들어 있어, --all 로 두 이미지를 옮기자 레지스트리 저장소가 893MB 가 됐다(리눅스 레이어는 몇 MB 뿐이다). VM 루트 디스크(2.4GiB)에 두었을 때는 첫 이미지에서 디스크가 찼다 — 미러의 저장소 자리는 반입 용량을 보고 정해야 한다. 목록째 옮긴 pause 의 다이제스트는 바깥과 같은 sha256:ee6521f2… 였고, --all 없이 한 번 더 옮긴 사본은 sha256:7c38f247… 로 달라졌다. k3s 는 registries.yaml 을 읽어 /var/lib/rancher/k3s/agent/etc/containerd/certs.d/registry.k8s.io/hosts.toml 을 # File generated by k3s. DO NOT EDIT. 라는 머리와 함께 만들었다 — 손으로 쓴 hosts.toml 과 같은 형식이다. 레지스트리 키를 root 소유로 두자 docker-registry.service 가 status=1/FAILURE 로 죽었다.
미러를 쓰면 fde-delivery 모듈에서 실패하던 imagePullPolicy: Always 파드가 뜬다. Always 는 매번 레지스트리에 태그의 다이제스트를 묻는데, 이제 그 질문을 사내 미러가 받아 주기 때문이다. 반대로 미러가 가진 적 없는 이미지를 부르면, 런타임은 미러에서 못 찾고 원래 레지스트리로 물러섰다가 막혀서 실패한다 — 오류 줄에 원래 레지스트리 주소가 찍혀 있어도 원인은 "반입 목록에 없었다" 다.
다음 실습에서 할 것
k3s VM 에서 사설 CA 로 사내 레지스트리를 HTTPS 로 세우고, 연결된 동안 두 이미지를 다이제스트를 지키며 옮긴다. 바깥 송신을 막은 뒤 podman(registries.conf), containerd(손으로 쓴 hosts.toml), k3s(registries.yaml) 세 길로 원래 이름 그대로 받는다. Always 파드를 띄우고, 반입 기록을 레지스트리·런타임 다이제스트와 대조해 남긴다.
참고 문서: distribution Configuration · Registry as a pull through cache · skopeo-copy(1) · containers-registries.conf(5) · containerd Registry Configuration(hosts.md) · K3s Private Registry Configuration