본문 바로가기

컨테이너/쿠버네티스 네트워크

[KANS] 쿠버네티스 네트워크 (8) Calico Mode / eBPF

✅  다음과 같이 3가지 모드를 간략하게 정리하고 넘어가자

  • IPIP - 실습을 default 모드인 IPIP로 진행했음으로 간략하게 복습하고 넘어가자.
  • VXLAN - Flannel의 VXLAN 방식과 1도 다르지 않으므로 간략하게 복습하고 넘어가자.
  • Direct - 쓸일이 없을것 같으므로, 간략하게 넘어가자.

IPIP 

 

  • 다른 노드 간의 파드 통신은 tunl0 인터페이스를 통해 IP 헤더에 감싸져서 상대측 노드로 도달 후 tunl0 인터페이스에서 Outer 헤더를 제거하고 내부의 파드와 통신
  • 다른 노드의 파드 대역은 BGP로 전달 받아 호스트 라우팅 테이블에 업데이트됨
    • ( VXLAN 모드를 사용할 경우 ) FELIX 가 ETCD 에서 타 노드의 podCIDR 정보를 가져와서 업데이트 
  • Azure 네트워크에서는 IPIP 통신이 불가능하여 IPIP 모드 대신 VXLAN 모드 사용

Direct 

  • AWS CNI와 같이 Direct 모드는출발지 노드의 라우팅 정보를 확인하여, 목적지 노드로 원본 패킷을 그대로 전달하는 방식 
  • 클라우드 사업자 네트워크의 경우 NIC 에 매칭되지 않는 IP 패킷은 차단되기 때문에 NIC에 Source/ Destination Check 기능을 끔
  • crosssubnet 모드 
    • Direct 방식과 Overlay 방식이 혼합되어 있는 hybrid 형태입니다. 기본적으로 Node 간 같은 네트워크 대역일 경우에는 Direct 방식으로, Node간 다른 네트워크 대역일 경우에는 Overlay 방식의 IPIP 모드로 동작합니다.동일한 네트워크 기반의 일정 규모의 Node까지는 Direct 방식으로 빠르게 처리합니다.

VXLAN

  • 다른 노드 간의 파드 통신은 vxlan 인터페이스를 통해 L2 프레임이 UDP - VXLAN에 감싸져서 상대측 노드로 도달 후 vxlan 인터페이스에서 Outer 헤더를 제거하고 내부의 파드와 통신
  • BGP 미사용, VXLAN L3 라우팅을 통해서 동작
  • Azure 네트워크에서도 사용 가능 🙆🏻 (UDP를 사용하므로)
  • 이때 출발지 Node에서는 FELIX를 통해 미리 etcd에서 네트워크 정보를 가져다가 라우팅 테이블을 업데이트 합니다. 그렇기 때문에 잦은 BGP 업데이트로 인한 시스템 부하를 줄일 수 있습니다.

목표:

1) eBPF 왜 쓰는걸까?

2) eBPF 구성 방법 

3) eBPF 통신 테스트  

1. eBPF 왜 쓰는걸까?  SNAT 

해당 영상 26분 30초를 보면 왜 앞단의 스위치가 SNAT 때문에 뻗을까요?

https://www.youtube.com/watch?v=0otDu4GIieY

 


해당 영상 26분 30초를 보면 SNAT 때문에 왜 앞단의 스위치가 뻗을까요?
사실 해당 환경은 외부 스위치에서 k8s service 처리를 처리 하는 일반적인 msa 환경에서는 경험하기 힘든 하둡같은 분산 서비스를 k8s 기본 설치환경에서 돌리기 위해 생길수 있는 위협이기 때문에 eBPF에 대한 도입 배경이라고 소개할순없음 ㅠ....  

SNAT 어디서 발생하는가?

 

클러스터 내에서의 Pod 네트워킹은 꽤나 잘 만들었습니다. 하지만 그것만으로는 내구성을 가진 시스템을 만들기에는 조금 부족합니다. 그 이유는 쿠버네티스에서 Pod는 쉽게 대체될 수 있는 존재이기 때문입니다. (pods are ephemeral) Pod IP를 어떤 서비스의 Endpoint로 설정하는 것은 가능합니다. 하지만 Pod가 새로 생성되었을 때 그 주소가 같을 것이라고는 보장하지 못합니다.

사실 이러한 문제는 예전 문제이고 이미 우리에겐 정해진 해결책이 있습니다. 바로 서비스 앞단에 reverse-proxy (혹은 load balancer)를 위치시키는 것이죠.

클라이언트에서 proxy로 연결을 하면 proxy의 역할은 서버들 목록을 관리하며 현재 살아있는 서버에게 트래픽을 전달하는 것입니다. 이는 proxy 서버가 몇가지 요구사항을 만족해야합니다. proxy 서버 스스로 내구성이 있어야 하며 장애에 대응할 수 있어야 합니다. 또한 트래픽을 전달할 서버 리스트를 가지고 있어야 하고 해당 서버가 정상적인지 확인할 수 있는 방법을 알아야 합니다. 쿠버네티스 설계자들은 이 문제를 굉장히 우아한 방법으로 풀었습니다. 그들은 기존의 시스템을 잘 활용하여 위의 3가지 요구사항을 만족하는 것을 만들었고 그것을 service 리소스 타입이라고 정의하였습니다.

- 커피고래님 블로그 

위와 같이 SVC 타입의 리소스는 사설 IP를 가집니다. 따라서 SNAT가 발생할수 있죠. 
그렇다면 어디선가 해당 SNAT를 처리하면서 부하를 받게 될겁니다.
그게 해당 영상에선 앞단의 스위치였겠죠. ( 해당 환경에 대해서 자세히 설명드릴순 없지만 외부 로드벨런서와 내부 로드벨런서, 엄격한 arp, ExternalTrafificPolicy , ipvs 등이 엮여 있다고 하네요.)  

물론, 이러한 네트워크 설계가 일반적인 MSA 환경에서 자주 볼수 있는 환경은 아닐겁니다.
또한 NAT를 없애는것 역시, SVC 영역, POD영역, NODE 영역 전부 물리 스위치에서 Native Routing 처리도 할수 있습니다.  하지만 운영환경에서 전체 시스템의 네트워크 영역에 영향을 줄만한 결정을 내리기 어렵다는 관점에서  
 그래서 차라리 쿠버네티스 아키텍처에 변화를 주자 이런 생각을 했을거에요.

eBPF에 대해서 알아보는 시간이 다가왔습니다.



다양한 NLB of Client IP preservation 기능


사실 NLB / ALB 그리고 물리 LB나 기타 벤더의 L3~L7 로드벨런서에 클라이언트 IP를 보존하는 여러가지 기술들이 있습니다. 아래 링크를 참고해보면 좋습니다.
https://www.martysweet.co.uk/aws-nlb-and-ip-preservation/


물론 PPv2나 X-forwarded-for와 같은 기술들이 가진 benefit이 스위치의 부하 및 SNAT 방지를 위해 
존재 한다고 보긴 힘들지만 ( ACL 및 Security 상 이점 +
클라이언트 IP 주소가 있는 경우 클라이언트 IP 주소를 기반으로 통계 및 Log를 수집할 수 있습니다 ) 
이외에도 여러가지 advantage가 존재할수 있습니다.



2. eBPF 구성 

사전준비

 

  • 리눅스  : 우분투 20.04, Red Hat v8.2...이상
  • 각 노드 별로 the BPF filesystem must be mounted at /sys/fs/bpf
  •  eBPF 모드에서 Calico를 사용할 때 kube-proxy를 건너뛰게 되므로 네트워크 정책과 설정을 유지하기 위해 Calico는 Kubernetes API 서버에 접근할 수 있는 다른 방법이 필요하다는 의미입니다.
# 커널 버전 확인
uname -rv
5.4.0-77-generic #86-Ubuntu SMP Thu Jun 17 02:35:03 UTC 2021

# BPF filesystem 마운트 확인
mount | grep "/sys/fs/bpf"
none on /sys/fs/bpf type bpf (rw,nosuid,nodev,noexec,relatime,mode=700)

# kube-proxy 사용하지 않으니, 대신 Calico-node 가 직접 K8S API server 와 통신이 될 수 있게 설정
# API 서버 정보 확인
kubectl get endpoints kubernetes -o wide

# 컨피그맵 생성
APIHOST=<K8S API server>
APIPORT=<K8S API server 접속 포트>
APIHOST=192.168.100.10
APIPORT=6443

cat <<EOF | kubectl apply -f -
kind: ConfigMap
apiVersion: v1
metadata:
  name: kubernetes-services-endpoint
  namespace: kube-system
data:
  KUBERNETES_SERVICE_HOST: "$APIHOST"
  KUBERNETES_SERVICE_PORT: "$APIPORT"
EOF

# calico-node 데몬셋 파드에 kubernetes-services-endpoint 컨피그맵이 기본 적용 설정이 되어 있음
kubectl describe configmaps -n kube-system kubernetes-services-endpoint
kubectl describe pod -n kube-system calico-node-9mczq | grep 'kubernetes-services-endpoint' -B1
    Environment Variables from:
      kubernetes-services-endpoint  ConfigMap  Optional: true

# calico-node 파드를 재시작하여 컨피그맵 적용 시키기
watch "kubectl get pods -n kube-system | grep calico"
kubectl delete pod -n kube-system -l k8s-app=calico-node
kubectl delete pod -n kube-system -l k8s-app=calico-kube-controllers

# kube-proxy 데몬셋 파드 Disable
watch "kubectl get pods -n kube-system | grep kube-proxy"
kubectl patch ds -n kube-system kube-proxy -p '{"spec":{"template":{"spec":{"nodeSelector":{"non-calico": "true"}}}}}'

# eBPF mode 활성화
calicoctl patch felixconfiguration default --patch='{"spec": {"bpfEnabled": true}}'

  • pod / svc 배포 
apiVersion: v1
kind: Pod
metadata:
  name: webpod1
  labels:
    app: websrv
spec:
  #nodeName: k8s-w1
  containers:
  - name: container
    image: k8s.gcr.io/echoserver:1.5
    ports:
    - containerPort: 8080
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Pod
metadata:
  name: webpod2
  labels:
    app: websrv
spec:
  #nodeName: k8s-w2
  containers:
  - name: container
    image: k8s.gcr.io/echoserver:1.5
    ports:
    - containerPort: 8080
  terminationGracePeriodSeconds: 0
---
apiVersion: v1
kind: Service
metadata:
  name: svc-nodeport
spec:
  ports:
    - name: svc-webport
      port: 9000
      targetPort: 8080
  selector:
    app: websrv
  type: NodePort
  •  pod와 svc 배포
# 생성
kubectl apply -f podsvc.yaml

# 확인
kubectl get pod -o wide
NAME      READY   STATUS    RESTARTS   AGE   IP              NODE     NOMINATED NODE   READINESS GATES
webpod1   1/1     Running   0          39s   172.16.228.68   k8s-w1   <none>           <none>
webpod2   1/1     Running   0          39s   172.16.46.14    k8s-w2   <none>           <none>

kubectl get svc svc-nodeport
NAME           TYPE       CLUSTER-IP    EXTERNAL-IP   PORT(S)          AGE
svc-nodeport   NodePort   10.111.7.64   <none>        9000:30405/TCP   52s

# (옵션) 파드1 과 파드2가 통신 : 일반 Calico Directo Mode 처럼 동작함을 확인

3) eBPF 통신 테스트 

노드 ----- 1번에 접속하여 curl 요청시 -----
패킷 Flow
: Control(마스터 노드)에 NodePort로 인입 → DNAT 되어 파드가 있는 Node1(혹은 Node2)의 파드 도착 → 리턴 트래픽은 Control에서 Reverse DNAT 되어 외부로 빠져나감

  • 예상되는 결과  - >> ebpf route 확인 
  • 172.16.0.0/16: remote in-pool nat-out
    172.16.34.0/24: remote workload in-pool nat-out nh 192.168.20.100
    172.16.34.0/32: remote host in-pool nat-out 
    172.16.116.0/32: local host ( Master Node ) 
    172.16.116.1/32: local workload in-pool nat-out idx 3
    172.16.116.3/32: local workload in-pool nat-out idx 8
    172.16.158.0/24: remote workload in-pool nat-out nh 192.168.10.101
    172.16.158.0/32: remote host in-pool nat-out
    172.16.184.0/24: remote workload in-pool nat-out nh 192.168.10.102
    172.16.184.0/32: remote host in-pool nat-out
    172.17.0.1/32: local host
    192.168.10.10/32: local host
    192.168.10.101/32: remote host
    192.168.10.102/32: remote host
    192.168.20.100/32: remote host

  • 클라이언트 IP가 로그로 찍힌다.

 

eBPF 요약 정리

1.  클라이언트  IP 보존 확인

  • 외부 클라이언트 IP -> 마스터 노드 NodePort로 인입 -> DNAT 되어 파드가 있는 Node1의 파드에 도착  
  • 리턴 트래픽은 Control(마스터노드) 에서 Reverse DNAT 되어 외부로 빠져나감

2.  DSR (Direct Server Return) 기능

  • 외부 클라이언트 IP -> 마스터 노드 NodePort로 인입 -> VXLAN 감싸져서 Node1에 도착 후 DNAT 되어 파드에 도착
  • 리턴 트래픽해당 노드에서 바로 외부로 빠져나감 (이때 출발지 IP가 최초 인입한 마스터 IP로 SNAT 됨!)