LabHub
배우기 러닝패스 코스

FDE 캡스톤: 창고가 같은 주문을 세 번 받았다 · 폐쇄망 클러스터에 이미지 반입 · 이론

폐쇄망 클러스터에 이미지를 들이는 세 갈래

LabHub 에서 이어서 보기

한 줄 요약

폐쇄망 클러스터의 노드는 이미지를 스스로 구하지 못한다. 누군가 런타임 저장소에 이미지를 넣어 주어야 하고, 넣은 뒤에도 파드의 끌어오기 정책이 레지스트리에 물으러 나가지 않아야 한다. k3s 는 이 일을 위해 사설 레지스트리, 노드별 이미지 아카이브, 내장 레지스트리 미러 세 갈래를 준다.

왜 이게 필요했나

고객사 보안 규정으로 운영 클러스터가 인터넷에 닿지 않는다. 납품 전날 매니페스트를 적용하자 파드가 ErrImagePull 을 거쳐 ImagePullBackOff 에 머문다. 연결된 개발 환경에서는 한 번도 본 적 없는 상태다. 거기서는 kubelet 이 이미지를 알아서 받아 왔기 때문이다. 폐쇄망에서는 그 "알아서" 가 없다. FDE 가 할 일은 두 가지다. 연결된 곳에서 무엇을 가져갈지 정해 옮기고, 받은 쪽에서 그것이 옮기기 전과 같은 것인지 증명한 뒤 런타임에 넣는 것.

어떻게 동작하나

파드는 레지스트리가 아니라 노드의 런타임 저장소를 본다. kubelet 은 컨테이너를 띄울 때 끌어오기 정책을 따른다. 쿠버네티스 문서에 따르면 IfNotPresent 는 로컬에 없을 때만 받고, Never 는 받으러 가지 않으며, Always 는 컨테이너를 띄울 때마다 런타임이 레지스트리에 연락해 태그를 다이제스트로 풀고 없는 레이어만 받는다. 정책을 적지 않으면 태그가 :latest 이거나 없을 때 Always, 그 밖의 태그면 IfNotPresent 가 된다. 끌어오기가 실패하면 kubelet 은 지수적으로 늘어나는 간격으로 다시 시도하고, 그 간격은 300초에서 멈춘다. 그래서 반입한 직후에도 파드가 몇 분씩 가만히 있을 수 있다.

k3s 의 세 갈래. k3s 의 에어갭 설치 문서는 이미지를 들이는 방법을 셋으로 나눈다.

사설 레지스트리       /etc/rancher/k3s/registries.yaml 로 미러·인증을 설정. 바꾸면 노드마다 k3s 재시작노드에 직접 배포      /var/lib/rancher/k3s/agent/images/ 에 이미지 tar 를 둔다내장 레지스트리 미러  한 노드의 containerd 저장소에 있는 이미지를 다른 노드가 받아 간다

사설 레지스트리 문서에는 놓치기 쉬운 문장이 있다. containerd 에는 모든 레지스트리에 대한 암묵적인 기본 엔드포인트가 있고, registries.yaml 에 다른 엔드포인트를 적어도 그 기본 엔드포인트가 마지막 수단으로 늘 시도된다. 또 레지스트리를 적지 않은 이미지 이름은 역사적 이유로 docker.io 로 간주된다. 폐쇄망에서 nginx:1.25 처럼 짧게 적은 매니페스트가 어디로 물으러 가는지가 여기서 정해진다.

이미지 디렉터리는 언제 읽히나. 에어갭 문서는 이 디렉터리의 아카이브를 k3s 가 시작할 때마다 들인다고 적는다. 지워지거나 정리된 이미지가 있어도 늘 다시 갖추기 위해서이고, 그 대신 모든 아카이브를 처리할 때까지 kubelet 이 시작되지 않아 기동이 늦어진다. 그래서 2025년 5월 릴리스(v1.33.1+k3s1, v1.32.5+k3s1 등)부터는 디렉터리에 .cache.json 을 두면 크기와 수정 시각이 같은 아카이브를 건너뛰는 조건부 반입을 쓸 수 있다. 이 경우 지워진 이미지는 ctr image import 로 손수 다시 들이거나 아카이브를 touch 해야 한다. 한편 이미지 반입 문서는 실행 중에 넣은 tar 도 들이는 기능이 2025년 1월 릴리스(v1.32.0+k3s1, v1.31.5+k3s1, v1.30.9+k3s1, v1.29.13+k3s1)부터이고 그 전에는 부팅 때만 들였다고 적는다. 고객 클러스터의 k3s 버전을 먼저 확인해야 하는 이유다. 같은 디렉터리에 이미지 이름을 줄마다 적은 텍스트 파일을 두면 반대로 온라인에서 미리 끌어오는 용도가 된다는 점도 헷갈리지 말자.

반입은 해시로 끝난다. 옮기기 전에 아카이브의 sha256 을 기록하고, 받은 쪽에서 같은 명령으로 대조한 뒤 넣는다. 넣은 뒤에는 런타임이 보고하는 이미지 ID(설정 해시)가 아카이브 안의 설정 해시와 같은지 본다. 이 둘이 이어져야 "고객 클러스터에서 도는 것이 우리가 검증한 그것" 이라고 말할 수 있다.

현장에서 만나는 모습

실습 VM 에서 이렇게 재 보았다(실측, v1.33.3+k3s1, containerd v2.0.5-k3s2). 바깥 송신을 REJECT 로 막자 https://registry.k8s.io/v2/ 에 대한 curl 상태 코드가 401 에서 000 으로 바뀌었고, 새 파드는 만든 지 2초 안에 ErrImagePull 을 거쳐 ImagePullBackOff 가 되었다. 이벤트의 원인 줄은 failed to resolve reference 로 시작해 Head "https://registry.k8s.io/v2/e2e-test-images/busybox/manifests/1.36.1-1" 요청이 connection refused 로 끝났다고 적는다. 레이어를 받기 전에, 태그를 다이제스트로 풀려고 레지스트리에 묻는 첫 요청에서 이미 막힌 것이다. busybox 아카이브(4.5MB)를 k3s ctr -n k8s.io images import 로 넣자 출력에 saved 와 매니페스트 다이제스트가 찍혔다. 다만 이 출력은 진행 표시가 섞여, 파일로 저장한 이미지 이름이 하이픈과 태그가 빠진 모양으로 남았다. 반입 기록은 이름보다 다이제스트로 대조하는 편이 안전하다. nginx 아카이브(17MB)를 실행 중인 k3s 의 이미지 디렉터리에 복사하자 k3s 로그에 Importing images fromImported images ... in 1.094242885s 가 찍혔고, 재시작 없이 런타임 저장소에 나타났다. 반입해 둔 busybox 로 imagePullPolicy: Always 파드를 만들면 이미지가 저장소에 있는데도 똑같은 HEAD 요청 실패로 ErrImagePull 이 났다.

가장 흔한 두 번째 사고는 반입을 끝낸 뒤에 난다. 누군가 매니페스트에 imagePullPolicy: Always 를 적어 두었거나 태그를 latest 로 두었다. 이미지는 저장소에 있지만 kubelet 은 레지스트리에 물으러 나가고, 막힌 네트워크에서 실패한다. 이때 파드의 컨테이너 imagePullPolicy 는 만든 뒤에 바꿀 수 없어서 파드를 다시 만들어야 한다. 차단을 잠깐 풀어 넘기면 연결된 환경에서는 되지만, 고객 폐쇄망에는 그 선택지가 없다.

폐쇄망을 흉내 낼 때도 함정이 있다. 이 실습의 VM 은 바깥에서 들어오는 채점 연결(8899)로 조작되므로, 바깥 송신을 막으면서 이미 맺은 연결과 루프백, 클러스터 내부 대역을 살려 두지 않으면 채점과 API 가 함께 끊긴다. 규칙은 체인 하나에 모아 몇 번을 돌려도 같은 모양이 되게 만들어야 인계받은 사람이 다시 돌려도 안전하다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

연결된 상태의 k3s VM 에서 두 이미지를 아카이브로 받아 해시를 기록하고, iptables 로 바깥 송신을 막아 폐쇄망을 만든다. 막힌 상태에서 새 이미지 파드가 끌어오기에 실패하는 것을 증거로 남기고, k3s ctr images import 와 이미지 디렉터리 두 경로로 반입해 파드를 띄운다. 반입해 둔 이미지로 Always 파드가 실패하는 것을 확인하고 정책을 고친 뒤, 반입 기록을 런타임과 대조해 남긴다.

참고 문서: [K3s Air-Gap Install](https://docs.k3s.io/installation/airgap) · [K3s Import Images](https://docs.k3s.io/add-ons/import-images) · [K3s Private Registry Configuration](https://docs.k3s.io/installation/private-registry) · [Kubernetes Images(imagePullPolicy)](https://kubernetes.io/docs/concepts/containers/images/)