| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- xfs_quota
- docker 상태
- EBS 최적화
- 테라폼 자동완성
- ebs 재부팅
- 리눅스 시간대
- 볼륨 연결
- 볼륨추가
- MFA 분실
- 테라폼 캐시
- 리눅스
- EC2
- 디스크 성능테스트
- EBS
- 컨테이너 터미널 로그아웃
- /etc/fstab 뜻
- AWS
- AWS EBS
- ebs 마운트
- 테라폼 맥
- Mac Terraform
- MFA 인증
- 테라폼 설치
- 텔레메트리란
- Authenticator
- /etc/fstab 설정
- Terrafrom
- 컨테이너 터미널
- docker -i -t
- epxress-generator
- Today
- Total
I got IT
EKS 네트워크 실습 본문
※ 해당 글은 CloudNet@ gasida 님의 EKS 스터디 내용을 참고하여 작성하였습니다.
EKS에 대한 개념과 기본 구성은 다음 링크를 참고 바랍니다. 🔗
EKS 기초 Hands-on
※ 해당 글은 CloudNet@ gasida 님의 EKS 스터디 내용을 참고하여 작성하였습니다. EKS에 대한 간단한 소개는 이전 글을 참조바랍니다. https://joshhoxy.tistory.com/33EKS를 구성하는데에는 여러 방법이 있지
joshhoxy.tistory.com
이번 포스팅에서는 EKS의 네트워크를 중점적으로 다뤄보도록 하겠습니다.
실습환경 아키텍처

▪️ 클러스터가 구성되는 myeks-vpc와 운영서버가 프로비저닝된 operator-vpc 를 분리하여 VPC를 생성합니다.
▪️ 이 때 VPC 간 통신을 위해서 VPC Peering을 구성합니다.
▪️ 워커노드는 퍼블릭 서브넷에 삼중화 합니다.
▪️ 액세스포인트는 지난번과 마찬가지로 퍼블릭에 오픈합니다. (주의: 테스트 환경에서만 설정할 것)
실습 환경 구성
1. 로컬 작업환경 구성 (MacOS)
로컬 PC에 brew가 설치되어 있다고 가정하고 brew 사용법은 넘어가겠습니다. 🔗
다음 명령어를 순차적으로 실행하여 필요한 패키지 설치 및 kubectl 명령어 사용 편의를 위한 설정 작업을 해줍니다.
# Install awscli
brew install awscli
aws --version
# Install eksctl
brew install eksctl
eksctl version
# Install kubectl
brew install kubernetes-cli
kubectl version --client=true
# Install Helm
brew install helm
helm version
# krew 툴 및 플러그인 설치
brew install krew
kubectl krew version
kubectl krew install neat get-all df-pv stern
kubectl krew list
# 편리성 툴 설치
brew install kube-ps1
brew install kubectx
# kubectl 단축 및 하이라이트 설정
brew install kubecolor
echo "alias k=kubectl" >> ~/.zshrc
echo "alias kubectl=kubecolor" >> ~/.zshrc
echo "compdef kubecolor=kubectl" >> ~/.zshrc
※ 툴 설명
- krew: kubectl 플러그인 매니저. 플러그인 검색 및 설치에 용이한 툴입니다.
- neat: kubectl 출력에서 불필요한 메타데이터(managedFields, status 등)를 제거하여 깔끔하게 보여줍니다.
- get-all: kubectl get all 명령어보다 더 확장된 정보를 조회할 수 있도록 해줍니다.
- df-dv: 클러스터 내 Persistent Volume (PV) 의 사용량을 확인하는 플러그인입니다.
- stern: 여러 개의 Pod 로그를 실시간으로 스트리밍할 수 있도록 도와주는 플러그인입니다.
- kube-ps1: bash 또는 zsh 프롬프트에서 현재 컨텍스트와 네임스페이스를 표시해주는 플러그인입니다.
- kubectx: 쿠버네티스 클러스터 간 빠르게 전환(switch)할 수 있도록 해주는 툴입니다.
- kubecolor: kubectl 명령어의 출력을 컬러화하여 가독성을 높여주는 툴입니다.
이 외에 추가로 유용한 툴을 사용하고 싶으시다면 다음 툴을 설치하셔도 좋습니다.
# AWS 세션매니저로 관리 노드 EC2 접속 시 사용
brew install --cask session-manager-plugin
# Install sshpass
brew install sshpass
# Install Wireshark : 패킷 캡쳐 및 캡쳐된 파일에서 패킷 내용 확인
brew install --cask wireshark
2. AWS Configure 자격 증명 설정
필요한 툴 설치가 완료 됐으면 지난 번 실습 포스팅 때 처럼 aws 자격증명을 설정해 줍니다.
# default profile 설정
aws configure
# 기존에 되어 있으면 확인
cat ~/.aws/credential
cat ~/.aws/config
# 현재 IAM 확인하기
aws sts get-caller-identity
3. AWS CloudFormation 을 통해 기본 실습 환경 배포
지난 번 처럼 VPC, Subnet 등의 기본 뼈대는 클라우드포메이션을 통해 생성합니다. 아래 명령어를 참고하여 본인의 환경에 맞게 파라미터를 입력합니다. (리전, 키페어)
# CFyaml 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/myeks-2week.yaml
# 배포
# aws cloudformation deploy --template-file myeks-1week.yaml --stack-name mykops --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 --region <리전>
예시) aws cloudformation deploy --template-file ~/Downloads/myeks-2week.yaml \
--stack-name myeks --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 --region ap-northeast-2
# CloudFormation 스택 배포 완료 후 운영서버 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[*].OutputValue' --output text
예시) 3.35.137.31
# 운영서버 EC2 에 SSH 접속
예시) ssh ec2-user@3.35.137.31
ssh -i <ssh 키파일> ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)



실제 배포된 리소스를 콘솔에서 확인해 봅니다.


세개의 가용영역에 서브넷이 생긴걸 확인할 수있습니다.
eksctl을 통해 EKS 배포
export CLUSTER_NAME=myeks
# myeks-VPC/Subnet 정보 확인 및 변수 지정
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
echo $VPCID
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)
echo $PubSubnet1 $PubSubnet2 $PubSubnet3
# ssh 퍼블릭 키 경로 지정
SshPublic=<각자 자신의 ssh 퍼블릭 키 경로>
# 출력된 내용 참고 : 아래 yaml 파일 참고해서 vpc/subnet id, ssh key 경로 수정
eksctl create cluster --name $CLUSTER_NAME --region=ap-northeast-2 --nodegroup-name=ng1 --node-type=t3.medium --nodes 3 --node-volume-size=30 --vpc-public-subnets "$PubSubnet1","$PubSubnet2","$PubSubnet3" --version 1.31 --with-oidc --external-dns-access --full-ecr-access --alb-ingress-access --node-ami-family AmazonLinux2023 --ssh-access --ssh-public-key $SshPublic --dry-run > myeks.yaml
위 명령어를 수행하면 myeks.yaml 파일이 만들어집니다.

이제 마지막으로 kubeconfig 파일을 지정해주고 실제 create 명령어로 생성합니다.
# kubeconfig 파일 경로 위치 지정 :
export KUBECONFIG=$HOME/kubeconfig
혹은 각자 편한 경로 위치에 파일 지정
export KUBECONFIG=~/Downloads/kubeconfig
# 배포
eksctl create cluster -f myeks.yaml --verbose 4
Kubernetes CNI의 대표적인 Calico CNI와 EKS VPC CNI의 차이점


- 네트워크 대역: AWS VPC CNI는 노드와 파드가 동일한 네트워크 대역을 사용합니다24. 반면, Calico CNI는 파드 네트워크 CIDR와 노드 네트워크 CIDR가 서로 다릅니다
- 통신 방식: Calico CNI는 일반적으로 오버레이(VXLAN, IP-IP 등) 통신을 사용하지만, AWS VPC CNI는 동일 대역으로 직접 통신합니다
- IP 할당: AWS VPC CNI는 VPC 대역망의 IP를 파드에 직접 할당합니다2. Calico CNI는 별도의 파드 네트워크 대역을 사용합니다.
- 보안 및 격리: Calico CNI는 파드 간 통신 네트워크를 격리하여 보안적 이점과 IP 대역의 충돌을 방지합니다
- AWS 호환: AWS VPC CNI는 VPC와 통합되어 Security Group, VPC Flow Logs, VPC 라우팅 정책을 사용할 수 있습니다
- 성능 최적화: AWS VPC CNI는 네트워크 통신의 최적화(성능, 지연)를 위해 노드와 파드의 네트워크 대역을 동일하게 설정합니다
Amazon VPC CNI 톺아보기
그렇다면 AWS 에서는 어떻게 이러한 VPC CNI를 구현하였을까요?
이에 대해서는 조금 더 자세히 살펴볼 필요가 있습니다.
VPC CNI의 컴포넌트
Amazon VPC CNI(컨테이너 네트워크 인터페이스)는 두 가지 주요 구성 요소로 이루어져 있습니다.
1. CNI 바이너리
- Pod 간 네트워크를 설정하여 Pod-to-Pod 통신을 가능하게 합니다.
- 노드의 루트 파일 시스템에서 실행되며, 새로운 Pod가 추가되거나 삭제될 때 kubelet에 의해 호출됩니다.
2. ipamd (IP Address Management Daemon)
- 노드에서 ENI(Elastic Network Interface)를 관리하고,
- 사용 가능한 IP 주소 또는 프리픽스 풀(Warm Pool)을 유지하는 역할을 합니다.
VPC CNI 플러그인은 다음과 같은 플로우로 새로운 ENI를 생성하고 연결합니다.

※ warm pool: VPC CNI가 인스턴스의 ENI에 미리 할당된 IP 주소를 확보하는 데 이때 이 관리 pool을 warm pool이라 합니다.
Pod가 생성되면 Warm Pool에서 즉시 IP를 할당하여 빠르게 배포 하고, IP가 부족해지면 CNI가 추가 ENI(Secondary ENI)를 생성하여 더 많은 IP를 확보 하게 됩니다.
Pod 밀도와 최대 ENI 제한
지금껏 설명해 온 것처럼 VPC CNI를 사용하면 Pod는 VPC의 IP를 사용하므로 무한정 파드를 생성하는 것은 IP의 정해진 제한이 있기 때문에 불가능합니다. 그렇다면 Pod는 몇개까지 실행이 가능할까요?
- 각 EC2 인스턴스가 지원하는 ENI와 슬롯(사용 가능한 IP 주소 개수)은 인스턴스 유형에 따라 다릅니다.
- 각 Pod는 하나의 IP를 사용하므로, 특정 EC2 인스턴스에서 실행할 수 있는 Pod 수는 연결 가능한 ENI 개수와 ENI당 지원하는 슬롯 개수에 의해 결정됩니다.
- hostNetwork 모드를 사용하는 Pod는 이 계산에서 제외됩니다.
+ EKS에서 인스턴스당 최대 Pod 수를 올바르게 설정하려면 max-pod-calculator.sh 스크립트를 활용할 수 있습니다.
따라서 EC2 인스턴스 유형에 따라 지원 가능한 ENI 개수와 슬롯 개수가 제한되므로, Pod 밀도를 고려하여 설정해야 합니다.
참고로 EKS VPC CNI 를 통한 최대 파드 개수를 구하는 식은 다음과 같습니다.

보조 IP를 사용하는 경우: ENI x (ENI 당 지원하는 IPv4 개수 -1) + 2
접두사 위임을 사용하는 경우: (ENI x (ENI 당 지원하는 IPv4 개수 -1)) x 16
EKS 네트워크 실습하기
이제 VPC CNI의 개념과 원리에 대해서 어느정도 살펴봤으니 실습을 통해서 어떻게 작동하는지 직접 살펴 보도록 합니다.
실습에서 자주 사용하는 변수들을 미리 저장하여 활용합니다.
#
export KUBECONFIG=~/Downloads/kubeconfig
export CLUSTER_NAME=myeks
# myeks-VPC/Subnet 정보 확인 및 변수 지정
export VPCID=$(aws ec2 describe-vpcs --filters "Name=tag:Name,Values=$CLUSTER_NAME-VPC" --query 'Vpcs[*].VpcId' --output text)
echo $VPCID
export PubSubnet1=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet1" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet2=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet2" --query "Subnets[0].[SubnetId]" --output text)
export PubSubnet3=$(aws ec2 describe-subnets --filters Name=tag:Name,Values="$CLUSTER_NAME-Vpc1PublicSubnet3" --query "Subnets[0].[SubnetId]" --output text)
echo $PubSubnet1 $PubSubnet2 $PubSubnet3
# 인스턴스 IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{InstanceID:InstanceId, PublicIPAdd:PublicIpAddress, PrivateIPAdd:PrivateIpAddress, InstanceName:Tags[?Key=='Name']|[0].Value, Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# 인스턴스 공인 IP 변수 지정
#export N1=<az1 배치된 EC2 공인 IP>
#export N2=<az2 배치된 EC2 공인 IP>
#export N3=<az3 배치된 EC2 공인 IP>
export N1=43.203.169.0
export N2=13.125.28.29
export N3=13.125.255.7
echo $N1, $N2, $N3
# 노드 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i hostnamectl; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo iptables -t nat -S; echo; done
# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].metadata.name}')
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].metadata.name}')
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].metadata.name}')
# 파드 IP 변수 지정
PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].status.podIP}')
PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].status.podIP}')
PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].status.podIP}')
# 자신의 도메인 변수 지정 : 소유하고 있는 자신의 도메인을 입력하시면 됩니다
MyDomain=<자신의 도메인>
MyDomain=gasida.link
MyDnzHostedZoneId=`aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text`
echo $MyDnzHostedZoneId
# A 레코드 값 반복 조회
while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done
네트워크 기본 정보 확인
# CNI 정보 확인
kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2
# kube-proxy config 확인 : 모드 iptables 사용 >> ipvs 모드로 변경 해보자!
kubectl describe cm -n kube-system kube-proxy-config
...
mode: "iptables"
...
# 노드 IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,PrivateIPAdd:PrivateIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value,Status:State.Name}" --filters Name=instance-state-name,Values=running --output table
# 파드 IP 확인
kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase
# 파드 이름 확인
kubectl get pod -A -o name
# 파드 갯수 확인
kubectl get pod -A -o name | wc -l






노드에 직접 들어가서 네트워크 정보를 확인해봅니다. 각자 환경에 맞는 키를 지정해서 코드를 수정합니다.
# CNI 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i tree /var/log/aws-routed-eni; echo; done
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/plugin.log | jq
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/ipamd.log | jq
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/egress-v6-plugin.log | jq
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/ebpf-sdk.log | jq
ssh ec2-user@$N1 sudo cat /var/log/aws-routed-eni/network-policy-agent.log | jq
# 네트워크 정보 확인 : eniY는 pod network 네임스페이스와 veth pair
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -br -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c addr; echo; done
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done
ssh ec2-user@$N1 sudo iptables -t nat -S
ssh ec2-user@$N1 sudo iptables -t nat -L -n -v


노드에서 기본 네트워크 정보 확인


- Network 네임스페이스는 호스트(Root)와 파드 별(Per Pod)로 구분됩니다.
- 특정한 파드(kube-proxy, aws-node)는 호스트(Root)의 IP를 그대로 사용합니다. ⇒ 파드의 Host Network 옵션 ※ [Kubernetes] Pod 관련 Host Network 옵션과 동작원리
- t3.medium 의 경우 ENI 마다 최대 6개의 IP를 가질 수 있습니다.
- ENI0, ENI1 으로 2개의 ENI는 자신의 IP 이외에 추가적으로 5개의 보조 프라이빗 IP를 가질수 있습니다.
- coredns 파드는 veth 으로 호스트에는 eniY@ifN 인터페이스와 파드에 eth0 과 연결되어 있습니다.
노드에서 기본 네트워크 정보 확인 (실습)
# coredns 파드 IP 정보 확인
kubectl get pod -n kube-system -l k8s-app=kube-dns -owide
# 노드의 라우팅 정보 확인 >> EC2 네트워크 정보의 '보조 프라이빗 IPv4 주소'와 비교
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done

netshoot-pod 디플로이먼트 생성해서 테스트 해보기
# [터미널1~3] 노드 모니터링
ssh ec2-user@$N1
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh ec2-user@$N2
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh ec2-user@$N3
watch -d "ip link | egrep 'ens|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"



# 파드 생성
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

파드가 생성되면 워커노드에 새로운 eni가 추가되고 라우팅 테이블에도 정보가 추가되는 것을 확인할 수 있습니다.
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].metadata.name}')
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].metadata.name}')
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].metadata.name}')
# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
# 노드에 라우팅 정보 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo ip -c route; echo; done


Pod ENI 정보 확인해보기
파드가 생성되면서 생긴 ENI는 워커노드와 파드에서 둘다 확인이 가능합니다
워커노드에서 확인

파드에 들어가서 확인
# 테스트용 파드 접속(exec) 후 Shell 실행
kubectl exec -it $PODNAME1 -- zsh
# 아래부터는 pod-1 Shell 에서 실행 : 네트워크 정보 확인
----------------------------
ip -c addr
ip -c route
route -n
ping -c 1 <pod-2 IP>
ps
cat /etc/resolv.conf
exit
----------------------------
# 파드2 Shell 실행
kubectl exec -it $PODNAME2 -- ip -c addr
# 파드3 Shell 실행
kubectl exec -it $PODNAME3 -- ip -br -c addr

노드 간 파드 통신
타 노드에 있는 파드 간 통신 시 네트워크 플로우를 알아봅니다.
AWS VPC CNI 경우 별도의 오버레이(Overlay) 통신 기술 없이, VPC Native 하게 파드간 직접 통신이 가능합니다. 따라서 SNAT 의 처리가 필요 없어지므로 많은 이점이 있습니다.


파드간 통신 시 시퀀스 다이어그램인데 이에 대한 자세한 과정은 다음에 포스팅 해보도록 하겠습니다.
우선 지금은 파드간 통신 시 실제로 NAT 동작 없이 되는지 확인해보겠습니다. 패킷 분석에 가장 근본인 tcpdump로 src dst ip가 어떻게 찍히는지 확인해봅니다.
# 파드 IP 변수 지정
PODIP1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].status.podIP}')
PODIP2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].status.podIP}')
PODIP3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].status.podIP}')
# 파드1 Shell 에서 파드2로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
# 파드2 Shell 에서 파드3로 ping 테스트
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP3
# 파드3 Shell 에서 파드1로 ping 테스트
kubectl exec -it $PODNAME3 -- ping -c 2 $PODIP1
# 워커 노드 EC2 : TCPDUMP 확인
## For Pod to external (outside VPC) traffic, we will program iptables to SNAT using Primary IP address on the Primary ENI.
sudo tcpdump -i any -nn icmp
sudo tcpdump -i ens5 -nn icmp
sudo tcpdump -i ens6 -nn icmp
sudo tcpdump -i eniYYYYYYYY -nn icmp
[워커 노드1]
# routing policy database management 확인
ip rule
# routing table management 확인
ip route show table local
# 디폴트 네트워크 정보를 ens5 을 통해서 빠져나간다
ip route show table main
default via 192.168.1.1 dev ens5
...
영상처럼 워커노드에서 패킷이 캡쳐되는데 eni id가 파드에 물린 Eni인 것을 확인할 수 있습니다.
파드에서 외부로 통신
파드에서 외부로 통신할때는 iptable 에 SNAT 을 통하여 노드의 eth0(ens5) IP로 변경되어서 외부와 통신되는 것을 알 수 있습니다.

VPC CNI 의 External source network address translation 설정에 따라, 외부(인터넷) 통신 시 SNAT 하거나 혹은 SNAT 없이 통신을 할 수 있다고 합니다. 자세한 정보는 다음을 참조: 링크
# pod-1 Shell 에서 외부로 ping
kubectl exec -it $PODNAME1 -- ping -c 1 www.google.com
kubectl exec -it $PODNAME1 -- ping -i 0.1 www.google.com
kubectl exec -it $PODNAME1 -- ping -i 0.1 8.8.8.8
# 워커 노드 EC2 : TCPDUMP 확인
sudo tcpdump -i any -nn icmp
sudo tcpdump -i ens5 -nn icmp

# 퍼블릭IP 확인
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s ipinfo.io/ip; echo; echo; done
# 작업용 EC2 : pod-1 Shell 에서 외부 접속 확인 - 공인IP는 어떤 주소인가?
## The right way to check the weather - 링크
for i in $PODNAME1 $PODNAME2 $PODNAME3; do echo ">> Pod : $i <<"; kubectl exec -it $i -- curl -s ipinfo.io/ip; echo; echo; done
kubectl exec -it $PODNAME1 -- curl -s wttr.in/seoul
kubectl exec -it $PODNAME1 -- curl -s wttr.in/seoul?format=3
kubectl exec -it $PODNAME1 -- curl -s wttr.in/Moon
kubectl exec -it $PODNAME1 -- curl -s wttr.in/:help
Pod의 퍼블릭 IP를 확인해보면 위에서 aws cli 로 조회했던 노드의 퍼블릭 IP와 일치하는 것을 확인할 수 있습니다.
wttr 사이트는 재미로 해보면 좋을 것 같습니다.


# 워커 노드 EC2
## 출력된 결과를 보고 어떻게 빠져나가는지 고민해보자!
ip rule
ip route show table main
sudo iptables -L -n -v -t nat
sudo iptables -t nat -S
# 파드가 외부와 통신시에는 아래 처럼 'AWS-SNAT-CHAIN-0' 룰(rule)에 의해서 SNAT 되어서 외부와 통신!
# 참고로 뒤 IP는 eth0(ENI 첫번째)의 IP 주소이다
# --random-fully 동작 - 링크1 링크2
sudo iptables -t nat -S | grep 'A AWS-SNAT-CHAIN'
-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.1.251 --random-fully
## 아래 'mark 0x4000/0x4000' 매칭되지 않아서 RETURN 됨!
-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
...
# 카운트 확인 시 AWS-SNAT-CHAIN-0에 매칭되어, 목적지가 192.168.0.0/16 아니고 외부 빠져나갈때 SNAT 192.168.1.251(EC2 노드1 IP) 변경되어 나간다!
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'
# conntrack 확인 : EC2 메타데이터 주소(169.254.169.254) 제외 출력
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i sudo conntrack -L -n |grep -v '169.254.169'; echo; done
conntrack v1.4.5 (conntrack-tools):
icmp 1 28 src=172.30.66.58 dst=8.8.8.8 type=8 code=0 id=34392 src=8.8.8.8 dst=172.30.85.242 type=0 code=0 id=50705 mark=128 use=1
tcp 6 23 TIME_WAIT src=172.30.66.58 dst=34.117.59.81 sport=58144 dport=80 src=34.117.59.81 dst=172.30.85.242 sport=80 dport=44768 [ASSURED] mark=128 use=1
iptable 로 NAT 설정을 조회해보면 192.168.0.0/16 대역하고 통신할때는 SNAT 를 리턴하라 라는 의미로 해석할 수 있다. 이는 Pod간 통신 시 IP가 바뀌지 않는 이유이다. 이외에 매치되지 않는 대역은 ens5 IP로 바뀐다.

운영서버EC2 에서 파드로 통신 (타 VPC 호스트간 연결)
# 운영서버 EC2 SSH 접속
ssh <운영서버 EC2 공인 IP>
-----------------------
POD1IP=<파드1 IP 지정>
POD1IP=192.168.1.101
ping -c 1 $POD1IP
exit
-----------------------
# 워커노드1 에서 tcpdump 확인 : NAT 동작 적용 여유 확인
sudo tcpdump -i any -nn icmp
이 때 만약 통신이 안된다면 워커노드 쪽 보안그룹을 살펴보고 적절한 Rule이 없으면 추가해줍니다.


이때 파드의 IP로 직접 통신이 가능한걸 확인할 수 있습니다. 이는 Pod 내부에서 운영서버로 패킷을 보낼때도 마찬가지 입니다.
+ 추가로 사내 내부에 연결 확장된 네트워크 대역과 SNAT 없이 통신 가능하게 설정 해보기 🔗 🔗
# 파드 상태 모니터링
# kubectl set env 명령어는 내부적으로 kubectl patch를 실행하여 PodSpec을 변경 → 이로 인해 aws-node 데몬셋이 자동으로 롤링 업데이트
watch -d kubectl get pod -n kube-system
# 파드1 배치 워커노드 iptables rule 모니터링 : iptables rule 추가됨
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'
# 사내 내부에 연결 확장된 네트워크 대역과 SNAT 없이 통신 가능하게 설정
kubectl set env daemonset aws-node -n kube-system AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=172.20.0.0/16
#
kubectl get ds aws-node -n kube-system -o json | jq '.spec.template.spec.containers[0].env'
...
{
"name": "AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS",
"value": "172.20.0.0/16"
}
# 운영서버 EC2 SSH 접속
kubectl exec -it $PODNAME1 -- ping 172.20.1.100
# 파드1 배치 워커노드 : NAT 적용 정책 확인
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 AWS-SNAT-CHAIN-0 (1 references)
pkts bytes target prot opt in out source destination
1 84 RETURN all -- * * 0.0.0.0/0 172.20.0.0/16 /* AWS SNAT CHAIN EXCLUSION */
730 45228 RETURN all -- * * 0.0.0.0/0 192.168.0.0/16 /* AWS SNAT CHAIN */
...
노드에 파드 생성 갯수 제한
우선 이전에 네트워크 플로우를 확인하기 위해 생성했던 netshoot 파드는 삭제해 줍니다.
kubectl delete deploy netshoot-pod

cli 환경에서 작업 가시성을 높이기 위해서 kube-ops-view를 설치합니다.
# kube-ops-view
helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=LoadBalancer --set env.TZ="Asia/Seoul" --namespace kube-system
# kube-ops-view 접속 URL 확인 (1.5 배율)
kubectl get svc -n kube-system kube-ops-view -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' | awk '{ print "KUBE-OPS-VIEW URL = http://"$1":8080/#scale=1.5"}'
위에서 언급한 것 처럼 파드 생성 개수 제한은 EC2 인스턴스 타입에 따라 다르고 그 이유는 인스턴스 타입별 ENI 할당 갯수가 다르기 때문에 다 다릅니다.

아래 그림을 통해 더 자세히 살펴볼 수 있습니다.


kube-ops-view 로 생성된 페이지의 url로 들어가면 다음처럼 실시간으로 파드의 스케줄링을 볼 수 있습니다.

이때 현재 실습에 쓰이는 t3 패밀리의 최대 ENI 개수를 살펴봅시다.
aws ec2 describe-instance-types \
--filters "Name=instance-type,Values=t3.*" \
--query "InstanceTypes[*].[InstanceType, NetworkInfo.MaximumNetworkInterfaces]" \
--output table

# nginx 이미지 파드 30개 생성
k create deployment nginx --image nginx --replicas 30

30개 생성하니 ens가 7번까지 생성이 된것을 확인할 수 있습니다. 또한 kube-ops-view 에 변경사항이 적용됐습니다.

AWS LoadBalancer Controller
AWS EKS에서는 로드밸런서를 통해서 워크로드 트래픽을 처리합니다. 다음 구조는 가장 일반적인 구조입니다.

AWS LoadBalancer는 클러스터 외부에 존재하는 리소스 인데 어떻게 워커노드와의 로드밸런싱을 수행할 수 있을까요? 그것을 해결해 주는게 바로 LoadBalancer Controller 입니다. LoadBalancer Controller의 아키텍처는 다음과 같습니다.

이 때 NLB 동작에는 두가지 모드가 있습니다.
1) 인스턴스 유형: 노드에 NodePort 방식으로 전달

- 노드는 외부에 공개되지 않고 로드밸런서만 외부에 공개되어, 외부 클라이언트는 로드밸랜서에 접속을 할 뿐 내부 노드의 정보를 알 수 없습니다.
- 로드밸런서가 부하분산하여 파드가 존재하는 노드들에게 전달한다, iptables 룰에서는 자신의 노드에 있는 파드만 연결한다 (externalTrafficPolicy: local)
- DNAT 2번 동작 : 첫번째(로드밸런서 접속 후 빠져 나갈때), 두번째(노드의 iptables 룰에서 파드IP 전달 시)

- 외부 클라이언트 IP 보존(유지) : AWS NLB 는 타켓이 인스턴스일 경우 클라이언트 IP를 유지, iptables 룰 경우도 externalTrafficPolicy 로 클라이언트 IP를 보존
2) IP 유형: Pod IP가 필요하기 때문에 반드시 AWS Loadbalancer Controller 파드 및 정책 설정이 필요합니다. ❗️❗️❗️❗️
# AWS Load Balancer Controller 설치
# 설치 전 CRD 확인
kubectl get crd
# Helm Chart 설치
helm repo add eks https://aws.github.io/eks-charts
helm repo update
helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system --set clusterName=$CLUSTER_NAME
## 설치 확인
kubectl get crd
kubectl explain ingressclassparams.elbv2.k8s.aws
kubectl explain targetgroupbindings.elbv2.k8s.aws
kubectl get deployment -n kube-system aws-load-balancer-controller
kubectl describe deploy -n kube-system aws-load-balancer-controller
kubectl describe deploy -n kube-system aws-load-balancer-controller | grep 'Service Account'
Service Account: aws-load-balancer-controller
# 클러스터롤, 롤 확인
kubectl describe clusterrolebindings.rbac.authorization.k8s.io aws-load-balancer-controller-rolebinding
kubectl describe clusterroles.rbac.authorization.k8s.io aws-load-balancer-controller-role
※ CRD란🧐 ?
사용자가 새로운 리소스 타입을 정의할 수 있도록 해주는 기능입니다. 기본적으로 Kubernetes는 Pod, Service, Deployment 같은 표준 리소스만 제공합니다. 하지만 CRD를 사용하면 사용자가 원하는 리소스 타입을 추가할 수 있습니다.

이제 NLB와 그에 맵핑된 서비스를 생성합니다.
# 모니터링
watch -d kubectl get pod,svc,ep,endpointslices
# 디플로이먼트 & 서비스 생성
cat << EOF > echo-service-nlb.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-echo
spec:
replicas: 2
selector:
matchLabels:
app: deploy-websrv
template:
metadata:
labels:
app: deploy-websrv
spec:
terminationGracePeriodSeconds: 0
containers:
- name: aews-websrv
image: k8s.gcr.io/echoserver:1.5
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: svc-nlb-ip-type
annotations:
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: LoadBalancer
loadBalancerClass: service.k8s.aws/nlb
selector:
app: deploy-websrv
EOF
kubectl apply -f echo-service-nlb.yaml
# 확인
aws elbv2 describe-load-balancers --query 'LoadBalancers[*].State.Code' --output text
kubectl get deploy,pod
kubectl get svc,ep,ingressclassparams,targetgroupbindings
kubectl get targetgroupbindings -o json | jq
# AWS 관리콘솔에서 NLB 정보 확인
# 빠른 실습을 위해서 등록 취소 지연(드레이닝 간격) 수정 : 기본값 300초
echo-service-nlb.yaml 파일 IDE(VS code)에서 수정
..
apiVersion: v1
kind: Service
metadata:
name: svc-nlb-ip-type
annotations:
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8080"
service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: deregistration_delay.timeout_seconds=60
...
kubectl apply -f echo-service-nlb.yaml
# AWS ELB(NLB) 정보 확인
aws elbv2 describe-load-balancers | jq
aws elbv2 describe-load-balancers --query 'LoadBalancers[*].State.Code' --output text
ALB_ARN=$(aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-default-svcnlbip`) == `true`].LoadBalancerArn' | jq -r '.[0]')
aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN | jq
TARGET_GROUP_ARN=$(aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN | jq -r '.TargetGroups[0].TargetGroupArn')
aws elbv2 describe-target-health --target-group-arn $TARGET_GROUP_ARN | jq
{
"TargetHealthDescriptions": [
{
"Target": {
"Id": "192.168.2.153",
"Port": 8080,
"AvailabilityZone": "ap-northeast-2b"
},
"HealthCheckPort": "8080",
"TargetHealth": {
"State": "initial",
"Reason": "Elb.RegistrationInProgress",
"Description": "Target registration is in progress"
}
},
...
# 웹 접속 주소 확인
kubectl get svc svc-nlb-ip-type -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' | awk '{ print "Pod Web URL = http://"$1 }'
# 파드 로깅 모니터링
kubectl logs -l app=deploy-websrv -f
kubectl stern -l app=deploy-websrv
# 분산 접속 확인
NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
curl -s $NLB
for i in {1..100}; do curl -s $NLB | grep Hostname ; done | sort | uniq -c | sort -nr
52 Hostname: deploy-echo-55456fc798-2w65p
48 Hostname: deploy-echo-55456fc798-cxl7z
# 지속적인 접속 시도 : 아래 상세 동작 확인 시 유용(패킷 덤프 등)
while true; do curl -s --connect-timeout 1 $NLB | egrep 'Hostname|client_address'; echo "----------" ; date "+%Y-%m-%d %H:%M:%S" ; sleep 1; done

콘솔에서 NLB가 생성되고 svc에 연결된 파드가 NLB의 타겟 대상으로 등록된 것이 확인 가능합니다.


NLB 엔드포인트로 curl을 수행해보면 파드로 부터 응답이 오는 것을 확인할 수 있습니다.
또한 replicas를 늘리면 host가 증가하는 것을 확인할 수 있습니다. (1 ->3)


Ingress
7 layer 통신을 구성할때, AWS ALB 를 사용하여 구현합니다. ALB도 위에서 예제로 들었던 NLB와 마찬가지로 LoadBalancer Controller 파드의 도움을 받습니다.

실습 테스트 !!
❗️ 이때 ALB를 인그레스로 사용하기 위해서는 AWS Load Balancer Controller가 필요하다. 이전 단계에서 생성했기 때문에 스킵합니다.
# 게임 파드와 Service, Ingress 배포
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
name: game-2048
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: game-2048
name: deployment-2048
spec:
selector:
matchLabels:
app.kubernetes.io/name: app-2048
replicas: 2
template:
metadata:
labels:
app.kubernetes.io/name: app-2048
spec:
containers:
- image: public.ecr.aws/l6m2t8p7/docker-2048:latest
imagePullPolicy: Always
name: app-2048
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
namespace: game-2048
name: service-2048
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
type: NodePort
selector:
app.kubernetes.io/name: app-2048
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
namespace: game-2048
name: ingress-2048
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
spec:
ingressClassName: alb
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: service-2048
port:
number: 80
EOF
# 모니터링
watch -d kubectl get pod,ingress,svc,ep,endpointslices -n game-2048
# 생성 확인
kubectl get ingress,svc,ep,pod -n game-2048
kubectl get-all -n game-2048
kubectl get targetgroupbindings -n game-2048
# ALB 생성 확인
aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-game2048`) == `true`]' | jq
ALB_ARN=$(aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName, `k8s-game2048`) == `true`].LoadBalancerArn' | jq -r '.[0]')
aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN
TARGET_GROUP_ARN=$(aws elbv2 describe-target-groups --load-balancer-arn $ALB_ARN | jq -r '.TargetGroups[0].TargetGroupArn')
aws elbv2 describe-target-health --target-group-arn $TARGET_GROUP_ARN | jq
# Ingress 확인
kubectl describe ingress -n game-2048 ingress-2048
kubectl get ingress -n game-2048 ingress-2048 -o jsonpath="{.status.loadBalancer.ingress[*].hostname}{'\n'}"
# 게임 접속 : ALB 주소로 웹 접속
kubectl get ingress -n game-2048 ingress-2048 -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' | awk '{ print "Game URL = http://"$1 }'
# 파드 IP 확인
kubectl get pod -n game-2048 -owide


ingress 와 svc 생성확인
위 NLB에서 테스트한것과 마찬가지로 레플리카를 조정하는대로 타겟에 반영됩니다.