컨테이너 오케스트레이션 심화 — 스케줄러·DNS·CNI·스토리지·프로덕션 패턴

이 글의 핵심

오케스트레이터는 "컨테이너를 어디에 띄울지, 어떻게 붙일지, 디스크는 어떻게 줄지"를 지속적으로 결정합니다. 이 글은 Kubernetes와 Docker Swarm의 스케줄링·DNS·오버레이 네트워크·영구 스토리지·프로덕션 운영 패턴을 내부 동작 수준에서 정리합니다.

들어가며: 오케스트레이션의 네 축

컨테이너 오케스트레이션은 배치(스케줄링), 연결(네트워크·DNS), 상태(스토리지), 운영 규율(가용성·배포 전략)의 네 축으로 이해하면 내부 구조가 선명해집니다. 아래에서는 Kubernetes(이하 K8s)와 Docker Swarm을 대비하며, 각 축이 어떤 컴포넌트로 구현되는지와 프로덕션에서의 트레이드오프를 다룹니다.


스케줄러 알고리즘: Kubernetes와 Docker Swarm

Kubernetes kube-scheduler와 스케줄링 프레임워크

K8s에서 새 파드(또는 바인딩되지 않은 파드)는 스케줄러가 적합한 노드 하나에 할당합니다. 현재 kube-scheduler는 스케줄링 프레임워크(Scheduling Framework) 위에서 동작하며, 각 단계에 플러그인을 끼울 수 있는 구조입니다. 개념적인 흐름은 다음과 같습니다.

  1. 큐잉: 스케줄링이 필요한 파드를 워크 큐에서 꺼냅니다.
  2. 필터링(Filter / Predicates에 해당하는 확장): 노드가 하드 제약을 만족하는지 판별합니다. 예: CPU·메모리 요청량(requests)과 할당 가능 용량, 노드 셀렉터, 테인트/톨러레이션, 노드 어피니티, 파드 어피니티/안티 어피니티, 볼륨 토폴로지 제약(특정 존/리전에 볼륨이 있어야 함), 일부 스토리지는 지연 바인딩과 결합됩니다.
  3. 스코어링(Scoring / Priorities에 해당): 필터를 통과한 노드들에 점수를 매겨 최적 노드를 고릅니다. 예: 리소스 잔여량 균형, 파드 간 스프레드(같은 노드에 몰리지 않게), 이미지 로컬 캐시 존재 여부 등.
  4. 바인딩: 선택된 노드를 파드의 spec.nodeName에 기록하며, 해당 노드의 kubelet이 이를 보고 실제로 컨테이너를 기동합니다.

선점(Preemption): 큐에 있던 파드가 스케줄될 수 없을 때(리소스 부족 등), 스케줄러는 우선순위가 낮은 실행 중 파드를 축출해 자리를 만들 수 있습니다. 이는 클러스터 전체의 SLA·우선순위 클래스 설계와 직결되므로, 운영 정책(어떤 워크로드가 양보하는지)을 명시해야 합니다.

운영에서 자주 부딪히는 지점도 이 흐름에서 나옵니다. 스케줄러는 실제 사용량이 아니라 requests만 보고 노드 용량을 계산하므로, requests를 실제보다 낮게 잡으면 한 노드에 파드가 과하게 몰리고 메모리 압박이 오면 kubelet이 파드를 축출(eviction)합니다. 이때 requests를 초과해 쓰는 파드가 먼저 축출 대상이 됩니다. 안티 어피니티는 가용성을 높여 주지만 조건을 만족하는 노드 조합이 없으면 파드가 Pending에 머물므로, kubectl describe pod의 이벤트에서 어떤 필터에 걸렸는지 확인해야 합니다. DaemonSet 파드도 Kubernetes 1.12부터는 기본 스케줄러가 노드 어피니티를 이용해 배치하므로, 노드 리소스가 부족하면 DaemonSet 파드 역시 Pending이 될 수 있습니다.

Docker Swarm의 배치 전략

Docker 엔진에 내장된 Swarm 모드(swarmkit)의 스케줄러는 구조가 단순합니다. 먼저 제약 조건과 리소스 예약(--reserve-cpu, --reserve-memory)을 만족하지 못하는 노드를 걸러 내고, 남은 노드 중 해당 서비스의 태스크가 가장 적은 노드에 새 태스크를 배치하는 스프레드 방식으로 동작합니다. 예전 독립형 Docker Swarm(classic)에는 binpack과 random 전략이 있었지만 Swarm 모드에는 이 선택지가 없습니다.

  • 제약 조건(--constraint): node.labels.zone==east, node.role==worker처럼 후보 노드를 제한합니다.
  • 배치 선호(--placement-pref spread=node.labels.zone): 라벨 값별로 태스크를 고르게 나눠 존 단위 분산을 만듭니다.
  • 모드: replicated는 지정한 복제 수만큼 태스크를 띄우고, global은 조건에 맞는 모든 노드에 태스크를 하나씩 띄웁니다. 모니터링 에이전트처럼 노드마다 필요한 서비스에 씁니다.

Kubernetes의 필터·스코어 플러그인 체계와 비교하면 Swarm은 조절할 수 있는 항목이 적은 대신 동작을 예측하기 쉽습니다.


서비스 디스커버리와 DNS

Kubernetes: CoreDNS와 서비스 레코드

클러스터 내부 DNS는 보통 CoreDNS가 담당합니다. 핵심은 서비스가 안정적인 이름을 제공한다는 점입니다.

  • 클러스터 DNS 이름: my-svc.my-namespace.svc.cluster.local 형태의 FQDN이 일반적입니다. 같은 네임스페이스에서는 my-svc만으로도 조회됩니다.
  • ClusterIP 서비스: DNS 조회 결과는 서비스의 가상 IP(ClusterIP)이고, 이 IP로 보낸 패킷을 각 노드의 kube-proxy가 설정한 iptables·IPVS·nftables 규칙이 백엔드 파드 중 하나로 전달합니다.
  • 헤드리스 서비스(clusterIP: None): 단일 가상 IP 없이 DNS A/AAAA 레코드가 파드 IP 목록으로 풀립니다. 스테이트풀셋의 개별 파드 DNS(pod-name.service.namespace...)와 궁합이 좋으며, 클라이언트가 모든 레플리카를 직접 순회해야 할 때 유용합니다.

즉 서비스 디스커버리는 두 층으로 나뉩니다. DNS는 이름을 ClusterIP로 바꿔 주고, 그 뒤에서 kube-proxy(또는 이를 대체하는 CNI 데이터플레인)가 EndpointSlice 변화에 맞춰 ClusterIP에서 실제 파드 IP로 가는 규칙을 갱신합니다. 파드가 늘거나 옮겨 가도 DNS 응답은 그대로이고, 바뀌는 것은 데이터플레인 규칙뿐입니다.

운영 시 DNS 타임아웃·NXDOMAIN은 CoreDNS 자체 이슈뿐 아니라 네임스페이스·서비스 이름 오타, 네트워크 정책으로 DNS(보통 kube-system의 CoreDNS)까지 막힌 경우도 흔합니다.

Docker Swarm: VIP와 DNSRR

Swarm 서비스는 가상 IP(VIP) 모드가 기본입니다. 같은 오버레이 네트워크에서 서비스 이름을 조회하면 VIP가 나오고, 커널의 IPVS 기반 L4 로드밸런서가 이를 태스크들로 분산합니다.

  • DNSRR(--endpoint-mode dnsrr): DNS가 태스크 IP 목록을 돌려줍니다. 클라이언트 측 로드밸런싱·헬스체크 패턴과 맞물릴 때 선택합니다. 반면 짧은 TTL·캐시 이슈에 더 민감할 수 있습니다.

대응 관계로 보면 Kubernetes의 ClusterIP 서비스는 Swarm의 VIP 모드에, 헤드리스 서비스는 DNSRR 모드에 가깝습니다.


네트워크 오버레이: Flannel과 Calico

CNI가 하는 일

컨테이너 런타임이 만든 네트워크 네임스페이스에 CNI(Container Network Interface) 플러그인이 IP를 붙이며, 라우팅·브리지·오버레이를 구성합니다. 오케스트레이터는 “파드 CIDR을 노드에 나눠 준다”는 상위 정책을 두며, CNI가 실제 데이터플레인을 완성합니다.

Flannel: 경량 오버레이

Flannel은 대표적으로 VXLAN 오버레이로 노드 간 L2/L3 터널을 만들어 모든 파드가 클러스터 전체에서 라우팅 가능하게 합니다. host-gw 모드는 오버레이 대신 호스트 라우팅 테이블에 경로를 박는 방식으로, 환경에 따라 오버헤드가 적을 수 있지만 L2 인접성 요구가 있습니다.

구성이 단순해서 파드 간 연결을 빠르게 갖추는 데 유리합니다. 다만 Flannel은 Kubernetes NetworkPolicy를 구현하지 않으므로, Flannel만 설치한 클러스터에서는 NetworkPolicy 리소스를 만들어도 아무 효과가 없습니다. 정책이 필요하면 Flannel 연결에 Calico 정책 엔진을 얹은 Canal 구성을 쓰거나 다른 CNI를 선택합니다.

Calico: 라우팅·정책·eBPF

Calico는 전통적으로 BGP로 파드 서브넷을 노드 밖에 라우팅 전파하는 모델이 강점입니다(오버레이 없이도 동작 가능한 토폴로지가 있으면 성능·관찰성에 유리). 또한 NetworkPolicy로 L3/L4 정책(네임스페이스·라벨 셀렉터 기반)을 강하게 지원합니다.

최근 데이터플레인으로 eBPF 옵션을 쓰면 kube-proxy 우회·성능 이점을 노릴 수 있지만, 커널 버전·디버깅 난이도가 올라갑니다.

관점Flannel 중심Calico 중심
목표빠른 연결·단순 오버레이정책·BGP·대규모 라우팅
정책NetworkPolicy 미지원(별도 엔진 필요)NetworkPolicy와 자체 정책 리소스 지원
운영단순BGP·eBPF 등 옵션만큼 복잡도 증가 가능

스토리지 오케스트레이션: PV·PVC·StorageClass

추상화 계층

  • PV(PersistentVolume): 클러스터 수준의 스토리지 조각(NFS, CSI 볼륨, 클라우드 디스크 등).
  • PVC(PersistentVolumeClaim): 파드가 요청하는 용량·액세스 모드·스토리지 클래스.
  • StorageClass: 동적 프로비저닝의 템플릿. provisioner, parameters로 실제 스토리지 백엔드(CSI 드라이버)를 가리킵니다.

애플리케이션 매니페스트는 보통 PVC만 참조하며, 관리자가 스토리지 클래스와 바인딩 정책을 맡습니다.

액세스 모드와 바인딩

  • ReadWriteOnce(RWO): 한 노드에서 읽기/쓰기로 마운트합니다(일반 블록 디스크). 같은 노드의 여러 파드는 함께 쓸 수 있으며, 파드 하나로 제한하려면 ReadWriteOncePod를 씁니다.
  • ReadOnlyMany(ROX) / ReadWriteMany(RWX): 다수 노드 접근이 필요한 워크로드(일부 NFS·파일 스토리지).

WaitForFirstConsumer 볼륨 바인딩 모드에서는 PVC가 생성 즉시 PV에 묶이지 않고, 그 PVC를 쓰는 파드가 스케줄될 노드가 정해진 뒤에 그 노드의 존에 맞는 볼륨을 프로비저닝하거나 바인딩합니다. 존·리전에 묶인 디스크와 스케줄러를 맞출 때 중요합니다.

운영 시나리오

PVC가 Pending이면 스토리지 클래스가 지정되지 않았거나 기본 클래스가 없는 경우, 프로비저너가 실패한 경우, 요청한 액세스 모드를 백엔드가 지원하지 않는 경우를 먼저 의심합니다. WaitForFirstConsumer 모드에서는 파드가 아직 스케줄되지 않았다면 PVC가 Pending인 것이 정상입니다. 파드는 노드에 배치됐는데 마운트에 실패한다면 노드 쪽 CSI 드라이버, 클라우드 권한(IAM), 파일시스템 마운트 옵션을 확인합니다. 백업은 CSI 스냅샷 API나 벤더 도구로 구성하되, 실제 복구 절차와 목표 복구 시간을 함께 문서화해 둡니다.

Swarm은 볼륨 드라이버·로컬 vs 클러스터 볼륨 모델로 유사 문제를 다루지만, K8s만큼 PV/PVC/StorageClass 계층이 표준화되어 있지는 않습니다. 팀이 K8s로 이전 중이라면 CSI 기반 설계를 기준으로 삼는 편이 이후 이식에 유리합니다.


프로덕션 오케스트레이션 패턴

가용성과 안전한 롤링

  • PodDisruptionBudget(PDB): Eviction API를 거치는 자발적 중단(kubectl drain, 클러스터 업그레이드 중 노드 교체)에서 minAvailable 또는 maxUnavailable로 동시에 내려갈 수 있는 파드 수를 제한합니다. Deployment 자체의 롤링 업데이트는 PDB가 아니라 maxUnavailable/maxSurge 설정을 따르므로 둘을 따로 맞춰야 합니다.
  • topologySpreadConstraints: 존(availability zone)·호스트명 등 토폴로지에 파드를 분산합니다. 단일 존 장애에 대비합니다.
  • readiness / liveness: readiness가 실패한 파드는 서비스 엔드포인트에서 빠지고, liveness가 실패한 컨테이너는 재시작됩니다. liveness를 너무 민감하게 잡거나 기동이 느린 앱에 startupProbe 없이 걸면 재시작 루프가 생깁니다.

배포와 GitOps

  • 롤링·카나리·블루그린: 배포 리소스(Deployment 등)의 전략과 Ingress/서비스 가중으로 점진적 전환을 조합합니다.
  • GitOps(Argo CD·Flux 등): 선언적 상태를 Git에 두고 클러스터가 수렴합니다. 변경 추적·롤백에 강하지만, 시크릿 관리·드리프트 대응 프로세스가 필요합니다.

오토스케일링과 비용

HPA는 CPU·메모리·커스텀 메트릭을 기준으로 파드 수를 조절하고, VPA는 파드의 requests를 조정합니다. Cluster Autoscaler는 Pending 파드가 생기면 노드를 추가하고 활용도가 낮은 노드를 줄입니다. HPA와 VPA를 같은 CPU·메모리 지표에 함께 걸면 서로 반대로 움직일 수 있으므로, 한 지표에는 하나만 적용하는 것이 일반적입니다.

관찰 가능성

분산 시스템에서는 메트릭·로그·트레이스가 필수입니다. CNI·CSI·DNS·스케줄러 이벤트는 각각 다른 컴포넌트에 로그가 흩어지므로, 장애 시 징후(어느 레이어에서 끊겼는지)를 빠르게 좁히는 런북을 마련해 두는 것이 좋습니다.


자주 묻는 질문 (FAQ)

Q. 노드 드레인이나 클러스터 업그레이드 중에 서비스가 잠깐 끊기지 않게 하려면 무엇을 설정하나요?

A. PodDisruptionBudget(PDB)으로 노드 드레인이나 롤링 업그레이드 같은 자발적 중단 때 동시에 내려갈 수 있는 파드 수를 제한합니다. 여기에 topologySpreadConstraints로 파드를 여러 노드와 가용 영역에 나눠 두고, readiness probe로 준비가 끝난 파드에만 트래픽이 가게 해야 합니다. Deployment 롤링 업데이트 중의 공백은 PDB가 아니라 maxUnavailable/maxSurge로 조절합니다. liveness probe를 너무 민감하게 잡으면 재시작 루프가 생길 수 있으므로 임계값은 실제 기동 시간에 맞춰 설정해야 합니다.


같이 보면 좋은 글