
이번 주차에는 지난 2주차에서 짧게 언급됐던 Flannel CNI와 마찬가지로 Kubernetes CNI 중 하나인 Calico에 대해 학습했습니다. Calico에 대해 학습하기 전에 Kubernets CNI의 네 가지 요구사항에 대한 리마인드 입니다. 인터페이스라는 말 자체가 일종의 규격을 의미하는데, EKS VPC CNI, Calico, Flannel 등과 같이 다양한 CNI 종류가 있고, 각 CNI 마다 특징과 동작방식이 있지만, 아래 네 가지 요구사항은 Interface로써 모두 지켜야 합니다.
- Pod 간 통신 시 NAT 없이 통신 가능해야 합니다.
- 노드와 Pod 간 통신 가능해야 합니다.
- Host mode를 사용하는 Pod는 NAT 없이 Pod와 통신 가능해야 합니다.
- 서비스 IP 대역과 Pod IP 대역은 중복되지 않아야 합니다.
Calico에 대한 전반적인 설명
Calico는 CNI 중 가장 많이 쓰이는 CNI 중 하나고, 다양한 방식(mode)를 지원해 선택의 폭이 넓습니다. Calico는 Kubernets 뿐만 아니라, 다양한 환경에서도 사용 가능한 네트워크 인터페이스 입니다. 아래 이미지는 Calico 공식 문서에서 설명하는 Cliaco를 사용하기에 적합한 환경을 보여주는 이미지 입니다. (거의 모든 환경에 적합하다고 설명하고 있습니다.)

공식 문서를 읽다 처음 알게된 것은 Calico에 여러 제품이 있어, 제품 마다 지원하는 기능이 다르다는 것입니다. 각 제품별 차이에 대한 설명은 아래 문서를 참고하시면 됩니다.
Calico Documentation | Calico Documentation
Tigera and Calico (projectcalico) documentation
docs.tigera.io
아키텍처를 살펴보면 아래와 같습니다. Calico의 장점 중 하나라고 생각하는 것은 Calico를 관리하기 위한 별도 CLI가 있다는 것 입니다. 아래 이미지와 같이 kubectl 뿐만 아니라, calicoctl을 통해 API를 호출할 수 있고, Calico 구성요소를 관리할 수 있습니다.
- Bird : Felix에서 경로를 가져와 네트워크의 BGP Peer(노드)에 배포하여 호스트 간 라우팅을 수행합니다. Felix 에이전트를 호스팅하는 각 노드에서 실행되는 오픈 소스입니다.
- Felix : 호스트의 엔드포인트에 원하는 연결을 제공하기 위해 호스트에서 필요한 경로 및 ACL 및 기타 설정을 관리합니다. 엔드포인트를 호스팅하는 각 머신에 에이전트 데몬으로 실행됩니다.
- Confd : Calico Datastore에서 BGP 구성 및 AS 번호, 로깅 수준, IPAM 정보와 같은 글로벌 기본값의 변경 사항을 모니터링하며, 오픈 소스 기반의 가벼운 구성 관리 도구입니다.
- IPAM : Calico의 IP 풀 리소스를 사용하여 클러스터 내 Pod에 IP 주소가 할당되는 방식을 제어합니다. 대부분의 Calico 설치에서 사용하는 기본 플러그인입니다.
- Typha : 각 노드의 데이터 저장소(Confd)에 미치는 영향을 줄여 규모를 확대하기 위해 캐시(Cache) 역할을 합니다. 데이터 저장소와 Felix 인스턴스 사이에서 데몬으로 실행됩니다. 기본적으로 설치되지만 구성되지 않습니다.
- Datastore : Kubernetes의 etcd와 같은 데이터 저장소로 Calico 관련 설정을 포함하고 있습니다. Kubernetes API 방식과 etcd 저장 방식이 있는데, API 저장 방식이 추가 데이터 저장소가 필요하지 않아 관리가 더 간단하다는 장점이 있습니다.

Calico에서 사용 가능한 모드입니다.

IPIP 모드 : 이전 주차에서 진행한 Flannel 방식과 동일한 방식입니다.
Direct 모드 : Pod 통신 시 패킷이 출발지 노드의 라우팅 정보를 보고 목적지 노드로 원본 패킷을 그대로 전달하는 방식입니다.
VXLAN 모드 : 다른 노드 간의 파드 통신은 VXLAN 인터페이스를 통해 L2 프레임이 UDP - VXLAN에 감싸져서 상대측 노드로 도달 후 VXLAN 인터페이스에서 Outer 헤더를 제거하고 내부의 파드와 통신하는 방식입니다.
Pod 패킷 암호화 모드 : WireGuard 터널을 자동 생성 및 파드 트래픽을 암호화하여 노드 간 전달하는 방식입니다.
실습 환경 구성
실습 환경 구성을 위한 코드입니다.
# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/kans/kans-3w.yaml
# CloudFormation 스택 배포
# aws cloudformation deploy --template-file kans-3w.yaml --stack-name mylab --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 --region ap-northeast-2
예시) aws cloudformation deploy --template-file kans-3w.yaml --stack-name mylab --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2
## Tip. 인스턴스 타입 변경 : MyInstanceType=t2.micro
예시) aws cloudformation deploy --template-file kans-3w.yaml --stack-name mylab --parameter-overrides MyInstanceType=t2.micro KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2
# CloudFormation 스택 배포 완료 후 k8s-m EC2 IP 출력
aws cloudformation describe-stacks --stack-name mylab --query 'Stacks[*].Outputs[0].OutputValue' --output text --region ap-northeast-2
# [모니터링] CloudFormation 스택 상태 : 생성 완료 확인
while true; do
date
AWS_PAGER="" aws cloudformation list-stacks \
--stack-status-filter CREATE_IN_PROGRESS CREATE_COMPLETE CREATE_FAILED DELETE_IN_PROGRESS DELETE_FAILED \
--query "StackSummaries[*].{StackName:StackName, StackStatus:StackStatus}" \
--output table
sleep 1
done
# k8s-m EC2 SSH 접속
ssh -i ~/.ssh/kp-gasida.pem ubuntu@$(aws cloudformation describe-stacks --stack-name mylab --query 'Stacks[*].Outputs[0].OutputValue' --output text --region ap-northeast-2)
CloudFormation 스택 정상 배포한 결과입니다.

EC2 대시보드입니다.

CloudFormation으로 배포한 EC2 인스턴스를 SSH로 접속하기 위해 ControlPlane의 공인 IP를 확인합니다.

EC2 인스턴스에 접속한 결과입니다. kube-ps1, kubens 명령어들이 설치된 것을 알 수 있습니다.

context가 너무 길어서 업데이트 했습니다.

설치한 쿠버네티스 이미지입니다. Calico CNI에 대한 실습을 위해 CNI가 설치되지 않은 상태라서 CoreDNS Pod가 Pending 상태입니다.

CNI 설정은 /opt/cni/bin 하위에서 관리됩니다. (1주차에서 학습한 것처럼, 리눅스에서는 모든 것이 파일로 관리됩니다.)

CNI가 설치되지 않은 상태와 Calico CNI를 설치한 이후의 라우팅 테이블과 IPTables를 비교해보려고 합니다. 아래 이미지는 CNI가 설치되지 않은 상태의 라우팅 테이블과 인터페이스 상태입니다.

IPTables의 filter, nat의 결과입니다.


개수는 각각 50개, 48개 입니다. (Calico CNI 설치만으로 규칙의 개수가 얼마나 바뀌는지 확인해보려고 합니다.)

Calico CNI 설치
kubectl apply -f https://raw.githubusercontent.com/gasida/KANS/main/kans3/calico-kans.yaml

Calico가 DaemonSet으로 배포되는 모습입니다.

CNI 배포 후 Pending 상태였던 CoreDNS도 정상 배포되었습니다. (Pending => Running으로 변경)

calicoctl 설치 방법입니다.
curl -L https://github.com/projectcalico/calico/releases/download/v3.28.1/calicoctl-darwin-amd64 -o calicoctl
chmod +x calicoctl

Calico CNI 설치 후 비교
CNI 바이너리 하위에 Calico 파일이 추가됐습니다.

라우팅 테이블에는 Bird로 향하는 tunnel0번 인터페이스가 생성되었습니다.

IPTables 내 Calico 설치 후 추가된 Calico 관련 규칙입니다. (grep을 통해 필터링 했습니다.)


규칙만 약 60개씩 추가된 것을 알 수 있습니다.

calicoctl을 활용한 Calico 검증
EC2 인스턴스에 calictoctl 설치하는 방법은 아래와 같습니다.
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

IPAM 관련 명령어는 아래와 같습니다. IPAM CIDR 172.16.0.0/16 대역 중 사용 중인 IP는 몇 개인지, 가용할 수 있는 IP는 몇 개인지와 각 노드에서 Pod에 할당하는 CIDR는 무엇인지 설정은 어떻게 되어 있는지 등을 알 수 있습니다. --show-blocks의 결과로 보이는 /24 대역이 바로 Bird에서 BGP로 광고한다는 그 대역입니다.
# 칼리코 IPAM 정보 확인 : 칼리코 CNI 를 사용한 파드가 생성된 노드에 podCIDR 네트워크 대역 확인 - 링크
calicoctl ipam show
# Block 는 각 노드에 할당된 podCIDR 정보
calicoctl ipam show --show-blocks
calicoctl ipam show --show-borrowed
calicoctl ipam show --show-configuration

노드 상태를 확인하는 명령어 입니다. 아래 이미지에서 Peer Address란 각 노드의 IP입니다. (위에서 설명한 것처럼, Bird를 통해 각 노드의 CIDR 대역을 광고한다고 설명했습니다.)
calicoctl node status
calicoctl node checksum

IPAM 관련 설정을 확인할 수 있는 명령어 입니다.
calicoctl get ippool -o wide

Pod CIDR, Service CIDR를 아래와 같이 확인할 수 있습니다.
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
kubectl get cm -n kube-system kubeadm-config -oyaml | grep -i subnet

배포한 Pod(Workload)에 대한 정보를 확인할 수 있습니다. WORKLOAD는 PodName을 의미하고, NODE는 Pod가 스케줄링된 노드 이름, NETWORKS는 Pod에 할당된 IP, INTERFACE는 할당된 InterfaceName입니다.
calicoctl get workloadEndpoint
calicoctl get workloadEndpoint -A
calicoctl get workloadEndpoint -o wide -A

Calico CNI Pod 정보 상세 확인
Calico CNI를 배포하면 DaemonSet으로 calico-node라는 Pod에 대해 자세하게 알아보려고 합니다. calico-node 컨테이너와 bird 프로세스를 확인한 결과입니다.

bird.cfg 파일 내용입니다.
find / -name bird.cfg
cat /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/61/fs/etc/calico/confd/config/bird.cfg
function apply_communities ()
{
}
# Generated by confd
include "bird_aggr.cfg";
include "bird_ipam.cfg";
router id 192.168.10.10;
# Configure synchronization between routing tables and kernel.
protocol kernel {
learn; # Learn all alien routes from the kernel
persist; # Don't remove routes on bird shutdown
scan time 2; # Scan kernel routing table every 2 seconds
import all;
export filter calico_kernel_programming; # Default is export none
graceful restart; # Turn on graceful restart to reduce potential flaps in
# routes when reloading BIRD configuration. With a full
# automatic mesh, there is no way to prevent BGP from
# flapping since multiple nodes update their BGP
# configuration at the same time, GR is not guaranteed to
# work correctly in this scenario.
merge paths on; # Allow export multipath routes (ECMP)
}
# Watch interface up/down events.
protocol device {
debug { states };
scan time 2; # Scan interfaces every 2 seconds
}
protocol direct {
debug { states };
interface -"cali*", -"kube-ipvs*", "*"; # Exclude cali* and kube-ipvs* but
# include everything else. In
# IPVS-mode, kube-proxy creates a
# kube-ipvs0 interface. We exclude
# kube-ipvs0 because this interface
# gets an address for every in use
# cluster IP. We use static routes
# for when we legitimately want to
# export cluster IPs.
}
# Template for all BGP clients
template bgp bgp_template {
debug { states };
description "Connection to BGP peer";
local as 64512;
gateway recursive; # This should be the default, but just in case.
add paths on;
graceful restart; # See comment in kernel section about graceful restart.
connect delay time 2;
connect retry time 5;
error wait time 5,30;
}
# -------------- BGP Filters ------------------
# No v4 BGPFilters configured
# ------------- Node-to-node mesh -------------
# For peer /bgp/v1/host/k8s-m/ip_addr_v4
# Skipping ourselves (192.168.10.10)
# For peer /bgp/v1/host/k8s-w0/ip_addr_v4
protocol bgp Mesh_192_168_20_100 from bgp_template {
neighbor 192.168.20.100 as 64512;
source address 192.168.10.10; # The local address we use for the TCP connection
import all; # Import all routes, since we don't know what the upstream
# topology is and therefore have to trust the ToR/RR.
export filter {
calico_export_to_bgp_peers(true);
reject;
}; # Only want to export routes for workloads.
passive on; # Mesh is unidirectional, peer will connect to us.
}
# For peer /bgp/v1/host/k8s-w1/ip_addr_v4
protocol bgp Mesh_192_168_10_101 from bgp_template {
neighbor 192.168.10.101 as 64512;
source address 192.168.10.10; # The local address we use for the TCP connection
import all; # Import all routes, since we don't know what the upstream
# topology is and therefore have to trust the ToR/RR.
export filter {
calico_export_to_bgp_peers(true);
reject;
}; # Only want to export routes for workloads.
passive on; # Mesh is unidirectional, peer will connect to us.
}
# For peer /bgp/v1/host/k8s-w2/ip_addr_v4
protocol bgp Mesh_192_168_10_102 from bgp_template {
neighbor 192.168.10.102 as 64512;
source address 192.168.10.10; # The local address we use for the TCP connection
import all; # Import all routes, since we don't know what the upstream
# topology is and therefore have to trust the ToR/RR.
export filter {
calico_export_to_bgp_peers(true);
reject;
}; # Only want to export routes for workloads.
passive on; # Mesh is unidirectional, peer will connect to us.
}
# ------------- Global peers -------------
# No global peers configured.
# ------------- Node-specific peers -------------
# No node-specific peers configured.
통신 확인
동일 노드 내 Pod 간 통신
동일 노드 내 Pod 간 통신은 노드 내부에서 직접 통신이 가능하며, IPTables FORWARD 규칙에 의해 통신이 됩니다. 노드 내 calice# 인터페이스에 Proxy ARP 설정으로 Pod에서 바라보는 게이트웨이의 MAC 정보를 Pod가 전달받습니다. (노드의 Tunnel 인터페이스는 관여하지 않습니다.)

동일 노드에 Pod 2개를 배포하기 위해 단순하게 nodeName 속성을 사용했습니다.
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
Pod 생성 후 변경된 정보입니다. Pod IP는 달라질 수 있습니다.

정상 배포된 모습입니다.

Pod 배포 후 노드에 접속해 변경된 인터페이스와 라우팅 테이블 정보를 확인합니다. cali* 인터페이스와 라우팅 테이블 규칙이 2개 추가되었습니다. (Pod의 IP로 호스팅 라우팅 대역이 라우팅 테이블이 추가되었습니다.)

통신할 Pod 접속 후 상태 확인한 정보입니다. 라우팅 테이블을 살펴보면, 디폴트 라우팅으로 게이트웨이를 향하고 있습니다. 위 이미지에서 Virtual Router를 통해 Pod 간 통신한다는 의미입니다.

당연히 정상 통신된다.

노드에서 TCPDUMP를 실행한 결과이다.

서로 다른 노드의 Pod 통신
서로 다른 노드의 Pod 간 통신에는 IPIP 터널 모드를 통해 통신이 됩니다. 각 노드의 Pod 네트워크 대역은 Bird에 의해 BGP로 광고되고, Felix에 의해 호스트의 라우팅 테이블에 자동으로 추가 및 삭제됩니다. 다른 노드 간 Pod 통신은 Tunnel 인터페이스를 통해 IP 헤더에 감싸져 상대측 노드로 전달되고, Tunnel 인터페이스에서 Outer 헤더를 제거하고 내부의 Pod와 통신합니다.

IPIP 패킷을 Wireshark로 확인한 결과입니다. Outer IP는 노드의 IP이고, Inner IP는 Pod의 IP입니다.


Pod에서 외부 인터넷 통신
Pod에서 외부 인터넷 통신 시에는 노드의 네트워크 인터페이스 IP로 NAT한 후 통신하는 구조입니다. Calico 기본 설정은 아웃바운드 NAT에 대한 설정이 True입니다. IPTables에 MASQUERADE 규칙에 의해 외부에 연결됩니다. (Pod와 외부 간 직접 통신에는 Tunnel 인터페이스는 관여하지 않습니다.)

IPTables 규칙 확인한 결과입니다.

테스트를 위한 Pod를 배포합니다.
curl -O https://raw.githubusercontent.com/gasida/NDKS/main/4/node1-pod1.yaml
kubectl apply -f node1-pod1.yaml

Pod에서 Google DNS로 통신을 시도하고, 노드에서 TCPDUMP를 뜨면, Pod의 IP가 아닌 노드의 IP가 찍히는 것을 알 수 있습니다.

감사합니다.