DevOps
k3s Registry Bootstrap: Nexus self-dependency 해소
k3s 환경에서 Nexus가 자기 자신에게 의존해 이미지 pull이 막히는 bootstrap deadlock을 끊기 위해 registry:latest 기반 임시 bootstrap registry를 구성하고 검증한 기록이다.
- Published
- Updated
- Area
- Troubleshooting
- Type
- runbook
- Category
- DevOps
운영 배경
k3s 클러스터에서 Nexus를 private Docker registry처럼 사용하면 대부분의 workload 이미지를 Nexus에서 pull하도록 구성할 수 있다. 문제는 Nexus 자체도 Kubernetes 위에서 실행되는 Pod라는 점이다.
Nexus Pod가 재시작되거나 재배치될 때 Nexus 이미지를 다시 pull해야 하는데, 그 이미지 source가 Nexus 자신으로만 잡혀 있으면 다음과 같은 자기참조 문제가 생긴다.
Nexus Pod 재시작 필요
→ Nexus 이미지 pull 필요
→ 이미지 source가 Nexus
→ 하지만 Nexus가 아직 떠 있지 않음
→ pull 실패
→ Nexus 복구 실패
이 글의 목적은 전체 registry HA를 만드는 것이 아니라, Nexus를 다시 띄우기 위한 최소 bootstrap dependency를 분리하는 것이다.
bootstrap registry는 “전체 서비스 복구용 registry”가 아니라 “Nexus self-dependency를 끊는 임시 안전장치”로 둔다.
목표 범위
이번 구성의 목표는 명확하다.
| 구분 | 목표 |
|---|---|
| 해결하려는 문제 | Nexus가 자기 이미지를 자기 자신에게서 pull해야 하는 self-dependency |
| bootstrap registry 역할 | Nexus와 k3s 핵심 시스템 이미지의 최소 fallback |
| 운영 registry | Nexus |
| bootstrap registry 범위 | rancher 하위 k3s 시스템 이미지 + Nexus 실행 이미지 |
| 제외한 목표 | 완전한 HA registry, 전체 workload 이미지 mirror, registry backend 이중화 |
즉, 최종 흐름은 다음과 같다.
bootstrap registry
→ k3s core component와 Nexus Pod 기동
→ Nexus 정상화
→ 나머지 workload는 Nexus에서 이미지 pull
CNI 이미지에 대한 구분
처음 헷갈리기 쉬운 부분은 CNI 이미지다. 일반적으로 CNI는 개념상 container runtime이 호출하는 network plugin binary다. 하지만 많은 Kubernetes 배포에서는 CNI가 DaemonSet 형태의 이미지로 배포된다.
다만 k3s 기본 flannel은 환경에 따라 별도 flannel Pod나 이미지로 보이지 않을 수 있다.
확인한 상태는 다음과 같았다.
| 확인 항목 | 결과 | 해석 |
|---|---|---|
kubectl에서 flannel Pod 검색 | 결과 없음 | 별도 DaemonSet 형태로 보이지 않음 |
crictl images 또는 image 목록에서 flannel 검색 | 결과 없음 | 외부 flannel image pull에 의존하지 않는 상태로 보임 |
| `ip a | grep cni` | flannel.1 확인 |
ps -ef에서 --flannel-backend 확인 | 명시 옵션 없음 | k3s default 설정으로 동작하는 것으로 판단 |
따라서 이 환경에서는 flannel 이미지를 bootstrap registry에 반드시 넣어야 하는 대상으로 보지 않았다.
ip a | grep cni
예상되는 확인 포인트는 flannel.1 인터페이스 존재 여부다.
flannel.1
주의할 점은 다음과 같다.
flannel이름의 Pod가 없다고 해서 CNI가 없는 것은 아니다.flannel.1인터페이스가 있으면 flannel overlay network 자체는 동작 중인 것으로 볼 수 있다.- 이 환경에서는 bootstrap registry의 핵심이 CNI가 아니라
pause,CoreDNS,Nexus이미지 쪽이었다.
bootstrap registry에 넣을 이미지 범위
이번 목적은 Nexus self-dependency 해소이므로 bootstrap registry에는 과하게 많은 이미지를 넣지 않는다.
| 이미지 그룹 | 포함 여부 | 이유 |
|---|---|---|
rancher 하위 k3s 시스템 이미지 | 포함 | k3s 기본 컴포넌트 복구에 필요 |
| Nexus 실행 이미지 | 포함 | Nexus를 bootstrap으로 띄우기 위한 핵심 |
| 모든 workload 이미지 | 제외 | Nexus가 정상화된 뒤 Nexus에서 pull |
| flannel 이미지 | 확인 결과 제외 | 현재 환경에서는 외부 flannel image 의존이 확인되지 않음 |
확인된 rancher 하위 이미지 예시는 다음과 같다.
rancher/klipper-helm
rancher/klipper-lb
rancher/local-path-provisioner
rancher/mirrored-coredns-coredns
rancher/mirrored-library-busybox
rancher/mirrored-library-traefik
rancher/mirrored-metrics-server
rancher/mirrored-pause
여기서 특히 중요한 이미지는 다음이다.
| 이미지 | 역할 |
|---|---|
rancher/mirrored-pause | Pod sandbox 생성 |
rancher/mirrored-coredns-coredns | Service DNS |
rancher/mirrored-library-busybox | 일부 init/debug 작업에서 사용 가능 |
rancher/klipper-helm | k3s packaged component 설치에 사용 |
rancher/klipper-lb | k3s 기본 LoadBalancer 구현 |
rancher/local-path-provisioner | local-path StorageClass |
rancher/mirrored-library-traefik | k3s 기본 Ingress Controller |
rancher/mirrored-metrics-server | metrics-server |
Nexus Pod가 사용하는 이미지 확인
bootstrap registry에 넣어야 하는 Nexus 이미지는 “Nexus에 저장된 이미지”가 아니라 Nexus Pod를 실행하는 데 필요한 이미지다.
namespace를 알고 있다면 다음 명령으로 확인한다.
kubectl get pod -n <nexus-namespace> -o jsonpath="{..image}" \
| tr -s ' ' '\n' \
| sort | uniq
init container까지 포함해 보기 좋게 확인하려면 다음 형태를 사용할 수 있다.
kubectl get pod -n <nexus-namespace> -o jsonpath="{range .items[*]}{.metadata.name}{':\n'}{.spec.containers[*].image}{'\n'}{.spec.initContainers[*].image}{'\n\n'}{end}"
kubectl describe pod로도 직접 확인할 수 있다.
kubectl describe pod <nexus-pod> -n <nexus-namespace>
확인할 위치는 다음이다.
Containers:
Init Containers:
Helm chart를 사용하는 경우 init container나 sidecar가 있을 수 있으므로 .spec.containers[*].image만 보면 부족할 수 있다.
registry:latest 기반 bootstrap registry 구성
bootstrap registry는 registry:latest 이미지로 구성했다. 외부 노출 포트는 20002를 사용했다.
docker-compose.yml 예시는 다음과 같다.
version: "3.8"
services:
registry:
image: registry:latest
container_name: bootstrap-registry
restart: unless-stopped
ports:
- "20002:5000"
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry
volumes:
- ./data:/var/lib/registry
실행은 다음과 같다.
docker compose up -d
주의할 점은 environment 철자다. 다음과 같이 오타가 있으면 compose validation에서 실패한다.
services.registry Additional property envirnoments is not allowed.
잘못된 키는 다음이다.
envirnoments:
올바른 키는 다음이다.
environment:
Docker에서 insecure registry 테스트
registry:latest는 별도 TLS 설정을 하지 않으면 기본적으로 plain HTTP로 동작한다.
즉, 접근 URL은 다음 형태가 된다.
http://<registry-host>:20002
Docker CLI에서 먼저 테스트하려면 /etc/docker/daemon.json에 insecure registry를 추가한다.
{
"insecure-registries": ["<registry-host>:20002"]
}
registry가 같은 노드에서만 테스트되는 경우에는 다음처럼 둘 수 있다.
{
"insecure-registries": ["localhost:20002"]
}
Docker를 재시작한다.
sudo systemctl daemon-reexec
sudo systemctl restart docker
적용 여부를 확인한다.
docker info | grep -i insecure
이미지 push 테스트는 다음과 같은 흐름으로 진행한다.
docker pull registry.k8s.io/pause:3.9
docker tag registry.k8s.io/pause:3.9 <registry-host>:20002/pause:3.9
docker push <registry-host>:20002/pause:3.9
다시 pull이 되는지 확인한다.
docker pull <registry-host>:20002/pause:3.9
registry HTTP API로 이미지 목록 확인
Docker Registry HTTP API를 사용하면 registry에 올라간 repository와 tag를 확인할 수 있다.
전체 repository 목록은 다음으로 확인한다.
curl http://<registry-host>:20002/v2/_catalog
예상 형태는 다음과 같다.
{"repositories":["pause","coredns","nexus"]}
특정 repository의 tag 목록은 다음으로 확인한다.
curl http://<registry-host>:20002/v2/pause/tags/list
예상 형태는 다음과 같다.
{"name":"pause","tags":["3.9"]}
repository가 많으면 n 파라미터를 줄 수 있다.
curl "http://<registry-host>:20002/v2/_catalog?n=100"
k3s에서 registry mirror로 등록
k3s는 containerd를 사용하므로 Docker의 daemon.json 설정만으로는 충분하지 않다. k3s의 registry mirror는 다음 파일에서 설정한다.
/etc/rancher/k3s/registries.yaml
bootstrap registry를 mirror endpoint로 등록하는 기본 형태는 다음과 같다.
mirrors:
"registry.k8s.io":
endpoint:
- "http://<registry-host>:20002"
"docker.io":
endpoint:
- "http://<registry-host>:20002"
configs:
"<registry-host>:20002":
tls:
insecure_skip_verify: true
HTTP registry라면 endpoint는 반드시 http://로 적는다.
endpoint:
- "http://<registry-host>:20002"
tls.insecure_skip_verify: true는 TLS 검증을 건너뛰는 설정이다. HTTP endpoint를 쓰는 경우에도 insecure registry 성격의 endpoint를 containerd에 명시한다는 점에서 같이 관리하는 경우가 많다.
적용 후 k3s를 재시작한다.
sudo systemctl restart k3s
agent node가 있다면 해당 node에서도 k3s agent를 재시작한다.
sudo systemctl restart k3s-agent
확인은 다음처럼 할 수 있다.
sudo crictl info | grep -i registry -A 20
또는 실제 pull 테스트를 한다.
sudo k3s ctr images pull <registry-host>:20002/pause:3.9
localhost 사용 시 주의점
k3s에서 localhost는 항상 각 노드 자신을 의미한다.
따라서 registries.yaml에 다음처럼 쓰면,
mirrors:
"registry.k8s.io":
endpoint:
- "http://localhost:20002"
각 노드는 자기 자신의 localhost:20002에 registry가 있다고 가정한다.
| registry 배치 | localhost:20002 사용 가능 여부 | 설명 |
|---|---|---|
| 모든 노드에 registry 실행 | 가능 | 각 노드가 자기 로컬 registry를 사용 |
| 특정 노드 하나에만 registry 실행 | 불가 또는 제한적 | 다른 노드의 localhost에는 registry가 없음 |
| 특정 노드 하나에만 registry 실행 후 IP로 접근 | 가능 | endpoint를 <registry-host>:20002로 통일 |
모든 노드에서 다음 명령이 성공해야 localhost:20002 mirror 전략이 안전하다.
curl http://localhost:20002/v2/_catalog
registry가 특정 노드에만 있다면 다음처럼 명시적인 host 또는 IP를 사용해야 한다.
endpoint:
- "http://<registry-host>:20002"
단, 내부 IP나 private hostname은 공개 문서에 남기지 않고 [REDACTED] 또는 <registry-host>로 표기한다.
두 번째 노드에 registry 복제
bootstrap registry를 노드 A에 하나 띄운 뒤, 노드 B에도 같은 registry를 띄우고 싶을 수 있다. 이때 data 디렉토리를 그대로 복사하는 방식은 가능하지만 조건이 있다.
Docker registry의 storage는 단순 파일처럼 보이지만 내부적으로 content-addressable layout과 manifest/index 구조를 가진다. push 중이거나 GC 중인 상태에서 복사하면 일부 데이터가 깨질 수 있다.
안전한 절차는 다음과 같다.
- 노드 A에서 registry를 중지한다.
docker compose down
data디렉토리를 노드 B로 복사한다.
scp -r ./data <node-b>:/path/to/registry/
또는 rsync를 사용할 수 있다.
rsync -avz ./data <node-b>:/path/to/registry/
- 노드 B에서 registry를 실행한다.
docker compose up -d
비교하면 다음과 같다.
| 방식 | 안전성 | 비고 |
|---|---|---|
registry 중지 후 data 복사 | 높음 | 권장 |
registry 실행 중 data 복사 | 낮음 | push/GC와 겹치면 깨질 수 있음 |
| registry-to-registry pull/tag/push | 높음 | 가장 정석이지만 이미지 수가 많으면 번거로움 |
이미지가 몇 개 안 된다면 다음처럼 registry-to-registry로 복제하는 방식도 안전하다.
docker pull <node-a-registry>:20002/pause:3.9
docker tag <node-a-registry>:20002/pause:3.9 <node-b-registry>:20002/pause:3.9
docker push <node-b-registry>:20002/pause:3.9
증상별 점검표
| 증상 | 가능 원인 | 확인 명령 |
|---|---|---|
curl이 connection refused | registry container 미실행 또는 포트 미노출 | docker ps, curl http://<registry-host>:20002/v2/_catalog |
| Docker push 시 HTTPS 관련 오류 | insecure registry 미설정 | `docker info |
| k3s Pod pull 실패 | Docker 설정만 하고 k3s registries.yaml 미설정 | /etc/rancher/k3s/registries.yaml |
| 특정 노드에서만 pull 실패 | localhost mirror를 썼지만 해당 노드에 registry 없음 | 각 노드에서 curl http://localhost:20002/v2/_catalog |
flannel image가 안 보임 | k3s 내부 방식으로 flannel 동작 가능 | `ip a |
| compose validation 실패 | environment 키 오타 | docker compose config |
최종 구조
이번 구성의 핵심은 bootstrap registry를 과도하게 키우지 않는 것이다.
[bootstrap registry]
├─ rancher 하위 k3s 시스템 이미지
└─ Nexus 실행에 필요한 이미지
[Nexus]
└─ 일반 workload 이미지
정리하면 다음 흐름이 된다.
1. k3s core component가 bootstrap registry에서 필요한 이미지를 pull
2. Nexus Pod가 bootstrap registry에서 자기 실행 이미지를 pull
3. Nexus가 정상 기동
4. 이후 workload는 Nexus에서 pull
이 구조는 registry 전체 HA를 보장하지는 않는다. 하지만 Nexus가 자기 자신에게 의존해서 다시 뜨지 못하는 bootstrap deadlock을 끊는 데에는 충분하다.