본 포스팅은 CloudNet@팀의 AWES스터디 활동을 통한 포스팅임을 밝힌다. 이번 포스팅에서는 EKS Network에 대해 알아보겠다.
스터디에서 제공해준 one-click 배포 스크립트를 통해 EKS를 배포해보자. 배포는 보통 20분정도 걸린다.
# YAML 파일 다운로드
curl -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/K8S/eks-oneclick.yaml
# CloudFormation 스택 배포
# aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=<My SSH Keyname> SgIngressSshCidr=<My Home Public IP Address>/32 MyIamUserAccessKeyID=<IAM User의 액세스키> MyIamUserSecretAccessKey=<IAM User의 시크릿 키> ClusterBaseName='<eks 이름>' --region ap-northeast-2
예시) aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks --region ap-northeast-2
## Tip. 워커노드 인스턴스 타입 변경 : WorkerNodeInstanceType=t3.xlarge
예시) aws cloudformation deploy --template-file eks-oneclick.yaml --stack-name myeks --parameter-overrides KeyName=kp-gasida SgIngressSshCidr=$(curl -s ipinfo.io/ip)/32 MyIamUserAccessKeyID=AKIA5... MyIamUserSecretAccessKey='CVNa2...' ClusterBaseName=myeks --region ap-northeast-2 WorkerNodeInstanceType=t3.xlarge
# CloudFormation 스택 배포 완료 후 작업용 EC2 IP 출력
aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text
# 마스터노드 SSH 접속
ssh -i ~/.ssh/kp-gasida.pem ec2-user@$(aws cloudformation describe-stacks --stack-name myeks --query 'Stacks[*].Outputs[0].OutputValue' --output text)
Container Network Interface로, 컨테이너간 통신을 할 수 있게 해주는 인터페이스로Calico, Flannel 등 여러 3rd Party들이 존재한다. CNI 자체도 CNCF의 프로젝트에 속한다.
AWS에서 사용하는 CNI로, Node와 Pod의 IP대역이 같은 특징이 있다. 대역이 같이 때문에 Pod에서 다른 Pod 또는 외부 통신할 때, 오버레이 네트워크를 건너뛰는 장점이 있다.
IP대역이 같기 때문에 패킷 전송 시 경로최적화, 디버깅, 통신효율성 등의 이점을 취한다.
# Node 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
---------------------------------------------------------------------
| DescribeInstances |
+-------------------+-----------------+------------------+----------+
| InstanceName | PrivateIPAdd | PublicIPAdd | Status |
+-------------------+-----------------+------------------+----------+
| myeks-ng1-Node | 192.168.3.75 | 15.164.231.37 | running |
| myeks-ng1-Node | 192.168.2.128 | 3.38.176.32 | running |
| myeks-bastion-EC2| 192.168.1.100 | 13.124.132.143 | running |
| myeks-ng1-Node | 192.168.1.218 | 3.35.149.217 | running |
+-------------------+-----------------+------------------+----------+
# Pod IP
$ kubectl get pod -n kube-system -o=custom-columns=NAME:.metadata.name,IP:.status.podIP,STATUS:.status.phase
NAME IP STATUS
aws-node-kgdfc 192.168.3.75 Running
aws-node-p659k 192.168.1.218 Running
aws-node-z49rm 192.168.2.128 Running
coredns-6777fcd775-54j4b 192.168.2.192 Running
coredns-6777fcd775-zz995 192.168.1.214 Running
kube-proxy-dggvv 192.168.3.75 Running
kube-proxy-ghjf4 192.168.2.128 Running
kube-proxy-kt6h6 192.168.1.218 Running
Node에서 배포된 Pod를 확인하면서, 내부적으로는 어떻게 네트워크가 구성되는 지 알아보자.
AWS VPC CNI에서는 각 Node가 ENI를 통해 IP주소들을 관리한다. 위 그림에서처럼 기본적으로 ENI0이 존재하고, Node에 Pod가 배포되면 ENI1이 생성되고 관리되는 IP주소들도 늘어나게 된다.
아래 실습을 통해 확인해보자.
# 노드에 툴 설치
ssh ec2-user@$N1 sudo yum install links tree jq tcpdump -y
ssh ec2-user@$N2 sudo yum install links tree jq tcpdump -y
ssh ec2-user@$N3 sudo yum install links tree jq tcpdump -y
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# k get po -A -owide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system aws-node-kgdfc 1/1 Running 0 34m 192.168.3.75 ip-192-168-3-75.ap-northeast-2.compute.internal <none> <none>
kube-system aws-node-p659k 1/1 Running 0 34m 192.168.1.218 ip-192-168-1-218.ap-northeast-2.compute.internal <none> <none>
kube-system aws-node-z49rm 1/1 Running 0 34m 192.168.2.128 ip-192-168-2-128.ap-northeast-2.compute.internal <none> <none>
kube-system coredns-6777fcd775-54j4b 1/1 Running 0 32m 192.168.2.192 ip-192-168-2-128.ap-northeast-2.compute.internal <none> <none>
kube-system coredns-6777fcd775-zz995 1/1 Running 0 32m 192.168.1.214 ip-192-168-1-218.ap-northeast-2.compute.internal <none> <none>
kube-system kube-proxy-dggvv 1/1 Running 0 33m 192.168.3.75 ip-192-168-3-75.ap-northeast-2.compute.internal <none> <none>
kube-system kube-proxy-ghjf4 1/1 Running 0 33m 192.168.2.128 ip-192-168-2-128.ap-northeast-2.compute.internal <none> <none>
kube-system kube-proxy-kt6h6 1/1 Running 0 33m 192.168.1.218 ip-192-168-1-218.ap-northeast-2.compute.internal <none> <none>
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# k get nodes -owide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
ip-192-168-1-218.ap-northeast-2.compute.internal Ready <none> 36m v1.24.11-eks-a59e1f0 192.168.1.218 3.35.149.217 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
ip-192-168-2-128.ap-northeast-2.compute.internal Ready <none> 36m v1.24.11-eks-a59e1f0 192.168.2.128 3.38.176.32 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
ip-192-168-3-75.ap-northeast-2.compute.internal Ready <none> 36m v1.24.11-eks-a59e1f0 192.168.3.75 15.164.231.37 Amazon Linux 2 5.10.176-157.645.amzn2.x86_64 containerd://1.6.19
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# ssh ec2-user@$N1 sudo ip -c route
default via 192.168.1.1 dev eth0
169.254.169.254 dev eth0
192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.218
192.168.1.214 dev eni6822b7a32a0 scope link
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# ssh ec2-user@$N2 sudo ip -c route
default via 192.168.2.1 dev eth0
169.254.169.254 dev eth0
192.168.2.0/24 dev eth0 proto kernel scope link src 192.168.2.128
192.168.2.192 dev eni5fdf6e8a992 scope link
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# ssh ec2-user@$N3 sudo ip -c route
default via 192.168.3.1 dev eth0
169.254.169.254 dev eth0
192.168.3.0/24 dev eth0 proto kernel scope link src 192.168.3.75
Node1,2에는 coredns Pod가 존재하여, eni가 추가되었다. pod가 존재하지 않는 Node3은 eni가 없는 것을 볼 수 있다. AWS콘솔에서도 각 Node의 Network 탭을 조회해보면, 할당된 IP들의 형태가 다른 것을 알 수 있다.
tcp dump를 통해, Pod간 통신에 대해 알아보자. 위에서 설명한대로, AWS VPC CNI는 Node와 Pod의 대역기 같이 때문에 오버레이 통신을 하지 않을 것이다.
실습용 Pod를 생성해보자.
# 테스트용 파드 netshoot-pod 생성
cat <<EOF | kubectl create -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
# 파드 이름 변수 지정
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
#파드 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})
# tcp dump 모니터링 상태
$sudo tcpdump -i any -nn icmp
# 파드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
Pod1 에서 Pod2로 패킷전송을 하면,
$ kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
netshoot-pod-7757d5dd99-8ktsl 1/1 Running 0 6m2s 192.168.1.167 ip-192-168-1-218.ap-northeast-2.compute.internal <none> <none>
netshoot-pod-7757d5dd99-j88z8 1/1 Running 0 6m2s 192.168.3.137 ip-192-168-3-75.ap-northeast-2.compute.internal <none> <none>
netshoot-pod-7757d5dd99-q2h7v 1/1 Running 0 6m2s 192.168.2.206 ip-192-168-2-128.ap-northeast-2.compute.internal <none> <none
[ec2-user@ip-192-168-1-218 ~]$ sudo tcpdump -i any -nn icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
05:59:44.049074 IP 192.168.1.167 > 192.168.3.137: ICMP echo request, id 33728, seq 1, length 64
05:59:44.049120 IP 192.168.1.167 > 192.168.3.137: ICMP echo request, id 33728, seq 1, length 64
05:59:44.050401 IP 192.168.3.137 > 192.168.1.167: ICMP echo reply, id 33728, seq 1, length 64
05:59:44.050482 IP 192.168.3.137 > 192.168.1.167: ICMP echo reply, id 33728, seq 1, length 64
예상대로, 오버레이 통신이 되지 않고, 각 Pod의 IP로 통신이 되는 것을 볼 수 있다. 참고로, 위 그림을 다시 보면 Pod 간 패킷 전송시 메인인터페이스인 ENI0(eth0)으로 통신하는 것을 볼 수 있다. 실제로 tcpdump를 eth1, eth0을 떠보면 eth1에는 tcp가 잡히지 않는다.
sudo tcpdump -i eth1 -nn icmp
sudo tcpdump -i eth0 -nn icmp
그림참조: https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md
Pod에서 외부통신은 iptables 룰에 의해 SNAT이 된다. SNAT이 되는 이유는 Pod는 Private IP만 가지고 있기 때문에 외부와 통신 시 응답을 받을 수 있는 Public IP가 필요하다. 따라서 kube-proxy로 세팅해준 iptables 룰에 따라 Node의 Public IP로 SNAT이 되는 것이다.
실습
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# kubectl exec -it $PODNAME1 -- curl -s ipinfo.io/ip ; echo
3.35.149.217
Pod가 외부통신할 때 나가는 IP가 3.35.149.217이다. 이건 Node Public IP일 것이다. 그럼 콘솔을 통해 확인해보자.
Node에 직접 접근해서 iptables 룰이 어떻게 세팅되어 있는지 보면,
[ec2-user@ip-192-168-1-218 ~]$ 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 AWS-SNAT-CHAIN-1
-A AWS-SNAT-CHAIN-1 ! -o vlan+ -m comment --comment "AWS, SNAT" -m addrtype ! --dst-type LOCAL -j SNAT --to-source 192.168.1.218 --random-fully
Pod cidr인 192.168.0.0/16 대역이 아닐 경우 AWS-SNAT-CHAIN-1 룰로 보내지고, 이것은 Node의 Public IP로 SNAT되는 것을 확인할 수 있다.
AWS VPC CNI 환경에서 Service는 어떻게 구현되는 지 알아보자. 지난 Kops Study 때와 똑같은 방법을 사용하는데, 복습차원에서 간단히 실습을 해보겠다.
AWS VPC CNI의 가장 큰 특징인 Node와 Pod의 IP 대역이 같다는 점 때문에 Loadbalancer type의 Service에서 Pod IP로 다이렉트 통신이 가능하다. 일반적으로는 iptables 룰을 통해서 트래픽을 분산키는데, 네트워크홉이 추가되고 iptables 룰도 타기 때문에 약간은 비효율적이다.
# OIDC 확인
aws eks describe-cluster --name $CLUSTER_NAME --query "cluster.identity.oidc.issuer" --output text
aws iam list-open-id-connect-providers | jq
# IAM Policy (AWSLoadBalancerControllerIAMPolicy) 생성
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.7/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
# 생성된 IAM Policy Arn 확인
aws iam list-policies --scope Local
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --query 'Policy.Arn'
# AWS Load Balancer Controller를 위한 ServiceAccount를 생성 >> 자동으로 매칭되는 IAM Role 을 CloudFormation 으로 생성됨!
# IAM 역할 생성. AWS Load Balancer Controller의 kube-system 네임스페이스에 aws-load-balancer-controller라는 Kubernetes 서비스 계정을 생성하고 IAM 역할의 이름으로 Kubernetes 서비스 계정에 주석을 답니다
eksctl create iamserviceaccount --cluster=$CLUSTER_NAME --namespace=kube-system --name=aws-load-balancer-controller \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --override-existing-serviceaccounts --approve
## 서비스 어카운트 확인
kubectl get serviceaccounts -n kube-system aws-load-balancer-controller -o yaml | yh
# 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 \
--set serviceAccount.create=false --set serviceAccount.name=aws-load-balancer-controller
# 작업용 EC2 - 디플로이먼트 & 서비스 생성
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/2/echo-service-nlb.yaml
cat echo-service-nlb.yaml | yh
kubectl apply -f echo-service-nlb.yaml
Nginx Ingress Controller를 설치할 때 처럼 helm을 통해 AWS Load Balancer Controller를 설치해준다. AWS VPC CNI의 특징 때문인지는 몰라도, AKS(Azure)와는 많이 다른 방법을 사용한다. (내가 기능을 제대로 안써서 모르는 것일 수 도 있다.)
어쨋든, 위 방법으로 설치를하면 IAM Policy를 통해 EKS에 권한을 할당해주고, Loadbalancer type의 Service가 생성되면 AWS의 EC2 Target Group에 매핑되는 Pod가 들어가게 된다.
(pkos-admin@myeks:default) [root@myeks-bastion-EC2 ~]# k get po -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deploy-echo-5c4856dfd6-lfbxn 1/1 Running 0 3m9s 192.168.3.91 ip-192-168-3-75.ap-northeast-2.compute.internal <none> <none>
deploy-echo-5c4856dfd6-ltnqs 1/1 Running 0 3m9s 192.168.2.139 ip-192-168-2-128.ap-northeast-2.compute.internal <none> <none>
Pod IP와 target Group의 IP가 같다. 즉, Loadbalancer에서 바로 Pod로 트래픽이 Bypass 되는 것이 증명된다.
Service와 크게 다를 게 없다. L7에서 작동하는 ALB를 통해서 Pod로 Bypass 되는데, HTTP/HTTPS 통신을 하게된다.
# 게임 파드와 Service, Ingress 배포
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/3/ingress1.yaml
cat ingress1.yaml | yh
kubectl apply -f ingress1.yaml
# 모니터링
watch -d kubectl get pod,ingress,svc,ep -n game-2048
# 생성 확인
kubectl get-all -n game-2048
kubectl get ingress,svc,ep,pod -n game-2048
kubectl get targetgroupbindings -n game-2048
NAME SERVICE-NAME SERVICE-PORT TARGET-TYPE AGE
k8s-game2048-service2-e48050abac service-2048 80 ip 87s
# Ingress 확인
kubectl describe ingress -n game-2048 ingress-2048
# 게임 접속 : 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
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
deployment-2048-6bc9fd6bf5-2cwxf 1/1 Running 0 4m19s 192.168.2.216 ip-192-168-2-128.ap-northeast-2.compute.internal <none> <none>
deployment-2048-6bc9fd6bf5-nfcjn 1/1 Running 0 4m19s 192.168.1.231 ip-192-168-1-218.ap-northeast-2.compute.internal <none> <none>
Service와 마찬가지로 Pod IP가 target group에 바로 인입된다.
kops 스터디에서 똑같이 배웠던 내용인데, 너무 새록새록하다. 이번에는 AWS VPC CNI에 대해 조금은 더 친숙해진 느낌이다. 실무에서 AWS를 쓰지 않아서, 조금 헷갈리는 부분도 있는 것 같다. 시간내서 몇 번 더 복습하면 더 잘 이해가 갈 것 같기도 하다.