Back to DevOps

DevOps

k3s Registry Bootstrap: Nexus self-dependency 해소

k3s 환경에서 Nexus가 자기 자신에게 의존해 이미지 pull이 막히는 bootstrap deadlock을 끊기 위해 registry:latest 기반 임시 bootstrap registry를 구성하고 검증한 기록이다.

Published
Updated
Area
Troubleshooting
Type
runbook
Category
DevOps
k3scontainerdNexusDocker Registrybootstrap registryinsecure registryCoreDNSpause image

운영 배경

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
운영 registryNexus
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 agrep 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-pausePod sandbox 생성
rancher/mirrored-coredns-corednsService DNS
rancher/mirrored-library-busybox일부 init/debug 작업에서 사용 가능
rancher/klipper-helmk3s packaged component 설치에 사용
rancher/klipper-lbk3s 기본 LoadBalancer 구현
rancher/local-path-provisionerlocal-path StorageClass
rancher/mirrored-library-traefikk3s 기본 Ingress Controller
rancher/mirrored-metrics-servermetrics-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 중인 상태에서 복사하면 일부 데이터가 깨질 수 있다.

안전한 절차는 다음과 같다.

  1. 노드 A에서 registry를 중지한다.
docker compose down
  1. data 디렉토리를 노드 B로 복사한다.
scp -r ./data <node-b>:/path/to/registry/

또는 rsync를 사용할 수 있다.

rsync -avz ./data <node-b>:/path/to/registry/
  1. 노드 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

증상별 점검표

증상가능 원인확인 명령
curlconnection refusedregistry 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을 끊는 데에는 충분하다.