궁금한게 많은 개발자 노트

[ K8S ] 쿠버네티스 네트워크 모델 본문

DevOps

[ K8S ] 쿠버네티스 네트워크 모델

궁금한게 많은 개발자 2025. 4. 24. 17:00

쿠버네티스의 모든 파드는 고유한 IP를 가집니다. 이는 즉 파드간 연결을 명시적으로 만들 필요가 없으며, 컨테이너 포트를 호스트 포트에 매핑할 필요가 없음을 의미합니다. 이를 통해 포트 할당, 네이밍, 서비스 디스커버리, 로드 밸런싱, 애플리케이션 구성, 마이그레이션 관점에서 파드를 VM / 물리 호스트처럼 다를 수 있는 깔끔한 모델이 제시됩니다.

 

또한, 쿠버네티스의 파드는 NAT없이 노드 상의 모든 파드와 통신이 가능하며, 노드 상의 에이전트(시스템 데몬, kubelet)는 해당 노드의 모든 파드와 통신할 수 있음이 전제되어 있습니다. 쿠버네티스에서 IP주소는 파드 범주에 존재하며 파드 내의 컨테이너들은 IP주소, MAC 주소를 포함하는 네트워크 네임스페이스를 공유합니다. (네트워크 네임스페이스란 독립된 가상의 네트워크 공간)

이는 곧 파드 내의 컨테이너들은 각자의 포트에 localhost로 접근할 수 있음을 의미하며 파드 내에서는 고유한 포트를 사용합니다. 이러한 것들이 어떻게 구현되는지는 컨테이너 런타임의 상세사항으로 CNI와 관련이 있을 수 있습니다. 

위에서 정의된 내용에 의해 쿠버네티스 네트워킹은 다음 네 가지 문제를 해결합니다.

  • 파드 내 컨테이너는 루프백을 통한 네트워킹을 사용하여 통신 > 127.0.0.1을 통해 로컬호스트로 서로 통신이 가능
    하나의 네트워크 네임스페이스를 공유하기에 가능
  •  클러스터 네트워킹은 서로 다른 파드 간의 통신을 제공
  • 서비스 API를 사용하면 파드에서 실행 중인 애플리케이션을 클러스터 외부에서 접근 가능
    Ingress는 HTTP애플리케이션, 웹사이트, API를 노출하기 위한 추가 기능을 제공
  •  서비스를 통해 Cluster IP로 내부에서만 사용하도록 게시할 수도 있음

 

 

이제 위에서 정리한 내용을 토대로 네트워크 모델에 대해 살펴보고자 합니다. 우선, 쿠버네티스 네트워크 모델은 각 노드의 컨테이너 런타임에 의해 구현됩니다. 이때, 가장 일반적인 컨테이너 런타임은 CNI 플러그인을 사용하여 네트워크 및 보안 기능을 관리합니다. 여러 공급 업체의 다양한 CNI가 존재하며 이들 중 일부는 네트워크 인터페이스를 추가 및 제거하는 기본 기능만 제공하는 반면 다른 일부는 컨테이너 오케스트레이션 시스템과의 통합, 여러 CNI플러그인 실행, 고급 IPAM 기능 등과 같은 보다 정교한 솔루션을 제공합니다. 

https://github.com/kubernetes/design-proposals-archive/blob/main/network/networking.md

https://kubernetes.io/ko/docs/concepts/cluster-administration/addons/#network-and-networking-policy

 

 

 

컨테이너 런타임 (containerd, CRI-O 등)

파드가 노드에서 정상 실행될 수 있도록 클러스터의 각 노드에 컨테이너 런타임을 설치해야 합니다. 컨테이너 런타임은 컨테이너 런타임 인터페이스 (CRI)를 만족하는 실행 가능한 프로그램으로 containerd, CRI-O, docker engine등이 존재합니다.

그렇다면, 컨테이너 런타임은 어떤 원리로 동작하며 어떤 역할을 수행하는지 알아보려 합니다. 우선, 리눅스에서 cgroup은 프로세스에 할당된 리소스를 제한하는데 사용됩니다. kubelet과 컨테이너 런타임은 모두 그와 연계된 cgroup들과 상호 작용을 해야 하는데 그 이유는 cgroup을 통해 동일하게 파드 및 컨테이너의 자원을 관리하기 때문입니다. 그렇기에 kubelet과 컨테이너 런타임이 같은 cgroup 드라이버를 사용해야 하며, 구성도 동일해야 합니다. (cgroupfs, systemd 사용 가능)

 

예를 들어, systemd를 init 시스템으로 사용하고 있는 노드에서 kubelet과 컨테이너 런타임이 cgroupfs 드라이버를 사용하게 되면 그 시스템은 두 개의 서로 다른 cgroup관리자를 갖게 되어 사용 중인 자원에 대해 두 곳에서 제어하려 하며 혼동을 초래합니다.

불안정성을 줄이기 위해 systemd가 init 시스템으로 선택되었을 때는 kubelet과 컨테이너 런타임의 cgroup드라이버로 systemd를 사용해야 합니다. (kubeletconfiguration의 cgroupdriver옵션을 수정하여 설정, 각 컨테이너 런타임의 cgroup드라이버 설정은 문서 참고)

 

이제 컨테이너 런타임의 종류에 대해 알아보겠습니다.

컨테이너 런타임은 크게 저수준과 고수준으로 나뉩니다. 저수준 런타임은 네트워크 네임스페이스와 cgroup, 루트 디렉터리 격리 등을 통해 컨테이너 생성하고 실행를하며,  고수준은 논리적으로 실행됩니다. 조금 더 상세히 설명하면 저수준 런타임은 실제 리눅스 기술을 사용해 컨테이너 프로세스를 실행하며, cgroup/namespace를 통해 프로세스를 격리합니다. 대표적인 예로는 runc, crun, youki등이 있습니다. 이러한 저수준 런타임을 내부적으로 실행하는 것이 고수준 런타임이며, 컨테이너 생성/관리 전체 흐름을 조율합니다.

여러 컨테이너 이미지 관리, 이미지 pull, 저장, 네트워크 설정 등 종합 관리를 진행하며 containerd, CRI-O, Docker등이 있습니다. kubelet이 고수준 런타임과 gRPC(CRI)로 통신하게 됩니다. 고수준 런타임은 저수준 런타임을 선택해서 사용할  수 있도록 설계되어 있고 저수준 런타임은 뒤에 나올 OCI Runtime Spec을 따라야 합니다. 이에 저수준 런타임이 바뀌어도 고수준 런타임쪽에서는 변경사항이 거의 없습니다.

 

여기서 CRI에 대해 짚고 넘어가면, 이전에는 쿠버네티스가 Docker런타임만 사용했으나 Docker는 쿠버네티스를 위해 만들어진 런타임이 아니므로 불필요한 레이어가 많고, Docker만 지원하게되면 다른 런타임 사용에 어려움이 있습니다.

그래서 런타임과 kubelet사이에 추상화된 표준 인터페이스를 생성하여 여러 런타임을 사용할 수 있고 교체도 쉽게 하기 위해 만들어진 것이 CRI(Container Runtime Interface)입니다. (내부적으로는 RuntimeServie, ImageService로 구성) CRI를 구현한 주요 컨테이너 런타임은 CRI-O, containerd가 있으며 쿠버네티스와 긴밀한 통합을 위해 설계된 경량 컨테이너 런타임입니다.

 

OCI는 컨테이너 이미지 및 런타임에 대한 개방형 표준을 정의하는 리눅스 재단 산하의 오픈 소스 프로젝트입니다. 위에서 언급했듯 저수준 런타임은 OCI를 준수해야 하며, 고수준 런타임 종류 중 하나인 CRI-O는 쿠버네티스의 CRI를 구현하여 OCI를 준수하는 컨테이너 런타임과 통합할 수 있도록 설계된 경량 컨테이너 런타임입니다.

 

이제 위에서 설명한 내용을 토대로 파드 생성 과정에 대해 알아보려 합니다. kubelet이 파드 생성 요청을 받으면, containerd등의 컨테이너 런타임에게 gRPC로 통신하여 명령을 내립니다. gRPC는 CRI에 맞게 선택된 통신 프로토콜로 CRI가 gRPC를 사용하도록 의도적으로 설계되었습니다. (멀티 플렉싱, 바이너리 프로토콜로 매우 빠르고 가벼움) 런타임은 .proto 파일에 따라 서버를 구현하고, kubelet은 해당 .proto파일에 따라 클라이언트를 구현해 런타임에게 요청을 보냅니다. 요청 순서는 다음과 같습니다. (RunPodSandBox > PullImage > CreateContainer > StartContainer, 주기적으로 ListContainer) kubelet으로부터 요청을 받은 고수준 런타임은 내부적으로 저수준 런타임에게 파드 생성 관련 명령을 전달하여 실제 파드가 생성됩니다.

 

 

 

CNI(Container Network Interface)란 CNCF(Cloud Native Computing Foundation)의 프로젝트 중 하나로 컨테이너 간의 네트워킹을 제어할 수 있는 플러그인을 만들기 위한 표준이며, 컨테이너 런타임과 네트워크 플러그인 사이의 인터페이스 사양입니다.

 

쿠버네티스에서는 파드간의 통신을 위해 CNI를 사용하고 쿠버네티스 뿐만 아니라 Amazon ECS, Cloud Foundry 등 컨테이너 런타임을 포함하고 있는 다양한 플랫폼들은 CNI를 사용합니다. 쿠버네티스는 기본적으로 kubenet이라는 자체적인 CNI플러그인을 제공하지만, 네트워크 기능이 매우 제한적인 단점이 존재합니다. 그 단점을 극복하기 위해 3rd-party CNI 플러그인 Bridge, Flannel, Calico, Weavenet, NSX 등이 존재합니다.

 

네트워크 플러그인은 컨테이너에 네트워크 기능을 부여하는 CNI 스펙을 따르는 실행 가능한 프로그램입니다. 파드에 고유한 IP를 할당하거나 가상 네트워크 인터페이스 생성, 노드간 통신을 위한 라우팅/터널 구성, 네트워크 정책 적용 등의 실제 네트워크 관련 기능을 수행합니다.

ex) 쿠버네티스의 NetworkPolicy 리소스를 적용하려면, 해당 네트워크 플러그인이 정책을 지원해야

네트워크 플러그인 브리지 사용 여부 설명
bridge (기본 플러그인) O cni0 브리지를 만들어 Pod 인터페이스 연결
flannel (vxlan 모드) cni0 사용, 내부적으로 bridge 사용
calico (L3 모드) 브리지 사용 안 함. IP 라우팅 기반 (직접 노드 간 BGP)
weave O/X 자체 Overlay 네트워크 구현, 브리지 없이 직접 연결 (필요 시 생성)
kubenet O cbr0라는 브리지를 생성해 veth 연결
cilium eBPF 기반으로 브리지 없이 작동

 

[ 기본 흐름 - Bridge Network plugin (노드 내부 통신만 가능) ]

  1. 컨트롤 플레인의 API서버로부터 파드 생성 요청 → 쿠버네티스가 해당 노드의 kubelet에게 요청 전달
    • 각 노드의 kubelet에서는 어떤 네트워크 플러그인을 사용할지 설정하며 CNI 구현체인 네트워크 플러그인 바이너리가 존재하는 디렉토리, 설정 파일 디렉토리 등을 명시 (k8s v1.24 이전)
    • CNI 관리는 컨테이너 런타임에서 관리하는 것으로 변경 (k8s v1.24 이후)
  2. kubelet은 container runtime (예: containerd)에 컨테이너 생성 요청
  3. container runtime은 CNI 구현체인 네트워크 플러그인 실행
    • $CNI_PATH/<plugin>을 실행 (예: bridge, calico, flannel, …)
    • ADD, DEL 등의 명령어로 네트워크 설정
  4. 파드에 가상 네트워크 인터페이스(veth)가 생성되고, IP가 할당되며, 라우팅이 구성됨
    • Bridge 네트워크 플러그인이 cni0이라는 Linux Bridge를 생성
    • Pod용 veth pair 생성 -> 하나는 파드의 eht0에 하나는 호스트 네임스페이스의 cni0 (Linux Bridge)에 연결
      • 이때, cni0은 Linux Bridge 객체이며 네트워크 플러그인은 이 작업을 수행한 Brdige (네트워크 플러그인)
      • Linux Brige는 각 파드의 veth를 갖고 있으며, 브릿지 포트로 각 파드의 veth가 등록되어 전달 (L2스위치)
    • L2스위치로 동작할 때는 MAC 주소를  활용하여 학습하여 그에 맞는 파드의 veth로 전달
      • 파드 A가 트래픽을 보내면 이 트래픽이 cni0에 들어가고, source MAC을 확인하여 vethA에 존재함을 학습
      • 다른 파드가 해당 MAC으로 트래픽을 보내면 cni0는 vethA로 전달하는 방식
      • 만약 등록되지 않은 MAC주소로 보내야할 경우 브로드캐스트 방식으로 모두에게 전달 (flooding)
        • 브로드캐스트가 많이 일어나면 비효율이 커지고 이러한 제어를 Calico/Cilium 고급 플러그인에서는 해결
      • 파드가 종료되면, 해당 veth가 사라지고 Linux Bridge에서도 MAC과 인터페이스 매핑이 자동 제거
항목 CNI (Container Network Interface) 네트워크 플러그인
무엇인가요? 네트워크를 설정할 수 있는 표준/사양(spec) CNI 사양을 구현한 실행 가능한 프로그램
형태 JSON 포맷과 실행 방식이 정의된 인터페이스 /opt/cni/bin/에 있는 바이너리 실행 파일
누가 만드나요? CNCF에서 사양 관리 Calico, Flannel 등 다양한 오픈소스 커뮤니티
예시 ADD, DEL 명령 형식, 네트워크 설정 방식 등 bridge, calico, flannel, weave, cilium
역할 컨테이너에 네트워크 설정을 하기 위한 인터페이스 정의 실제로 IP를 할당하고 인터페이스 연결 등 작업 수행

 

 

 

노드 간 통신 (Bridge + Flannel)

전제하는 조건으로는 모든 파드는 고유한 IP를 가지고 있어야 하며, 모든 파드는 다른 파드와 IP로 직접 통신이 가능해야 하며, 파드간 통신은 노드 내부든 외부든 동일한 방식으로 가능해야 합니다.

  • Node1의 PodA가 Node2의 PodB로 트래픽을 전송
  • PodA > vethA > cni0 L2수준에서 MAC주소를 포고 전달
  • netfilter에 의해 iptables명령을 거쳐 PREROUTING 체인에서 DNAT로 목적지 Pod IP로 변경
  • Node1의 eth0 (Node1 NIC)의 라우팅 테이블로 이동하여 어떤 노드로 전달해야 할지 결정
  • 노드 외부간 통신에는 Bridge CNI로 부족하고 Flannel, Calico, Weave, Cilium같은 CNI 플러그인 필요
    • 노드 간 통신을 위해 VXLAN인터페이스 생성 > 패킷 전달시 encapsulation/decapsulation 담당
  • flannel 인터페이스를 통해 Node2로 전달 
  • Node2의 NIC에서 netfilter PREROUTING
  • Node2의 커널 라우팅 테이블에서 PodB의 CIDR인 것을 확인
  • netfilter가 FORWARD(Pod) or INPUT(Node자체가 목적지 - kubelet), POSTROUTING체인 실행(SNAT가능) > cni0로 전달
  • cni0에서는 PodB의 veth MAC주소를 알고 있으므로 전달하고 veth > PodB내부 eth0으로 도달

파드 간 IP를 통해 직접 통신의 경우에는 CNI 플러그인과 커널 라우팅을 통해 처리하지만, 서비스를 통한 통신은 kube-proxy의 역할이 있습니다. kube-API서버에서 cluster IP서비스에 대한 정보를 watch하고 있다가 받아오고, iptables규칙을 노드에 설정합니다. 설정한 규칙에 의해 커널이 backend 파드 중 하나로 포워딩하게 됩니다. (kube-proxy는 각 서버의 iptables에 모든 노드의 서비스, 파드 IP 등록)

 

kube-proxy역할

kube-proxy는 서비스 레벨에서 통신을 중개하는 역할을 담당하며, 모든 노드에서 실행되는 컴포넌트로 서비스 접근 요청을 적절한 파드로 라우팅/포워딩할 수 있게 도움을 주는 역할을 합니다. 즉, 서비스 IP, Port로 전달된 내/외부 요청을 서비스의 엔드포인트에 해당하는 파드 중 어떤 파드로 포워딩할 지에 대한 규칙을 생성하고 관리하는 역할입니다. 이를 위해 kubeadm은 모든 노드에 kube-proxy pod를 daemonset으로 배포합니다. (pod ip를 어떤 서비스의 endpoint로 정하는 것은 가능하지만, pod가 새로 생성되었을 때 그 주소가 같을 것이라고는 보장하지 못합니다. 이러한 이유로 Service가 필요합니다.) kube-proxy는 각 노드에 daemonset으로 뜨고 각 노드에 개별로 있다고 하더라도 모든 노드에서 실행되고 있는 kube-proxy는 같은 iptable rules를 갖고 있기에 어떤 노드에 어떤 서비스/파드가 있는지 알고 있습니다. 그리고 Pod가 업데이트 되더라도 해당 정보를 kube-apiserver로 부터 etcd에서 정보를 얻어와 iptable을 업데이트합니다. 또한 iptables로 들어간 트래픽은 NAT기능 뿐만 아니라 LB기능도 수행하여 알고리즘에 따라 여러 파드에 분산됩니다.

쿠버네티스에서 파드들은 그 자체로 반영속적(ephemeral)입니다. 필요에 따라 파드들이 각기 다른 노드들에 배치되기도 하며, 때로는 특정 노드가 죽으면서 기존의 파드들이 다른 노드로 옮겨지기도 합니다. 이처럼 노드와 파드의 상태는 언제나 유동적이므로 네트워크 상에서 접근 가능한 IP도 항상 바뀌게 됩니다. 이런 환경에서 파드들 간의 상호 네트워킹을 보장할 수 있는 방법이 바로 서비스(Service)를 이용하는 것입니다.
kube-proxy 모드 설명
iptables (기본) iptables에 DNAT/MASQUERADE 룰 생성 (커널 netfllter를 사용)
L3 (IP) 소스/목적지 IP기반 필터링, L4 (포트,프로토콜) TCP/UDP 포트 기반 필터링 및 NAT
IPVS (성능 모드) 커널 레벨에서 IPVS 라우팅 사용 (더 빠름) 커널 내장 로드밸런서(L4)로 높은 처리량을 유지
다양한 알고리즘 지원, 해시 기반 직접 매핑, 실시간 엔드포인트 반영 빠름, 연결 상태 추척 가능
userspace (거의 안 씀) 실제로 프록시 프로세스로 중계함
NAT 종류 동작 사용처
SNAT (Source NAT) 출발지 IP 주소를 변경 내부 → 외부로 나갈 때
DNAT (Destination NAT) 목적지 IP 주소를 변경 외부 → 내부로 들어올 

 

 

그렇다면 가장 많이 사용되는 AWS EKS에서는 어떤 네트워크 플러그인이 사용되며, 노드간 통신은 어떤 컴포넌트를 거쳐 이루어지는지 자세히 한번 살펴보고자 합니다.

Comments