들어가며
Kubernetes에서 오토스케일로 노드가 늘어날 때마다 같은 이미지를 사설 레지스트리(JFrog, Harbor 등)에서 반복 pull 하면 배포가 느려집니다.
이번에 Spegel을 도입해, 노드풀 단위 P2P 레지스트리 미러를 구성하고 해피패스까지 검증했습니다.
핵심은 다음과 같습니다.
- 레지스트리 mirror + P2P로 이미 노드에 있는 이미지 레이어를 피어에서 받기
- 노드풀별로 독립 배포 (워크로드 A / 워크로드 B 분리)
- GitOps(ArgoCD)가 아닌 수동
kubectl kustomize --enable-helm | kubectl apply
Spegel이란?

Spegel은 Kubernetes 노드마다 DaemonSet으로 뜨는 P2P OCI 레지스트리 미러입니다.
동작 흐름:
- containerd가 외부 레지스트리 pull 시도
- Spegel mirror가 가로챔
- 같은 클러스터(또는 같은 Spegel 그룹) 노드에 이미지가 있으면 P2P로 전송
- 없으면 upstream(사설 레지스트리)으로 fallback
[노드 A - 이미지 보유] [노드 B - 신규/오토스케일]
containerd <── P2P ── Spegel:5000 ──> Spegel:5000 <── containerd
│ │
└──────────── Registry (fallback) ─────────────┘
환경과 설계
| 항목 | 워크로드 A 노드풀 | 워크로드 B 노드풀 |
|---|---|---|
| Overlay 경로 | clusters/<cluster>/spegel-workload-a |
clusters/<cluster>/spegel-workload-b |
| Namespace | spegel-workload-a |
spegel-workload-b |
| nodeSelector | agentpool=workload-a |
agentpool=workload-b |
| NodePort | 30021 | 30022 |
| 대상 워크로드 | app-dev, app-demo (같은 노드풀) | office-app-dev, office-app-demo |
Chart / Image
- Helm chart: 0.7.3 (
base/spegel/charts/spegel-0.7.3/) - Image:
registry.example.com/spegel-org/spegel:sha-97d6dabc(사내 미러 경유) registry-pull-secret은 Git에 넣지 않고, 배포 후 기존 ns에서 복사
ArgoCD
- Spegel 자체는 Argo 미관리 (수동 apply/delete)
app-demo앱은 Argo Application으로 모니터링만 등록 가능 (배포는kubectl apply)
디렉터리 구조
base/spegel/charts/spegel-0.7.3/
clusters/<cluster-name>/
├── spegel-workload-a/
│ ├── kustomization.yaml
│ ├── values.yaml
│ ├── namespaces.yaml
│ └── patches/
│ ├── daemonset-hostnetwork.yaml
│ └── delete-cleanup-hooks.yaml
└── spegel-workload-b/
└── (동일 구조, nodePort 30022, agentpool=workload-b)
배포 명령 (정답 패턴)
워크로드 A 노드풀
kubectl kustomize --enable-helm clusters/<cluster>/spegel-workload-a | kubectl apply -f -
kubectl get secret registry-pull-secret -n <source-ns> -o yaml \
| sed 's/namespace: <source-ns>/namespace: spegel-workload-a/' | kubectl apply -f -
워크로드 B 노드풀
kubectl kustomize --enable-helm clusters/<cluster>/spegel-workload-b | kubectl apply -f -
kubectl get secret registry-pull-secret -n <source-ns> -o yaml \
| sed 's/namespace: <source-ns>/namespace: spegel-workload-b/' | kubectl apply -f -
주의: --enable-helm은 kubectl apply -k가 아니라 kubectl kustomize --enable-helm 에 붙입니다.
삭제 (fallback 테스트)
kubectl kustomize --enable-helm clusters/<cluster>/spegel-workload-a | kubectl delete -f -
values.yaml 핵심
nodeSelector:
agentpool: workload-a # workload-b는 agentpool=workload-b
service:
registry:
hostPort: 5000 # hostNetwork 시 containerPort와 동일 필수
nodePort: 30021 # workload-b는 30022
port: 5000
spegel:
mirroredRegistries:
- "https://registry.example.com"
containerdMirrorAdd: true
트러블슈팅 요약
1. no route to host (192.168.x.x:5001)
- 원인: P2P가 Pod overlay IP를 쓰는데, 오토스케일 노드 간 overlay는 라우팅이 안 됨
- 해결: DaemonSet에
hostNetwork: true,dnsPolicy: ClusterFirstWithHostNet. P2P는 노드 IP(서브넷 대역, 예: 10.x.x.x) 로 통신
2. hostPort 3020 must match containerPort
- 원인:
hostNetwork: true일 때 hostPort ≠ containerPort(5000) - 해결:
hostPort: 5000으로 통일
3. Kustomize patch 실패 [noNs]
- 해결: patch
target에namespace명시
4. Helm post-delete cleanup이 전 노드에 뜸
- 해결:
delete-cleanup-hooks.yaml로 cleanup DS/Service/Pod를 kustomize render에서 제외
5. 로그의 broken pipe, BLOB_UNKNOWN, context canceled
- 대부분 정상적인 P2P race — 여러 피어가 동시에 응답하거나, 아직 blob이 없는 피어에 찔렀다가 registry/다른 피어로 fallback
OCI event/CREATE가 이어지고 Pod가Running이면 OK
6. no distribution source labels
- containerd 메타데이터 이슈. P2P 광고율만 약간 떨어질 수 있고, pull 자체를 막지는 않음
해피패스 검증
시나리오: workload-a 노드풀 오토스케일 → 새 노드에서 앱 이미지 pull
정상 신호
- Spegel Pod 전부
Running, Pod IP = 노드 IP (hostNetwork) - bootstrap endpoints가 노드 IP 대역과 일치
- 로그:
bootstrap completed connectivity is reached,OCI event/CREATE - 트래픽:
노드IP:5000(더 이상192.168.x.x:5001 no route없음) - 앱 Pod
ImagePullBackOff없음
측정
- 동일 조건 전/후 초 단위 비교는 공식 벤치마크는 별도 수행 필요
- 예: 대형 앱 이미지(
270MB) pull 이벤트 **12초** (Spegel ON 상태, 단일 관측값) - “몇 초 줄었는지”는 Spegel OFF vs ON을 같은 새 노드에서 재현해야 함
app-demo와의 관계
spegel-workload-a는 네임스페이스가 아니라 노드풀 단위입니다.
app-demo도 node-pool: workload-a로 같은 노드풀을 쓰므로, 별도 Spegel 없이 spegel-workload-a만 켜두면 mirror/P2P 혜택을 받습니다.
app-demo 배포 (Argo Sync 대신):
kubectl kustomize clusters/k8s/overlays/app-demo | kubectl apply -f -
kubectl get secret registry-pull-secret -n <source-ns> -o yaml \
| sed 's/namespace: <source-ns>/namespace: app-demo/' | kubectl apply -f -
Argo UI에 Application만 등록:
kubectl apply -f clusters/k8s/base/argocd/application-app-demo.yaml
argocd app sync app-demo # 리소스 트리 표시용 1회
한 장 요약 — “누가 뭘 소유하나”
┌─────────────────────────────────────────────────────────────┐
│ Git (gitops-repo) │
│ • spegel kustomization / values / patches │
│ • vendored chart spegel-0.7.3 │
│ • app-demo overlay (secret 제외) │
│ • application-app-demo.yaml (Argo 등록용) │
└──────────────────────────┬──────────────────────────────────┘
│ kubectl kustomize | apply
▼
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes API (클러스터 상태) │
│ • registry-pull-secret (ns별 복사) │
│ • memory-service-secrets (수동 생성) │
│ • agent-workspace-azurefile (수동) │
│ • Spegel DaemonSet / Services │
└──────────────────────────┬──────────────────────────────────┘
│ Spegel init
▼
┌─────────────────────────────────────────────────────────────┐
│ Node (Git이 모름) │
│ • containerd registry mirror (/etc/containerd/certs.d) │
│ • 이미지 레이어 캐시 (P2P 공유 대상) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ .gitignore / local only │
│ • clusters/<cluster>/private/ (트러블슈팅 노트) │
│ • spegel-0.7.3.tgz (임시 pull) │
└─────────────────────────────────────────────────────────────┘
운영 확인 명령 모음
# Spegel Pod
kubectl get pods -n spegel-workload-a -o wide
kubectl get pods -n spegel-workload-b -o wide
# Bootstrap endpoints
kubectl get endpoints -n spegel-workload-a spegel-workload-a-bootstrap
kubectl get endpoints -n spegel-workload-b spegel-workload-b-bootstrap
# P2P / mirror 로그
kubectl logs -n spegel-workload-a -l app.kubernetes.io/instance=spegel-workload-a --since=15m --tail=100 \
| grep -E 'OCI event|connectivity|broken pipe|BLOB_UNKNOWN|no route'
# app-demo Pod
kubectl get pods -n app-demo
kubectl get pods -n app-demo | grep -v Running
Spegel 도입으로 얻은 것
- 노드풀별 독립 ON/OFF 가능한 P2P 미러
- 오토스케일 시 레지스트리 egress 절감 + pull 가속 가능성 (정량은 별도 벤치 필요)
- AKS + overlay 네트워크 환경에서는
hostNetwork가 사실상 필수에 가까움
Spegel은 “GitOps로 선언만 하고 끝”이 아니다. Chart와 values는 Git에 두고, registry secret·Argo 미관리 배포·노드 containerd 상태는 Git 밖에서 운영한다. 특히 pull secret 복사 순서와
hostNetwork패치가 없으면 P2P는 문서만 있고 실제로는 동작하지 않는다.
사진 출처 : https://spegel.dev/
'Kubernetes' 카테고리의 다른 글
| Kubernetes User Namespace 정리: 왜 필요하고, AKS에서 어디까지 가능한가 (0) | 2026.06.10 |
|---|---|
| Ingress NGINX에서 AKS Istio Gateway API로의 전환 (0) | 2026.03.30 |
| Private AKS 내부망에서 Harbor OCI Registry를 활용한 Container 환경 구성 (1) | 2025.08.28 |
| kubeadm 기반 바닐라 Kubernetes 클러스터를 Terraform + Cloud-init 자동화로 설치/운영 (2) | 2025.08.12 |
| Kubernetes에서 외부로부터 들어오는 요청의 Source IP 파악하기 (0) | 2025.07.30 |