이 내용은 CloudNet@ 에서 진행하는 쿠버네티스 실무 실습 스터디에 대한 연재글입니다.
스터디에서 사용하는 교재는 24단계 실습으로 정복하는 쿠버네티스 입니다.
본 2주차에는 교재의 제 2부 전체 내용 중 상반기 부분을 살펴보고 있습니다. 전체 컨텍스트를 이해하시려면 교재를 참고하시기를 추천드립니다.
분량조절에 실패하여 2장 2부로 이어집니다.
쿠버네티스를 클라우드 환경에서 사용하고자 하는 것 자체가 이미 하기 사항들에 대한 기본적인 이해와 준비사항을 요합니다.
이번 연재글에서는 개념에 대한 상세한 추가설명은 가급적 없이 작성하려 합니다. 다만 제가 공부하며 기본적으로 알아야겠다 싶은 연재글에 대해 대신 소개드립니다.
주의! 비용이 많이 발생할 수 있으니, 빠르게 실습 후 종료하시기를 권장드립니다.
t3.medium
c5d.large
t3.small
아래 커맨드를 통해 배포하여 주십시오.
앞선 장에서 언급되었듯, 쿠버네티스 환경에서의 파드는 가축(cattle)같은 개념입니다. 문제가 생겼다면 내렸다가 다시 올리지요. 각 파드별 네트워크 구성 또한 무언가가 관리하고 있을 것입니다. 스토리지도 마찬가집니다. 무언가가 스토리지를 파드와 데이터를 분리하여 별도의 추상화된 리소스로 처리합니다. 이와 같은 작업을 해주는 것을 각각 쿠버네티스 서비스 와 쿠버네티스 스토리지 로 부릅니다.
네트워크 구성요소로는 아래 요소들이 있습니다.
스토리지 구성요소르는 아래 요소들이 있습니다.
이번 장에서는 쿠버네티스 클러스터를 구성하는 쿠버네티스 서비스와 쿠버네티스 스토리지에 대해 알아보고자 합니다.
쿠버네티스 서비스는 동적으로 각 파드 간 연결을 제공합니다[^1]. 잠깐, 쿠버네티스 서비스라고요? 흔히 서비스는 웹 서비스, RDMBS같은 서비스를 말하지만, 쿠버네티스에서의 서비스는, 파드 간의 연결을 serve 하므로 서비스 라는 용어를 사용합니다.
서비스들의 특징을 살펴봅시다.
CNI는 컨테이너 간의 네트워킹을 제어할 수 있는 플러그인을 만들기 위한 표준입니다[^2]. 다만 그 구현체가 클라우드 서비스 프로바이더에 따라 다를 수 있고, 벤더사 혹은 오픈소스에 따라 달라질 수 있습니다. 이 글에서는 AWS VPC CNI와 Calico에 대해 설명합니다.
아래 커맨드를 통해, 배포 후 실습환경의 기본정보를 조회할 수 있습니다.
# CNI 정보 확인
kubectl describe daemonset aws-node --namespace kube-system | grep Image | cut -d "/" -f 2
amazon-k8s-cni-init:v1.12.2
amazon-k8s-cni:v1.12.2
# 노드 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
kubectl ktop # 파드 정보 출력에는 다소 시간 필요
# [master node] aws vpc cni log
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME ls /var/log/aws-routed-eni
기본적으로 AWS EKS에는 VPC CNI를 사용합니다. 파드의 IP를 할당해주는 것이 주요 역할입니다. 아래와 같은 특징이 있습니다.
Calico는 매우 유명한 서드파티 CNI 중 하나이므로, 여기서는 Calico를 소개하지는 않겠습니다. 다만 관련하여 좋은 블로그 게시글을 몇가지 소개드리겠습니다[^3]. 그리고, 이하에서는 Calico와 AWS VPC CNI의 주요 차이점을 살펴보겠습니다.
![그림 2 - Calico CNI의 오버레이 통신 및 AWS VPC CNI의 직접 통신 비교]
(https://velog.velcdn.com/images/s3ich4n/post/1c4ca8ef-0969-4791-940f-22135d3d82ff/image.png)
컨트롤 플레인에 접속 후 CNI 정보, 네트워크 정보를 확인해봅시다.
# [master node] SSH 접속
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME
# 툴 설치
sudo apt install -y tree jq net-tools
# CNI 정보 확인
ls /var/log/aws-routed-eni
cat /var/log/aws-routed-eni/plugin.log | jq
cat /var/log/aws-routed-eni/ipamd.log | jq
# 네트워크 정보 확인 : eniY는 pod network 네임스페이스와 veth pair
ip -br -c addr
ip -c addr
ip -c route
sudo iptables -t nat -S
sudo iptables -t nat -L -n -v
# 빠져나오기
exit
이어서 워커 노드에 접속 후 CNI 정보를 확인해봅시다.
# 워커 노드 Public IP 확인
aws ec2 describe-instances --query "Reservations[*].Instances[*].{PublicIPAdd:PublicIpAddress,InstanceName:Tags[?Key=='Name']|[0].Value}" --filters Name=instance-state-name,Values=running --output table
# 워커 노드 Public IP 변수 지정
W1PIP=<워커 노드 1 Public IP>
W2PIP=<워커 노드 2 Public IP>
W1PIP=43.201.59.201
W2PIP=15.165.159.169
# 워커 노드 SSH 접속
ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP
exit
ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP
exit
# [워커 노드1~2] SSH 접속 : 접속 후 아래 툴 설치 등 정보 각각 확인
ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP
ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP
# 툴 설치
sudo apt install -y tree jq net-tools
# CNI 정보 확인
ls /var/log/aws-routed-eni
cat /var/log/aws-routed-eni/plugin.log | jq
cat /var/log/aws-routed-eni/ipamd.log | jq
# 네트워크 정보 확인
ip -br -c addr
ip -c addr
ip -c route
sudo iptables -t nat -S
sudo iptables -t nat -L -n -v
# 빠져나오기
exit
워커 노드의 기본 네트워크 구성을 살펴보며, 주요 특징을 이해해봅시다.
![그림 3 - 네트워크 네임스페이스 및 인스턴스 사이즈 별 특징]
(https://velog.velcdn.com/images/s3ich4n/post/c5f2d821-68b3-4bc3-b78f-efe3ecd7bb87/image.png)
kube-proxy
, aws-node
)는 호스트(Root)의 IP를 그대로 사용합니다.ENI0
, ENI1
으로 2개의 ENI는 자신의 IP 이외에 추가적으로 5개의 보조 프라이빗 IP를 가질수 있습니다.coredns
파드는 veth
으로 호스트에는 eniY@ifN
인터페이스와 파드에 eth0
과 연결되어 있습니다.아래 커맨드를 입력하여, ebs-csi-node
파드의 IP 정보를 확인합니다. 아울리 워커 노드1 EC2 인스턴스의 네트워크 정보를 직접 확인해보시길 바랍니다.
# ebs-csi-node 파드 IP 정보 확인
kubectl get pod -n kube-system -l app=ebs-csi-node -owide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
ebs-csi-node-64b66 3/3 Running 0 4h15m 172.30.50.15 i-0f63faa6cc1869699 <none> <none>
ebs-csi-node-7h8xs 3/3 Running 0 4h15m 172.30.76.209 i-07d2a01a597c171bc <none> <none>
ebs-csi-node-z9xcq 3/3 Running 0 4h15m 172.30.44.223 i-0744dad15f33219cc <none> <none>
# 노드의 라우팅 정보 확인 >> EC2 네트워크 정보의 '보조 프라이빗 IPv4 주소'와 비교해보자
ssh -i ~/.ssh/id_rsa ubuntu@api.$KOPS_CLUSTER_NAME ip -c route
ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP ip -c route
ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP ip -c route
상기 언급한 정보를 확인하기 위해, 테스트용 파드를 생성하고 이 파드의 워커노드에 테스트용 파드 생성 및 접속 확인을 수행해봅시다.
# [터미널1~2] 워커 노드 1~2 모니터링
ssh -i ~/.ssh/id_rsa ubuntu@$W1PIP
watch -d "ip link | egrep 'ens5|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
ssh -i ~/.ssh/id_rsa ubuntu@$W2PIP
watch -d "ip link | egrep 'ens5|eni' ;echo;echo "[ROUTE TABLE]"; route -n | grep eni"
# 테스트용 파드 netshoot-pod 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: netshoot-pod
spec:
replicas: 2
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})
# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP
파드가 생성되면, 워커 노드에 eniY@ifN
파드가 추가되고 라우팅 테이블에도 정보가 추가됩니다.
아울러 테스트용 파드 eniY@ifN
의 정보를 워커노드에서 확인해봅시다.
# 노드에서 네트워크 인터페이스 정보 확인
ip -br -c addr show
ip -c link
ip -c addr
ip route # 혹은 route -n
# 마지막 생성된 네임스페이스 정보 출력 -t net(네트워크 타입)
sudo lsns -o PID,COMMAND -t net | awk 'NR>2 {print $1}' | tail -n 1
# 마지막 생성된 네임스페이스 net PID 정보 출력 -t net(네트워크 타입)를 변수 지정
MyPID=$(sudo lsns -o PID,COMMAND -t net | awk 'NR>2 {print $1}' | tail -n 1)
# PID 정보로 파드 정보 확인
sudo nsenter -t $MyPID -n ip -c addr
sudo nsenter -t $MyPID -n ip -c route
이어서 테스트용 파드를 exec
으로 접속 후 네트워크 정보를 확인해봅시다.
# 테스트용 파드 접속(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
앞서 잠시 언급하였듯, AWS VPC CNI는 별도의 오버레이 통신기술 없이 VPC의 네트워크 대역을 통해 바로 통신이 가능합니다.
아래 도식은 파드 간의 통신 과정을 보다 자세히 풀어둔 도식입니다. 링크
별도의 NAT 동작 없이, 파드간 통신이 이루어짐을 확인해봅시다.
# 파드 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})
# 파드1 Shell 에서 파드2로 ping 테스트
kubectl exec -it $PODNAME1 -- ping -c 2 $PODIP2
# 파드2 Shell 에서 파드1로 ping 테스트
kubectl exec -it $PODNAME2 -- ping -c 2 $PODIP1
# 워커 노드 EC2 : TCPDUMP 확인 - ens6 에서 패킷 덤프 확인이 되나요?
sudo tcpdump -i any -nn icmp
sudo tcpdump -i ens5 -nn icmp
sudo tcpdump -i ens6 -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 172.30.64.1 dev ens5 proto dhcp src 172.30.85.242 metric 100
이번에는 파드에서 외부로 나가는 통신의 흐름을 살펴봅시다. iptable의 SNAT 을 통하여 노드의 eth0 IP로 변경된 후 외부와 통신을 진행합니다.
파드의 쉘 실행 후, 외부로 ping을 테스트 해봅니다. 아울러 워커노드에서 tcpdump 및 iptables 정보를 확인해봅니다.
# 작업용 EC2 : 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
# 워커 노드 EC2 : 퍼블릭IP 확인, TCPDUMP 확인
curl -s ipinfo.io/ip ; echo
sudo tcpdump -i any -nn icmp
sudo tcpdump -i ens5 -nn icmp
# 작업용 EC2 : pod-1 Shell 에서 외부 접속 확인 - 공인IP는 어떤 주소인가?
## The right way to check the weather - 링크
kubectl exec -it $PODNAME1 -- curl -s ipinfo.io/ip ; echo
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
# 워커 노드 EC2
## 출력된 결과를 보고 어떻게 빠져나가는지 고민해보자!
ip rule
ip route show table main
sudo iptables -L -n -v -t nat
sudo iptables -t nat -S
# 파드가 외부와 통신시에는 아래 처럼 'AWS-SNAT-CHAIN-0, AWS-SNAT-CHAIN-1' 룰(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 172.30.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 172.30.85.242 --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, AWS-SNAT-CHAIN-1 에 매칭되어, 목적지가 172.30.0.0/16 아니고 외부 빠져나갈때 SNAT 172.30.85.242 변경되어 나간다!
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 AWS-SNAT-CHAIN-1; echo ; sudo iptables -v --numeric --table nat --list KUBE-POSTROUTING'
# conntrack 확인
sudo conntrack -L -n |grep -v '169.254.169'
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
다음 실습을 위해 테스트용으로 생성한 파드를 삭제합니다: kubectl delete deploy netshoot-pod
원인
aws-node
와 kube-proxy
파드는 호스트의 IP를 사용함으로 최대 갯수에서 제외합니다!해결방안
멤버분의 상세한 가이드!
윈도우랑 리눅스가 공식이 다르긴한데 리눅스의 경우는 -1 을 하는 이유는 ENI별로 아이피 할당이 되서이고 +2는 노드별로 kube-proxy랑 VPC CNI와 같은 호스트 네트워크에 필요한 파드때문에 추가하고 있습니다.
참고링크 1: https://aws.github.io/aws-eks-best-practices/networking/vpc-cni/
참고링크 2: https://aws.github.io/aws-eks-best-practices/windows/docs/networking/
워커 노드 1대에 50대 이상의 pod를 배포해봅시다!
# 파드 갯수 모니터링
watch "kubectl get pod | grep -v NAME | wc -l"
# 파드 100개 배포
kubectl apply -f ~/pkos/2/nginx-dp.yaml
kubectl scale deployment nginx-deployment --replicas=0
kubectl scale deployment nginx-deployment --replicas=10
kubectl scale deployment nginx-deployment --replicas=30
kubectl scale deployment nginx-deployment --replicas=100
# Nitro 인스턴스 유형 확인
aws ec2 describe-instance-types --filters Name=hypervisor,Values=nitro --query "InstanceTypes[*].[InstanceType]" --output text | sort | egrep 't3\.|c5\.|c5d\.'
# 노드 인스턴스 타입 확인
kubectl describe nodes | grep "node.kubernetes.io/instance-type"
# 노드 상세 정보 확인 : 노드 상세 정보의 Allocatable 에 노드별 최대 생성 가능한 pods 정보 확인 - 각각 마스터 노드, 워커 노드
kubectl describe node | grep Allocatable: -A6
# 파드 배포 확인
kubectl get pod
kubectl get pod | grep -v NAME | wc -l
kubectl get replicasets
# LimitRanges 기본 정책 확인
kubectl describe limitranges
# 수정 전 env 정보 확인
kubectl describe ds -n kube-system aws-node | grep ADDITIONAL_ENI_TAGS: -A22
kubectl describe daemonsets.apps -n kube-system aws-node | egrep 'ENABLE_PREFIX_DELEGATION|WARM_PREFIX_TARGET'
가능하게 된 이유는 아래와 같습니다[^4].
Service
확인서비스에 대해서는 상단에 설명하였으므로, 기본적인 도식 확인 및 AWS 에서 제공하는 별도의 서비스를 중점으로 확인해봅시다.
인스턴스 유형
externalTrafficPolicy
: ClusterIP ⇒ 2번 분산 및 SNAT으로 Client IP 확인이 불가능합니다 (LoadBalancer
타입 (기본 모드) 동작)externalTrafficPolicy
: Local ⇒ 1번 분산 및 ClientIP 유지, 워커 노드의 iptables를 사용합니다.상세 설명
통신 흐름
요약 : 외부 클라이언트가 '로드밸런서' 접속 시 부하분산 되어 노드 도달 후 iptables 룰로 목적지 파드와 통신됩니다.
externalTrafficPolicy: local
).externalTrafficPolicy
로 클라이언트 IP를 보존합니다.부하분산 최적화 : 노드에 파드가 없을 경우 '로드밸런서'에서 노드에 헬스 체크(상태 검사)가 실패하여 해당 노드로는 외부 요청 트래픽을 전달하지 않습니다.
IP 유형 ⇒ 반드시 AWS LoadBalancer 컨트롤러 파드 및 정책 설정이 필요합니다!
Proxy Protocol v2 비활성화
⇒ NLB에서 바로 파드로 인입됩니다. 단 ClientIP가 NLB로 SNAT 되어 Client IP 확인이 불가능합니다.Proxy Protocol v2 활성화
⇒ NLB에서 바로 파드로 인입됩니다. 그리고 ClientIP 확인이 가능합니다(→ 단 PPv2 를 애플리케이션이 인지할 수 있게 설정이 필요합니다).Ingress
확인서비스를 외부망으로 노출하는 웹프록시가 인그레스입니다. AWS 쿠버네티스의 인그레스는 AWS Load Balancer Controller + Ingress (ALB) IP 모드로 동작합니다(with AWS VPC CNI).
# 마스터/워커 노드에 EC2 IAM Role 에 Policy (AWSLoadBalancerControllerIAMPolicy) 추가
## IAM Policy 정책 생성 : 2주차에서 IAM Policy 를 미리 만들어두었으니 Skip
curl -o iam_policy.json https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.4.5/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
# EC2 instance profiles 에 IAM Policy 추가(attach)
aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name masters.$KOPS_CLUSTER_NAME
aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --role-name nodes.$KOPS_CLUSTER_NAME
# IAM Policy 정책 생성 : 2주차에서 IAM Policy 를 미리 만들어두었으니 Skip
curl -s -O https://s3.ap-northeast-2.amazonaws.com/cloudformation.cloudneta.net/AKOS/externaldns/externaldns-aws-r53-policy.json
aws iam create-policy --policy-name AllowExternalDNSUpdates --policy-document file://externaldns-aws-r53-policy.json
# EC2 instance profiles 에 IAM Policy 추가(attach)
aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AllowExternalDNSUpdates --role-name masters.$KOPS_CLUSTER_NAME
aws iam attach-role-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AllowExternalDNSUpdates --role-name nodes.$KOPS_CLUSTER_NAME
# kOps 클러스터 편집 : 아래 내용 추가
kops edit cluster
-----
spec:
certManager:
enabled: true
awsLoadBalancerController:
enabled: true
externalDns:
provider: external-dns
-----
# 업데이트 적용
kops update cluster --yes && echo && sleep 3 && kops rolling-update cluster
제 2장에서는 분량을 조절해야 읽힐 듯 하여, 앞서 말씀드렸듯 AWS EKS의 네트워크에 대해서만 살펴보았습니다.
이번 장에서는 아래 내용을 반드시 기억하셨으면 좋겠습니다.
이것으로 제 2장 파트 1을 마칩니다. 긴 글 읽어주셔서 감사합니다.
[^1]: 이런 개념은 Service Discovery 라고 합니다.
[^2]: 이 분의 블로그 게시글이 이해에 많은 도움이 되었습니다.
[^3]: 간략 소개는 이 게시글을, 보다 근본적인 이해를 원하신다면 이 게시글을 반드시 일독하시기를 권합니다.
[^4]: 해당 링크를 일독하시기를 권장드립니다!