개요
k8s에서 사용하는 CNI 중 Cilium에 대해서 학습
Istio는 실습이랑 이론을 잘 분리하려고 했었는데, 이론 비중이 매우 작고 실습에 이론이 포함된 것처럼 구성했었던 실패를 극복하고자, 이론과 실습을 명확하게 구분
개념
2021년 10월에 CNCF 소속 프로젝트가 되었고, 현재는 Google GKE Data Plane, AWS EKS Anywhere에 Cilium을 기본 CNI로 이용
Cilium이 없는 경우의 Pod에서 동작하는 애플리케이션은 좌측 그림처럼 많은 레이어를 거치며 통신하게 되는데, Cilium을 CNI로 이용하는 경우엔 단순하게 Cilium 계층 하나만 거치며 복잡하지 않도록 통신을 수행
k8s에서 구체적으로 살펴보면, Cilium이 없는 경우엔 좌측 그림처럼 호스트와 Pod 환경에서 동일한 동작을 수행하면서 오버헤드가 존재하게 되는데, Cilium을 이용하게 되면 기타 중복되는 오버헤드 없이 통신을 수행하게 되면서 성능에 많은 이점이 존재
k8s에서는 kube-proxy를 통해 iptables를 조작하여 통신을 수행했었는데, Cilium을 이용하게 되면 iptables를 대체하여 이를 사용하지 않아도 데이터 경로 최적화 덕에 매우 잘 동작
예를 들면, 이전 글들을 보면 iptables를 통해 sNAT를 처리하는 경우가 허다했는데 eBPF 만으로도 Masquerading하여 sNAT을 처리 가능
동작 원리
Cilium eBPF는 애플리케이션에 추가적인 설정이나 변경 없이도 우측 그림처럼 리눅스 커널을 자유롭게 프로그래밍하여 동작 (Cilium 없이는 Sidecar 형태로 컨테이너를 구성하여 비슷하게 동작)
네트워크 모드
Cilium의 네트워크 모드는 Tunnel 모드와 Native Routing 모드로 나뉨
•
Tunnel 모드 : 트래픽을 전달할 VXLAN (UDP 8472) 혹은 Geneve (UDP 6081) 인터페이스를 다수 구축
•
Native Routing 모드 : 트래픽을 받을 중간 인터페이스를 사용하지 않고 그대로 두어 이용
•
Cilium Agent : DaemonSet으로 실행할 수 있고, k8s API 설정으로 부터 네트워크 설정, 네트워크 정책, Service 부하 분산, 모니터링 등을 수행하여 eBPF 프로그램을 관리
•
Cilium CLI : Cilium 명령어를 이용할 수 있는 도구로써 eBPF Maps에 직접 접속하여 확인 가능
•
Cilium Operator : k8s 클러스터에서 한 번씩 처리해야 하는 작업들을 관리 및 수행
•
Hubble : 네트워크와 보안 모니터링 플랫폼 역할을 수행하고, Server, Relay, Client, Graphical UI로 구성
•
Data Store : Cilium Agent 간의 상태를 저장하고 전파하는 역할을 수행하고, k8s CRD 혹은 Key-Value 형태의 Store를 저장소로 이용 가능
리눅스 커널에 있는 네트워크 스택은 BPF 프로그램을 동작시킬 수 있도록 BPF 훅들을 지원
이 때 XDP를 이용하는데, 이는 네트워크 드라이버 가장 앞 단에서 XDP BPF 훅을 통해서 BPF 프로그램들이 트리거 되기 때문이고, 덕분에 가능한 최고의 패킷 처리 성능을 갖게 됨
BPF 프로그램 실행
1.
네트워크 인터페이스에 TC Ingress 훅에서 BPF 프로그램이 실행
2.
Pod와 연결된 veth pair의 lxc에 있는 TC Ingress 훅에서 BPF 프로그램이 실행
Cilium에서 사용하는 eBPF 데이터 경로는 패킷을 맵핑과 조회하여 일치 시 L3, L4로 전달하는 책임을 가짐
•
엔드포인트 정책이라 함은 정책에 따라 패킷을 차단하고 전달하는 것을 말하는데, 전달의 경우 Service 정책 혹은 L7 정책으로 전달 가능
•
Service 정책은 모든 패킷에 대해 <dest-ip>:<dest-port>을 맵핑과 조회하여 일치하게 되면 L3 및 L4 목적지로 전달하게 되는데, 모든 인터페이스의 TC Ingress 훅에서 동작 가능
•
L3 정책으로는 암호화 등이 있음
•
L7 정책은 Proxy 트래픽을 사용자 영역에 있는 Cilium Proxy로 리다이렉션이 가능하고, Cilium Proxy로는 Envoy를 이용
Egress와 Ingress가 설정된 L7 e2e 흐름에 대해 확인
L7 정책에 대해선 커널 훅과 사용자 영역의 Proxy 사용으로 성능이 떨어질 수 있음
•
Endpoint to Endpoint
소켓 계층이 적용되었기 때문에 핸드쉐이크 과정에서 TCP의 상태가 ESTABLISHED 될 때까진 엔드포인트 정책을 순회하다가, TCP의 상태가 ESTABLISHED가 되면 L7 정책만 이용
•
Egress from Endpoint
옵셔널하게 존재하는 오버레이 네트워크가 있을 수 있는데 이 때는 리눅스 네트워크 인터페이스인 cilium_vxlan으로 전송하고, 마찬가지로 소켓 계층 적용이 되어 있고 L7 Proxy를 이용하고 있는 경우에는 엔드포인트 정책을 수행하지 않을 수 있음 (암호화가 적용된 경우엔 L3 암호화를 거쳐감)
•
Ingress to Endpoint
패킷이 암호화 되어 있다면 먼저 L3 복호화 과정을 거친 뒤에 일반적인 과정으로 처리되며, 위에서 패킷이 소켓 계층에서 처리되었던 것과 유사하게도 Proxy와 엔드포인트 간의 소켓에서 여러 정책들의 순회를 피하여 처리 되는 것을 볼 수 있음
Service 통신 흐름
Network based LB vs Socket based LB
Cilium에서 ClusterIP로 통신할 때 부하 분산이 이뤄지는 과정은 네트워크 기반의 LB, 소켓 기반의 LB가 있음
동작 과정
Pod 1에서 동작하는 애플리케이션이 connect() 시스템 콜을 이용하여, 소켓을 연결할 때 목적지의 IP 주소가 Service의 IP 주소 (10.10.8.55)이면 소켓의 목적지 IP 주소를 백엔드 주소 (10.0.0.31)로 설정
이 때 해당 소켓을 이용하는 모든 패킷의 목적지 IP 주소는 이미 백엔드 주소 (10.0.0.31)로 설정되기 때문에 dNAT 과정이 필요하지 않음
소켓 동작 상 BPF Socket Operation 프로그램은 root cgroup에 연결되어 TCP 상태가 ESTABLISHED에서 실행
소켓의 send / recv 훅은 TCP의 모든 송수신 시에 실행되는데, 훅에서 검사 / 삭제 / 리다이렉션 가능
connect() 및 sendto() 시스템 콜에 연결되어 있는 프로그램 (connect4, sendmsg4)에서는 목적지 IP 주소를 백엔드 주소와 포트로 변환하게 되고, cilium_lb4_backends Maps에 백엔드 주소와 포트를 등록
이후엔 recvmsg() 시스템 콜에 연결된 프로그램 (recvmsg4)에서 cilium_lb4_reverse_nat Maps를 이용하여 목적지 IP 주소와 포트를 다시 Service의 IP 주소와 포트로 변환
eBPF Maps의 용량 제한은 없다고 했었는데, eBPF Maps에 저장하려는 각 항목들은 상한 제한이 있으니 limit 관련 옵션을 잘 확인하여 사용 필요
Map Name | Scope | Default Limit | Scale Implications |
Connection Tracking | node or endpoint | 1M TCP/256k UDP | Max 1M concurrent TCP connections, max 256k expected UDP answers |
NAT | node | 512k | Max 512k NAT entries |
Neighbor Table | node | 512k | Max 512k neighbor entries |
Endpoints | node | 64k | Max 64k local endpoints + host IPs per node |
IP cache | node | 512k | Max 256k endpoints (IPv4+IPv6), max 512k endpoints (IPv4 or IPv6) across all clusters |
Load Balancer | node | 64k | Max 64k cumulative backends across all services across all clusters |
Policy | endpoint | 16k | Max 16k allowed identity + port + protocol pairs for specific endpoint |
Proxy Map | node | 512k | Max 512k concurrent redirected TCP connections to proxy |
Tunnel | node | 64k | Max 32k nodes (IPv4+IPv6) or 64k nodes (IPv4 or IPv6) across all clusters |
IPv4 Fragmentation | node | 8k | Max 8k fragmented datagrams in flight simultaneously on the node |
Session Affinity | node | 64k | Max 64k affinities from different clients |
IP Masq | node | 16k | Max 16k IPv4 cidrs used by BPF-based ip-masq-agent |
Service Source Ranges | node | 64k | Max 64k cumulative LB source ranges across all services |
Egress Policy | endpoint | 16k | Max 16k endpoints across all destination CIDRs across all clusters |
kube-proxy는 리눅스의 CT (Connection Tracking) 테이블을 이용하고 코어 수에 따라 최대 수가 결정되는데 반해, Cilium은 eBPF Maps에 자체 CT 테이블을 이용하여 메모리에 따라 최대 수를 결정
vCPU | Memory (GiB) | Kube-proxy CT entries | Cilium CT entries |
1 | 3.75 | 131072 | 131072 |
2 | 7.5 | 131072 | 131072 |
4 | 15 | 131072 | 131072 |
8 | 30 | 262144 | 284560 |
16 | 60 | 524288 | 569120 |
32 | 120 | 1048576 | 1138240 |
64 | 240 | 2097152 | 2276480 |
96 | 360 | 3145728 | 4552960 |
eBPF 소개를 본대로면 iptables를 전혀 사용하지 않을 것 같지만, 커널 버전이 낮아서 iptables로만 구현되어 있는 경우 혹은 Cilium의 정상 동작이 안 되는 경우 등에도 통신 보장을 위해서 iptables를 이용하도록 설정
통신 시 네트워크 인프라 동작에 대해 가시성을 투명하게 제공하는 플랫폼
애플리케이션 코드 수정 없이 동작하고, IP 주소 기반이 아닌 Service 및 Pod의 ID를 기반으로 동작
Hubble API는 UDS로 제공되고, 이에 대한 쿼리는 Hubble CLI로 이용할 수 있음
Hubble API는 Cilium Agent가 실행되는 개별 노드 범위 내에서만 동작하는데, 이는 네트워크 통찰을 로컬에서 관찰한 트래픽을 대상으로 제한하기 위한 것
Hubble Relay를 배포하게 되면 개별 노드가 아닌 전체 클러스터 혹은 Cluster Mesh 시나리오의 여러 클러스터에 대한 Observability를 얻을 수 있음
Hubble Relay 모드에서는 Hubble CLI를 Hubble Relay로 Service를 지정하거나 Hubble UI를 이용하는 방법으로 데이터에 접근할 수 있음
Hubble UI는 L3/L4/L7 계층에서 Service 간의 종속성 그래프를 검색할 수 있는 인터페이스이며, 데이터 흐름에 대한 필터링을 제공
Hubble은 지표 제공을 위한 Exporter를 제공하여 Prometheus와 통합도 쉽게 가능
kube-proxy를 이용하여 NodePort로 접속 시 기본적으로는 sNAT을 이용
sNAT을 이용하던 것과 달리 DSR을 이용하면, 포워딩을 해주던 LB로 돌려주는 것이 아니라 해당 요청을 보낸 클라이언트에게 바로 응답
TCP는 DSR을 이용하고 UDP는 sNAT을 이용하도록 지원하는 Hybrid DSR도 있음
Ubuntu focal64 LTS (Linux 5.4.0-99-generic)은 DSR이 동작하지 않음
동작 과정은 아래와 같음
1.
DSR 사용 시 IP 옵션 헤더에 최초 접속한 노드의 IP 정보를 담아서 타겟 노드로 전달
2.
타겟 노드는 전달 받은 IP 옵션 헤더에 저장되어 있는 원래 목적지 주소를 cilium_snat_v4_external라는 Maps에 저장해두고, CT에 변환 정보를 추가
3.
이후 응답 패킷이 생성되면 Pod의 veth lxc의 bpf_lxc.c #from-container에서 CT에 저장된 정보를 이용하여 cilium_snat_v4_external Maps에서 필요한 정보를 얻은 뒤, 패킷의 목적지 주소를 클라이언트가 접속한 주소로 치환
4.
클라이언트가 접속했던 IP는 그대로 보존되어 Pod로 전달
전통적인 방법으로 L4 장비에서 L2 DSR을 동작시키기 위해선 서버에 Loopback 설정이 필요
•
ID 기반
→ L3 기반의 엔드포인트 간 연결 정책에 의한 것으로, 예를 들어 role=frontend는 role=backend로 접속 가능하도록 설정 가능
•
포트 기반
→ L4 기반의 I/O 접근 가능한 특정 포트를 지정하는 것으로 role=frontend는 443으로만 나가고, role=backend는 443으롬나 요청을 받도록 설정 가능
•
HTTP 기반
→ RPC와 HTTPS를 이용하는 애플리케이션을 대상으로 하는 접근 제어 방식으로, role=frontend는 오로지 REST API의 [GET] /userdata만 호출 할 수 있고, 해당 API를 제외한 나머지 API는 role=backend에서 제한되도록 설정 가능
이러한 네트워크 정책은 보안과 특히 밀접하므로 아래 내용들을 매우 매우 참고할 것
(개인적으로 많이 이용하는 범주로 기재)
TCP, UDP 기반의 부하에 대해 최적으로 Rate Limit을 제공할 수 있도록 Bandwidth Manager를 지원하는데, 이는 EDT (Earliest Departure Time)과 eBPF를 이용
이용 방법은 단순하게 kubernetes.io/egress-bandwidth 애너테이션을 이용하며, Direct Routing 모드와 Tunneling 모드를 모두 지원
L2 Announcements / L2 Aware LB
L2 Announcements는 로컬 네트워크에서 Service를 확인하고 도달할 수 있도록 만드는 기능으로써, 사무실 혹은 캠퍼스 처럼 BGP 기반의 라우팅이 없는 네트워크에서 온프레미스 배포를 위해 고안된 기능
해당 기능을 L2 단에서 이용되는 LB와 함께 이용하면 ExternalIP 또는 LB IP에 대해 ARP 쿼리에 응답하게 되는데, 이 때 이용하는 IP 주소는 여러 노드에 걸친 일종의 VIP 이므로 MAC 주소를 응답할 때 여러 노드들의 MAC 주소 중 하나를 돌아가며 응답
NodePort에서는 IP 주소와 포트가 특정 노드에 한정되기 때문에 해당 노드가 다운되면 이용할 수 없다는 문제가 있으므로, L2 Announcements 기능을 이용했을 때는 여러 Service들이 고유한 IP 주소에 기반하므로 이러한 문제가 없음
특정 Pod들이 특정 Egress Gateway로 접속할 수 있도록 고정하여 통신하도록 설정 가능
해당 기능은 Egress Gateway의 기능을 활성화 함으로써 이용할 수 있고, Egress의 sNAT에 기반하여 동작
IPSec, WireGuard
각 노드들은 자동으로 암호화를 위한 키 페어를 만들게 되는데, 이 중 퍼블릭 키를 CiliumNode라는 커스텀 리소스의 io.cilium.network.wg-pub-key라는 애너테이션을 이용하여 노드들이 나눠 갖도록 분배
각 노드들의 퍼블릭 키는 Cilium에서 관리되는 엔드포인트들의 노드에서 트래픽을 주고 받을 때 암호화 및 복호화에 이용
노드 내에서 Pod 간 통신 시에는 암호화를 수행하지 않으므로 이와 같은 동작은 이뤄지지 않고, WireGuard의 엔드포인트는 UDP 51871 포트를 각 노드에서 사용
Maglev는 대규모 병렬 L4 LB 환경에서 L4 클러스터링 및 분산 컨시스턴트 해싱을 통해서 장애 수용성을 높이고 확장성을 높이는 방식
Maglev 혹은 Random 등 백엔드에서 처리 방식을 단순 선택하는 것만으로도 적용 가능