이 글은 가시다님과 함께하는 AEWS 3기의 내용을 정리한 것입니다.
CNI란?
Container Network Interface의 약자로 쿠버네티스 내의 컨테이너간 통신을 위한 인터페이스 역할을 한다.
대표적으로 calico, flannel 등이 있다.
AWS VPC CNI
https://docs.aws.amazon.com/eks/latest/best-practices/vpc-cni.html
Amazon VPC CNI - Amazon EKS
The most frequently used fields such as WARM_ENI_TARGET, WARM_IP_TARGET, and MINIMUM_IP_TARGET are not managed and will not be reconciled. The changes to these fields will be preserved upon updating of the add-on.
docs.aws.amazon.com
말그대로 AWS에서 제공하는 CNI이고, VPC내에서 존재하는 CNI이다. 그렇기 때문에 노드와 파드의 네트워크 대역을 동일하게 설정해준다. 노드는 당연히 ENI를 잡으면서 VPC내에 존재하기 때문에 가지는 대역이 있는데, 일반적으로 pod대역은 다르게 생성되는 경우가 많다.
그러나 VPC CNI를 통해서 노드와 같은 대역에 존재하므로써 네트워크 통신의 성능을 높이고, 지연을 줄일 수 있게 된다.
아래 그림과 같이 파드 간 통신시 일반적으로 쿠버네티스 CNI는 오버레이 통신을 한다.
AWS VPC CNI의 경우는 동일 대역으로 직접 통신을 한다. 그림이 정말 단순해진 걸 확인할 수 있다.
VPC CNI 확인하기 (aws-node 파드 정보)
aws-node라는 이름이 AWS VPC CNI 역할을 하는 pod이다. 이렇게 describe를 해보면 알 수 있다.
실제로 eks 클러스터를 배포해보자.
첫째로 node의 IP와 pod의 IP 대역이 모두 192.168.1, 2, 3으로 일치하는 것을 확인할 수 있다.
둘째로 aws-node, kube-proxy의 경우 node와 같은 IP를 가지는데, 이는 노드 내에서 포트로 구분되고 있기 때문이다.
Pod의 IP 할당을 위한 IPAM
EC2 인스턴스 유형당 제공가능한 pod할당 갯수가 있다. IPAM에서 pod의 IP를 할당할 수 있는 pool을 관리하게 된다.
VPC CNI는 L-IPAM을 가지고 있어, 이를 통해서 VPC 대역의 일부 IP pool을 가지고 있으면서 여기서 할당받아서 사용하게 된다. 그렇기 때문에 pod의 IP를 매우 빠르게 받아갈 수 있도록하였다.(fast start)
따라서 kubelet은 VPC CNI와 소통하면서 pod가 추가되면 L-IPAM에서 신속하게 IP를 할당받아 pod를 생성한다.
만약 L-IPAM은 기본적으로 노드에 할당된 ENI에서 선별하게되며 ENI에 할당된 IP Pool을 모두 사용하였다면 ENI를 추가로 할당받아 IP pool을 늘리게 된다. 다만 AWS에서는 인스턴스 유형마다 할당가능한 ENI의 갯수가 제한되기 때문에 이 부분에 대해서 반드시 염두해 두어야 한다.
파드를 추가시 VPC CNI가 동작하는 방법
현재 클러스터 상황은 다음과 같다. 지금부터 해보려고하는 것은 파드가 추가될때 라우팅 테이블이 어떻게 변화되며, 추가적으로 IP는 어떻게 발급받게되는지 확인해본다.
# pod ip조회
kubectl get pod -o wide -A
# 각 노드 내의 라우팅 테이블 조회
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
노드 1번에 보면 현재 pod에 5개가 있는데 이중 192.168.1.30은 aws-node, kube-proxy 그리고 실제 노드의 IP이다.
그 외에 3개의 IP 모두 라우팅 테이블에 등록되어 있는걸 확인할 수 있다.
나머지도 마찬가지로 pod의 IP가 모두 라우팅 테이블에 등록되어 있고, pod마다 eni가 잡혀있는것을 확인할 수 있다.
우리는 여기서 ens가 노드에 붙은 실제 ENI이며, 나머지 eni~~ 하는 것들이 보조 IP임을 확인할 수 있다.
Every 2.0s: ip link | egrep 'ens|eni' ;e... ip-192-168-1-30.ap-northeast-2.compute.internal: Sat Feb 15 04:58:48 2025
2: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT group default qlen 1000
3: enid1f42c567fa@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP mode DEFAULT group default
4: eni1b983785fd9@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP mode DEFAULT group default
5: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT group default qlen 1000
6: eni4699a528818@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP mode DEFAULT group default
[ROUTE TABLE]
192.168.1.7 0.0.0.0 255.255.255.255 UH 0 0 0 eni4699a528818
192.168.1.61 0.0.0.0 255.255.255.255 UH 0 0 0 eni1b983785fd9
192.168.1.253 0.0.0.0 255.255.255.255 UH 0 0 0 enid1f42c567fa
자 이번에는 콘솔에서도 확인해보자. 노드 1의 예시이다.
ENI로 잡고있는 것은 192.168.1.30, 192.168.1.202이다.
그리고 네트워킹에서 잘 보면 보조 프라이빗 IPv4주소에 ENI당 5개씩 보조 아이피를 가지고 있다.
따라서 총 10개가 IPAM에 할당되는 것이다.
아까 보았지만 실제로 pod의 경우 192.168.1.7, 192.168.1.61, 192.168.253 모두 보조 IP에서 볼 수 있었다.
만약 여기서 pod가 실제로 추가되면 저 보조 IP에서 할당될 것이다.
ENI에서 확인해봐도 실질적으로 ENI당 5개의 보조 IP를 받은것을 확인할 수 있다.
나머지 노드 들도 동일하게 구성되어있다.
자 이제 테스트용 netshoot pod를 추가하면서 각 IP들이 어떻게 추가되는지 살펴보자.
아래 예시는 디플로이먼트로 3개의 파드를 배포한다.
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: netshoot-pod
spec:
replicas: 3
selector:
matchLabels:
app: netshoot-pod
template:
metadata:
labels:
app: netshoot-pod
spec:
containers:
- name: netshoot-pod
image: nicolaka/netshoot
command: ["tail"]
args: ["-f", "/dev/null"]
terminationGracePeriodSeconds: 0
EOF
자 이렇게 배포했을때 각 노드별 1개씩 배포되었다. 노드1만 예시로 살펴보자.
Every 2.0s: ip link | egrep 'ens|eni' ;e... ip-192-168-1-30.ap-northeast-2.compute.internal: Sat Feb 15 05:07:35 2025
2: ens5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT group default qlen 1000
3: enid1f42c567fa@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP mode DEFAULT group default
4: eni1b983785fd9@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP mode DEFAULT group default
5: ens6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc mq state UP mode DEFAULT group default qlen 1000
6: eni4699a528818@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP mode DEFAULT group default
7: eni113b0238a49@if3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9001 qdisc noqueue state UP mode DEFAULT group default
[ROUTE TABLE]
192.168.1.7 0.0.0.0 255.255.255.255 UH 0 0 0 eni4699a528818
192.168.1.61 0.0.0.0 255.255.255.255 UH 0 0 0 eni1b983785fd9
192.168.1.143 0.0.0.0 255.255.255.255 UH 0 0 0 eni113b0238a49
192.168.1.253 0.0.0.0 255.255.255.255 UH 0 0 0 enid1f42c567fa
노드 1의 정보는 192.168.1.143이 새로 생겼고 이 또한 보조 private IP에서 나온것을 확인할 수 있다.
노드에 파드 생성 갯수 제한
인스턴스 타입별로 ENI 갯수가 제한이 되어 있고, ENI별로 할당 가능한 보조 private ip도 제한되어 있기 때문에
리소스와 상관없이 IP 풀이 없으면 파드 생성이 제한된다. 따라서 반드시 해당 파드에 사용가능한 IP의 갯수도 몇개인지 파악하는 작업이 필요하다.
최대 파드 생성 갯수 공식은 다음과 같다.
최대 파드 생성 갯수 = (IPv4addr 값 - 1) * 최대 ENI + 2
-1을 하는 이유는 랜카드 ENI에서 사용하는 IP가 되기 때문이며, +2는 aws-node와 kube-proxy pod는 노드 IP를 공유하기 때문이다.
예를 들면 t3타입의 경우 아래와 같이 확인해볼 수 있다.
aws ec2 describe-instance-types --filters Name=instance-type,Values=t3.\* \
--query "InstanceTypes[].{Type: InstanceType, MaxENI: NetworkInfo.MaximumNetworkInterfaces, IPv4addr: NetworkInfo.Ipv4AddressesPerInterface}" \
--output table
--------------------------------------
| DescribeInstanceTypes |
+----------+----------+--------------+
| IPv4addr | MaxENI | Type |
+----------+----------+--------------+
| 15 | 4 | t3.xlarge |
| 12 | 3 | t3.large |
| 15 | 4 | t3.2xlarge |
| 6 | 3 | t3.medium |
| 2 | 2 | t3.nano |
| 2 | 2 | t3.micro |
| 4 | 3 | t3.small |
+----------+----------+--------------+
(END)
예를 들면 t3.medium의 경우 (6-1)*3 + 2로 17개가 된다. 다만 aws-node와 kube-proxy는 제외하고 생성가능한 파드는 15개가 된다.
사실 이렇게 복잡하게 계산할 필요없이 다음 명령어를 통해서 확인이 가능하며, pods의 값에 -2를 하면 생성가능한 pod 수가 되겠다.
kubectl describe node | grep Allocatable: -A6
➜ util kubectl describe node | grep Allocatable: -A6
Allocatable:
cpu: 1930m
ephemeral-storage: 27845546346
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 3364536Ki
pods: 17
--
Allocatable:
cpu: 1930m
ephemeral-storage: 27845546346
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 3364528Ki
pods: 17
--
Allocatable:
cpu: 1930m
ephemeral-storage: 27845546346
hugepages-1Gi: 0
hugepages-2Mi: 0
memory: 3364528Ki
pods: 17
지금 내가 쓰는 노드도 t3.medium이기 때문에 17개로 보인다.
해당 명령어를 통해 네트워크 인터페이스를 계속 호출해본다. 파드의 갯수를 확인해볼 수 있다.
while true; do ip -br -c addr show && echo "--------------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done
이 상태로 pod 생성을 늘리면서 어떤 일이 발생할지 확인해보자.
일단 각 노드별 17개씩 할당이 가능하고, 각 노드당 2개의 랜카드 및 10개의 보조 IP를 가지고 있다.
그렇다면 pod를 12개씩 할당하도록 총 36개의 replicas를 만들면 아마 랜카드가 추가될 것이다.
현재 노드 2의 상황이다.
--------------
2025-02-15 06:26:07
lo UNKNOWN 127.0.0.1/8 ::1/128
ens5 UP 192.168.2.251/24 metric 1024 fe80::473:78ff:fed7:9397/64
eni7a65bed750e@if3 UP fe80::10c5:72ff:fe28:5dfc/64
ens6 UP 192.168.2.31/24 fe80::4f5:d8ff:fec7:e7db/64
eni766716ab1fd@if3 UP fe80::5c97:bfff:fe53:4127/64
enia1ddb09938f@if3 UP fe80::9c3a:58ff:fe94:366e/64
--------------
이제 이렇게 pod를 36개를 늘려보겠다.
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 36
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
EOF
파드가 일정량 늘어나더니 IP풀을 모두 소진했을때 ens7이라는 랜카드를 하나 더 붙이고 나서야 나머지 파드들이 생성되었다.
보면 ens7이 붙은것을 확인할 수 있다.
2025-02-15 06:27:04
lo UNKNOWN 127.0.0.1/8 ::1/128
ens5 UP 192.168.2.251/24 metric 1024 fe80::473:78ff:fed7:9397/64
eni7a65bed750e@if3 UP fe80::10c5:72ff:fe28:5dfc/64
ens6 UP 192.168.2.31/24 fe80::4f5:d8ff:fec7:e7db/64
eni766716ab1fd@if3 UP fe80::5c97:bfff:fe53:4127/64
enia1ddb09938f@if3 UP fe80::9c3a:58ff:fe94:366e/64
eni357f5ff1ea8@if3 UP fe80::80ae:c7ff:fe73:bcb3/64
eni0d5a46dc5a5@if3 UP fe80::b0b8:97ff:fe70:d0cd/64
eni92042bd03c6@if3 UP fe80::2441:64ff:febd:66c0/64
enif9b9fd354f7@if3 UP fe80::f0c1:55ff:fe72:ea40/64
enib7f882193cf@if3 UP fe80::588e:42ff:fef5:5a33/64
eni2f59c0a4d21@if3 UP fe80::a093:65ff:fe1d:ade3/64
eni53857b9a2a4@if3 UP fe80::f482:c4ff:febd:3652/64
ens7 UP 192.168.2.91/24 fe80::4cd:59ff:fecd:26a9/64
eni86e78aa78d9@if3 UP fe80::e000:2aff:fe7f:8e07/64
eni1cb309a2637@if3 UP fe80::a034:1aff:fe3c:5ddd/64
eni67ce56fb6a4@if3 UP fe80::904a:87ff:fef5:8a3c/64
eni2139469f1a7@if3 UP fe80::401f:a9ff:fecf:6a63/64
eni47e8a8dc261@if3 UP fe80::7ce8:36ff:fefc:488d/64
--------------
실제로 콘솔에서도 다음과 같이 ENI가 추가되고, 보조 프라이빗 IP가 15개로 늘은것을 확인할 수 있다.
자 그럼 이제 t3.medium의 경우 노드당 총 17개씩, 그 중 2개는 제외하고 15개씩 45개의 pod생성이 가능하다.
극단적으로 100개를 생성할 경우 아마 모두 생성하지 못하게 될 것이다. 확인해보자.
pod가 45개 이상이 되니까 더이상 생성하지 못하고 지속적으로 Pending이 되는 것을 볼 수 있다.
임의의 파드를 골라서 kubectl describe 명령어로 확인해보니 too many pods 에러가 발생한 것을 확인할 수 있었다.
0/3 nodes are available: 3 Too many pods. preemption: 0/3 nodes are available: 3 No preemption victims found for incoming pod.
노드 간 파드 통신
AWS VPC CNI를 사용할 경우 오버레이 통신 기술 없이 바로 파드간 통신이 가능하다.
# pod2에서 pod3 호출
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP3
# 각노드에서 tcp dump
sudo tcpdump -i any -nn icmp
실제 통신 로그를 보면 단 하나의 NAT도 없이 그대로 통신하는 것을 확인할 수 있다.
참고로 pod2는 192.168.2.24, pod3는 192.168.3.123이다.
05:21:27.365895 enia1ddb09938f In IP 192.168.2.24 > 192.168.3.123: ICMP echo request, id 25, seq 2, length 64
05:21:27.365927 ens5 Out IP 192.168.2.24 > 192.168.3.123: ICMP echo request, id 25, seq 2, length 64
05:21:27.367215 ens5 In IP 192.168.3.123 > 192.168.2.24: ICMP echo reply, id 25, seq 2, length 64
05:21:27.367226 enia1ddb09938f Out IP 192.168.3.123 > 192.168.2.24: ICMP echo reply, id 25, seq 2, length 64
파드에서 외부통신
그럼 파드가 yahoo.com과 같이 외부로 통신될 경우 어떻게 진행되는지 확인해보자.
결론부터 말하면 iptable에 의해 SNAT되어 노드의 인터페이스 정보 IP로 나가게된다.
해당 그림에서 잘 설명하고 있는데 그림처럼 www.yahoo.com으로 실제 실험해보자.
그림상 eth0은 나에게 ens5와 같다.
util kubectl exec -it $PODNAME3 -- ping -c 1 www.yahoo.com
PING me-ycpi-cf-www.g06.yahoodns.net (180.222.106.12) 56(84) bytes of data.
64 bytes from e2.ycpi.vip.tpb.yahoo.com (180.222.106.12): icmp_seq=1 ttl=41 time=62.6 ms
--- me-ycpi-cf-www.g06.yahoodns.net ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 62.583/62.583/62.583/0.000 ms
자 다음은 pod3에서 확인한 결과다
enida~ 즉 pod 192.168.3.123에서는 180.222.106.11로 ICMP를 보내고,
ens5인 192.168.3.113에서는 180.222.106.11로 ICMP를 보냈다.
참고로 192.168.3.123은 netshoot파드이며, 192.168.3.113은 node의 IP이다.
이와 같은 이유를 ip route table에서 확인해보자. 다음은 노드 3의 routetable이다.
192.168.3.123은 enida583fe540b로 연결되어 할당받고, 해당 대역의 값은 ens5로 넘어간다.
ip route show table main
[ec2-user@ip-192-168-3-113 ~]$ ip route show table main
default via 192.168.3.1 dev ens5 proto dhcp src 192.168.3.113 metric 1024
192.168.0.2 via 192.168.3.1 dev ens5 proto dhcp src 192.168.3.113 metric 1024
192.168.3.0/24 dev ens5 proto kernel scope link src 192.168.3.113 metric 1024
192.168.3.1 dev ens5 proto dhcp scope link src 192.168.3.113 metric 1024
192.168.3.123 dev enida583fe540b scope link
192.168.3.190 dev eni77094b7c47e scope link
192.168.3.233 dev eni6c5a99d55c0 scope link
iptable 정보도 확인해보면 정말 많이 나와서 필요한 것만 확인해보자.
sudo iptables -t nat -S
sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
AWS-SNAT-CHAIN 룰에 의해서 외부와 통신하게 된다.
SNAT 정보에 의하면 192.168.0.0/16은 내부로 RETURN되며,
그 외의 값은 SNAT으로 반환되면서 to source값이 192.168.3.113이 되는 것을 확인할 수 있다.
-A AWS-SNAT-CHAIN-0 -d 192.168.0.0/16 -m comment --comment "AWS SNAT CHAIN" -j RETURN
-A AWS-SNAT-CHAIN-0 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.3.113 --random-fully
또한 해당 명령어로 SNAT 되는 트래픽 수를 알 수 있는데
sudo iptables -t filter --zero; sudo iptables -t nat --zero; sudo iptables -t mangle --zero; sudo iptables -t raw --zero
watch -d 'sudo iptables -v --numeric --table nat --list AWS-SNAT-CHAIN-0; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING; echo ; sudo iptables -v --numeric --table nat --list POSTROUTING'
chain의 내용이 지속적으로 올라가는 것을 확인할 수 있었다.
Chain AWS-SNAT-CHAIN-0 (1 references)
pkts bytes target prot opt in out source destination
59 4022 RETURN all -- * * 0.0.0.0/0 192.168.0.0/16 /* AWS SNAT CHAIN */
82 5176 SNAT all -- * !vlan+ 0.0.0.0/0 0.0.0.0/0 /* AWS, SNAT */ ADDRTYPE match dst-type !LOCAL to:192.168.3.113 random-fully
그리고 SNAT이 될때 conntrack-tools에서 해당 정보를 잠시 보관하고 있는데,
icmp 1 29 src=192.168.3.123 dst=8.8.8.8 type=8 code=0 id=50 src=8.8.8.8 dst=192.168.3.113
이렇게 명확하게 src, dst를 명시하고 있는 것을 확인할 수 있다.
'프로젝트&&스터디 > AWES 3기 (2025.01~)' 카테고리의 다른 글
AEWS 3기 - EKS 네트워킹 (3) TAR, kube-proxy IPVS 모드 (0) | 2025.02.15 |
---|---|
AEWS 3기 - EKS 네트워킹 (2) EKS Loadbalancer Controller, Ingress, ExternalDNS (0) | 2025.02.15 |
[AEWS 3기] eksctl을 활용해 yaml로 EKS Cluster 생성하기 (0) | 2025.02.09 |
[AEWS 3기] AWS EKS Fargate 사용하기 (0) | 2025.02.08 |
[AEWS] AWS Fargate 학습자료 (0) | 2025.02.08 |