[Cilium Study] 1 주 차 - 2. CNI와 Kubernetes 네트워킹에서 이어집니다.

 

[Cilium Study] 1주차 - 2. CNI와 Kubernetes 네트워킹

이전 포스팅 [Ciliu Study] 1주 차 - 1. 실습 환경 구성에서 이어집니다. [Cilium Study] 1주차 - 1. 실습 환경 구성'가시다'님의 [Cilium Study] 1기의 내용을 정리하는 시리즈 구성의 포스팅을 시작하려고 합니

tech-recipe.tistory.com


iptables의 복잡한 작동 방식

 지난 포스팅 말미에 'webpod' Service의 ClusterIP에 대한 라우팅 규칙에 대해서 잠깐 살펴보았습니다. 그 내용을 다시 복기해보면 다음과 같습니다.

# webpod svc의 clsuter ip에 대한 iptables 정보 확인
kubectl get svc webpod -o jsonpath="{.spec.clusterIP}"
SVCIP=$(kubectl get svc webpod -o jsonpath="{.spec.clusterIP}")
iptables -t nat -S | grep $SVCIP

10.96.56.150
-A KUBE-SERVICES -d 10.96.56.150/32 -p tcp -m comment --comment "default/webpod cluster IP" -m tcp --dport 80 -j KUBE-SVC-CNZCPOCNCNOROALA
-A KUBE-SVC-CNZCPOCNCNOROALA ! -s 10.244.0.0/16 -d 10.96.56.150/32 -p tcp -m comment --comment "default/webpod cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
순서 출력 문구 설명
첫 번째 규칙 -A KUBE-SERIVCE KUBE-SERVICE라는 체인에 규칙을 추가
-d 10.96.56.150/32 목적지(-d)가 10.96.56.150인 트래픽을 의미, 여기에서는 webapp service의 Cluster IP를 의미
-p tcp --dport 80 protocol이 TCP이고 목적지 포트(--port)가 80인 트래픽을 의미
-m commnet --commnet "..." 규칙에 대한 주석
-j KUBE-SVC-CNZ... 앞의 규칙을 만족하는 트래픽을 다음 체인(KUBE-SVC-CNZCPOCNCNOROALA)으로 점프
두 번째 규칙 -A KUBE-SVC-CNZCPOCNCNOROALA webpod 서비스 전용 체인에 규칙을 추가
! -s 10.244.0.0/16 출발지 IP가 10.244.0.0/16 대역(Pod CIDR)이 아닌(!) 트래픽을 의미
-j KUBE_MARK_MASQ  Masquerading 표식을 남기라는 의미

 여기서 'SVC-CNZCPOCNCNOROALA'라는 이름의 iptables 규칙에 대해서 살펴보면 정말 어마어마한 것을 알 수 있습니다. 다음 명령어를 통해 한번 알아보도록 하죠.

# CNZCPOCNCNOROALA 문자열이 들어가는 iptables 규칙 조회
iptables-save | grep CNZCPOCNCNOROALA

:KUBE-SVC-CNZCPOCNCNOROALA - [0:0]
-A KUBE-SERVICES -d 10.96.56.150/32 -p tcp -m comment --comment "default/webpod cluster IP" -m tcp --dport 80 -j KUBE-SVC-CNZCPOCNCNOROALA
-A KUBE-SVC-CNZCPOCNCNOROALA ! -s 10.244.0.0/16 -d 10.96.56.150/32 -p tcp -m comment --comment "default/webpod cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-CNZCPOCNCNOROALA -m comment --comment "default/webpod -> 10.244.1.3:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-SVF7XOS5TDWJWJZZ
-A KUBE-SVC-CNZCPOCNCNOROALA -m comment --comment "default/webpod -> 10.244.2.3:80" -j KUBE-SEP-UUGX2XES2F42KZPX

 여기서 중요하게 볼 것은 바로 위 출력 중 마지막 2개 줄입니다.

  • -A KUBE-SVC-CNZCPOCNCNOROALA -m comment --comment "default/webpod -> 10.244.1.3:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-SVF7XOS5TDWJWJZZ
  • -A KUBE-SVC-CNZCPOCNCNOROALA -m comment --comment "default/webpod -> 10.244.2.3:80" -j KUBE-SEP-UUGX2XES2F42KZPX

각각을 해석해 보면 다음과 같습니다.

  • "default/webpod -> 10.244.1.3:80"이라는 주석과 함께 50% 확률로 트래픽을 'KUBE-SEP-SVF7XOS5TDWJWJZZ' 체인으로 점프
  • (위 규칙이 적용되지 않은 경우)"default/webpod -> 10.244.2.3:80"이라는 주석과 함께 50% 확률로 트래픽을 'KUBE-SEP-UUGX2XES2F42KZPX' 체인으로 점프

 여기서 등장하는 IP 주소인 10.244.1.3과 10.244.2.3은 앞서 생성한 webpod들의 IP 주소입니다. 이것이 의미하는 바는 매우 중요합니다. 바로 Kubernetes에서 Service와 End point Pod들에 대한 로드밸런싱을 이런 규칙들을 순차적으로 적용하면서 이루어진다는 것입니다.

 조금 더 깊은 확인을 위해 'KUBE-SEP-SVF7XOS5TDWJWJZZ' 체인 규칙과 'KUBE-SEP-UUGX2XES2F42KZPX' 체인 규칙에 대해서 살펴보면 아래와 같은 것을 볼 수 있습니다.

# 'KUBE-SEP-SVF7XOS5TDWJWJZZ' 체인 규칙
-A KUBE-SEP-SVF7XOS5TDWJWJZZ -s 10.244.1.3/32 -m comment --comment "default/webpod" -j KUBE-MARK-MASQ
-A KUBE-SEP-SVF7XOS5TDWJWJZZ -p tcp -m comment --comment "default/webpod" -m tcp -j DNAT --to-destination 10.244.1.3:80

# 'KUBE-SEP-UUGX2XES2F42KZPX' 체인 규칙
-A KUBE-SEP-UUGX2XES2F42KZPX -s 10.244.2.3/32 -m comment --comment "default/webpod" -j KUBE-MARK-MASQ
-A KUBE-SEP-UUGX2XES2F42KZPX -p tcp -m comment --comment "default/webpod" -m tcp -j DNAT --to-destination 10.244.2.3:80

 위 두 개의 체인 규칙 중 하나(50% 확률로 로드 밸런싱 되므로)에 도달한 트래픽은 'KUBE-MARK-MASQ' 체인 규칙을 적용한 후, DNAT을 수행하여 비로소 최종 목적지 Pod(10.244.1.3:80 또는 10.244.2.3:80)에 도달하게 됩니다. 이를 간단히 정리해 보면 아래와 같습니다.

  1. 목적지 IP가 10.96.56.150/32 이면서 목적지 포트가 80인 트래픽이 'KUBE-SERVICE'라는 체인에 감지
  2. 'KUBE-SERVICE' 체인의 규칙(Rule)에 따라 'KUBE-SVC-CNZCPOCNCNOROALA' 체인으로 점프
  3. 패킷은 'KUBE-SVC-CNZCPOCNCNOROALA' 체인에서 확률을 통해 구성된 로드 밸런싱 룰을 거치면서 목적지 체인으로 점프
  4. 목적지 체인(각 체인은 Service의 endpoint Pod를 가리킴)에서 최종적으로 DNAT을 통해 목적지 Pod로 라우팅

 kube-proxy가 iptables를 기반으로 작동하게 된다면, kubernetes에서 움직이는 패킷은 항상 위와 같은 단계(실제로는 조금 더 복잡)를 거치게 됩니다. 조금 더 엄밀한 작동 방식에 대한 이해를 위해서 다음 포스팅의 링크를 남깁니다.

 

[network] Packet flow in netfilter ( iptables)

TL;DR linux 기반 OS에서 Firewall설정을 이야기하면 iptables에 대한 이야기를 많이 한다. iptables는 packet filtering에 대한 network rule을 만드는 userspace영역의 command line program이다.

velog.io

시간 복잡도 O(n) 문제

 앞서 살펴본 바와 같이, iptables를 사용하게 되면 여러 복잡한 단계를 거쳐 패킷이 실제 목적지에 도달하게 됩니다. 그 규칙의 수가 적은 경우에는 이러한 복잡도가 크게 문제가 되지 않지만, kubernetes의 서비스가 10,000개로 늘어나 그 규칙이 추가되면 과연 어떻게 될까요? 당연한 이야기지만, 이때부터는 문제가 발생하기 시작합니다.

iptables의 기본 동작 방식

 iptables(netfilter)는 엄밀하게는 user spcae에서 네트워크 규칙을 만드는 명령어입니다. 여기서 생성된 규칙은 netfilter에 의해서 적용되는데, 가장 첫 번째로 일치하는 규칙이 나올 때까지(First Match Winds) 순차적으로 처리되게 됩니다. 바로 이 부분이 iptables(netfilter)의 가장 큰 약점이 되는 부분입니다.

 첫 번째로, 어떤 패킷이 iptables에서 생성한 규칙 중 뒤에 위치한 규칙과 일치하는 경우, 해당 규칙을 적용받아야 하는 트래픽의 규칙 검사 시간이 길어질 수 있습니다. 실제로 아래와 같은 명령어를 통해서 규칙의 순서를 확인할 수 있으며, 이것이 실제 문제가 될 수 있다는 것은 쉽게 유추할 수 있습니다.

# 1. 첫 번째 Service 생성
kubectl create service clusterip nginx-service --tcp=80:80

# 2. 두 번째 Service 생성  
kubectl create service clusterip api-service --tcp=8080:8080

# 3. 세 번째 Service 생성
kubectl create service clusterip db-service --tcp=5432:5432

# KUBE-SERVICES 체인 확인
iptables -t nat -L KUBE-SERVICES -n --line-numbers

# 결과:
Chain KUBE-SERVICES (2 references)
num  target     prot opt source               destination         
1    KUBE-SVC-NPX46M4PTMTKRN6Y  6    --  0.0.0.0/0            10.96.0.1            /* default/kubernetes:https cluster IP */ tcp dpt:443
2    KUBE-SVC-TCOU7JCQXEZGVUNU  17   --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:dns cluster IP */ udp dpt:53
3    KUBE-SVC-ERIFXISQEP7F7OF4  6    --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:dns-tcp cluster IP */ tcp dpt:53
4    KUBE-SVC-JD5MR3NA4I4DYORP  6    --  0.0.0.0/0            10.96.0.10           /* kube-system/kube-dns:metrics cluster IP */ tcp dpt:9153
5    KUBE-SVC-CNZCPOCNCNOROALA  6    --  0.0.0.0/0            10.96.56.150        /* default/webpod cluster IP */ tcp dpt:80
6    KUBE-NODEPORTS  0    --  0.0.0.0/0            0.0.0.0/0            /* kubernetes service nodeports; NOTE: this must be the last rule in this chain */ ADDRTYPE match dst-type LOCAL

 게다가, 해당 Service에 endpoint Pod가 많은 경우를 생각해 봅시다. Replica가 20인 nginx Deployment를 만들어 보고, 이를 ClusterIP Service로 노출시킨 후 iptables를 통해 체인을 확인해 보겠습니다.

# replica가 20인 Deployment 생성
kubectl create deployment nginx --image=nginx --replicas 20
deployment.apps/nginx created

# Deployment 노출
kubectl expose deployment nginx --port=80
service/nginx exposed

# nginx Service의 iptables 규칙 확인
kubectl get svc nginx -o jsonpath="{.spec.clusterIP}"
SVCIP=$(kubectl get svc nginx -o jsonpath="{.spec.clusterIP}")
iptables -t nat -S | grep $SVCIP
10.96.246.55-A KUBE-SERVICES -d 10.96.246.55/32 -p tcp -m comment --comment "default/nginx cluster IP" -m tcp --dport 80 -j KUBE-SVC-2CMXP7HKUVJN7L6M
-A KUBE-SVC-2CMXP7HKUVJN7L6M ! -s 10.244.0.0/16 -d 10.96.246.55/32 -p tcp -m comment --comment "default/nginx cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ

# 'KUBE-SVC-2CMXP7HKUVJN7L6M' 규칙 조회
iptables-save | grep KUBE-SVC-2CMXP7HKUVJN7L6M
:KUBE-SVC-2CMXP7HKUVJN7L6M - [0:0]
-A KUBE-SERVICES -d 10.96.246.55/32 -p tcp -m comment --comment "default/nginx cluster IP" -m tcp --dport 80 -j KUBE-SVC-2CMXP7HKUVJN7L6M
-A KUBE-SVC-2CMXP7HKUVJN7L6M ! -s 10.244.0.0/16 -d 10.96.246.55/32 -p tcp -m comment --comment "default/nginx cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.10:80" -m statistic --mode random --probability 0.04999999981 -j KUBE-SEP-WANUSTWS24GIDBO5
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.11:80" -m statistic --mode random --probability 0.05263157887 -j KUBE-SEP-WVRGOMK67P4Z4SUM
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.12:80" -m statistic --mode random --probability 0.05555555550 -j KUBE-SEP-WX7L4YZKYX75Q2LJ
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.13:80" -m statistic --mode random --probability 0.05882352963 -j KUBE-SEP-U3D6P2DGJPD3GTJQ
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.4:80" -m statistic --mode random --probability 0.06250000000 -j KUBE-SEP-P33L4LSPB7KVLSK6
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.5:80" -m statistic --mode random --probability 0.06666666688 -j KUBE-SEP-IZW656N5ZXYN5BEC
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.6:80" -m statistic --mode random --probability 0.07142857136 -j KUBE-SEP-C4VXQDW52UV45WW3
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.7:80" -m statistic --mode random --probability 0.07692307699 -j KUBE-SEP-DST4PJJC54MIXJRG
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.8:80" -m statistic --mode random --probability 0.08333333349 -j KUBE-SEP-IZ3BWJXID3BT25MP
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.1.9:80" -m statistic --mode random --probability 0.09090909082 -j KUBE-SEP-EX7QVTQRSKU3VWAS
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.10:80" -m statistic --mode random --probability 0.10000000009 -j KUBE-SEP-PZD5WGOLWE26ORLM
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.11:80" -m statistic --mode random --probability 0.11111111101 -j KUBE-SEP-DJUNAFMAXUZFPIRM
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.12:80" -m statistic --mode random --probability 0.12500000000 -j KUBE-SEP-6REO6EX33YRWBWTC
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.13:80" -m statistic --mode random --probability 0.14285714272 -j KUBE-SEP-NMXX52BXGUI3RUM3
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.4:80" -m statistic --mode random --probability 0.16666666651 -j KUBE-SEP-6TTN4SHIRO4JAVGT
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.5:80" -m statistic --mode random --probability 0.20000000019 -j KUBE-SEP-XW6I7J74P22Q2AZM
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.6:80" -m statistic --mode random --probability 0.25000000000 -j KUBE-SEP-UOZCNMEBPO5JGMU4
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.7:80" -m statistic --mode random --probability 0.33333333349 -j KUBE-SEP-66LGOVHJ4OIBFECU
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.8:80" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LPKO42H7F2UE25RC
-A KUBE-SVC-2CMXP7HKUVJN7L6M -m comment --comment "default/nginx -> 10.244.2.9:80" -j KUBE-SEP-LKN6QCAH6MR2O4YF

 보시는 바와 같이 nginx service의 endpoint에 대해서 로드 밸런싱 규칙으로 총 20개의 규칙이 생성된 것을 확인할 수 있습니다. 이처럼 iptables를 기반으로 한 kube-proxy는 service의 수가 많이 지면 많아질수록 그 규칙 역시 선형적으로 늘어나고, 이를 적용받는 패킷은 O(n)의 시간 복잡도 문제를 일으키게 됩니다.

 또한, Kubernetes에서 빈번하게 일어나는 Pod 스케일링이나, 새로운 서비스의 생성에 따라 매번 iptables는 새로운 규칙을 생성해야 하고 업데이트해야 합니다. 이것 역시 Kubernetes에 많은 부담을 주게 도비니다.

eBPF와 Cilium

 

이미지 출처: https://cilium.io/use-cases/cni/

 iptables가 가진 문제점을 해결하기 위한 방법 중 하나로 등장하게 된 것이 eBPF를 활용하는 CNI인 Cilium입니다. Cilium은 iptalbes가 가진 시간 복잡도 O(n) 문제와 netfilter 체인의 오버헤드 비용을 eBPF 기술을 통해 해결하고자 합니다. eBPF는 리눅스 커널 기술 중 하나로 BPF(Berkely Patcket Filter)의 확장(extneded)입니다. 네트워크 패킷 캡처 및 분석을 위한 도구인 tcpdump나 Wireshark에서도 사용하는 기술입니다.

eBPF(extended BPF)란?

 최초의 BPF는 말 그대로 패킷 필터의 기능을 수행하였으나, 지금의 확장된 BPF(eBPF)는 패킷 필터뿐만 아니라 Linux 커널에서 실행되는 프로그램을 안전하고 효율적으로 개발하고 로드할 수 있는 기술로 발전하였습니다. Kubernetes의 네트워크 관점에서 살펴보면 iptables(netfilter)와 eBPF는 다음과 같이 비교할 수 있습니다.

구분 iptables eBPF
핵심 동작 방식 규칙 체인(Chain) 순서 탐색
 패킷이 들어오면 PREROUTING → KUBE-SERIVCE → KUBE-SVC-XXX 등 미리 정해진 규칙 목록을 순서대로 따라가며 일치하는 규칙을 찾음
이벤트 기반 프로그램 실행 및 맵(Map) 조회
 네트워크 인터페이스에 eBPF 프로그램을 부착, 패킷이 도착하면 이 프로그램이 즉시 실행되어, 서비스 IP를 해시맵(BPF Map)에서 한 번에 조회하고 목적지 Pod IP를 찾음
성능(규칙 증가 시) O(n) - 선형적 성능 저하
 서비스와 Pod가 많이져 규칙이 늘어나면, 패킷이 일치하는 규칙을 찾기까지 탐색해야 할 목록이 길어져 성능이 저하
O(n) - 거의 일정한 성능
 해시맵을 사용하여 서비스 수와 관계없이 거의 일정한 속도록 조회가 가능하여 대규모 클러스터 환경에서 성능 저하가 거의 없음
유연성 및 자원 Netfilter 프레임 워크에 종속적
 정해진 훅(Hook)과 체인 구조 안에서만 작동
높은 프로그래밍 유연성
 커널의 다양한 지점에 프로그램을 연결할 수 있어, 네트워킹 뿐만 아니라 보안, 모니터링에도 활용 가능
규칙 업데이트 전체 테이블 잠금(LooK) 가능성
 규칙을 업데이트할 때 테이블 전체에 잠금이 걸릴 수 있어, 동적인 환경(kubernetes)에서 지연을 유발
원자적(Atomic) 맵 업데이트
 맵의 특정 항목만을 빠르고 안전하게 업데이트하여 동적인 환경에서 더욱 유리

Cilium의 라우팅 모드

 Cilium은 eBPF 기반의 CNI로 라우팅 여러 가지 라우팅 모드를 지원합니다. 특별한 설정이 없이 배포하면 기본적으로 vxlan을 사용하는 방식으로 배포됩니다. 이는 Flannel과 같은 오버레이 네트워크를 사용하는 것으로 Cilium의 성능을 100% 사용하기에는 다소 부족한 방법입니다. Cilium에서는 Native Routing 모드를 지원하고 있으며, 이를 통해 vxlan의 오버헤드 비용을 감소시킵니다. 

# 최초 실습 환경이 아닌 다른 환경에서 구성
# Node IP 대역: 192.168.200.0/24
# Pod CIDR: 10.0.0.0/8
# vxlan을 기반으로 한 라우팅 방식을 사용하는 경우 Routing table
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.200.1   0.0.0.0         UG    0      0        0 ens160
10.0.0.0        10.0.2.39       255.255.255.0   UG    0      0        0 cilium_host
10.0.1.0        10.0.2.39       255.255.255.0   UG    0      0        0 cilium_host
10.0.2.0        10.0.2.39       255.255.255.0   UG    0      0        0 cilium_host
10.0.2.39       0.0.0.0         255.255.255.255 UH    0      0        0 cilium_host
10.0.3.0        10.0.2.39       255.255.255.0   UG    0      0        0 cilium_host
192.168.200.0   0.0.0.0         255.255.255.0   U     0      0        0 ens160

# native-routing을 기반으로 한 라우팅 방식을 사용하는 경우 Routing table
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         192.168.200.1   0.0.0.0         UG    0      0        0 ens160
10.0.0.0        192.168.200.111 255.255.255.0   UG    0      0        0 ens160
10.0.1.0        192.168.200.112 255.255.255.0   UG    0      0        0 ens160
10.0.2.182      0.0.0.0         255.255.255.255 UH    0      0        0 lxc_health
10.0.3.0        192.168.200.113 255.255.255.0   UG    0      0        0 ens160
192.168.200.0   0.0.0.0         255.255.255.0   U     0      0        0 ens160

 위 내용에서 알 수 있듯, vxlan과 같은 오버레이 네트워크를 사용하는 경우에는 vtep과 같은 역할을 하는 cilium_host를 통해 각 노드별 Pod CIDR에 대한 라우팅 정보가 구성되어 있는 것을 확인할 수 있습니다. 이에 반하여 native-routing을 사용하는 경우에는 위와 같이 호소트의 인터페이스를 통해 직접 라우팅 테이블이 구성되어 있는것을 확인할 수 있습니다. 이를 활용하면 Pod의 트래픽이 마치 노드의 네이티브 패킷인 것처럼 전달되어 그 오버헤드 비용을 상당히 줄일 수 있습니다.


 이 외에도 Cilium이 가진 장점은 매우 많습니다. 이에 관해서는 별도의 포스팅을 통해 다시 한번 정리하도록 하겠습니다. 그럼 이쯤에서 '[Cilium Study] 1주 차'와 관련된 포스팅 시리즈를 마무리하도록 하겠습니다.

 이전 포스팅 [Ciliu Study] 1주 차 - 1. 실습 환경 구성에서 이어집니다.

 

[Cilium Study] 1주차 - 1. 실습 환경 구성

'가시다'님의 [Cilium Study] 1기의 내용을 정리하는 시리즈 구성의 포스팅을 시작하려고 합니다. 이번 스터디를 통해 Kubernetes의 Network에 대해 깊이 이해할 수 있기를 희망합니다. 본 포스팅은 [Cilium

tech-recipe.tistory.com


 실습 환경인 VirtualBox와 Vagrant에 대해서는 어느 정도 파악이 완료되었습니다. 이제 본격적으로 CNI와 Kubernetes 네트워킹에 대해서 이해해 보는 시간을 가져보겠습니다.

Kubernetes network model의 핵심 원칙

 

Services, Load Balancing, and Networking

Concepts and resources behind networking in Kubernetes.

kubernetes.io

 위의 Kubernetes 공식 문서에는 Kubernetes의 네트워크 모델에 대한 여러 구성과 핵심에 대한 내용이 기록되어 있습니다. 주요 내용을 번역해 보면 다음과 같습니다.

  1. 클러스터 내에서 각 Pod는 클러스터 전체에서 고유한 IP 주소를 할당 받습니다.
  2. 동일한 Pod 내의 여러 컨테이너에서 실행되는 프로세스들은 localhost를 통해 서로 통신할 수 있습니다.
  3. Pod 네트워크(또는 Cluster 네트워크라고도 함)는 의도적인 네트워크 분리가 없는 한, 동일한 Node에 있든 다른 Node에 있든 상관없이 Pod 간 통신이 가능합니다.
  4. Pod들은 프록시나 NAT 없이 직접 서로 통신할 수 있습니다.
  5. Node 상의 Agent(예: system daemon, kubelet)는 해당 노드의 모든 Pod와 통신할 수 있습니다.
  6. Service API를 사용하여, 개별 백앤드 Pod 들에 대한 안정적인 접근이 가능한 IP 주소나 호스트 이름을 제시할 수 있습니다.

 이러한 모델 덕분에 Kubernetes에서 Pod를 가상 머신(VM)이나 물리적인 호스트처럼 취급할 수 있게 되었으며, 포트 할당, 서비스 검색, 로드 밸런싱 등이 크게 단순화됩니다.

NAT와 Capsulation의 미묘한 차이

 우리가 일반적으로 NAT를 가장 많이 경험할 수 있는 구간은 Wi-Fi 공유기와 그 아래에 연결된 PC나 핸드폰이 외부 인터넷과 통신을 할 때 일 것입니다.

이미지 출처: https://en.wikipedia.org/wiki/Network_address_translation

 NAT에서 눈여겨봐야 할 것은 IP 헤더의 출발지 또는 목적지 주소를 직접 수정(Rewirte) 한다는 점입니다. 즉 원본 IP 헤더가 변경되어 훼손된다는 점이죠. 하지만 변경된 정보를 NAT 기능을 하는 Router가 가지고 있으므로, 되돌아오는 응답에 대해서 올바른 목적지로 라우팅이 가능한 것입니다.

 하지만 Kubernetes network model은 Pod 간 통신에 있어 NAT 없이 직접 통신할 수 있어야 한다고 규정하고 있습니다. 이를 구현하기 위해서 여러 CNI 플러그인들은 vxlan 또는 geneve와 같은 오버레이 네트워크 기술을 사용하고 있습니다.

이미지 출처: https://addozhang.medium.com/learning-kubernetes-vxlan-network-with-flannel-2d6a58c95300

 위 그림을 보면 VXLAN을 통해 구현된 오버레이 네트워크가 어떻게 작동하는지 쉽게 알 수 있습니다. 프레임이 Node A에 있는 flannel.1이라는 인터페이스를 지날 때 빨간 테두리로 감싸지고, Node B의 flannel.1 인터페이스에서는 이것이 사라집니다. 이 과정이 바로 encapsulationdecapsulation입니다. 10.42.0.5의 IP 주소를 가진 Pod에서 출발한 프레임은 Node A의 flannel.1 인터페이스에서 encapsulation이 이루어집니다. 기존 L2 프레임을 그대로 보존하면서 새로운 UDP/IP 헤더로 포장(encapsulation)되며, 이때 이 프레임의 원래 목적지인 10.42.1.12의 IP 주소를 가지고 있는 Pod가 위치한 Node B가 이 포장의 새로운 목적지가 됩니다. Node B의 flannel.1 인터페이스에 도착한 프레임은 decapsulation 과정을 거치고 본래 목적지인 IP 주소가 10.42.1.2인 Pod로 향하게 됩니다.

 이 과정이 언뜻 보기에는 SNAT 또는 DNAT이 이루어진 것으로 보이지만, 실상은 그렇지 않습니다. 원래 프레임의 출발지와 목적지는 그대로 유지되었으며, 단지 이것이 한번 포장되었다가 풀리는 과정을 거칠 뿐입니다. 이 부분이 앞서 살펴본 NAT 기능이 있는 Router와 확연하게 차이나는 점입니다.


Flannel 배포와 VXLAN

배포 전 네트워크 정보 확인

# Pod/Service cidr 확인
kubectl cluster-info dump | grep -m 2 -E "cluster-cidr|service-cluster-ip-range"
                            "--service-cluster-ip-range=10.96.0.0/16",
                            "--cluster-cidr=10.244.0.0/16",
# 인터페이스 정보 확인
ip -c link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:71:19:d8 brd ff:ff:ff:ff:ff:ff
    altname enp0s8
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:7e:bb:e4 brd ff:ff:ff:ff:ff:ff
    altname enp0s9
    
ip -c route
default via 10.0.2.2 dev eth0 proto dhcp src 10.0.2.15 metric 100 
10.0.2.0/24 dev eth0 proto kernel scope link src 10.0.2.15 metric 100 
10.0.2.2 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 
10.0.2.3 dev eth0 proto dhcp scope link src 10.0.2.15 metric 100 
192.168.10.0/24 dev eth1 proto kernel scope link src 192.168.10.100

ip -c addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host noprefixroute 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:71:19:d8 brd ff:ff:ff:ff:ff:ff
    altname enp0s8
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic eth0
       valid_lft 48922sec preferred_lft 48922sec
    inet6 fd17:625c:f037:2:a00:27ff:fe71:19d8/64 scope global dynamic mngtmpaddr noprefixroute 
       valid_lft 86160sec preferred_lft 14160sec
    inet6 fe80::a00:27ff:fe71:19d8/64 scope link 
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 08:00:27:7e:bb:e4 brd ff:ff:ff:ff:ff:ff
    altname enp0s9
    inet 192.168.10.100/24 brd 192.168.10.255 scope global eth1
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe7e:bbe4/64 scope link 
       valid_lft forever preferred_lft forever
       
ifconfig | grep -iEA1 'eth[0-9]:'
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 10.0.2.15  netmask 255.255.255.0  broadcast 10.0.2.255
--
eth1: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.10.100  netmask 255.255.255.0  broadcast 192.168.10.255
# iptables 확인
iptables-save
# Generated by iptables-save v1.8.10 (nf_tables) on Sun Jul 20 03:13:39 2025
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:KUBE-IPTABLES-HINT - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-PROXY-CANARY - [0:0]
COMMIT
# Completed on Sun Jul 20 03:13:39 2025
# Generated by iptables-save v1.8.10 (nf_tables) on Sun Jul 20 03:13:39 2025
*filter
:INPUT ACCEPT [6306167:1161458547]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [6281634:1152248059]
:KUBE-EXTERNAL-SERVICES - [0:0]
:KUBE-FIREWALL - [0:0]
:KUBE-FORWARD - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-NODEPORTS - [0:0]
:KUBE-PROXY-CANARY - [0:0]
:KUBE-PROXY-FIREWALL - [0:0]
:KUBE-SERVICES - [0:0]
-A INPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A INPUT -m comment --comment "kubernetes health check service ports" -j KUBE-NODEPORTS
-A INPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes externally-visible service portals" -j KUBE-EXTERNAL-SERVICES
-A INPUT -j KUBE-FIREWALL
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes externally-visible service portals" -j KUBE-EXTERNAL-SERVICES
-A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -j KUBE-FIREWALL
-A KUBE-FIREWALL ! -s 127.0.0.0/8 -d 127.0.0.0/8 -m comment --comment "block incoming localnet connections" -m conntrack ! --ctstate RELATED,ESTABLISHED,DNAT -j DROP
-A KUBE-FORWARD -m conntrack --ctstate INVALID -m nfacct --nfacct-name  ct_state_invalid_dropped_pkts -j DROP
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A KUBE-SERVICES -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns has no endpoints" -m udp --dport 53 -j REJECT --reject-with icmp-port-unreachable
-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp has no endpoints" -m tcp --dport 53 -j REJECT --reject-with icmp-port-unreachable
-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:metrics has no endpoints" -m tcp --dport 9153 -j REJECT --reject-with icmp-port-unreachable
COMMIT
# Completed on Sun Jul 20 03:13:39 2025
# Generated by iptables-save v1.8.10 (nf_tables) on Sun Jul 20 03:13:39 2025
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-MARK-MASQ - [0:0]
:KUBE-NODEPORTS - [0:0]
:KUBE-POSTROUTING - [0:0]
:KUBE-PROXY-CANARY - [0:0]
:KUBE-SEP-ETI7FUQQE3BS2IXE - [0:0]
:KUBE-SERVICES - [0:0]
:KUBE-SVC-NPX46M4PTMTKRN6Y - [0:0]
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
-A KUBE-SEP-ETI7FUQQE3BS2IXE -s 192.168.10.100/32 -m comment --comment "default/kubernetes:https" -j KUBE-MARK-MASQ
-A KUBE-SEP-ETI7FUQQE3BS2IXE -p tcp -m comment --comment "default/kubernetes:https" -m tcp -j DNAT --to-destination 192.168.10.100:6443
-A KUBE-SERVICES -d 10.96.0.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-SVC-NPX46M4PTMTKRN6Y ! -s 10.244.0.0/16 -d 10.96.0.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-MARK-MASQ
-A KUBE-SVC-NPX46M4PTMTKRN6Y -m comment --comment "default/kubernetes:https -> 192.168.10.100:6443" -j KUBE-SEP-ETI7FUQQE3BS2IXE
COMMIT
# Completed on Sun Jul 20 03:13:39 2025
iptables -t nat -S
iptables -t filter -S
iptables -t mangle -S

Flannel 배포

#flannel 배포
kubectl create ns kube-flannel
kubectl label --overwrite ns kube-flannel pod-security.kubernetes.io/enforce=privileged

helm repo add flannel https://flannel-io.github.io/flannel/
helm repo list
helm search repo flannel
helm show values flannel/flannel

# k8s 관련 트래픽 통신 동작하는 nic 지정
cat << EOF > flannel-values.yaml
podCidr: "10.244.0.0/16"

flannel:
  args:
  - "--ip-masq"
  - "--kube-subnet-mgr"
  - "--iface=eth1"  
EOF

# helm 설치
helm install flannel --namespace kube-flannel flannel/flannel -f flannel-values.yaml
helm list -A

# 확인 : install-cni-plugin, install-cni
kc describe pod -n kube-flannel -l app=flannel

tree /opt/cni/bin/ # flannel
tree /etc/cni/net.d/
cat /etc/cni/net.d/10-flannel.conflist | jq
kc describe cm -n kube-flannel kube-flannel-cfg
...
net-conf.json:
----
{
  "Network": "10.244.0.0/16",
  "EnableNFTables": false,
  "Backend": {
    "Type": "vxlan"
  }
}
# k8s-ctr routing 정보
route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.0.2.2        0.0.0.0         UG    100    0        0 eth0
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 eth0
10.0.2.2        0.0.0.0         255.255.255.255 UH    100    0        0 eth0
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 eth0
10.244.1.0      10.244.1.0      255.255.255.0   UG    0      0        0 flannel.1
10.244.3.0      10.244.3.0      255.255.255.0   UG    0      0        0 flannel.1
192.168.10.0    0.0.0.0         255.255.255.0   U     0      0        0 eth1

# k8s-w1 routing 정보
route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.0.2.2        0.0.0.0         UG    100    0        0 eth0
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 eth0
10.0.2.2        0.0.0.0         255.255.255.255 UH    100    0        0 eth0
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 eth0
10.244.0.0      10.244.0.0      255.255.255.0   UG    0      0        0 flannel.1
10.244.3.0      10.244.3.0      255.255.255.0   UG    0      0        0 flannel.1
192.168.10.0    0.0.0.0         255.255.255.0   U     0      0        0 eth1

# k8s-w2 routing 정보
route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.0.2.2        0.0.0.0         UG    100    0        0 eth0
10.0.2.0        0.0.0.0         255.255.255.0   U     100    0        0 eth0
10.0.2.2        0.0.0.0         255.255.255.255 UH    100    0        0 eth0
10.0.2.3        0.0.0.0         255.255.255.255 UH    100    0        0 eth0
10.244.0.0      10.244.0.0      255.255.255.0   UG    0      0        0 flannel.1
10.244.1.0      10.244.1.0      255.255.255.0   UG    0      0        0 flannel.1
10.244.3.0      0.0.0.0         255.255.255.0   U     0      0        0 cni0
192.168.10.0    0.0.0.0         255.255.255.0   U     0      0        0 eth1

 특히 k8s-w2에는 cni0 인터페이스에 대한 Routing 정보가 있는데 이는 Coredns가 k8s-w2에 스케줄링되면서 생성된 인터페이스로, 다른 Node에도 일반적인 Pod가 스케줄링 되면 생성된다는 것을 알 수 있습니다.

# flannel 배포 후 iptables 확인
iptables-save
# Generated by iptables-save v1.8.10 (nf_tables) on Sun Jul 20 21:34:45 2025
*mangle
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:KUBE-IPTABLES-HINT - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-PROXY-CANARY - [0:0]
COMMIT
# Completed on Sun Jul 20 21:34:45 2025
# Generated by iptables-save v1.8.10 (nf_tables) on Sun Jul 20 21:34:45 2025
*filter
:INPUT ACCEPT [136548:77933491]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [131989:32183148]
:FLANNEL-FWD - [0:0]
:KUBE-EXTERNAL-SERVICES - [0:0]
:KUBE-FIREWALL - [0:0]
:KUBE-FORWARD - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-NODEPORTS - [0:0]
:KUBE-PROXY-CANARY - [0:0]
:KUBE-PROXY-FIREWALL - [0:0]
:KUBE-SERVICES - [0:0]
-A INPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A INPUT -m comment --comment "kubernetes health check service ports" -j KUBE-NODEPORTS
-A INPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes externally-visible service portals" -j KUBE-EXTERNAL-SERVICES
-A INPUT -j KUBE-FIREWALL
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A FORWARD -m conntrack --ctstate NEW -m comment --comment "kubernetes externally-visible service portals" -j KUBE-EXTERNAL-SERVICES
-A FORWARD -m comment --comment "flanneld forward" -j FLANNEL-FWD
-A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes load balancer firewall" -j KUBE-PROXY-FIREWALL
-A OUTPUT -m conntrack --ctstate NEW -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -j KUBE-FIREWALL
-A FLANNEL-FWD -s 10.244.0.0/16 -m comment --comment "flanneld forward" -j ACCEPT
-A FLANNEL-FWD -d 10.244.0.0/16 -m comment --comment "flanneld forward" -j ACCEPT
-A KUBE-FIREWALL ! -s 127.0.0.0/8 -d 127.0.0.0/8 -m comment --comment "block incoming localnet connections" -m conntrack ! --ctstate RELATED,ESTABLISHED,DNAT -j DROP
-A KUBE-FORWARD -m conntrack --ctstate INVALID -m nfacct --nfacct-name  ct_state_invalid_dropped_pkts -j DROP
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
-A KUBE-FORWARD -m comment --comment "kubernetes forwarding conntrack rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
COMMIT
# Completed on Sun Jul 20 21:34:45 2025
# Generated by iptables-save v1.8.10 (nf_tables) on Sun Jul 20 21:34:45 2025
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:FLANNEL-POSTRTG - [0:0]
:KUBE-KUBELET-CANARY - [0:0]
:KUBE-MARK-MASQ - [0:0]
:KUBE-NODEPORTS - [0:0]
:KUBE-POSTROUTING - [0:0]
:KUBE-PROXY-CANARY - [0:0]
:KUBE-SEP-CLAGU7VMF4VCXE4X - [0:0]
:KUBE-SEP-DLP2S2N3HX5UKLVP - [0:0]
:KUBE-SEP-ETI7FUQQE3BS2IXE - [0:0]
:KUBE-SEP-H7FN6LU3RSH6CC2T - [0:0]
:KUBE-SEP-TCIZBYBD3WWXNWF5 - [0:0]
:KUBE-SEP-TFTZVOJFQDTMM5AB - [0:0]
:KUBE-SEP-ZHICQ2ODADGCY7DS - [0:0]
:KUBE-SERVICES - [0:0]
:KUBE-SVC-ERIFXISQEP7F7OF4 - [0:0]
:KUBE-SVC-JD5MR3NA4I4DYORP - [0:0]
:KUBE-SVC-NPX46M4PTMTKRN6Y - [0:0]
:KUBE-SVC-TCOU7JCQXEZGVUNU - [0:0]
-A PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
-A POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
-A POSTROUTING -m comment --comment "flanneld masq" -j FLANNEL-POSTRTG
-A FLANNEL-POSTRTG -m mark --mark 0x4000/0x4000 -m comment --comment "flanneld masq" -j RETURN
-A FLANNEL-POSTRTG -s 10.244.0.0/24 -d 10.244.0.0/16 -m comment --comment "flanneld masq" -j RETURN
-A FLANNEL-POSTRTG -s 10.244.0.0/16 -d 10.244.0.0/24 -m comment --comment "flanneld masq" -j RETURN
-A FLANNEL-POSTRTG ! -s 10.244.0.0/16 -d 10.244.0.0/24 -m comment --comment "flanneld masq" -j RETURN
-A FLANNEL-POSTRTG -s 10.244.0.0/16 ! -d 224.0.0.0/4 -m comment --comment "flanneld masq" -j MASQUERADE --random-fully
-A FLANNEL-POSTRTG ! -s 10.244.0.0/16 -d 10.244.0.0/16 -m comment --comment "flanneld masq" -j MASQUERADE --random-fully
-A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
-A KUBE-POSTROUTING -m mark ! --mark 0x4000/0x4000 -j RETURN
-A KUBE-POSTROUTING -j MARK --set-xmark 0x4000/0x0
-A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -j MASQUERADE --random-fully
-A KUBE-SEP-CLAGU7VMF4VCXE4X -s 10.244.2.2/32 -m comment --comment "kube-system/kube-dns:metrics" -j KUBE-MARK-MASQ
-A KUBE-SEP-CLAGU7VMF4VCXE4X -p tcp -m comment --comment "kube-system/kube-dns:metrics" -m tcp -j DNAT --to-destination 10.244.2.2:9153
-A KUBE-SEP-DLP2S2N3HX5UKLVP -s 10.244.2.3/32 -m comment --comment "kube-system/kube-dns:dns-tcp" -j KUBE-MARK-MASQ
-A KUBE-SEP-DLP2S2N3HX5UKLVP -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp" -m tcp -j DNAT --to-destination 10.244.2.3:53
-A KUBE-SEP-ETI7FUQQE3BS2IXE -s 192.168.10.100/32 -m comment --comment "default/kubernetes:https" -j KUBE-MARK-MASQ
-A KUBE-SEP-ETI7FUQQE3BS2IXE -p tcp -m comment --comment "default/kubernetes:https" -m tcp -j DNAT --to-destination 192.168.10.100:6443
-A KUBE-SEP-H7FN6LU3RSH6CC2T -s 10.244.2.2/32 -m comment --comment "kube-system/kube-dns:dns-tcp" -j KUBE-MARK-MASQ
-A KUBE-SEP-H7FN6LU3RSH6CC2T -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp" -m tcp -j DNAT --to-destination 10.244.2.2:53
-A KUBE-SEP-TCIZBYBD3WWXNWF5 -s 10.244.2.2/32 -m comment --comment "kube-system/kube-dns:dns" -j KUBE-MARK-MASQ
-A KUBE-SEP-TCIZBYBD3WWXNWF5 -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.244.2.2:53
-A KUBE-SEP-TFTZVOJFQDTMM5AB -s 10.244.2.3/32 -m comment --comment "kube-system/kube-dns:metrics" -j KUBE-MARK-MASQ
-A KUBE-SEP-TFTZVOJFQDTMM5AB -p tcp -m comment --comment "kube-system/kube-dns:metrics" -m tcp -j DNAT --to-destination 10.244.2.3:9153
-A KUBE-SEP-ZHICQ2ODADGCY7DS -s 10.244.2.3/32 -m comment --comment "kube-system/kube-dns:dns" -j KUBE-MARK-MASQ
-A KUBE-SEP-ZHICQ2ODADGCY7DS -p udp -m comment --comment "kube-system/kube-dns:dns" -m udp -j DNAT --to-destination 10.244.2.3:53
-A KUBE-SERVICES -d 10.96.0.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-SVC-NPX46M4PTMTKRN6Y
-A KUBE-SERVICES -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-SVC-TCOU7JCQXEZGVUNU
-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp --dport 53 -j KUBE-SVC-ERIFXISQEP7F7OF4
-A KUBE-SERVICES -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:metrics cluster IP" -m tcp --dport 9153 -j KUBE-SVC-JD5MR3NA4I4DYORP
-A KUBE-SERVICES -m comment --comment "kubernetes service nodeports; NOTE: this must be the last rule in this chain" -m addrtype --dst-type LOCAL -j KUBE-NODEPORTS
-A KUBE-SVC-ERIFXISQEP7F7OF4 ! -s 10.244.0.0/16 -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:dns-tcp cluster IP" -m tcp --dport 53 -j KUBE-MARK-MASQ
-A KUBE-SVC-ERIFXISQEP7F7OF4 -m comment --comment "kube-system/kube-dns:dns-tcp -> 10.244.2.2:53" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-H7FN6LU3RSH6CC2T
-A KUBE-SVC-ERIFXISQEP7F7OF4 -m comment --comment "kube-system/kube-dns:dns-tcp -> 10.244.2.3:53" -j KUBE-SEP-DLP2S2N3HX5UKLVP
-A KUBE-SVC-JD5MR3NA4I4DYORP ! -s 10.244.0.0/16 -d 10.96.0.10/32 -p tcp -m comment --comment "kube-system/kube-dns:metrics cluster IP" -m tcp --dport 9153 -j KUBE-MARK-MASQ
-A KUBE-SVC-JD5MR3NA4I4DYORP -m comment --comment "kube-system/kube-dns:metrics -> 10.244.2.2:9153" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-CLAGU7VMF4VCXE4X
-A KUBE-SVC-JD5MR3NA4I4DYORP -m comment --comment "kube-system/kube-dns:metrics -> 10.244.2.3:9153" -j KUBE-SEP-TFTZVOJFQDTMM5AB
-A KUBE-SVC-NPX46M4PTMTKRN6Y ! -s 10.244.0.0/16 -d 10.96.0.1/32 -p tcp -m comment --comment "default/kubernetes:https cluster IP" -m tcp --dport 443 -j KUBE-MARK-MASQ
-A KUBE-SVC-NPX46M4PTMTKRN6Y -m comment --comment "default/kubernetes:https -> 192.168.10.100:6443" -j KUBE-SEP-ETI7FUQQE3BS2IXE
-A KUBE-SVC-TCOU7JCQXEZGVUNU ! -s 10.244.0.0/16 -d 10.96.0.10/32 -p udp -m comment --comment "kube-system/kube-dns:dns cluster IP" -m udp --dport 53 -j KUBE-MARK-MASQ
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.2.2:53" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-TCIZBYBD3WWXNWF5
-A KUBE-SVC-TCOU7JCQXEZGVUNU -m comment --comment "kube-system/kube-dns:dns -> 10.244.2.3:53" -j KUBE-SEP-ZHICQ2ODADGCY7DS
COMMIT

Sample App 배포와 iptables 확인

# 샘플 애플리케이션 배포
cat << EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webpod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: webpod
  template:
    metadata:
      labels:
        app: webpod
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
          - labelSelector:
              matchExpressions:
              - key: app
                operator: In
                values:
                - webpod
            topologyKey: "kubernetes.io/hostname"
      containers:
      - name: webpod
        image: traefik/whoami
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: webpod
  labels:
    app: webpod
spec:
  selector:
    app: webpod
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
EOF

# k8s-ctr 노드에 curl-pod 파드 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: curl-pod
  labels:
    app: curl
spec:
  nodeName: k8s-ctr
  containers:
    - name: curl
      image: alpine/curl
      command: ["sleep", "36000"]
EOF
# webpod svc의 clsuter ip에 대한 iptables 정보 확인
kubectl get svc webpod -o jsonpath="{.spec.clusterIP}"
SVCIP=$(kubectl get svc webpod -o jsonpath="{.spec.clusterIP}")
iptables -t nat -S | grep $SVCIP

10.96.56.150
-A KUBE-SERVICES -d 10.96.56.150/32 -p tcp -m comment --comment "default/webpod cluster IP" -m tcp --dport 80 -j KUBE-SVC-CNZCPOCNCNOROALA
-A KUBE-SVC-CNZCPOCNCNOROALA ! -s 10.244.0.0/16 -d 10.96.56.150/32 -p tcp -m comment --comment "default/webpod cluster IP" -m tcp --dport 80 -j KUBE-MARK-MASQ

 위 두 개의 iptables 규칙에 대해서 살펴보겠습니다.

순서 출력 문구 설명
첫 번째 규칙 -A KUBE-SERIVCE KUBE-SERVICE라는 체인에 규칙을 추가
-d 10.96.56.150/32 목적지(-d)가 10.96.56.150인 트래픽을 의미, 여기에서는 webapp service의 Clsuter IP를 의미
-p tcp --dport 80 protocol이 TCP이고 목적지 포트(--port)가 80인 트래픽을 의미
-m commnet --commnet "..." 규칙에 대한 주석
-j KUBE-SVC-CNZ... 앞의 규칙을 만족하는 트래픽을 다음 체인(KUBE-SVC-CNZCPOCNCNOROALA)으로 점프
두 번째 규칙 -A KUBE-SVC-CNZCPOCNCNOROALA webpod 서비스 전용 체인에 규칙을 추가
! -s 10.244.0.0/16 출발지 IP가 10.244.0.0/16 대역(Pod CIDR)이 아닌(!) 트래픽을 의미
-j KUBE-MARK-MASQ Masquerading 표식을 남기라는 의미
  • 첫 번째 규칙은 webpod service로 들어오는 tcp 80 포트의 프래픽을 두 번째 체인으로 전달하는 규칙
  • 두 번째 규칙은 PodCIDR이 아닌 외부에서 접근한 트래픽의 경우 Masquerading 표식을 남겨 응답에 NAT를 사용하라는 의미

이러한 iptables 규칙들은 Service들이 생성될 때마다 각 호스트에 등록되며, 트래픽을 처리하는 데 사용됩니다. 클러스터가 소규모이고, Service가 적다면 크게 문제 될 것이 없지만 규모가 커지고 Serivce의 수가 많아지면 그 효율성이 매우 떨어지는 문제가 발생하게 됩니다.


 다음 포스팅에서는 O(n) 시간 복잡도를 가지는 iptables의 문제를 조금 더 깊이 살펴보고, 이것을 해결할 수 있는 방법인 Cilium CNI에 대해서 살펴보도록 하겠습니다.

 

[Cilium Study] 1주차 - 3. iptables의 한계와 돌파구, eBPF와 Cilium

[Cilium Study] 1 주 차 - 2. CNI와 Kubernetes 네트워킹에서 이어집니다. [Cilium Study] 1주차 - 2. CNI와 Kubernetes 네트워킹이전 포스팅 [Ciliu Study] 1주 차 - 1. 실습 환경 구성에서 이어집니다. [Cilium Study] 1주차 -

tech-recipe.tistory.com

 

 '가시다'님의 [Cilium Study] 1기의 내용을 정리하는 시리즈 구성의 포스팅을 시작하려고 합니다. 이번 스터디를 통해 Kubernetes의 Network에 대해 깊이 이해할 수 있기를 희망합니다. 본 포스팅은 [Cilium Study 1기]의 1주 차 내용으로 실습 환경을 구성하는 방법부터 소개합니다.

VirtualBox와 Vagrant를 활용한 가상 머신 구성

 이번 [Cilium Study]를 시작하면서 평소 막연하게 생각해왔던 VirtualBox와 Vagrant를 체험해 볼 수 있게 되었습니다. 원래 VMware의 제품군에 많이 익숙해서 VMware Workstation Pro나 VMware vSphere를 자주 사용했었고, 소규모 환경은 주로 Manual 하게 구성하는 경우가 많아서 Vagrant를 사용해 볼 기회가 많이 없었습니다.

 조훈님의 강의인 '그림으로 배우는 Kubernetes'를 들을 때도, Kubernetes 클러스터를 직접 구축해보고 싶어 가상머신으로 매뉴얼 하게 설치하는 방법을 주로 사용하다 보니, 더욱 Vagrant와는 친해지지 못했던 것 같습니다. 이번 스터디를 기회로 Vagrant와도 친해져야겠습니다.

Apple silicon 환경에서 VirtualBox와 Vagrant 설치

# VirtualBox 설치
brew install --cask virtualbox

VBoxManage --version
7.1.10r169112				# 포스팅 시점 2025년 7월 기준

# Vagrant 설치
brew install --cask vagrant

vagrant version    	
Installed Version: 2.4.7	# 포스팅 시점 2025년 7월 기준

 MacOS의 터미널에서 brew 명령어를 통해 손쉽게 VirtualBox와 Vagrant를 설치할 수 있습니다.

실습 환경 네트워크 토폴로지

VirtualBox의 네트워크 구조에 대한 이해

실습환경 네트워크 다이어그램

 위 그림은 제가 이해한 VirtualBox의 네트워크 구조로(다소 잘못된 부분이 있을 수 있음) VMware Workstation Pro에서 제공하는 방식과 확연한 차이가 있습니다. VirtualBox의 경우 VM별로 별도의 격리된 NAT용 네트워크 공간(다이어그램의 붉은색 선)을 제공합니다. 그래서 모든 VM들의 eth0 인터페이스의 IP 주소가 같아도 문제가 없습니다. 그런데 이런 구조를 가지다 보니 VM 간 통신을 위해서는 따로 Private Network를 만들어 주어야 합니다. 바로 위 다이어그램의 파란색 선입니다.

 vagrant up 명령어를 통해 가상머신이 생성되면 Host(Mac)에는 bridege100이라는 인터페이스가 생기면서 VM들의 Private Network 공간인 192.168.10.0/24로 향하는 라우팅 테이블이 업데이트 됩니다. 게다가, ssh를 통한 접근이나 ping 테스트 같은 것이 모두 이 Private Network로 가능하게 됩니다. 즉 Private Network가 구성되어야 L2 주소 공간이 형성되고 이를 통해 Host - VM 간, 또는 VM -VM 간의 통신이 가능하게 되는 것입니다. 단, VirtualBox는 VM에 대한 ssh 경로의 경우 Host에서 localhost의 특정 포트를 호출하여 VM에 접근할 수 있는 우회 경로를 제공합니다.

VMware Workstaion Pro의 NAT 네트워크에 대한 이해

VMware Workstation Pro의 NAT 네트워크 토폴로지

 VMware의 경우에는 NAT 네트워크가 생성되면 Host와는 별개의 네트워크 공간이 생성됩니다. 별도의 NAT 네트워크 전용 인터페이스가 Host에 등록되며(예 - 192.168.10.1의 IP 주소를 가진 인터페이스) VM들에게는 같은 주소 공간의 특정 IP(예 - 192.168.10.2)를 Gateway로 지정하여 사용하도록 합니다. 그리고 보이지 않는 L2 스위치가 생성되어 여기에 192.168.10.0/24의 IP 주소 범위 내에서 IP를 할당하게 합니다. 이렇게 되면 NAT Interface와 VM Gateway는 마치 L3 라우터 간 세그먼트와 같은 모습이 되게 됩니다. VM들은 별도의 네트워크 인터페이스 추가 없이 192.168.10.0/24의 IP 주소 범위 내에서 통신을 할 수 있게 되고, 외부와의 통신은 192.168.10.2를 통해 하게 됩니다.

 개인적으로는 VMware Workstation Pro의 네트워크 토폴로지가 더욱 직관적이라고 느껴집니다. 별도의 네트워크 공간의 분리가 명확하고, Gateway 구조를 이해하기에도 쉽습니다. (다만 제가 처음부터 VMware 제품에 익숙하서 그런지도 모르겠다는 생각은 있습니다.)

Vagrantfile과 Kubernetes 클러스터 구성을 위한 Shell scripts

Vagrantfile

 이 부분이 애써 외면하고 있던 IaC의 한 부분인 Vagrantfile입니다. 수동으로 해오던 가상머신 생성 과정을 자동화 시켜주는 아주 기본적이면서도 중요한 부분이죠. 확실히 이런 부분은 제가 배워야 하고 극복해 나가야 할 부분입니다.

# Variables
K8SV = '1.33.2-1.1' # Kubernetes Version : apt list -a kubelet , ex) 1.32.5-1.1
CONTAINERDV = '1.7.27-1' # Containerd Version : apt list -a containerd.io , ex) 1.6.33-1
N = 2 # max number of worker nodes

# Base Image  https://portal.cloud.hashicorp.com/vagrant/discover/bento/ubuntu-24.04
## Rocky linux Image https://portal.cloud.hashicorp.com/vagrant/discover/rockylinux
BOX_IMAGE = "bento/ubuntu-24.04"
BOX_VERSION = "202502.21.0"

Vagrant.configure("2") do |config|
#-ControlPlane Node
    config.vm.define "k8s-ctr" do |subconfig|
      subconfig.vm.box = BOX_IMAGE
      subconfig.vm.box_version = BOX_VERSION
      subconfig.vm.provider "virtualbox" do |vb|
        vb.customize ["modifyvm", :id, "--groups", "/Cilium-Lab"]
        vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
        vb.name = "k8s-ctr"
        vb.cpus = 2
        vb.memory = 2048
        vb.linked_clone = true
      end
      subconfig.vm.host_name = "k8s-ctr"
      subconfig.vm.network "private_network", ip: "192.168.10.100"
      subconfig.vm.network "forwarded_port", guest: 22, host: 60000, auto_correct: true, id: "ssh"
      subconfig.vm.synced_folder "./", "/vagrant", disabled: true
      subconfig.vm.provision "shell", path: "init_cfg.sh", args: [ K8SV, CONTAINERDV]
      subconfig.vm.provision "shell", path: "k8s-ctr.sh", args: [ N ]
    end

#-Worker Nodes Subnet1
  (1..N).each do |i|
    config.vm.define "k8s-w#{i}" do |subconfig|
      subconfig.vm.box = BOX_IMAGE
      subconfig.vm.box_version = BOX_VERSION
      subconfig.vm.provider "virtualbox" do |vb|
        vb.customize ["modifyvm", :id, "--groups", "/Cilium-Lab"]
        vb.customize ["modifyvm", :id, "--nicpromisc2", "allow-all"]
        vb.name = "k8s-w#{i}"
        vb.cpus = 2
        vb.memory = 1536
        vb.linked_clone = true
      end
      subconfig.vm.host_name = "k8s-w#{i}"
      subconfig.vm.network "private_network", ip: "192.168.10.10#{i}"
      subconfig.vm.network "forwarded_port", guest: 22, host: "6000#{i}", auto_correct: true, id: "ssh"
      subconfig.vm.synced_folder "./", "/vagrant", disabled: true
      subconfig.vm.provision "shell", path: "init_cfg.sh", args: [ K8SV, CONTAINERDV]
      subconfig.vm.provision "shell", path: "k8s-w.sh"
    end
  end
end

Shell script

 Shell script의 경우에도 변수화가 잘 되어 있어서 향후 사용성이 상당히 뛰어나 배울점이 많았습니다.

# init_cfg.sh
# Kubernetes 구성 전 필요한 작업 및 패키지 설치를 위한 Shell script

#!/usr/bin/env bash

echo ">>>> Initial Config Start <<<<"

echo "[TASK 1] Setting Profile & Change Timezone"
echo 'alias vi=vim' >> /etc/profile
echo "sudo su -" >> /home/vagrant/.bashrc
ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime


echo "[TASK 2] Disable AppArmor"
systemctl stop ufw && systemctl disable ufw >/dev/null 2>&1
systemctl stop apparmor && systemctl disable apparmor >/dev/null 2>&1


echo "[TASK 3] Disable and turn off SWAP"
swapoff -a && sed -i '/swap/s/^/#/' /etc/fstab


echo "[TASK 4] Install Packages"
apt update -qq >/dev/null 2>&1
apt-get install apt-transport-https ca-certificates curl gpg -y -qq >/dev/null 2>&1

# Download the public signing key for the Kubernetes package repositories.
mkdir -p -m 755 /etc/apt/keyrings
K8SMMV=$(echo $1 | sed -En 's/^([0-9]+\.[0-9]+)\..*/\1/p')
curl -fsSL https://pkgs.k8s.io/core:/stable:/v$K8SMMV/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v$K8SMMV/deb/ /" >> /etc/apt/sources.list.d/kubernetes.list
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null

# packets traversing the bridge are processed by iptables for filtering
echo 1 > /proc/sys/net/ipv4/ip_forward
echo "net.ipv4.ip_forward = 1" >> /etc/sysctl.d/k8s.conf

# enable br_netfilter for iptables 
modprobe br_netfilter
modprobe overlay
echo "br_netfilter" >> /etc/modules-load.d/k8s.conf
echo "overlay" >> /etc/modules-load.d/k8s.conf


echo "[TASK 5] Install Kubernetes components (kubeadm, kubelet and kubectl)"
# Update the apt package index, install kubelet, kubeadm and kubectl, and pin their version
apt update >/dev/null 2>&1

# apt list -a kubelet ; apt list -a containerd.io
apt-get install -y kubelet=$1 kubectl=$1 kubeadm=$1 containerd.io=$2 >/dev/null 2>&1
apt-mark hold kubelet kubeadm kubectl >/dev/null 2>&1

# containerd configure to default and cgroup managed by systemd
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml

# avoid WARN&ERRO(default endpoints) when crictl run  
cat <<EOF > /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
EOF

# ready to install for k8s 
systemctl restart containerd && systemctl enable containerd
systemctl enable --now kubelet


echo "[TASK 6] Install Packages & Helm"
apt-get install -y bridge-utils sshpass net-tools conntrack ngrep tcpdump ipset arping wireguard jq tree bash-completion unzip kubecolor >/dev/null 2>&1
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash >/dev/null 2>&1


echo ">>>> Initial Config End <<<<"
# k8s-ctr.sh
# Kubernetes Clsuter Control Plane 구성을 위한 Shell script

#!/usr/bin/env bash

echo ">>>> K8S Controlplane config Start <<<<"

echo "[TASK 1] Initial Kubernetes"
kubeadm init --token 123456.1234567890123456 --token-ttl 0 --pod-network-cidr=10.244.0.0/16 --service-cidr=10.96.0.0/16 --apiserver-advertise-address=192.168.10.100 --cri-socket=unix:///run/containerd/containerd.sock >/dev/null 2>&1


echo "[TASK 2] Setting kube config file"
mkdir -p /root/.kube
cp -i /etc/kubernetes/admin.conf /root/.kube/config
chown $(id -u):$(id -g) /root/.kube/config


echo "[TASK 3] Source the completion"
echo 'source <(kubectl completion bash)' >> /etc/profile
echo 'source <(kubeadm completion bash)' >> /etc/profile


echo "[TASK 4] Alias kubectl to k"
echo 'alias k=kubectl' >> /etc/profile
echo 'alias kc=kubecolor' >> /etc/profile
echo 'complete -F __start_kubectl k' >> /etc/profile


echo "[TASK 5] Install Kubectx & Kubens"
git clone https://github.com/ahmetb/kubectx /opt/kubectx >/dev/null 2>&1
ln -s /opt/kubectx/kubens /usr/local/bin/kubens
ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx


echo "[TASK 6] Install Kubeps & Setting PS1"
git clone https://github.com/jonmosco/kube-ps1.git /root/kube-ps1 >/dev/null 2>&1
cat <<"EOT" >> /root/.bash_profile
source /root/kube-ps1/kube-ps1.sh
KUBE_PS1_SYMBOL_ENABLE=true
function get_cluster_short() {
  echo "$1" | cut -d . -f1
}
KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
KUBE_PS1_SUFFIX=') '
PS1='$(kube_ps1)'$PS1
EOT
kubectl config rename-context "kubernetes-admin@kubernetes" "HomeLab" >/dev/null 2>&1


echo "[TASK 6] Install Kubeps & Setting PS1"
echo "192.168.10.100 k8s-ctr" >> /etc/hosts
for (( i=1; i<=$1; i++  )); do echo "192.168.10.10$i k8s-w$i" >> /etc/hosts; done


echo ">>>> K8S Controlplane Config End <<<<"
# k8s-w.sh
# Kubernetes Cluster의 워커 노드 Join을 위한 Shell script

#!/usr/bin/env bash

echo ">>>> K8S Node config Start <<<<"

echo "[TASK 1] K8S Controlplane Join" 
kubeadm join --token 123456.1234567890123456 --discovery-token-unsafe-skip-ca-verification 192.168.10.100:6443  >/dev/null 2>&1


echo ">>>> K8S Node config End <<<<"

 특히, kubeadm init 시에 Token 값을 미리 지정하여 워커 노드의 Join까지 자동화 한 점은 제가 놓치고 있었던 부분이었습니다. 게다가 토큰 만료시간에 대한 옵션인 --token-ttl 0을 통해 만료시간을 설정하지 않아 언제든지 같은 토큰 값으로 워커 노드들이 Join 할 수 있게 한 것도 실습 환경에서는 중요한 요소인 것 같습니다.

Vagrant Up

mkdir cilium-lab && cd cilium-lab

curl -O https://raw.githubusercontent.com/gasida/vagrant-lab/refs/heads/main/cilium-study/1w/Vagrantfile

vagrant up

 위에서 소개된 Vagrantfile과 각종 Shell script를 한번에 다운로드할 수 있는 명령어와 가상머신을 자동으로 프로비저닝 할 수 있는 명령어입니다. vagrant up 명령어를 통해 VirtualBox에 가상머신을 생성하고 자동으로 Kubernetes 클러스터를 구성하게 됩니다.

Kubernetes 클러스터 구성 후 작업

가상머신 기본 정보 확인

# ssh 접속 전, 노드들의 eth0 IP 확인
for i in ctr w1 w2 ; do echo ">> node : k8s-$i <<"; vagrant ssh k8s-$i -c 'ip -c -4 addr show dev eth0'; echo; done #
>> node : k8s-ctr <<
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    altname enp0s8
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic eth0
       valid_lft 79621sec preferred_lft 79621sec

>> node : k8s-w1 <<
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    altname enp0s8
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic eth0
       valid_lft 79707sec preferred_lft 79707sec

>> node : k8s-w2 <<
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    altname enp0s8
    inet 10.0.2.15/24 metric 100 brd 10.0.2.255 scope global dynamic eth0
       valid_lft 79764sec preferred_lft 79764sec

 앞서 VirtualBox의 네트워크 구조에 대해서 논의하면서 살펴본 내용을 직접 확인할 수 있었습니다. 모든 VM들의 eth0 인터페이스의 IP 주소가 10.0.2.15로 동일하며, 이는 VirtualBox의 NAT 네트워크 구조의 특징에 기인합니다. 각각 독립된 NAT 네트워크 공간을 가지기 때문입니다.

# k8s-ctrl에 접속 후 기본 정보 확인
vagrant ssh k8s-ctr			# vagrant ssh 명령어를 통해 'ks8-ctr'이라는 이름을 가진 가상머신에 ssh로 접속

# 기본 호스트 정보 및 프로세스 확인
whoami
pwd
hostnamectl
htop

# 호스트 파일 내용 확인과 각 워커 노드 호스트 이름 확인
cat /etc/hosts
ping -c 1 k8s-w1
ping -c 1 k8s-w2
sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@k8s-w1 hostname
sshpass -p 'vagrant' ssh -o StrictHostKeyChecking=no vagrant@k8s-w2 hostname


# vagrant ssh 로 접속 시 tcp 연결 정보 : NAT Mode 10.0.2.2(GateWay)
ss -tnp |grep sshd
ESTAB 0      0           [::ffff:10.0.2.15]:22          [::ffff:10.0.2.2]:52791 users:(("sshd",pid=5176,fd=4),("sshd",pid=5129,fd=4))

# nic 정보
ip -c addr

# default 라우팅 정보 
ip -c route

# dns 서버 정보 : NAT Mode 10.0.2.3
resolvectl
Global
         Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
  resolv.conf mode: stub

Link 2 (eth0)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 10.0.2.3
       DNS Servers: 10.0.2.3
        DNS Domain: basphere.local

Link 3 (eth1)
    Current Scopes: none
         Protocols: -DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported

Kubernetes 클러스터 정보 확인 및 수정

# Kubernetes 클러스터 상태 정보 확인
kubectl cluster-info
Kubernetes control plane is running at https://192.168.10.100:6443
CoreDNS is running at https://192.168.10.100:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'

# 노드 정보 : 상태, INTERNAL-IP 확인
kubectl get node -owide
NAME      STATUS     ROLES           AGE    VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
k8s-ctr   NotReady   control-plane   123m   v1.33.2   192.168.10.100   <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27
k8s-w1    NotReady   <none>          122m   v1.33.2   10.0.2.15        <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27
k8s-w2    NotReady   <none>          121m   v1.33.2   10.0.2.15        <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27

# 파드 정보 : 상태, 파드 IP 확인 - kube-proxy 확인
kubectl get pod -A -owide
NAMESPACE     NAME                              READY   STATUS    RESTARTS   AGE    IP          NODE      NOMINATED NODE   READINESS GATES
kube-system   coredns-674b8bbfcf-d28x7          0/1     Pending   0          121m   <none>      <none>    <none>           <none>
kube-system   coredns-674b8bbfcf-llbdd          0/1     Pending   0          121m   <none>      <none>    <none>           <none>
kube-system   etcd-k8s-ctr                      1/1     Running   0          121m   10.0.2.15   k8s-ctr   <none>           <none>
kube-system   kube-apiserver-k8s-ctr            1/1     Running   0          121m   10.0.2.15   k8s-ctr   <none>           <none>
kube-system   kube-controller-manager-k8s-ctr   1/1     Running   0          121m   10.0.2.15   k8s-ctr   <none>           <none>
kube-system   kube-proxy-2sxhl                  1/1     Running   0          120m   10.0.2.15   k8s-w1    <none>           <none>
kube-system   kube-proxy-8k2rn                  1/1     Running   0          119m   10.0.2.15   k8s-w2    <none>           <none>
kube-system   kube-proxy-ldrml                  1/1     Running   0          121m   10.0.2.15   k8s-ctr   <none>           <none>
kube-system   kube-scheduler-k8s-ctr            1/1     Running   0          121m   10.0.2.15   k8s-ctr   <none>           <none>

# 단축어 확인(kc = kubecolor) & coredns 파드 상태 확인
k  describe pod -n kube-system -l k8s-app=kube-dns
kc describe pod -n kube-system -l k8s-app=kube-dns

 위 명령어들을 통해서 전반적인 Kubernetes 클러스터의 정보를 확인할 수 있습니다. 특히 노드의 정보를 보면 워커 노드 1번과 워커 노드 2번의 INTERNAL IP가 10.0.2.15로 같은 것으로 나옵니다. 또한 kube-system에 있는 Pod들의 IP 주소도 모두 10.0.2.15인 것을 확인할 수 있습니다. 이 부분에 대한 수정이 필요하며, 두 번째로 아직 CNI가 배포되지 않아 coredns pod가 Pending 상태인 것입니다.

# kubelet 서비스에 대한 환경 변수와 실행 인자 설정 확인
# k8s-w1/w2에서 모두 실행
cat /var/lib/kubelet/kubeadm-flags.env

# INTERNAL-IP 변경 설정
NODEIP=$(ip -4 addr show eth1 | grep -oP '(?<=inet\s)\d+(\.\d+){3}')
sed -i "s/^\(KUBELET_KUBEADM_ARGS=\"\)/\1--node-ip=${NODEIP} /" /var/lib/kubelet/kubeadm-flags.env
systemctl daemon-reexec && systemctl restart kubelet

cat /var/lib/kubelet/kubeadm-flags.env

# 워커노드에서 실행 완료 후 INTERNAL-IP 주소 변경 확인
kubectl get node -owide
NAME      STATUS     ROLES           AGE    VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
k8s-ctr   NotReady   control-plane   133m   v1.33.2   192.168.10.100   <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27
k8s-w1    NotReady   <none>          132m   v1.33.2   192.168.10.101   <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27
k8s-w2    NotReady   <none>          131m   v1.33.2   192.168.10.102   <none>        Ubuntu 24.04.2 LTS   6.8.0-53-generic   containerd://1.7.27

  우선 위 명령어를 통해 각 kubelet이 실행될 때 환경 변수 설정을 통해 올바른 INTERNAL-IP가 설정되도록 합니다. 워커 노드 1번과 2번에 모두 실행한 후 Control Plane에서 다시 노드들의 정보를 조회해 보면 INTERNAL-IP가 eth1의 인터페이스 IP 주소와 일치하게 설정된 것을 확인할 수 있습니다.

Static Pod의 IP 주소 변경

 위 명령어들을 통해 static pod의 IP 정보가 변경되지 않는다면 수동으로 이를 변경해 주어야 합니다. static pod의 manifests 파일은 control plane node에 /etc/kubernetes/manifests 디렉토리 아래에 위치하고 있습니다.

# static pod의 manifests 파일 정보
tree /etc/kubernetes/manifests
/etc/kubernetes/manifests
├── etcd.yaml
├── kube-apiserver.yaml
├── kube-controller-manager.yaml
└── kube-scheduler.yaml

 위 파일들을 직접 편집하여 10.0.2.15로 되어있는 IP 주소를 192.168.100.100으로 변경할 수 있습니다. 다만, 이번 실습 과정에서는 kubelet 서비스에 대한 환경 변수 설정만으로도 이것이 변경된 것을 확인할 수 있었습니다.


 이렇게 해서 CNI 배포 전 Kubernetes 클러스터를 VirtualBox와 Vagrant를 통해 구성해 보았습니다. 개인적으로는 이미 EKS나 OKE 환경에서 기존 CNI를 Cilium으로 마이그레이션 해본 경험이 있어 이후의 Flannel 배포와 Cilium 마이그레이션의 경우에는 실습을 진행하기에 수월했습니다. 단, Native Routing을 구현하는 부분은 On-premise 환경에서는 어렵다고 알고 있었는데 그렇지 않아서 또 한 번 배우는 시간이 되었습니다. Native Routing에 대한 구현에 대해서는 이어지는 포스팅에서 다뤄 보도록 하겠습니다.

 

[Cilium Study] 1주차 - 2. CNI와 Kubernetes 네트워킹

이전 포스팅 [Ciliu Study] 1주 차 - 1. 실습 환경 구성에서 이어집니다. [Cilium Study] 1주차 - 1. 실습 환경 구성'가시다'님의 [Cilium Study] 1기의 내용을 정리하는 시리즈 구성의 포스팅을 시작하려고 합니

tech-recipe.tistory.com

 

부팅 가능한 컨테이너와의 첫 만남

 한 달 정도 전, 오픈 인프라 커뮤니티 코리아의 오프라인 세미나에서 bootc를 접하게 되었습니다. 사실 처음에 들었을 때는 이게 대체 뭘 하는 건지 감이 잘 오질 않았습니다. 분명 제가 알고 있는 전통적인 관점에서는 가상머신의 단점을 해결하고자 컨테이너가 등장했다고 했는데... 근데 다시 그 컨테이너를 부팅한다고요? 아니 왜?

이미지 출처: https://www.wallarm.com/what/containers-vs-virtual-machines

 위 그림에서도 볼 수 있듯, 가상머신과 컨테이너는 아예 개념 자체가 다릅니다. 가상 머신은 하이퍼바이저 위에서 개별적인 커널로 구동되는 독립적인 시스템이고, 컨테이너는 OS위의 프로세스일 뿐이므로(물론 독립적인 특징이 있긴 하지만) 부팅이라는 개념이 없습니다. 컨테이너를 띄운다는 것은 OS 입장에서는 단지 프로세스를 실행하는 것뿐이죠. 그런데 '어떻게 부팅이 가능하다는 거지?', 너무 혼란스러웠습니다.

 'VM과 컨테이너 중간에 무엇인 건가? 아니 그럼 그건 VM이야 컨테이너야? 도대체 뭐 때문에 만들어진 거지?' 이런 많은 질문들이 머리를 가득 채웠습니다.


Bootable Container

 부팅 가능한 컨테이너는 애플리케이션 컨테이너로부터 영감을 받았습니다. OS 관리에 컨테이너 기술을 적용하려는 시도에서 등장하여 OS 업데이트를 더 효율적이고 안정적으로 만들 수 있는 방법을 제시하고 있습니다. 주로 Fedora, CentOS Stream에서 주도하고 있으며 부팅가능한 컨테이너 이미지 역시 이곳에서 공식적으로 제공하고 있습니다.

 

bootc란?

 bootc는 부팅 가능한 컨테이너 개념을 구현하는 구체적인 프로젝트 또는 도구를 의미합니다. OCI 컨테이너 이미지를 사용하여 트랜잭션 기반의 운영 체제 업데이트를 제공하는 CLI입니다. 따라서 컨테이너 이미지 내에서 사용되는 도구로 컨테이너 이미지의 일부입니다. 주요 목적은 업데이트 관리, 시스템 상태 점검에 있습니다.


스터디 진척도

 그래서 여러 문서를 참고하고, 데모를 따라 해 보면서 Fedora41에서 Podman을 통해 CentOS 9 Stream을 부팅 가능한 컨테이너 이미지로 만들고, 이를 다시 raw 파일로 변환하여 KVM을 통해 간단하게 가상머신으로 띄워보는데 까진 성공 했습니다. OS에 Nginx를 설치하고 Nginx 서비스를 띄워 초기 페이지에 'This image is built as a bootable container.'와 같은 문구가 출력되게 하여 제가 원하는 데로 이미지가 잘 빌드되는지 까지도 확인을 하였습니다.

 그러나 이를 ISO나 VMDK 파일로 빌드하니 부팅시에 에러가 발생하고, 심지어는 아예 OS로 인식을 못하는 경우도 발생해서, 이 부분에 대한 트러블 슈팅을 하고 있습니다. 앞으로 이 부분이 완료되면 정식 포스팅을 통해 Bootable container와 bootc에 대해서 다뤄 보도록 하겠습니다.

+ Recent posts