들어가며

지난주에 이어 이번주에는 Calico CNI와 Calico Network Mode에 대해 알아보겠습니다. KANS 3기 3주차 스터디를 시작하겠습니다.


Calico CNI

Calico 소개

Calico란?

Calico CNI는 Kubernetes 클러스터에서 네트워크를 관리하는 CNI(Container Network Interface) 플러그인 중 하나로 Kubernetes와 non-Kubernetes/legacy 네트워크를 연결하는 역할을 합니다. 특징으로는 L3/L4 네트워크를 제공하며, BGP 프로토콜을 사용하여 라우팅을 수행합니다. (모드에 따라 BGP를 사용하지 않을 수도 있습니다.)

Calico 설치

  • 컨트롤플레인에서 kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.1/manifests/calico.yaml 명령어를 실행하여 Calico CNI를 설치할 수 있습니다. 이때 실습환경에 맞추기 위해 CALICO_IPV4POOL_BLOCK_SIZE를 “24”로 설정해야 합니다.
  • 해당 부분이 적용된 yaml 파일인 calico-kans.yaml를 사용하여 설치하였습니다.

    # 모니터링
    $ watch -d 'kubectl get pod -A -owide'
      
    # 컨트롤플레인(k8s-m)에서 calico cni 설치 실행
    $ kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.28.1/manifests/calico.yaml
    # 기본 yaml 에 4946줄 이동 후 아래 내용 추가 해둠
    ##            # Block size to use for the IPv4 POOL created at startup. Block size for IPv4 should be in the range 20-32. default 24
    ##            - name: CALICO_IPV4POOL_BLOCK_SIZE
    ##              value: "24"
    $ kubectl apply -f https://raw.githubusercontent.com/gasida/KANS/main/kans3/calico-kans.yaml
    # => poddisruptionbudget.policy/calico-kube-controllers created
    #    serviceaccount/calico-kube-controllers created
    #    serviceaccount/calico-node created
    #    serviceaccount/calico-cni-plugin created
    #    ...
      
    # 설치 확인
    $ tree /opt/cni/bin/
    # => /opt/cni/bin/
    #    ├── bandwidth
    #    ├── bridge
    #    ├── calico
    #    ├── calico-ipam
    #    ├── dhcp
    #    ├── dummy
    #    ...
    $ ls -l /opt/cni/bin/
    $ ip -c route
    # => ...
    #    172.16.34.0/24 via 192.168.20.100 dev tunl0 proto bird onlink
    #    blackhole 172.16.116.0/24 proto bird
    #    172.16.158.0/24 via 192.168.10.101 dev tunl0 proto bird onlink
    #    172.16.184.0/24 via 192.168.10.102 dev tunl0 proto bird onlink
    #    ...
    $ ip -c addr
    $ iptables -t filter -L
    $ iptables -t nat -L
    $ iptables -t filter -L | wc -l
    $ iptables -t nat -L | wc -l
      
    # calicoctl 설치
    $ curl -L https://github.com/projectcalico/calico/releases/download/v3.28.1/calicoctl-linux-amd64 -o calicoctl
    $ chmod +x calicoctl && mv calicoctl /usr/bin
    $ calicoctl version
      
    # CNI 설치 후 파드 상태 확인
    $ kubectl get pod -A -o wide
    # => NAMESPACE     NAME                                       READY   STATUS    RESTARTS      AGE     IP               NODE     NOMINATED NODE   READINESS GATES
    #    kube-system   calico-kube-controllers-77d59654f4-wbzth   1/1     Running   0             4m22s   172.16.34.2      k8s-w0   <none>           <none>
    #    kube-system   calico-node-545hj                          1/1     Running   0             4m22s   192.168.20.100   k8s-w0   <none>           <none>
    #    kube-system   calico-node-p5xpt                          1/1     Running   0             4m22s   192.168.10.10    k8s-m    <none>           <none>
    #    kube-system   calico-node-rmzvb                          1/1     Running   0             4m22s   192.168.10.101   k8s-w1   <none>           <none>
    #    kube-system   calico-node-sd9x8                          1/1     Running   0             4m22s   192.168.10.102   k8s-w2   <none>           <none>
    #    ...
    
  • 설치 확인을 했을때 위와 같이 정상적으로 설치가 되었다면 Calico CNI가 정상적으로 동작하고 있는 것입니다.
  • ip -c route 명령어를 통해 Bird 라우팅 테이블에 Calico CNI가 적용된 것을 확인할 수 있습니다. 그 중에서 blackhole은 해당 명령어를 실행하는 노드를 의미합니다.
  • 실습을 위해서 metrics-server를 설치하고, kubectl top node 명령어를 통해 노드의 리소스 사용량을 확인해보겠습니다.
# metrics-server 설치
$ helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server/
# => "metrics-server" has been added to your repositories
$ helm upgrade --install metrics-server metrics-server/metrics-server --set 'args[0]=--kubelet-insecure-tls' -n kube-system
# => Release "metrics-server" does not exist. Installing it now.
#    ...
#      Chart version: 3.12.1
#      App version:   0.7.1
#      Image tag:     registry.k8s.io/metrics-server/metrics-server:v0.7.1
$ kubectl get all -n kube-system -l app.kubernetes.io/instance=metrics-server
$ kubectl get apiservices |egrep '(AVAILABLE|metrics)'
# => NAME                                   SERVICE                      AVAILABLE                  AGE
#    v1beta1.metrics.k8s.io                 kube-system/metrics-server   False (MissingEndpoints)   16s

# 확인
$ kubectl top node  # 노드 리소스 사용량 확인
# => NAME     CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%
#    k8s-m    218m         5%     1131Mi          29%
#    k8s-w0   95m          2%     807Mi           43%
#    k8s-w1   77m          1%     768Mi           41%
#    k8s-w2   62m          1%     806Mi           43%
$ kubectl top pod -A --sort-by='cpu'    # 파드 리소스 사용량을 CPU 사용량 순으로 정렬해서 확인
$ kubectl top pod -A --sort-by='memory' # 파드 리소스 사용량을 Memory 사용량 순으로 정렬해서 확인

# (참고) 삭제
$ helm uninstall -n kube-system metrics-server

Calico CNI 구성요소

img.png 출처: 추가예정

  • Calico Datastore : Calico의 구성 정보를 저장하는 데이터베이스입니다. Kubernetes API 서버(기본값) 또는 etcd에 저장됩니다.
  • Bird : 오픈소스 라우팅 데몬으로, Calico CNI에서 라우팅을 수행합니다. Bird는 BGP 프로토콜을 이용한 라우팅 데몬으로 Calico나 Kubernetes에서만 사용되는것이 아닌 일반적인 라우팅 데몬입니다. 노드의 파드 네트워크 대역을 BGP 라우팅 프로토콜을 통해서 광고(advertise)합니다.
  • Felix : Bird를 통해 배포된 라우팅 정보를 수신하여, 노드의 파드 네트워크 대역을 호스트의 라우팅 테이블에 업데이트 하는 역할을 합니다. 또한 Iptables 등 방화벽 규칙 설정 관리를 합니다.
  • Confd : Calico 구성 정보를 관리하는 데몬으로, BGP 설정등으로 Calico 데이터 저장소에 변경이 발생하면 Bird의 설정 파일을 만들고, 변경된 설정 파일을 반영하게 합니다.
  • CNI IPAM Plugin : Calico가 제공하는 IPAM(IP Address Management) 플러그인으로 Calico CNI에서 IP 주소를 할당하는 역할을 합니다. (Flannel CNI의 경우 기본 IPAM인 host-local IPAM을 사용합니다.)
  • calico-kube-controllers : Calico의 동작을 감시 및 제어하는 컨트롤러입니다.
  • Typha : Calico의 성능을 향상시키기 위한 컴포넌트로, Calico의 데이터베이스에 대한 읽기 전용 요청을 처리합니다. 워커노드 수가 많지 않은 경우 생략해도 무방합니다.
  • calicoctl : Calico를 제어할 수 있는 CLI 인터페이스로, datastore에 접근하여 Calico의 구성 정보를 관리할 수 있습니다.

Calico 구성요소 확인

  • 설치된 구성요소를 명령들을 사용해서 살펴보겠습니다.

    # 버전 확인 - 링크
    ## kdd 의미는 쿠버네티스 API 를 데이터저장소로 사용 : k8s API datastore(kdd)
    $ calicoctl version
    # => Client Version:    v3.28.1
    #    ...
      
    # calico 관련 정보 확인
    $ kubectl get daemonset -n kube-system
    # => NAME          DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
    #    calico-node   4         4         4       4            4           kubernetes.io/os=linux   26m
    $ kubectl get pod -n kube-system -l k8s-app=calico-node -owide
    # => NAME                READY   STATUS    RESTARTS   AGE   IP               NODE     NOMINATED NODE   READINESS GATES
    #    calico-node-545hj   1/1     Running   0          26m   192.168.20.100   k8s-w0   <none>           <none>
    #    calico-node-p5xpt   1/1     Running   0          26m   192.168.10.10    k8s-m    <none>           <none>
    #    calico-node-rmzvb   1/1     Running   0          26m   192.168.10.101   k8s-w1   <none>           <none>
    #    calico-node-sd9x8   1/1     Running   0          26m   192.168.10.102   k8s-w2   <none>           <none>
    # calico-node 는 데몬셋으로 모든 노드에 배포되어 있음을 확인할 수 있습니다.
      
    # calico-kube-controllers 정보 확인
    $ kubectl get deploy -n kube-system calico-kube-controllers
    # => NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
    #    calico-kube-controllers   1/1     1            1           27m
    $ kubectl get pod -n kube-system -l k8s-app=calico-kube-controllers -owide
    # => NAME                                       READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
    #    calico-kube-controllers-77d59654f4-wbzth   1/1     Running   0          28m   172.16.34.2   k8s-w0   <none>           <none>
      
    # 칼리코 IPAM 정보 확인 : 칼리코 CNI 를 사용한 파드가 생성된 노드에 podCIDR 네트워크 대역 확인 - 링크
    $ calicoctl ipam show
    # => +----------+---------------+-----------+------------+--------------+
    #    | GROUPING |     CIDR      | IPS TOTAL | IPS IN USE |   IPS FREE   |
    #    +----------+---------------+-----------+------------+--------------+
    #    | IP Pool  | 172.16.0.0/16 |     65536 | 8 (0%)     | 65528 (100%) |
    #    +----------+---------------+-----------+------------+--------------+
      
    # Block 는 각 노드에 할당된 podCIDR 정보
    $ calicoctl ipam show --show-blocks
    # => +----------+-----------------+-----------+------------+--------------+
    #    | GROUPING |      CIDR       | IPS TOTAL | IPS IN USE |   IPS FREE   |
    #    +----------+-----------------+-----------+------------+--------------+
    #    | IP Pool  | 172.16.0.0/16   |     65536 | 8 (0%)     | 65528 (100%) |
    #    | Block    | 172.16.116.0/24 |       256 | 1 (0%)     | 255 (100%)   |
    #    | Block    | 172.16.158.0/24 |       256 | 2 (1%)     | 254 (99%)    |
    #    | Block    | 172.16.184.0/24 |       256 | 1 (0%)     | 255 (100%)   |
    #    | Block    | 172.16.34.0/24  |       256 | 4 (2%)     | 252 (98%)    |
    #    +----------+-----------------+-----------+------------+--------------+
    $ calicoctl ipam show --show-borrowed
    $ calicoctl ipam show --show-configuration
      
    # host-local IPAM 정보 확인 : k8s-m 노드의 podCIDR 은 host-local 대신 칼리코 IPAM 를 사용함
      
    # 워커 노드마다 할당된 dedicated subnet (podCIDR) 확인
    $ kubectl get nodes -o jsonpath='{.items[*].spec.podCIDR}' ;echo
    # => 172.16.0.0/24 172.16.1.0/24 172.16.2.0/24 172.16.4.0/24
      
    # 컨트롤플레인의 pod에 할당되는 podCIDR 확인
    $ kubectl get node k8s-m -o json | jq '.spec.podCIDR'
    # => "172.16.0.0/24"
      
    # CNI Plugin 정보 확인 - 링크
    $ tree /etc/cni/net.d/
    # => /etc/cni/net.d/
    #    ├── 10-calico.conflist
    #    └── calico-kubeconfig
    $ cat /etc/cni/net.d/10-calico.conflist | jq
    # => ...
    #    "datastore_type": "kubernetes", # 칼리코 데이터저장소는 쿠버네티스 API 를 사용
    #    "ipam": { 
    #      "type": "calico-ipam" # IPAM 은 칼리코 자체 IPAM 을 사용
    #    },
    #    ...
      
    # calicoctl node 정보 확인 : Bird 데몬(BGP)을 통한 BGP 네이버 연결 정보(bgp peer 는 노드의 IP로 연결) - 링크
    $ calicoctl node status
    # => Calico process is running.
    #    
    #    IPv4 BGP status
    #    +----------------+-------------------+-------+----------+-------------+
    #    |  PEER ADDRESS  |     PEER TYPE     | STATE |  SINCE   |    INFO     |
    #    +----------------+-------------------+-------+----------+-------------+
    #    | 192.168.20.100 | node-to-node mesh | up    | 14:13:02 | Established |
    #    | 192.168.10.101 | node-to-node mesh | up    | 14:13:31 | Established |
    #    | 192.168.10.102 | node-to-node mesh | up    | 14:12:44 | Established |
    #    +----------------+-------------------+-------+----------+-------------+
    $ calicoctl node checksystem
    # => Checking kernel version...
    #       5.15.0-119-generic  					OK
    #    Checking kernel modules...
    #       xt_mark             					OK
      
    # ippool 정보 확인 : 클러스터가 사용하는 IP 대역 정보와 칼리코 모드 정보 확인
    $ calicoctl get ippool -o wide
    # => NAME                  CIDR            NAT    IPIPMODE   VXLANMODE   DISABLED   DISABLEBGPEXPORT   SELECTOR
    #    default-ipv4-ippool   172.16.0.0/16   true   Always     Never       false      false              all()
      
    # 파드와 서비스 사용 네트워크 대역 정보 확인 
    $ kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"  
    # => "--service-cluster-ip-range=10.200.1.0/24",
    #    "--cluster-cidr=172.16.0.0/16",
                                  
    $ kubectl get cm -n kube-system kubeadm-config -oyaml | grep -i subnet
    # => podSubnet: 172.16.0.0/16
    #    serviceSubnet: 10.200.1.0/24
       
    # calico endpoint (파드)의 정보 확인 : WORKLOAD 는 파드 이름이며, 어떤 노드에 배포되었고 IP 와 cali 인터페이스와 연결됨을 확인
    $ calicoctl get workloadEndpoint
    $ calicoctl get workloadEndpoint -A
    $ calicoctl get workloadEndpoint -o wide -A
    # => NAMESPACE     NAME                                                            WORKLOAD                                   NODE     NETWORKS          INTERFACE         PROFILES                                                  NATS
    #    kube-system   k8s--w0-k8s-calico--kube--controllers--77d59654f4--wbzth-eth0   calico-kube-controllers-77d59654f4-wbzth   k8s-w0   172.16.34.5/32    cali544ab3155a5   kns.kube-system,ksa.kube-system.calico-kube-controllers
    #    kube-system   k8s--w0-k8s-coredns--55cb58b774--7qvtv-eth0                     coredns-55cb58b774-7qvtv                   k8s-w0   172.16.34.6/32    cali6be4c908feb   kns.kube-system,ksa.kube-system.coredns
    #    kube-system   k8s--w0-k8s-coredns--55cb58b774--8q4f6-eth0                     coredns-55cb58b774-8q4f6                   k8s-w0   172.16.34.4/32    cali23a9e6edc85   kns.kube-system,ksa.kube-system.coredns
    #    kube-system   k8s--w1-k8s-metrics--server--68cfccbdf6--wjjs2-eth0             metrics-server-68cfccbdf6-wjjs2            k8s-w1   172.16.158.2/32   cali2789c1b51d6   kns.kube-system,ksa.kube-system.metrics-server
      
    # 노드에서 컨테이너(프로세스) 확인
    $ ps axf 
    # =>    1325 ?        Sl     0:02 /usr/bin/containerd-shim-runc-v2 -namespace k8s.io -id 4b2a9892a9147b1a3b73b4864bec5270f18da7d8393b82f8863dc8a6cee8ac0c -address /run/containerd/conta
    #       1352 ?        Ss     0:00  \_ /pause
    #       1762 ?        Ss     0:00  \_ /usr/local/bin/runsvdir -P /etc/service/enabled
    #       1838 ?        Ss     0:00      \_ runsv confd
    #       1853 ?        Sl     0:00      |   \_ calico-node -confd
    #       1839 ?        Ss     0:00      \_ runsv bird
    #       2032 ?        S      0:00      |   \_ bird -R -s /var/run/calico/bird.ctl -d -c /etc/calico/confd/config/bird.cfg
    #       1840 ?        Ss     0:00      \_ runsv node-status-reporter
    #       1847 ?        Sl     0:00      |   \_ calico-node -status-reporter
    #       1841 ?        Ss     0:00      \_ runsv monitor-addresses
    #       1851 ?        Sl     0:00      |   \_ calico-node -monitor-addresses
    #       1842 ?        Ss     0:00      \_ runsv felix
    #       1849 ?        Sl     0:27      |   \_ calico-node -felix
    #       1843 ?        Ss     0:00      \_ runsv allocate-tunnel-addrs
    #       1846 ?        Sl     0:00      |   \_ calico-node -allocate-tunnel-addrs
    #       1844 ?        Ss     0:00      \_ runsv cni
    #       1848 ?        Sl     0:00      |   \_ calico-node -monitor-token
    #       1845 ?        Ss     0:00      \_ runsv bird6
    #       2033 ?        S      0:00          \_ bird6 -R -s /var/run/calico/bird6.ctl -d -c /etc/calico/confd/config/bird6.cfg
    
  • felix : Host의 Network Interface, Routing Table, Iptables를 관리합니다.

    # Calico의 Felix를 통해 설정된 iptables 규칙 설정 확인
    $ iptables -t filter -S | grep cali
    # => -N cali-FORWARD
    #    -N cali-INPUT
    #    -N cali-OUTPUT
    #    -N cali-cidr-block
    #    -N cali-from-hep-forward
    #    -N cali-from-host-endpoint
    #    -N cali-from-wl-dispatch
    #    -N cali-to-hep-forward
    #    -N cali-to-host-endpoint
    #    -N cali-to-wl-dispatch
    #    -N cali-wl-to-host
    #    -A INPUT -m comment --comment "cali:Cz_u1IQiXIMmKD4c" -j cali-INPUT
    #    -A FORWARD -m comment --comment "cali:wUHhoiAYhphO9Mso" -j cali-FORWARD
    #    -A FORWARD -m comment --comment "cali:S93hcgKJrXEqnTfs" -m comment --comment "Policy explicitly accepted packet." -j ACCEPT
    #    -A FORWARD -m comment --comment "cali:mp77cMpurHhyjLrM" -j MARK --set-xmark 0x10000/0x10000
    #    -A OUTPUT -m comment --comment "cali:tVnHkvAo15HuiPy0" -j cali-OUTPUT
    #    -A cali-FORWARD -m comment --comment "cali:vjrMJCRpqwy5oRoX" -j MARK --set-xmark 0x0/0xe0000
    #    -A cali-FORWARD -m comment --comment "cali:A_sPAO0mcxbT9mOV" -j cali-from-hep-forward
    #    -A cali-FORWARD -i cali+ -m comment --comment "cali:8ZoYfO5HKXWbB3pk" -j cali-from-wl-dispatch
    #    -A cali-FORWARD -o cali+ -m comment --comment "cali:jdEuaPBe14V2hutn" -j cali-to-wl-dispatch
    #    -A cali-FORWARD -m comment --comment "cali:12bc6HljsMKsmfr-" -j cali-to-hep-forward
    #    -A cali-FORWARD -m comment --comment "cali:NOSxoaGx8OIstr1z" -j cali-cidr-block
    #    -A cali-INPUT -p ipencap -m comment --comment "cali:PajejrV4aFdkZojI" -m comment --comment "Allow IPIP packets from Calico hosts" -m set --match-set cali40all-hosts-net src -m addrtype --dst-type LOCAL -j ACCEPT
    #    -A cali-INPUT -p ipencap -m comment --comment "cali:_wjq-Yrma8Ly1Svo" -m comment --comment "Drop IPIP packets from non-Calico hosts" -j DROP
    #    -A cali-INPUT -i cali+ -m comment --comment "cali:8TZGxLWh_Eiz66wc" -g cali-wl-to-host
    #    -A cali-INPUT -m comment --comment "cali:6McIeIDvPdL6PE1T" -j ACCEPT
    #    -A cali-INPUT -m comment --comment "cali:YGPbrUms7NId8xVa" -j MARK --set-xmark 0x0/0xf0000
    #    -A cali-INPUT -m comment --comment "cali:2gmY7Bg2i0i84Wk_" -j cali-from-host-endpoint
    #    -A cali-INPUT -m comment --comment "cali:q-Vz2ZT9iGE331LL" -m comment --comment "Host endpoint policy accepted packet." -j ACCEPT
    #    -A cali-OUTPUT -m comment --comment "cali:Mq1_rAdXXH3YkrzW" -j ACCEPT
    #    -A cali-OUTPUT -o cali+ -m comment --comment "cali:69FkRTJDvD5Vu6Vl" -j RETURN
    #    -A cali-OUTPUT -p ipencap -m comment --comment "cali:AnEsmO6bDZbQntWW" -m comment --comment "Allow IPIP packets to other Calico hosts" -m set --match-set cali40all-hosts-net dst -m addrtype --src-type LOCAL -j ACCEPT
    #    -A cali-OUTPUT -m comment --comment "cali:9e9Uf3GU5tX--Lxy" -j MARK --set-xmark 0x0/0xf0000
    #    -A cali-OUTPUT -m comment --comment "cali:0f3LDz_VKuHFaA2K" -m conntrack ! --ctstate DNAT -j cali-to-host-endpoint
    #    -A cali-OUTPUT -m comment --comment "cali:OgU2f8BVEAZ_fwkq" -m comment --comment "Host endpoint policy accepted packet." -j ACCEPT
    #    -A cali-from-wl-dispatch -m comment --comment "cali:zTj6P0TIgYvgz-md" -m comment --comment "Unknown interface" -j DROP
    #    -A cali-to-wl-dispatch -m comment --comment "cali:7KNphB1nNHw80nIO" -m comment --comment "Unknown interface" -j DROP
    #    -A cali-wl-to-host -m comment --comment "cali:Ee9Sbo10IpVujdIY" -j cali-from-wl-dispatch
    #    -A cali-wl-to-host -m comment --comment "cali:nSZbcOoG1xPONxb8" -m comment --comment "Configured DefaultEndpointToHostAction" -j ACCEPT
      
    $ iptables -t nat -S | grep cali
    # => -N cali-OUTPUT
    #    -N cali-POSTROUTING
    #    -N cali-PREROUTING
    #    -N cali-fip-dnat
    #    -N cali-fip-snat
    #    -N cali-nat-outgoing
    #    -A PREROUTING -m comment --comment "cali:6gwbT8clXdHdC1b1" -j cali-PREROUTING
    #    -A OUTPUT -m comment --comment "cali:tVnHkvAo15HuiPy0" -j cali-OUTPUT
    #    -A POSTROUTING -m comment --comment "cali:O3lYWMrLQYEMJtB5" -j cali-POSTROUTING
    #    -A cali-OUTPUT -m comment --comment "cali:GBTAv2p5CwevEyJm" -j cali-fip-dnat
    #    -A cali-POSTROUTING -m comment --comment "cali:Z-c7XtVd2Bq7s_hA" -j cali-fip-snat
    #    -A cali-POSTROUTING -m comment --comment "cali:nYKhEzDlr11Jccal" -j cali-nat-outgoing
    #    -A cali-POSTROUTING -o tunl0 -m comment --comment "cali:SXWvdsbh4Mw7wOln" -m addrtype ! --src-type LOCAL --limit-iface-out -m addrtype --src-type LOCAL -j MASQUERADE --random-fully
    #    -A cali-PREROUTING -m comment --comment "cali:r6XmIziWUJsdOK6Z" -j cali-fip-dnat
    #    -A cali-nat-outgoing -m comment --comment "cali:flqWnvo8yq4ULQLa" -m set --match-set cali40masq-ipam-pools src -m set ! --match-set cali40all-ipam-pools dst -j MASQUERADE --random-fully
    
  • bird : BGP 라우팅 데몬으로, Calico CNI에서 라우팅을 수행합니다.

    $ kubectl get pod -n kube-system -l k8s-app=calico-node -o wide
    # => NAME                READY   STATUS    RESTARTS      AGE   IP               NODE     NOMINATED NODE   READINESS GATES
    #    calico-node-p5xpt   1/1     Running   1 (42m ago)   24h   192.168.10.10    k8s-m    <none>           <none>
    #    calico-node-545hj   1/1     Running   1 (42m ago)   24h   192.168.20.100   k8s-w0   <none>           <none>
    #    calico-node-rmzvb   1/1     Running   1 (42m ago)   24h   192.168.10.101   k8s-w1   <none>           <none>
    #    calico-node-sd9x8   1/1     Running   1 (42m ago)   24h   192.168.10.102   k8s-w2   <none>           <none>
      
    # Bird 라우팅 테이블 확인
    $ kubectl -n kube-system exec -it calico-node-545hj -- birdcl show route
    # => BIRD v0.3.3+birdv1.6.8 ready.
    #    0.0.0.0/0          via 10.0.2.2 on enp0s3 [kernel1 14:13:00] * (10)
    #    10.0.2.2/32        dev enp0s3 [kernel1 14:13:00] * (10)
    #    10.0.2.3/32        dev enp0s3 [kernel1 14:13:00] * (10)
    #    10.0.2.0/24        dev enp0s3 [direct1 14:12:59] * (240)
    #    172.16.184.0/24    via 192.168.20.254 on enp0s8 [Mesh_192_168_10_102 14:13:01 from 192.168.10.102] * (100/?) [i]
    #    192.168.10.0/24    via 192.168.20.254 on enp0s8 [kernel1 14:13:00] * (10)
    #    172.16.158.0/24    via 192.168.20.254 on enp0s8 [Mesh_192_168_10_101 14:13:30 from 192.168.10.101] * (100/?) [i]
    #    192.168.20.0/24    dev enp0s8 [direct1 14:12:59] * (240)
    #    172.16.116.0/24    via 192.168.20.254 on enp0s8 [Mesh_192_168_10_10 14:13:01 from 192.168.10.10] * (100/?) [i]
    #    172.16.34.0/24     blackhole [static1 14:12:59] * (200)
    #    172.16.34.0/32     dev tunl0 [direct1 14:12:59] * (240)
    #    172.16.34.6/32     dev cali6be4c908feb [kernel1 14:13:09] * (10)
    #    172.16.34.5/32     dev cali544ab3155a5 [kernel1 14:13:08] * (10)
    #    172.16.34.4/32     dev cali23a9e6edc85 [kernel1 14:13:08] * (10)
      
    $ kubectl -n kube-system exec -it calico-node-545hj -- birdcl show protocol
    # => BIRD v0.3.3+birdv1.6.8 ready.
    #    name     proto    table    state  since       info
    #    static1  Static   master   up     14:13:00
    #    kernel1  Kernel   master   up     14:13:00
    #    device1  Device   master   up     14:13:00
    #    direct1  Direct   master   up     14:13:00
    #    Mesh_192_168_10_10 BGP      master   up     14:13:02    Established
    #    Mesh_192_168_10_101 BGP      master   up     14:13:31    Established
    #    Mesh_192_168_10_102 BGP      master   up     14:13:02    Established
      
    $ kubectl -n kube-system exec -it calico-node-545hj -- birdcl show status
    # => BIRD v0.3.3+birdv1.6.8 ready.
    #    BIRD v0.3.3+birdv1.6.8
    #    Router ID is 192.168.20.100
    #    Current server time is 2024-09-19 14:57:39
    #    Last reboot on 2024-09-19 14:13:00
    #    Last reconfiguration on 2024-09-19 14:13:00
    #    Daemon is up and running
    

노드간의 BGP 전달과정 확인

  • Calico CNI에서는 BGP 프로토콜을 사용하여 노드간의 라우팅 정보를 교환합니다. 해당 역할을 BIRD가 수행하는데 그림으로 살펴보면 아래와 같습니다.

img.png Bird와 Felix의 역할 모식도 (출처: 추가예정)

  • 노드간의 BGP 전달을 패킷을 캡쳐해서 확인해보겠습니다.
# 워커 노드를 종료한 상태에서 아래의 작업을 시작합니다.

# 노드에서 패킷 캡쳐
$ vagrant ssh k8s-m

# BGP는 TCP 179 포트를 사용하므로 해당 포트로 패킷을 캡쳐합니다.
$ tcpdump -i enp0s8 tcp port 179 -w bgp.cap

# 종료된 워커 노드를 시작합니다.
# 워커 노드가 시작되면 BGP 연결이 재설정되고 패킷이 전송됩니다.
  • 캡쳐된 패킷이 bgp.cap 파일로 저장되었으면, Wireshark를 사용하여 패킷을 확인합니다.

    20240921_kans_w3_4.png

  • 워커 노드에 접속해서 노드 ip와 ipip tunneling ip를 확인합니다.

    # 워커노드 k8s-w2 접속
    $ vagrant ssh k8s-w2
      
    $ ip addr
    # => ...
    #    3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    #        link/ether 08:00:27:af:2e:7a brd ff:ff:ff:ff:ff:ff
    #        inet <span style="color: red;">192.168.10.102</span>/24 brd 192.168.10.255 scope global enp0s8  <span style="color: red;"># node ip</span>   
    #           valid_lft forever preferred_lft forever
    #    4: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
    #        link/ipip 0.0.0.0 brd 0.0.0.0                                  
    #        inet <span style="color: red;">172.16.184.0</span>/32 scope global tunl0                        <span style="color: red;"># ipip tunneling ip</span>
    #           valid_lft forever preferred_lft forever 
    
  • 컨트롤 플레인 노드에 접속해서 라우팅 테이블을 확인해보겠습니다.

    # 컨트롤 플레인 노드 k8s-m 접속
    $ vagrant ssh k8s-m
      
    # 라우팅 테이블 확인
    $ ip route show 172.16.184.0/24
    # => <span style="color: red;">172.16.184.0</span>/24 via <span style="color: red;">192.168.10.102</span> dev tunl0 proto bird onlink
    
  • 위와 같이 BGP를 통해 노드간 라우팅 정보를 교환하고, 라우팅 테이블을 업데이트하는 것을 확인할 수 있었습니다.

Calico 통신 흐름 확인

  • Calico CNI를 사용한 통신을 확인해보겠습니다.

동일 노드에서 파드(Pod) 간 통신

  • 동일 노드에서 파드간 통신은 기본적으로 직접 통신합니다.

img.png

  • iptables FORWARD Rule 정책에서 파드간 포워딩 정책을 허용합니다.
  • calice# 인터페이스에 proxy arp 설정을 통해 게이트웨이의 MAC 주소를 파드가 전달 받아 사용합니다.
  • 동일 노드 내의 파드 간 통신에서는 tunnel interface를 사용하지 않습니다.
파드 배포 전 기본 상태 확인
  • 파드 배포 전에는 아래와 같이 파드가 없는 상태입니다.

img.png 파드 배포 전 상태 (출처: 추가예정)

  • 파드 생성 전 노드(k8s-w1) Shell 에서 기본 정보 확인

    # 네트워크 인터페이스 정보 확인 : 터널(ipip) 인터페이스가 존재
    $ ip -c -d addr show tunl0
    # => 4: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN group default qlen 1000
    #        link/ipip 0.0.0.0 brd 0.0.0.0 promiscuity 0 minmtu 0 maxmtu 0
    #        <span style="color: red;">ipip</span> any remote any local any ttl inherit nopmtudisc numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535
    #        inet <span style="color: red;">172.16.158.0/32</span> scope global tunl0
    #           valid_lft forever preferred_lft forever
      
    # 네트워크 네임스페이스 확인
    $ lsns -t net
    # =>         NS TYPE NPROCS   PID USER     NETNSID NSFS                                                COMMAND
    #    4026531840 net     145     1 root  unassigned                                                     /sbin/i
    #    4026532204 net       2  2009 65535          0 /run/netns/cni-6923c332-7c35-669a-8787-232597fa3fa8 /pause
      
    # 네트워크 라우팅 경로 정보 확인
    # 이중 bird 는 bird 데몬이 BGP 라우팅 프로토콜에 의해 파드 네트워크 대역을 전달받거나 전달하는 경로 → 각각 노드의 파드 대역입니다
    $ ip -c route | grep bird
    # => blackhole 172.16.158.0/24 proto bird
    # => 172.16.34.0/24 via 192.168.20.100 dev tunl0 proto bird onlink
    # => 172.16.116.0/24 via 192.168.10.10 dev tunl0 proto bird onlink
    # => 172.16.184.0/24 via 192.168.10.102 dev tunl0 proto bird onlink
      
    # 아래 tunl0 Iface 에 목적지 네트워크 대역은 ipip 인캡슐레이션에 의해서 각 노드에 전달됩니다 → 각각 노드의 파드 대역입니다
    $ route -n
    # => Kernel IP routing table
    #    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
    #    172.16.34.0     192.168.20.100  255.255.255.0   UG    0      0        0 tunl0
    #    172.16.116.0    192.168.10.10   255.255.255.0   UG    0      0        0 tunl0
    #    172.16.158.0    0.0.0.0         255.255.255.0   U     0      0        0 *
    #    172.16.184.0    192.168.10.102  255.255.255.0   UG    0      0        0 tunl0
    #    ...
      
    # (옵션) iptables rule 갯수 확인
    $ iptables -t filter -S | grep cali | wc -l
    # => 69
    $ iptables -t nat -S | grep cali | wc -l
    # => 15
    
파드 배포 후 상태 확인
  • 노드(k8s-w1)에 파드가 배포되면 아래와 같이 파드가 생성되고, 통신이 가능한 상태입니다.
  • node1-pod2.yaml 파일 작성

    # node1-pod2.yaml 
    apiVersion: v1
    kind: Pod
    metadata:
      name: pod1
    spec:
      nodeName: k8s-w1  
      containers:
      - name: pod1
        image: nicolaka/netshoot  
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
    ---
    apiVersion: v1
    kind: Pod
    metadata:
      name: pod2
    spec:
      nodeName: k8s-w1
      containers:
      - name: pod2
        image: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
    
  • 파드 생성
    $ kubectl apply -f node1-pod2.yaml
    # => pod/pod1 created
    #    pod/pod2 created
    
  • 컨트롤 플레인에서 파드 생성 후 확인

img.png

$ vagrant ssh k8s-m 

# [터미널1] k8s-m 모니터링
$ watch -d calicoctl get workloadEndpoint

# 생성된 파드 정보 확인
$ kubectl get pod -o wide
# => NAME   READY   STATUS    RESTARTS        AGE   IP             NODE     NOMINATED NODE   READINESS GATES
#    pod1   1/1     Running   1 (6m56s ago)   16h   172.16.158.9   k8s-w1   <none>           <none>
#    pod2   1/1     Running   1 (6m56s ago)   16h   172.16.158.8   k8s-w1   <none>           <none>

# calicoctl 이용한 endpoint 확인
$ calicoctl get workloadendpoints
# => WORKLOAD   NODE     NETWORKS          INTERFACE
#    pod1       k8s-w1   172.16.158.9/32   calice0906292e2
#    pod2       k8s-w1   172.16.158.8/32   calibd2348b4f67
  • 워커노드에서 파드 생성 후 확인

    $ vagrant ssh k8s-w1 
      
    # 네트워크 인터페이스 정보 확인 : calice#~ 2개 추가됨!, 각각 net ns(네임스페이스) 0, 1로 호스트와 구별됨
    $ ip -c link
    # => ...
    #    4: tunl0@NONE: <NOARP,UP,LOWER_UP> mtu 1480 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    #        link/ipip 0.0.0.0 brd 0.0.0.0
    #    <span style="color: #393;">7: cali2789c1b51d6@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP mode DEFAULT group default qlen 1000</span>
    #        <span style="color: #393;">link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netns cni-a67773eb-f809-7ce1-72f5-e16e93cbe4c4</span>
    #    <span style="color: #393;">8: calibd2348b4f67@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP mode DEFAULT group default qlen 1000</span>
    #        <span style="color: #393;">link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netns cni-511c24a7-0a0a-0c9a-29c2-345cfcc0dd21</span>
    #    <span style="color: #393;">9: calice0906292e2@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP mode DEFAULT group default qlen 1000</span>
    #        <span style="color: #393;">link/ether ee:ee:ee:ee:ee:ee brd ff:ff:ff:ff:ff:ff link-netns cni-47754704-48d2-04a9-28fe-e8bff1be17ba</span>
      
    # 네트워크 네임스페이스 확인 : 아래 2개 pause(infra 컨테이너)가 각각 파드별로 생성됨 - 위 link-netnsid 0, link-netnsid 1 매칭됨
    $ lsns -t net
    # =>         NS TYPE NPROCS   PID USER     NETNSID NSFS                                                COMMAND
    #    4026531840 net     146     1 root  unassigned                                                     /sbin/init
    #    4026532205 net       2  2092 65535          0 /run/netns/cni-a67773eb-f809-7ce1-72f5-e16e93cbe4c4 /pause
    #    4026532282 net       2  2317 65535          1 /run/netns/cni-511c24a7-0a0a-0c9a-29c2-345cfcc0dd21 /pause
    #    4026532343 net       2  2431 65535          2 /run/netns/cni-47754704-48d2-04a9-28fe-e8bff1be17ba /pause
      
    # 파드의 IP/32bit 호스트 라우팅 대역이 라우팅 테이블에 추가됨
    $ ip -c route
    # => ...
    #    blackhole 172.16.158.0/24 proto bird
    #    <span style="color: #393;">172.16.158.7 dev cali2789c1b51d6 scope link</span>
    #    <span style="color: #393;">172.16.158.8 dev calibd2348b4f67 scope link</span>
    #    <span style="color: #393;">172.16.158.9 dev calice0906292e2 scope link</span>
    #    ...
      
    # (옵션) iptables rule 갯수 확인
    $ iptables -t filter -S | grep cali | wc -l
    # => 121
    $ iptables -t nat -S | grep cali | wc -l
    # => 15
    
파드간 통신 실행 및 확인
  • 파드간 통신 실행 이해를 위한 준비 img.png

    # iptables 필터 테이블에 FORWARD 리스트 중 cali-FORWARD 룰 정보를 필터링해서 watch 로 확인
    $ watch -d -n 1 "iptables -v --numeric --table filter --list FORWARD | egrep '(cali-FORWARD|pkts)'" 
      
    # (컨트롤플레인) 파드 연결된 veth 를 변수를 확인
    $ VETH1=$(calicoctl get workloadEndpoint | grep pod1 | awk '{print $4}')
    $ echo $VETH1
    # => calice0906292e2
    $ VETH2=$(calicoctl get workloadEndpoint | grep pod2 | awk '{print $4}')
    $ echo $VETH2
    # => calibd2348b4f67
      
    # (워커노드1) 위에서 확인한 파드 연결된 veth 를 변수에 지정
    $ VETH1=calice0906292e2
    $ VETH2=calibd2348b4f67
      
    # 노드1 calice# 인터페이스의 proxy arp 설정 확인
    # cat /proc/sys/net/ipv4/conf/<자신의 pod1에 연결된 calice# 이름>/proxy_arp
    $ cat /proc/sys/net/ipv4/conf/$VETH1/proxy_arp
    # => 1
    $ cat /proc/sys/net/ipv4/conf/$VETH2/proxy_arp
    # => 1
      
    # 파드1 혹은 파드2에 veth 로 연결된 호스트 네트워크 인터페이스 calice# 중 1개 선택해서 tcpdump
    $ tcpdump -i $VETH1 -nn
    $ tcpdump -i $VETH2 -nn
    
  • 파드1 -> 파드2 ping 통신을 확인해 보겠습니다.

img.png

# 파드1 Shell 에서 실행 : 정상 통신!
$ kubectl exec pod1 -it -- zsh
# --------------------
# ping -c 10 <파드2 IP>
$ ping -c 10 172.16.158.8

# 게이트웨이 169.254.1.1 의 MAC 주소를 ARP 에 의해서 학습되었습니다.
$ ip -c -s neigh
# => 10.0.2.15 dev eth0 lladdr ee:ee:ee:ee:ee:ee  used 675/735/675probes 0 STALE
#    <span style="color: #900;">169.254.1.1</span> dev eth0 lladdr <span style="color: #900;">ee:ee:ee:ee:ee:ee</span>  ref 1 used 3/3/3probes 1 REACHABLE

# 노드에서 확인
# iptables 에 기본 FORWARD 는 DROP 이지만, 아래 cali-FORWARD Rule에 의해 허용되며, pkts 카운트가 증가합니다.
$ iptables -v --numeric --table filter --list FORWARD | egrep '(cali-FORWARD|pkts)'
# => pkts bytes target     prot opt in     out     source               destination
#    29375 6505K cali-FORWARD  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* cali:wUHhoiAYhphO9Mso */
$ watch -d "iptables -v --numeric --table filter --list FORWARD | egrep '(cali-FORWARD|pkts)'"

# 파드1에서 게이트웨이의 IP인 169.254.1.1 의 MAC 주소를 알기 위해서 ARP Request 를 보낸다
# 이때 veth 연결된 calice#~ 에 proxy arp 설정이 되어 있고, 자신의 mac 주소(ee:ee:ee:ee:ee:ee)를 알려주고, 이후 정상 통신됨
$ tcpdump -i $VETH1 -nn
# => tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
#    listening on calice0906292e2, link-type EN10MB (Ethernet), snapshot length 262144 bytes
#    11:27:01.638238 IP 172.16.158.9 > 172.16.158.8: ICMP echo request, id 134, seq 1, length 64
#    11:27:01.638430 IP 172.16.158.8 > 172.16.158.9: ICMP echo reply, id 134, seq 1, length 64
#    ...
#    11:27:06.675426 <span style="color: #900;">ARP, Request who-has 169.254.1.1</span> tell 172.16.158.9, length 28
#    11:27:06.675498 <span style="color: #900;">ARP, Reply 169.254.1.1 is-at ee:ee:ee:ee:ee:ee</span>, length 28

$ tcpdump -i $VETH2 -nn
# => tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
#    listening on calibd2348b4f67, link-type EN10MB (Ethernet), snapshot length 262144 bytes
#    11:27:01.638389 IP 172.16.158.9 > 172.16.158.8: ICMP echo request, id 134, seq 1, length 64
#    11:27:01.638409 IP 172.16.158.8 > 172.16.158.9: ICMP echo reply, id 134, seq 1, length 64
#    ...
#    11:27:16.151004 <span style="color: #900;">ARP, Request who-has 169.254.1.1</span> tell 172.16.158.8, length 28
#    11:27:16.151780 <span style="color: #900;">ARP, Reply 169.254.1.1 is-at ee:ee:ee:ee:ee:ee</span>, length 28

# 호스트에서 calice0906292e2 의 MAC 주소 다시 확인
$ ip -c -d link
# => ... 
#    9: <span style="color: #900;">calice0906292e2</span>@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1480 qdisc noqueue state UP mode DEFAULT group default qlen 1000
#        link/ether <span style="color: #900;">ee:ee:ee:ee:ee:ee</span> brd ff:ff:ff:ff:ff:ff link-netns cni-47754704-48d2-04a9-28fe-e8bff1be17ba promiscuity 1 minmtu 68 maxmtu 65535
#        veth addrgenmode eui64 numtxqueues 1 numrxqueues 1 gso_max_size 65536 gso_max_segs 65535

img.png 파드간 통신 확인 실습

  • 실습결과 calice# 인터페이스에 proxy arp 설정을 통해 게이트웨이의 MAC 주소를 파드가 전달 받아 사용하는 것을 확인할 수 있었습니다.
  • 169.254.1.1은 Calico CNI에서 사용하는 게이트웨이 주소로, 같은 노드의 파드끼리 통신시 사용되는 것을 확인하였습니다.

파드 -> 외부(인터넷) 통신

  • 이번에는 파드에서 외부(인터넷)로 통신하는 과정을 확인해보겠습니다.

img.png

  • calico는 기본 설정에 natOutgoing: true여서 파드에서 외부로 통신할 때는 노드의 IP 주소로 MASQUERADE(Source NAT)을 수행하여 외부로 통신합니다.
  • calice# 인터페이스에 proxy arp 설정을 통해 게이트웨이의 MAC 주소를 파드가 전달 받아 사용합니다.
  • 외부로 통신할 때는 tunnel interface를 사용하지 않습니다.
파드 배포 전 기본 상태 확인
  • calico 설정 및 노드의 iptables 설정을 확인하겠습니다.
# 마스터 노드에서 확인 : natOutgoing 의 기본값은 true 이다
$ calicoctl get ippool -o wide
# => NAME                  CIDR            NAT    IPIPMODE   VXLANMODE   DISABLED   DISABLEBGPEXPORT   SELECTOR
#    default-ipv4-ippool   172.16.0.0/16   true   Always     Never       false      false              all()

# 노드에서 확인 : 노드에서 외부로 통신 시 MASQUERADE 동작 Rule 확인
$ iptables -n -t nat --list cali-nat-outgoing
# => Chain cali-nat-outgoing (1 references)
#    target     prot opt source               destination
#    MASQUERADE  all  --  0.0.0.0/0            0.0.0.0/0            /* cali:flqWnvo8yq4ULQLa */ match-set cali40masq-ipam-pools src ! match-set cali40all-ipam-pools dst random-fully

$ ipset list
$ ipset list cali40masq-ipam-pools
# => Name: cali40masq-ipam-pools
#    Type: hash:net
#    Revision: 7
#    Header: family inet hashsize 1024 maxelem 1048576 bucketsize 12 initval 0xe6c37fa0
#    Size in memory: 504
#    References: 1
#    Number of entries: 1
#    Members:
#    172.16.0.0/16
파드 배포 및 외부 통신 확인
  • node1-pod1.yaml 파일 작성

    apiVersion: v1
    kind: Pod
    metadata:
      name: pod1
    spec:
      nodeName: k8s-w1
      containers:
        - name: pod1
          image: nicolaka/netshoot
          command: ["tail"]
          args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
    
  • 파드 생성 후 확인

    # 파드 생성
    $ kubectl apply -f node1-pod1.yaml
      
    # 생성된 파드 확인
    $ kubectl get pod -o wide
    # => NAME   READY   STATUS    RESTARTS   AGE   IP              NODE     NOMINATED NODE   READINESS GATES
    #    pod1   1/1     Running   0          25s   172.16.158.10   k8s-w1   <none>           <none>
    
  • 파드간 통신 실행 이해를 위한 준비

    # 노드에서 실행
    # iptables NAT MASQUERADE 모니터링 : pkts 증가 확인
    $ watch -d 'iptables -n -v -t nat --list cali-nat-outgoing'
      
    # (컨트롤플레인 노드) 파드 연결된 veth 를 변수를 확인
    $ VETH1=$(calicoctl get workloadEndpoint | grep pod1 | awk '{print $4}')
    $ echo $VETH1
    # => calice0906292e2
      
    # (워커노드1) 위에서 확인한 파드 연결된 veth 를 변수에 지정
    $ VETH1=calice0906292e2
      
    # 패킷 덤프 실행
    $ tcpdump -i any -nn icmp
    $ tcpdump -i $VETH1 -nn icmp
    $ tcpdump -i tunl0 -nn icmp
    $ tcpdump -i ens5 -nn icmp    # [실습환경 A Type]
    $ tcpdump -i enp0s8 -nn icmp  # [실습환경 B Type]
    $ tcpdump -i enp0s3 -nn icmp  # [실습환경 B Type]
    
  • 외부 통신 실행 및 확인

    # 파드에서 외부 정상 통신 확인
    $ kubectl exec pod1 -it -- zsh
    ----------------------------
      
    # 혹은 통신 확인 
    $ ping -c 10 8.8.8.8
      
    # The right way to check the weather - 링크
    $ curl wttr.in/seoul
    $ curl 'wttr.in/seoul?format=3'
    $ curl 'wttr.in/busan?format=3'
    $ curl -s 'wttr.in/{London,Busan}'
    $ curl v3.wttr.in/Seoul.sxl
    $ curl wttr.in/Moon
    $ curl wttr.in/:help
      
    # 패킷 덤프 내용 확인
    $ tcpdump -i calice0906292e2 -nn icmp
    # => tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    #    listening on calice0906292e2, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    #    12:23:43.792218 IP 172.16.158.10 > 8.8.8.8: ICMP echo request, id 124, seq 1, length 64
    #    12:23:43.831040 IP 8.8.8.8 > 172.16.158.10: ICMP echo reply, id 124, seq 1, length 64
    #    12:23:44.797713 IP 172.16.158.10 > 8.8.8.8: ICMP echo request, id 124, seq 2, length 64
    #    12:23:44.836854 IP 8.8.8.8 > 172.16.158.10: ICMP echo reply, id 124, seq 2, length 64
    #    ...
      
    # 아래 10.0.2.15는 VM의 1번 네트워크 인터페이스의 IP이며, 출발지 IP가 변경되어서 외부로 나감
    $ tcpdump -i enp0s3 -nn icmp
    # => tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
    #    listening on enp0s3, link-type EN10MB (Ethernet), snapshot length 262144 bytes
    #    12:23:43.792299 IP 10.0.2.15 > 8.8.8.8: ICMP echo request, id 25687, seq 1, length 64
    #    12:23:43.830978 IP 8.8.8.8 > 10.0.2.15: ICMP echo reply, id 25687, seq 1, length 64
    #    12:23:44.797877 IP 10.0.2.15 > 8.8.8.8: ICMP echo request, id 25687, seq 2, length 64
    #    12:23:44.836812 IP 8.8.8.8 > 10.0.2.15: ICMP echo reply, id 25687, seq 2, length 64
      
    # nat MASQUERADE rule 카운트(pkts)가 증가!
    ## 출발지 매칭은 cali40masq-ipam-pools 을 사용
    # watch -d 'iptables -n -v -t nat --list cali-nat-outgoing'
    $ iptables -n -v -t nat --list cali-nat-outgoing
    # => Chain cali-nat-outgoing (1 references)
    #     pkts bytes target     prot opt in     out     source               destination
    #        9   636 MASQUERADE  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* cali:flqWnvo8yq4ULQLa */ match-set cali40masq-ipam-pools src ! match-set cali40all-ipa
    #    m-pools dst random-fully
      
    # IPSET 으로 의 cali40masq-ipam-pools IP 대역 정보 확인 : 172.16.0.0/16 대역임을 확인
    $ ipset list cali40masq-ipam-pools
    # => Name: cali40masq-ipam-pools
    #    Type: hash:net
    #    Revision: 7
    #    Header: family inet hashsize 1024 maxelem 1048576 bucketsize 12 initval 0x59a55159
    #    Size in memory: 504
    #    References: 1
    #    Number of entries: 1
    #    Members:
    #    172.16.0.0/16
    

    20240921_kans_w3_12.png

  • 이렇게 외부와의 통신시 터널링(tunl0)은 통하지 않고 calice# 인터페이스와 enp0s3 인터페이스를 통해 외부로 통신하는 것을 확인할 수 있었습니다.
  • enp0s8 인터페이스는 virtualbox상의 host only 인터페이스여서 패킷이 전달되지 않습니다.

다른 노드의 파드 <=> 파드간 통신

  • 이번에는 서로 다른 노드의 파드간의 통신을 알아보겠습니다.
  • 다른 노드의 파드와의 통신은 IPIP(IP in IP) 터널링을 통해 통신합니다.
  • 각 노드에서 파드 네트워크 대역은 Bird에 의해서 BGP로 전파되며, Felix에 의해 노드의 라우팅 테이블에 자동으로 추가/삭제 됩니다.
  • 다른 노드간의 통신은 tunl0 인터페이스를 통해 IP 헤더에 IPIP 헤더를 추가하여, 상대방 노드로 도착 후 tunl0 인터페이스에서 IPIP 헤더를 제거하고, 최종적으로 상대방 파드로 전달됩니다. img.png IPIP 동작방식 출처
  • 그림으로 나타내면 아래와 같습니다.

img.png

파드 배포 전 기본 상태 확인
  • 노드에서 Bird 라우팅 정보와 Felix 라우팅 테이블 정보를 확인합니다.
# 컨트롤플레인
$ route | head -2 ; route -n | grep tunl0
# => Kernel IP routing table
#    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
#    172.16.34.0     192.168.20.100  255.255.255.0   UG    0      0        0 tunl0
#    172.16.158.0    192.168.10.101  255.255.255.0   UG    0      0        0 tunl0
#    172.16.184.0    192.168.10.102  255.255.255.0   UG    0      0        0 tunl0

# 노드1
$ route | head -2 ; route -n | grep tunl0
# => Kernel IP routing table
#    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
#    172.16.34.0     192.168.20.100  255.255.255.0   UG    0      0        0 tunl0
#    172.16.116.0    192.168.10.10   255.255.255.0   UG    0      0        0 tunl0
#    172.16.184.0    192.168.10.102  255.255.255.0   UG    0      0        0 tunl0
  • 노드의 tunl0 정보 확인해 보겠습니다.
# 노드1
$ ifconfig enp0s3
# => enp0s3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  <span style="color: red;">mtu 1500</span>
#             inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255

$ ifconfig tunl0
# => tunl0: flags=193<UP,RUNNING,NOARP>  <span style="color: red;">mtu 1480</span>
#            inet 172.16.158.0  netmask 255.255.255.255
#            tunnel   txqueuelen 1000  (IPIP Tunnel)
#            RX packets 11581  bytes 917302 (917.3 KB)
#            RX errors 0  dropped 0  overruns 0  frame 0
#            TX packets 12036  bytes 2774296 (2.7 MB)
#            TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

# 노드2
$ ifconfig tunl0
# => tunl0: flags=193<UP,RUNNING,NOARP>  <span style="color: red;">mtu 1480</span>
#            inet 172.16.184.0  netmask 255.255.255.255
#            tunnel   txqueuelen 1000  (IPIP Tunnel)
#            RX packets 0  bytes 0 (0.0 B)
#            RX errors 0  dropped 0  overruns 0  frame 0
#            TX packets 0  bytes 0 (0.0 B)
#            TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
  • 터널 인터페이스인 tunl0에 IP가 할당되어있고, MTU는 1480으로 설정되어 있습니다. enp0s3 인터페이스는 mtu가 1500인데 tunl0 인터페이스는 mtu가 1480으로 설정된 이유는, IPIP의 20바이트 헤더를 추가하기 위해서입니다.
파드 배포
  • 테스트를 위해 노드1과 노드2에 각각 파드 1개씩 생성하겠습니다.
  • node2-pod2.yaml 생성
    # node2-pod2.yaml
    apiVersion: v1
    kind: Pod
    metadata:
      name: pod1
    spec:
      nodeName: k8s-w1
      containers:
        - name: pod1
          image: nicolaka/netshoot
          command: ["tail"]
          args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
    ---
    apiVersion: v1
    kind: Pod
    metadata:
      name: pod2
    spec:
      nodeName: k8s-w2
      containers:
        - name: pod2
          image: nicolaka/netshoot
          command: ["tail"]
          args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
    
  • 파드 생성 후 확인

    $ kubectl apply -f node2-pod2.yaml
    # => pod/pod1 created
    #    pod/pod2 created
      
    # calicoctl 이용한 endpoint 확인
    $ calicoctl get workloadendpoints
    # => WORKLOAD   NODE     NETWORKS           INTERFACE
    #    pod1       k8s-w1   172.16.158.11/32   calice0906292e2
    #    pod2       k8s-w2   172.16.184.16/32   calibd2348b4f67
    
  • pod1이 node1에, pod2가 node2에 생성되었음을 확인할 수 있습니다. 파드 생성 후 상태는 아래와 같습니다.

    img.png

파드간 통신 실행 및 확인
  • Calico CNI는 다른 노드의 파드간 통신을 위해 IPIP 터널링을 사용합니다.
  • IPIP를 사용하려면 클라우드 서비스에서 IPIP 터널링을 허용해야 합니다. 현재 Azure는 IPIP 패킷을 허용하지 않고 있으며, 이러한 경우 Flannel을 사용하거나, Calico의 VXLAN을 사용할 수 있습니다.
  • 다음 그림과 같이 tunl0와 eth0 인터페이스에 tcpdump를 사용하여 패킷을 캡쳐해보겠습니다.

img.png

  • 노드1과 노드2에서 각각 아래와 같이 실행합니다.
# tunl0 인터페이스 TX/RX 패킷 카운트 모니터링 실행
$ watch -d 'ifconfig tunl0 | head -2 ; ifconfig tunl0 | grep bytes'

# 패킷 덤프 tunl0
$ tcpdump -i tunl0 -nn

# 패킷 덤프 : IP 헤더의 상위 프로토콜을 IPIP(4)인 패킷만 필터링
$ tcpdump -i enp0s8 -nn ip proto 4
  • 파드1에서 파드2로 ping 통신을 실행해보겠습니다.

img.png

# 마스터 노드에서 pod1 Shell 접속
$ kubectl exec pod1 -it -- zsh

# pod1 에서 pod2 로 핑 통신 : 정상 통신!
# ping -c 10 <pod2 IP>
$ ping -c 10 172.16.184.16
# => PING 172.16.184.16 (172.16.184.16) 56(84) bytes of data.
#    64 bytes from 172.16.184.16: icmp_seq=1 ttl=62 time=1.42 ms
#    64 bytes from 172.16.184.16: icmp_seq=2 ttl=62 time=2.32 ms
#    ...

# tunl0 인터페이스 TX/RX 패킷 카운트 모니터링 확인 : TX/RX 패킷 카운트가 각각 10개로 증가했다
$ watch -d 'ifconfig tunl0 | head -2 ; ifconfig tunl0 | grep bytes'
# => tunl0: flags=193<UP,RUNNING,NOARP>  mtu 1480
#            inet 172.16.184.0  netmask 255.255.255.255
#            RX packets 10  bytes 840 (840.0 B)
#            TX packets 10  bytes 840 (840.0 B)

# 패킷 덤프 : tunl0 - 터널 인터페이스에 파드간 IP 패킷 정보 확인!
$ tcpdump -i tunl0 -nn
# => tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
#    listening on tunl0, link-type RAW (Raw IP), capture size 262144 bytes
#    15:42:06.220413 IP 172.16.158.11 > 172.16.184.16: ICMP echo request, id 259, seq 1, length 64
#    15:42:06.220720 IP 172.16.184.16 > 172.16.158.11: ICMP echo reply, id 259, seq 1, length 64
#    15:42:07.250941 IP 172.16.158.11 > 172.16.184.16: ICMP echo request, id 259, seq 2, length 64
#    15:42:07.252035 IP 172.16.184.16 > 172.16.158.11: ICMP echo reply, id 259, seq 2, length 64
#    ...
...

# 패킷 덤프 : eth0(enp#~) - IP Outer 헤더 안쪽에 IP 헤더 1개가 더 있음을 알 수 있습니다.
$ tcpdump -i enp0s8 -nn proto 4 
# => 15:42:06.220427 <span style="color: red;">IP</span> 192.168.10.101 > 192.168.10.102: <span style="color: red;">IP</span> 172.16.158.11 > 172.16.184.16: <span style="color: red;">ICMP echo</span> request, id 259, seq 1, length 64
#    15:42:06.220720 <span style="color: red;">IP</span> 192.168.10.102 > 192.168.10.101: <span style="color: red;">IP</span> 172.16.184.16 > 172.16.158.11: <span style="color: red;">ICMP echo</span> reply, id 259, seq 1, length 64
#    15:42:07.250959 <span style="color: red;">IP</span> 192.168.10.101 > 192.168.10.102: <span style="color: red;">IP</span> 172.16.158.11 > 172.16.184.16: <span style="color: red;">ICMP echo</span> request, id 259, seq 2, length 64
#    15:42:07.252035 <span style="color: red;">IP</span> 192.168.10.102 > 192.168.10.101: <span style="color: red;">IP</span> 172.16.184.16 > 172.16.158.11: <span style="color: red;">ICMP echo</span> reply, id 259, seq 2, length 64
  • 실습 결과 tunl0 인터페이스를 통한 패킷이 IP 헤더에 IPIP 헤더가 추가되어 노드의 enp0s8 인터페이스로 전달되는 것을 확인할 수 있었습니다.
  • enp0s8 인터페이스에는 파드의 IP인 172.16.x.x 대역의 IP 패킷이 노드의 IP 대역인 192.168.x.x 대역으로 감싸져서 전달되는 것을 확인할 수 있었습니다.
  • 캡쳐된 패킷을 wireshark로 확인해보겠습니다.

img.png

  • 실제 ICMP 프로토콜의 상위에 파드의 IP 프로토콜이 있고, 그 위에 노드의 IP 프로토콜이 있는 것을 확인할 수 있습니다.

Calico 네트워크 모드

  • Calico는 다양한 네트워크 모드를 지원합니다. 네트워크 모드는 Calico의 CNI 설정을 통해 설정할 수 있습니다.
  • Calico의 네트워크 모드는 다음과 같습니다.

    네트워크 모드 설명
    IPIP IPIP 터널링을 사용하여 노드 간 통신을 위한 네트워크 모드입니다.
    Direct 호스트의 물리 네트워크 인터페이스를 사용하여 노드 간 통신을 위한 네트워크 모드입니다.
    VXLAN Flannel에서 사용했던 VXLAN을 사용하여 노드 간 통신을 위한 네트워크 모드입니다.
    Pod 패킷 암호화 WireGuard를 사용하여 노드 간 통신을 암호화 하기 위한 네트워크 모드입니다.
  • 성능은 Direct > IPIP > VXLAN 순으로 높지만, IPIP는 클라우드 서비스에서 IPIP 터널링을 허용해야 하고, Direct는 물리 네트워크를 통해 직접 통신하므로 파드의 라우팅이나 방화벽 정책을 적용하는것이 까다로운 단점이 있습니다.
  • Pod 패킷 암호화는 WireGuard를 사용하여 노드 간 통신을 암호화하는 방법으로, 가장 안전하지만 암호화에 따른 부하와 지연이 발생하게 됩니다.
  • 이러한 특성들을 잘 이해하여 적절한 네트워크 모드를 선택하여 사용해야 합니다.
  • 각각의 mode에 대해 알아보겠습니다.

IPIP 모드

  • IPIP 모드는 노드 간 통신을 위한 네트워크 모드로, IPIP 터널링을 사용하여 노드 간 통신을 합니다.
  • 앞선 실습들이 모두 IPIP 모드로 진행되었습니다. Calico의 기본 네트워크 모드이며, 적절한 속도와 좋은 사용성을 제공합니다.
  • 단점으로는 클라우드 서비스에서 IPIP 터널링을 허용해야 하며, IPIP 터널링으로 인한 오버헤드가 발생할 수 있습니다.
IPIP 모드 설정
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: default-ipv4-ippool
spec:
  allowedUses:
  - Workload
  - Tunnel
  blockSize: 24
  cidr: 172.16.0.0/16
  natOutgoing: true
  ipipMode: Always  # IPIP 모드 설정
  vxlanMode: Never
통신 흐름

img.png

  • 다른 노드와의 파드간의 통신은 tunl0 인터페이스를 통해 IP 헤더에 IP 헤더를 감싸서(IP-in-IP) 상대 노드에 도달후 IPIP 헤더를 제거하고 최종적으로 상대방 파드로 전달됩니다.
  • 다른 노드의 파드 대역은 BGP로 전파되며, Felix에 의해 노드의 라우팅 테이블에 자동으로 추가/삭제 됩니다.

Direct 모드

  • Direct 모드는 노드 간 통신을 위한 네트워크 모드로, 호스트의 물리 네트워크 인터페이스를 사용하여 노드 간 통신을 합니다.
  • 클라우드 사업자 네트워크에서는 NIC에 매칭되지 않은 IP 패킷이 차단되기 때문에, NIC에서 Source/Destination Check 기능을 해제해야 합니다. 링크
    • AWS에서는 다음의 AWS CLI 명령으로 NIC의 Source/Destination Check 기능을 해제할 수 있습니다.
      $ aws ec2 modify-instance-attribute --instance-id <INSTANCE_ID> --source-dest-check "{\"Value\": false}"
      
  • Virtual box에서는 NIC의 promiscuous mode를 사용해야 합니다.
Direct 모드 설정
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: default-ipv4-ippool
spec:
  allowedUses:
  - Workload
  - Tunnel
  blockSize: 24
  cidr: 172.16.0.0/16
  natOutgoing: true
  ipipMode: Never   # IPIP 모드 사용하지 않음
  vxlanMode: Never
통신 흐름

img.png

Cross Subnet 모드
  • Direct 모드는 Cross Subnet 모드로 사용할 수 있습니다.
  • Cross Subnet 모드는 노드간의 네트워크 대역이 다를때 사용할 수 있으며, 노드간 같은 네트워크 대역이면 Direct 모드로 동작하고, 다른 네트워크 대역이면 IPIP/VXLAN 모드로 동작합니다.

  • IPIP를 이용한 Cross Subnet 모드 설정

    apiVersion: projectcalico.org/v3
    kind: IPPool
    metadata:
      name: default-ipv4-ippool
    spec:
      allowedUses:
      - Workload
      - Tunnel
      blockSize: 24
      cidr: 172.16.0.0/16
      natOutgoing: true
      ipipMode: CrossSubnet   # IPIP 모드를 Subnet이 다를때만 사용
      vxlanMode: Never
    
  • VXLAN를 이용한 Cross Subnet 모드 설정

    apiVersion: projectcalico.org/v3
    kind: IPPool
    metadata:
      name: default-ipv4-ippool
    spec:
      allowedUses:
      - Workload
      - Tunnel
      blockSize: 24
      cidr: 172.16.0.0/16
      natOutgoing: true
      ipipMode: Never   
      vxlanMode: CrossSubnet      # VXLAN 모드를 Subnet이 다를때만 사용
    

VXLAN 모드

  • VXLAN 모드는 Flannel에서 사용했던 VXLAN을 사용하여 노드 간 통신을 위한 네트워크 모드입니다.
  • IPIP 모드와 비슷하지만, VXLAN은 IPIP보다 속도가 느리지만, 클라우드 서비스에서 IPIP 터널링을 허용하지 않을 때 사용할 수 있습니다.
VXLAN 모드 설정
apiVersion: projectcalico.org/v3
kind: IPPool
metadata:
  name: default-ipv4-ippool
spec:
  allowedUses:
  - Workload
  - Tunnel
  blockSize: 24
  cidr: 172.16.0.0/16
  natOutgoing: true
  ipipMode: Never  
  vxlanMode: Always # VXLAN 모드 설정
통신 흐름

img.png

  • VXLAN은 vxlan 인터페이스를 통해 L2 패킷을 UDP - VXLAN으로 감싸서 상대측 노드로 전달 후 vxlan 인터페이스에서 VXLAN에서 실제 L2 프레임을 추출하여 최종적으로 상대방 파드로 전달됩니다.
  • 이때 BGP 는 미사용되며, VXLAN L3 라우팅을 통해서 동작합니다.

파드 패킷 암호화 (네트워크 레벨)

  • WireGuard를 사용하여 노드 간 통신을 암호화하는 방법으로, 가장 안전하지만 암호화에 따른 부하와 지연이 발생하게 됩니다.
  • 최근 대두되고 있는 제로 트러스트 네트워크 환경에서 사용할 수 있습니다.
  • 사용되는 WireGuard는 IPSec이나 OpenVPN과 같은 VPN 프로토콜이며, 기존의 VPN들 보다 빠르고 간단하며, 적은 리소스를 사용합니다.
WireGuard 모드 설정
apiVersion: projectcalico.org/v3
kind: FelixConfiguration
metadata:
  name: default
spec:
  bpfConnectTimeLoadBalancing: TCP
  bpfHostNetworkedNATWithoutCTLB: Enabled
  bpfLogLevel: ""
  floatingIPs: Disabled
  logSeverityScreen: Info
  reportingInterval: 0s
  wireguardEnabled: true    # wireguard 사용 설정
통신 흐름

img_1.png

  • 원본 패킷이 wireg 인터페이스를 통해 WireGuard로 암호화되어 상대방 노드로 전달되며, 상대방 노드에서는 wireg 인터페이스를 통해 복호화하여 최종적으로 상대방 파드로 전달됩니다.

마치며

지금까지 Calico의 기본적인 구성과 동작 방식에 대해 알아보았습니다. Flannel에 비해 많은 기능을 제공하고, 다양한 네트워크를 통해 좀 더 안전하고 빠른 네트워크 환경을 제공하는것 같습니다. IP-in-IP 개념은 처음 보는 내용이라 신선하였습니다. 하지만 IPIP의 경우 클라우드 환경에 따라 사용하기 어려울 수도 있고, Direct 모드는 promiscuous mode로 인해 불필요한 오버헤드와 보안 이슈가 발생할 수 있을 것 같습니다. VXLAN을 사용하자니 Flannel에 비해 우위가 크게 없어 보이고, 제로트러스트가 필요한 환경에서 파드 패킷 암호화를 사용하는 것은 좋아보입니다.

기존에 많이들 쓴다고 해서 Calico를 사용했었는데 이렇게 심오한 세계가 있다는 것을 알게 되어서 기쁩니다. 앞으로 배우게될 Cilium CNI가 더 기대가 됩니다.