하이브리드 노드의 Kubernetes 개념

하이브리드 노드의 Kubernetes 개념 (Kubernetes concepts for hybrid nodes)

이 페이지는 EKS Hybrid Nodes 시스템 아키텍처를 뒷받침하는 핵심 Kubernetes 개념을 자세히 설명해요.

출처: 문서

본문

VPC의 EKS 컨트롤 플레인 (EKS control plane in the VPC)

EKS 컨트롤 플레인 ENI의 IP는 default 네임스페이스의 kubernetes Endpoints 객체에 저장돼요. EKS가 새 ENI를 만들거나 이전 ENI를 제거하면 EKS가 이 객체를 업데이트해 IP 목록이 항상 최신 상태로 유지돼요.

이 엔드포인트는 default 네임스페이스에 있는 kubernetes Service를 통해 사용할 수 있어요. ClusterIP 유형인 이 서비스는 항상 클러스터의 서비스 CIDR의 첫 번째 IP를 할당받아요. 예를 들어 서비스 CIDR 172.16.0.0/16의 경우 서비스 IP는 172.16.0.1이 돼요.

일반적으로 이렇게 Pod(클라우드 또는 하이브리드 노드에서 실행되는지 여부와 무관)가 EKS Kubernetes API 서버에 접근해요. Pod는 서비스 IP를 대상 IP로 사용하며, 이는 EKS 컨트롤 플레인 ENI 중 하나의 실제 IP로 변환돼요. 주요 예외는 kube-proxy인데, 변환을 설정하기 때문이에요.

EKS API 서버 엔드포인트 (EKS API server endpoint)

kubernetes 서비스 IP가 EKS API 서버에 접근하는 유일한 방법은 아니에요. EKS는 클러스터를 만들 때 Route53 DNS 이름도 만들어요. 이는 EKS DescribeCluster API 액션을 호출할 때 EKS 클러스터의 endpoint 필드예요.

{
    "cluster": {
        "endpoint": "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.gr7.us-west-2.eks.amazonaws.com",
        "name": "my-cluster",
        "status": "ACTIVE"
    }
}

공용 엔드포인트 접근 또는 공용·프라이빗 엔드포인트 접근 클러스터에서 하이브리드 노드는 기본적으로 이 DNS 이름을 인터넷을 통해 라우팅 가능한 공용 IP로 해석해요. 프라이빗 엔드포인트 접근 클러스터에서는 DNS 이름이 EKS 컨트롤 플레인 ENI의 프라이빗 IP로 해석돼요.

이렇게 kubelet과 kube-proxy가 Kubernetes API 서버에 접근해요. 모든 Kubernetes 클러스터 트래픽이 VPC를 통해 흐르게 하려면 클러스터를 프라이빗 접근 모드로 구성하거나, 온프레미스 DNS 서버를 수정해 EKS 클러스터 엔드포인트를 EKS 컨트롤 플레인 ENI의 프라이빗 IP로 해석하도록 해야 해요.

kubelet 엔드포인트

kubelet은 여러 REST 엔드포인트를 노출해 시스템의 다른 부분이 각 노드와 상호작용하고 정보를 수집할 수 있게 해요. 대부분의 클러스터에서 kubelet 서버로 가는 트래픽의 대부분은 컨트롤 플레인에서 오지만, 특정 모니터링 에이전트가 이와 상호작용할 수도 있어요.

이 인터페이스를 통해 kubelet은 다양한 요청을 처리해요: 로그 가져오기(kubectl logs), 컨테이너 내 명령 실행(kubectl exec), 트래픽 포트 포워딩(kubectl port-forward). 이러한 각 요청은 kubelet을 통해 기본 컨테이너 런타임과 상호작용하며, 클러스터 관리자와 개발자에게 원활해 보여요.

이 API의 가장 일반적인 소비자는 Kubernetes API 서버예요. 앞서 언급한 kubectl 명령을 사용할 때마다 kubectl은 API 서버에 API 요청을 하고, API 서버는 Pod가 실행 중인 노드의 kubelet API를 호출해요. 이는 노드 IP가 EKS 컨트롤 플레인에서 도달 가능해야 하는 주요 이유이며, Pod가 실행 중이더라도 노드 경로가 잘못 구성되면 로그나 exec에 접근할 수 없는 이유이기도 해요.

노드 IP (Node IPs)

EKS 컨트롤 플레인이 노드와 통신할 때는 Node 객체 상태(status.addresses)에 보고된 주소 중 하나를 사용해요.

EKS 클라우드 노드에서는 kubelet이 노드 등록 중 EC2 인스턴스의 프라이빗 IP를 InternalIP로 보고하는 것이 일반적이에요. 이 IP는 Cloud Controller Manager(CCM)가 검증해 EC2 인스턴스에 속하는지 확인해요. 또한 CCM은 일반적으로 인스턴스의 공용 IP(ExternalIP)와 DNS 이름(InternalDNS 및 ExternalDNS)을 노드 상태에 추가해요.

하지만 하이브리드 노드에는 CCM이 없어요. 하이브리드 노드를 EKS Hybrid Nodes CLI(nodeadm)로 등록하면 CCM 없이 kubelet이 머신의 IP를 노드 상태에 직접 보고하도록 구성해요.

apiVersion: v1
kind: Node
metadata:
  name: my-node-1
spec:
  providerID: eks-hybrid:///us-west-2/my-cluster/my-node-1
status:
  addresses:
  - address: 10.1.1.236
    type: InternalIP
  - address: my-node-1
    type: Hostname

머신에 IP가 여러 개 있다면 kubelet이 자체 로직에 따라 그중 하나를 선택해요. nodeadm 구성의 spec.kubelet.flags에서 전달할 수 있는 --node-ip 플래그로 선택된 IP를 제어할 수 있어요. Node 객체에 보고된 IP만 VPC에서 경로가 필요해요. 머신에는 클라우드에서 도달할 수 없는 다른 IP가 있을 수 있어요.

kube-proxy

kube-proxy는 각 노드의 네트워킹 레이어에서 Service 추상화를 구현하는 역할을 해요. Kubernetes Service로 향하는 트래픽에 대한 네트워크 프록시이자 로드 밸런서 역할을 해요. Service와 Endpoints의 변경 사항에 대해 Kubernetes API 서버를 계속 감시함으로써 kube-proxy는 기본 호스트의 네트워킹 규칙을 동적으로 업데이트해 트래픽이 올바르게 전달되도록 해요.

iptables 모드에서 kube-proxy는 서비스 트래픽을 처리하기 위해 여러 netfilter 체인을 프로그래밍해요. 규칙은 다음 계층을 형성해요.

  • KUBE-SERVICES 체인: 모든 서비스 트래픽의 진입점. 각 서비스의 ClusterIP와 포트를 일치시키는 규칙이 있어요.
  • KUBE-SVC-XXX 체인: 서비스별 체인으로 각 서비스에 대한 로드 밸런싱 규칙이 있어요.
  • KUBE-SEP-XXX 체인: 엔드포인트별 체인으로 실제 DNAT 규칙이 있어요.

default 네임스페이스의 서비스 test-server에 대해 어떤 일이 일어나는지 살펴보겠습니다. 서비스 ClusterIP 172.16.31.14, 서비스 포트 80, 백킹 Pod 10.2.0.110, 10.2.1.39, 10.2.2.254.

iptables 규칙(iptables-save를 grep -A10 KUBE-SERVICES와 함께 사용)을 검사하면:

KUBE-SERVICES 체인에서 서비스를 일치시키는 규칙을 찾을 수 있어요.

-A KUBE-SERVICES -d 172.16.31.14/32 -p tcp -m comment --comment "default/test-server cluster IP" -m tcp --dport 80 -j KUBE-SVC-XYZABC123456
  • 이 규칙은 172.16.31.14:80으로 향하는 패킷을 일치시켜요.
  • 주석은 이 규칙의 목적을 나타내요: default/test-server cluster IP.
  • 일치하는 패킷은 KUBE-SVC-XYZABC123456 체인으로 점프해요.

KUBE-SVC-XYZABC123456 체인에는 확률 기반 로드 밸런싱 규칙이 있어요.

-A KUBE-SVC-XYZABC123456 -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-POD1XYZABC
-A KUBE-SVC-XYZABC123456 -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-POD2XYZABC
-A KUBE-SVC-XYZABC123456 -j KUBE-SEP-POD3XYZABC
  • 첫 번째 규칙: 33.3% 확률로 KUBE-SEP-POD1XYZABC로 점프.
  • 두 번째 규칙: 남은 트래픽(전체의 33.3%)의 50% 확률로 KUBE-SEP-POD2XYZABC로 점프.
  • 마지막 규칙: 모든 남은 트래픽(전체의 33.3%)이 KUBE-SEP-POD3XYZABC로 점프.

개별 KUBE-SEP-XXX 체인은 DNAT(Destination NAT)를 수행해요.

-A KUBE-SEP-POD1XYZABC -p tcp -m tcp -j DNAT --to-destination 10.2.0.110:80
-A KUBE-SEP-POD2XYZABC -p tcp -m tcp -j DNAT --to-destination 10.2.1.39:80
-A KUBE-SEP-POD3XYZABC -p tcp -m tcp -j DNAT --to-destination 10.2.2.254:80

이 DNAT 규칙은 대상 IP와 포트를 다시 작성해 특정 Pod로 트래픽을 보내요.

각 규칙은 트래픽의 약 33.3%를 처리해 10.2.0.110, 10.2.1.39, 10.2.2.254 사이에 균등한 로드 밸런싱을 제공해요.

이 다중 레벨 체인 구조는 kube-proxy가 데이터 경로에 프록시 프로세스 없이 커널 수준 패킷 조작을 통해 서비스 로드 밸런싱과 리디렉션을 효율적으로 구현할 수 있게 해줘요.

Kubernetes 운영에 미치는 영향

노드의 kube-proxy가 고장 나면 해당 노드가 Service 트래픽을 올바르게 라우팅하지 못해 클러스터 Service에 의존하는 Pod에 타임아웃이나 연결 실패가 발생해요. 노드가 처음 등록될 때 특히 혼란스러울 수 있어요. CNI는 Pod 네트워킹을 구성하기 전에 노드의 Pod CIDR 같은 정보를 얻기 위해 Kubernetes API 서버와 통신해야 해요. 이를 위해 kubernetes Service IP를 사용해요. 하지만 kube-proxy가 시작하지 못했거나 올바른 iptables 규칙을 설정하는 데 실패했다면, kubernetes 서비스 IP로 가는 요청이 EKS 컨트롤 플레인 ENI의 실제 IP로 변환되지 않아요. 결과적으로 CNI는 크래시 루프에 빠지고 어떤 Pod도 제대로 실행되지 못할 수 있어요.

우리는 Pod가 kubernetes 서비스 IP를 사용해 Kubernetes API 서버와 통신한다는 것을 알지만, kube-proxy는 이를 작동시키기 위해 먼저 iptables 규칙을 설정해야 해요.

kube-proxy는 API 서버와 어떻게 통신하나요?

kube-proxy는 Kubernetes API 서버의 실제 IP 또는 이를 해석하는 DNS 이름을 사용하도록 구성해야 해요. EKS의 경우 EKS는 클러스터를 만들 때 EKS가 만드는 Route53 DNS 이름을 가리키도록 기본 kube-proxy를 구성해요. 이 값은 kube-system 네임스페이스의 kube-proxy ConfigMap에서 볼 수 있어요. 이 ConfigMap의 내용은 kube-proxy Pod에 주입되는 kubeconfig이므로 clusters 섹션의 .cluster.server 필드를 찾아보세요. 이 값은 EKS DescribeCluster API 호출 시 EKS 클러스터의 endpoint 필드와 일치해요.

apiVersion: v1
data:
  kubeconfig: |-
    kind: Config
    apiVersion: v1
    clusters:
    - cluster:
        certificate-authority: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
        server: https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.gr7.us-west-2.eks.amazonaws.com
      name: default
    contexts:
    - context:
        cluster: default
        namespace: default
        user: default
      name: default
    current-context: default
    users:
    - name: default
      user:
        tokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
kind: ConfigMap
metadata:
  name: kube-proxy
  namespace: kube-system

라우팅 가능한 원격 Pod CIDR (Routable remote Pod CIDRs)

하이브리드 노드의 네트워킹 개념 페이지는 하이브리드 노드에서 웹훅을 실행하거나 클라우드 노드의 Pod가 하이브리드 노드의 Pod와 통신하게 하기 위한 요구사항을 자세히 설명해요. 핵심 요구사항은 온프레미스 라우터가 특정 Pod IP를 담당하는 노드가 어느 것인지 알아야 한다는 거예요. 이를 달성하는 방법은 몇 가지가 있으며 BGP(Border Gateway Protocol), 정적 라우트, ARP(Address Resolution Protocol) 프록시가 포함돼요. 이는 다음 섹션에서 다룹니다.

BGP(Border Gateway Protocol)

CNI가 지원한다면(Cilium 및 Calico 등) CNI의 BGP 모드를 사용해 노드별 Pod CIDR에 대한 라우트를 노드에서 로컬 라우터로 전파할 수 있어요. CNI의 BGP 모드를 사용하면 CNI가 가상 라우터 역할을 하므로, 로컬 라우터는 Pod CIDR이 다른 서브넷에 속하고 노드가 해당 서브넷의 게이트웨이라고 생각해요.

정적 라우트 (Static routes)

또는 로컬 라우터에서 정적 라우트를 구성할 수 있어요. 이는 온프레미스 Pod CIDR을 VPC로 라우팅하는 가장 간단한 방법이지만, 가장 오류가 발생하기 쉽고 유지관리가 어려운 방법이기도 해요. 라우트가 기존 노드 및 할당된 Pod CIDR과 항상 최신 상태로 유지되도록 해야 해요. 노드 수가 적고 인프라가 정적이라면 이는 실행 가능한 옵션이며 라우터의 BGP 지원이 필요 없어요. 이 옵션을 선택한다면 IPAM이 결정하도록 두는 대신 CNI를 각 노드에 할당하려는 Pod CIDR 조각으로 구성하는 것을 권장해요.

ARP(Address Resolution Protocol) 프록시

ARP 프록시는 온프레미스 Pod IP를 라우팅 가능하게 만드는 또 다른 접근 방식이며, 하이브리드 노드가 로컬 라우터와 같은 레이어 2 네트워크에 있을 때 특히 유용해요. ARP 프록시를 활성화하면 노드가 호스팅하는 Pod IP에 대한 ARP 요청에 응답하는데, 해당 IP가 다른 서브넷에 속하더라도 그렇게 해요.

로컬 네트워크의 장치가 Pod IP에 도달하려고 하면 먼저 "이 IP를 가진 사람은 누구인가?"를 묻는 ARP 요청을 보내요. 해당 Pod를 호스팅하는 하이브리드 노드는 자체 MAC 주소로 응답하며 "해당 IP에 대한 트래픽을 처리할 수 있다"고 알려줘요. 이는 라우터 구성 없이 로컬 네트워크의 장치와 Pod 사이에 직접 경로를 만들어요.

이를 위해서는 CNI가 프록시 ARP 기능을 지원해야 해요. Cilium은 구성을 통해 활성화할 수 있는 프록시 ARP에 대한 내장 지원이 있어요. 핵심 고려 사항은 Pod CIDR이 환경의 다른 네트워크와 겹치지 않아야 한다는 것이에요. 겹치면 라우팅 충돌이 발생할 수 있기 때문이에요.

이 접근 방식은 몇 가지 장점이 있어요.

  • 라우터를 BGP로 구성하거나 정적 라우트를 유지할 필요가 없어요.
  • 라우터 구성을 제어할 수 없는 환경에서 잘 작동해요.

Pod 간 캡슐화 (Pod-to-Pod encapsulation)

온프레미스 환경에서 CNI는 일반적으로 물리적 네트워크 위에서 작동할 수 있는 오버레이 네트워크를 만들기 위해 캡슐화 프로토콜을 사용하며, 네트워크를 다시 구성할 필요가 없어요. 이 섹션은 이 캡슐화가 어떻게 작동하는지 설명해요. 사용 중인 CNI에 따라 일부 세부 사항은 다를 수 있다는 점에 유의하세요.

캡슐화는 원래 Pod 네트워크 패킷을 기본 물리적 네트워크를 통해 라우팅할 수 있는 다른 네트워크 패킷 안에 감싸요. 이를 통해 물리적 네트워크가 해당 Pod CIDR을 라우팅하는 방법을 알 필요 없이 같은 CNI를 실행하는 노드 간에 Pod가 통신할 수 있어요.

Kubernetes에서 가장 일반적으로 사용되는 캡슐화 프로토콜은 VXLAN(Virtual Extensible LAN)이며, CNI에 따라 다른 프로토콜(Geneve 등)도 사용할 수 있어요.

VXLAN 캡슐화

VXLAN은 레이어 2 이더넷 프레임을 UDP 패킷 안에 캡슐화해요. Pod가 다른 노드의 Pod에 트래픽을 보낼 때 CNI는 다음을 수행해요.

  1. CNI가 Pod A의 패킷을 가로채요.
  2. CNI가 원래 패킷을 VXLAN 헤더로 감싸요.
  3. 이 감싼 패킷은 노드의 일반 네트워킹 스택을 통해 대상 노드로 전송돼요.
  4. 대상 노드의 CNI가 패킷을 풀고 Pod B에 전달해요.

VXLAN 캡슐화 중 패킷 구조에 일어나는 일은 다음과 같아요.

원래 Pod 간 패킷:

+-----------------+---------------+-------------+-----------------+
| Ethernet Header | IP Header     | TCP/UDP     | Payload         |
| Src: Pod A MAC  | Src: Pod A IP | Src Port    |                 |
| Dst: Pod B MAC  | Dst: Pod B IP | Dst Port    |                 |
+-----------------+---------------+-------------+-----------------+

VXLAN 캡슐화 후:

+-----------------+-------------+--------------+------------+---------------------------+
| Outer Ethernet  | Outer IP    | Outer UDP    | VXLAN      | Original Pod-to-Pod       |
| Src: Node A MAC | Src: Node A | Src: Random  | VNI: xx    | Packet (unchanged         |
| Dst: Node B MAC | Dst: Node B | Dst: 4789    |            | from above)               |
+-----------------+-------------+--------------+------------+---------------------------+

VXLAN 네트워크 식별자(VNI)는 서로 다른 오버레이 네트워크를 구분해요.

Pod 통신 시나리오

같은 하이브리드 노드의 Pod

같은 하이브리드 노드의 Pod가 통신할 때는 일반적으로 캡슐화가 필요 없어요. CNI는 노드의 내부 가상 인터페이스를 통해 Pod 간 트래픽을 전달하는 로컬 라우트를 설정해요.

Pod A -> veth0 -> node's bridge/routing table -> veth1 -> Pod B

패킷은 노드를 떠나지 않으며 캡슐화가 필요 없어요.

다른 하이브리드 노드의 Pod

다른 하이브리드 노드의 Pod 간 통신에는 캡슐화가 필요해요.

Pod A -> CNI -> [VXLAN encapsulation] -> Node A network -> router or gateway -> Node B network -> [VXLAN decapsulation] -> CNI -> Pod B

이를 통해 물리적 네트워크가 Pod IP 라우팅을 이해하지 않아도 Pod 트래픽이 물리적 네트워크 인프라를 통과할 수 있어요.

더 알아보기 (Learn more)