EKS 네트워크
March 2024 (3207 Words, 18 Minutes)
EKS Networking
원클릭배포로 실습을 진행합니다.
CloudFormation을 생성하지만, Bastion 인스턴스에 들어가서 직접 EKS 생성 명령어를 치는것이 아닌 한 단계로 약 20분정도 걸리는 CloudFormation을 실행합니다.
에서 진행이 가능합니다.
1. KeyName : 작업용 bastion ec2에 SSH 접속을 위한 SSH 키페어 선택 ← 미리 SSH 키 생성 해두자!
2. MyIamUserAccessKeyID : 관리자 수준의 권한을 가진 IAM User의 액세스 키ID 입력
3. MyIamUserSecretAccessKey : 관리자 수준의 권한을 가진 IAM User의 시크릿 키ID 입력 ← 노출되지 않게 보안 주의
4. SgIngressSshCidr : 작업용 bastion ec2에 SSH 접속 가능한 IP 입력 (집 공인IP/32 입력), 보안그룹 인바운드 규칙에 반영됨
해당 항목들을 확인후 프로비저닝을 진행하면 됩니다.
네트워크
컨테이너 네트워크 인터페이스(CNI, Container Network Interface)는 컨테이너의 네트워크를 설정하고 관리하기 위한 표준입니다. CNI는 컨테이너 오케스트레이션 시스템(예: Kubernetes)과 다양한 네트워크 구현체 간의 인터페이스를 제공하여, 컨테이너가 네트워크에 연결될 수 있도록 합니다.
사실 컨테이너 표준이 제정되면서, 네트워크의 표준인
Container Networking Interface 규격을 만족시키는경우 컨테이너간 네트워크를 광범위하게 활용할수있 방향으로 이어져갔습니다.
https://github.com/containernetworking/cni
이전에 Container Storage Interface에 대해서 정리해본적이 있는데, 비슷한 개념으로 컨테이너간 네트워크를 다룬것이라고 이해하면 될것같습니다.
EKS에서 사용하는 AWS VPC CNI는 특별한 점이 있습니다.
바로 pod가 VPC와 동일한 대역의 주소를 갖는다는것입니다.
기존 CNI들은 POD들간 통신을 할 때 POD1에서 POD2로 통신을 시도할떄
노드간 통신을 위해 패킷 오버레이가 발생해 한번의 오버헤드가 발생됩니다.
하지만 AWS VPC CNI의 경우 POD 에 물리적인 ENI에서 IP를 직접 할당받아 Pod의 네트워크 IP대역과 워커의 네트워크 IP대역이 동일하게 설정됩니다.
이렇게될경우 pod들간 통신에서 오버헤드가 줄어듭니다.
kube-proxy config를 조회하면
mode : iptables 를 사용중인것을 볼 수 있습니다.
iptables와 ipvs는 Kubernetes 클러스터 내에서 서비스의 트래픽을 포드로 라우팅하는 방식에 대한 구현 방법입니다:
- iptables: 이전에 널리 사용되던 방식으로, Linux의 iptables를 사용하여 트래픽을 관리합니다. 이 방식은 간단하고 널리 사용되지만, 대규모 클러스터에서는 성능 문제가 발생할 수 있습니다.
- ipvs: Linux의 IP Virtual Server를 기반으로 하며, iptables보다 향상된 성능과 확장성을 제공합니다. IPVS는 L4 로드밸런싱을 위해 설계되었으며, 대규모 네트워크 트래픽을 더 효율적으로 처리할 수 있습니다.
iptables
api-server에서 들어온 요청이 kube-proxy를 타고
ClusterIP (IPTables) 를 타고 패킷이 전달됩니다.
여기서 kube-proxy는 iptables에 룰을 적용만하고, pod의 정보를 전달하는 역할은 수행하지 않습니다.
ipvs 프록시 모드
IPVS 프록시 모드는 iptables와 유사하게 kube-proxy가 직접적인 패킷 전달을 담당하지 않습니다.
kube-proxy는 쿠버네티스 서비스와 엔드포인트슬라이스를 감시하고, netlink
인터페이스를 호출해 IPVS 규칙을 생성하고, IPVS 규칙을 쿠버네티스 서비스 및 엔드포인트를 주기적으로 동기화 합니다. 서비스에 접근하면, IPVS는 트래픽을 엔드포인트로 전송합니다.
이
테스트용 파드 생성 - 이미지: nicolaka/netshoot
ENI에서도 할당해준 POD IP를 확인 할 수 있습니다.
노드간 파드 통신
해당 명령어는 파드간 통신설정을 통해 별도의 NAT 동작 없이 통신이 가능한것을 확인 할 수 있습니다.
파드에서 외부 통신
https://github.com/aws/amazon-vpc-cni-k8s/blob/master/docs/cni-proposal.md
AWS VPC CNI를 이용할경우 SNAT(external source network address translation)을 설정할경우
외부 통신시 SNAT를 결정 할 수 있습니다.
# EC2 Public IP
for i in $N1 $N2 $N3; do echo ">> node $i <<"; ssh ec2-user@$i curl -s ipinfo.io/ip; echo; echo; done
다만 VPC CNI 플러그인을 이용할경우, ENI에서 할당 가능한 IP갯수가 정해져있기때문에,
노드 / ENI 갯수 별 최대 POD 갯수가 정해져있습니다.
다음과 같이 계산이 가능한데요
직접 계산을 할 수 있는 방법도 있습니다.
https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/using-eni.html
링크에서 확인 가능한 인스턴스 유형별 최대 ENI 갯수와 ENI 별 할당 가능한 ip 갯수가 명시되어있습니다
최대 파드 생성 갯수 : (Number of network interfaces for the instance type × (the number of IP addressess per network interface - 1)) + 2
와 같은 공식으로도 만들 수 있고
해당 문서뿐만아니라 AWS CLI에서도 확인이 가능합니다.
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.2xlarge |
| 6 | 3 | t3.medium |
| 12 | 3 | t3.large |
| 15 | 4 | t3.xlarge |
| 2 | 2 | t3.micro |
| 2 | 2 | t3.nano |
| 4 | 3 | t3.small |
+----------+----------+--------------+
현재 CloudFormation으로만든 EKS 클러스터의경우 t3.medium 인스턴스를 이용해 인스턴스당
ENI : 3개
IP per ENI : 6개
이므로 3(6-1) +2 = 17개의 pod를 배치 할 수 있습니다.
신기한건 describe node 에서도 Capacity 에 allocatable pod 갯수가 나오네요
Service & Loadbalancer
쿠버네티스의 Service는 다음과같이 ClusterIP, NodePort, LoadBalancer가 존재합니다.
service는 kube-proxy를 통해 클러스터 내부의 트래픽 라우팅을 해줍니다.
deployment등의 replicaset이 여러개 설정이 되어있어도, 한개의 서비스를 통해 라우팅 할 수 있으며
pod의 lifecycle에 맞게 “정상적으로 트래픽을 수신 할 수 있는” pod들을 대상으로 endpoint라는 리소스에 맞게 트래픽을 라우팅해줍니다.
예전에 블로그 게시글로 파드의 생명주기에 대해 다룬적이있는데
poststarthook, prestophook이나 probe, gracefulshutdownperiod 등으로 endpoint에 필요한 설정들을 진행 할 수 있습니다.
(아마 개인적으로 생각하는 ECS / Docker 과 큰 차별점이 아닐까 싶습니다)
ClusterIP는 service를 통해 내부 endpoint로의 부하분산을 해줍니다.
외부노출은 없이 core-dns 등을 통한 내부통신을 위한 서비스이거나, ingress controller가 존재한다면 ingress controller를 통해 외부 노출이 가능합니다.
nodeport는 3만번대 이상의 port를 외부로 노출해 extneral 통신이 가능하게 해주는 컴포넌트입니다.
내부의 Node:nodeport로 접근을 요청할경우 SNAT를 통해 엔드포인트로 연결이됩니다.
여기서 externalTrafficPolicy를 설정하게되면
다음과 같은 방식을 통해 다음과 같은 엔드포인트로 라우팅이 가능해집니다.
LoadBalancer
Pod가 Ready인 pod들로 소스 IP NAT를 해줍니다.
AWS 에서 별도 설정없이 LoadBalncer를 생성할경우 CLB생성이되는데, 엔드포인트 관리 및 로드밸런서 동작방식은 조금씩 상이하여 별도로 공부를 하는것이 더 좋을것같습니다.
AWS LoadBalancer
2.5 버전 이상에서는 AWS Load Balancer Controller이(가) type: LoadBalancer
와(과) 함께 Kubernetes 서비스 리소스의 기본 컨트롤러가 되며 각 서비스에 대한 AWS Network Load Balancer(NLB)를 만듭니다.
Kubernetes Ingress
Kubernetes Ingress
을(를) 생성할 때 AWS Load Balancer Controller은(는) AWS Application Load Balancer(ALB)를 생성합니다.
이제 AWS LoadBalancer의 동작방식을 살펴보겠습니다.
https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.6/how-it-works/
- AWS LB는 인스턴스 모드와 IP모드를 지원합니다.
- K8s의 경우 한 인스턴스(노드)에 여러 POD가 있을수 있겠죠.
- 인티스턴스모드의 경우 해당 노드로 트래픽이 도달 후 다시 POD를 찾아서 전달하는 방식으로 이를 위해서는 NodePort를 활용하며, LB에는 노드의 대표 IP가 등록됩니다.
- IP모드의 경우 노드의 대표 IP를 거치지 않고, 바로 PoD의 IP로 트래픽을 전달하며, 따라서 PoD IP가 직접 LB에 등록됩니다.
Instance mode
수신 트래픽은 ALB에서 시작하여 각 서비스의 NodePort를 통해 Kubernetes 노드에 도달합니다. type:NodePort
이는 ALB가 접근하려면 수신 리소스에서 참조되는 서비스를 노출해야 함을 의미합니다 .
IP mode
수신 트래픽은 ALB에서 시작하여 Kubernetes Pod에 직접 도달합니다. CNI는 ENI의 보조 IP 주소를 통해 직접 액세스 가능한 POD IP를 지원해야 합니다 .
- Instance mode: 이 모드에서는 ALB가 인그레스 트래픽을 받아 Kubernetes 노드로 전달합니다. 그리고 각 서비스의 NodePort를 통해 해당 서비스로 트래픽이 도달합니다. 즉, ALB로부터 트래픽을 받기 위해서는 서비스가 NodePort 타입으로 노출되어야 합니다.
- IP mode: 이 모드에서는 ALB가 인그레스 트래픽을 직접 Kubernetes의 포드로 전달합니다. 이를 위해서는 CNI(컨테이너 네트워크 인터페이스)가 포드의 IP를 ENI(탄력적 네트워크 인터페이스)의 보조 IP 주소로 직접 접근할 수 있어야 합니다.
- 인스턴스 유형
externalTrafficPolicy
: ClusterIP ⇒ 2번 분산 및 SNAT으로 Client IP 확인 불가능 ←LoadBalancer
타입 (기본 모드) 동작externalTrafficPolicy
: Local ⇒ 1번 분산 및 ClientIP 유지, 워커 노드의 iptables 사용함
- IP 유형 ⇒ 반드시 AWS LoadBalancer 컨트롤러 파드 및 정책 설정이 필요함!
Proxy Protocol v2 비활성화
⇒ NLB에서 바로 파드로 인입, 단 ClientIP가 NLB로 SNAT 되어 Client IP 확인 불가능Proxy Protocol v2 활성화
⇒ NLB에서 바로 파드로 인입 및 ClientIP 확인 가능(→ 단 PPv2 를 애플리케이션이 인지할 수 있게 설정 필요)
NLB 만들기
# 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 https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/v2.5.4/docs/install/iam_policy.json
aws iam create-policy --policy-name AWSLoadBalancerControllerIAMPolicy --policy-document file://iam_policy.json
# 혹시 이미 IAM 정책이 있지만 예전 정책일 경우 아래 처럼 최신 업데이트 할 것
# aws iam update-policy ~~~
# 생성된 IAM Policy Arn 확인
aws iam list-policies --scope Local | jq
aws iam get-policy --policy-arn arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy | jq
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 --role-name AmazonEKSLoadBalancerControllerRole \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy --override-existing-serviceaccounts --approve
## IRSA 정보 확인
eksctl get iamserviceaccount --cluster $CLUSTER_NAME
## 서비스 어카운트 확인
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
다음과 같은 설정들을 통해 OIDC Provider
가 활성화 되어있다는 가정하에 IRSA를 통해 AWS의 IAM을 쿠버네티스 클러스터에 할당하는 프로세스를 보실 수 있습니다.
Policy → ServiceAccount 생성 및 할당 의 프로세스입니다.
꼭 LB Controller 뿐만아니라, EKS에서 필요한 AWS 컴포넌트들에 대한 인증/인가를 부여할 수 있습니다.
이유는 모르지만, policy가 이미 있는데, 거기에 맞게 생성한 service account가 role을 제대로 못 담고있는듯해 새로운 역할을 신규로 생성했습니다.
eksctl create iamserviceaccount --cluster=$CLUSTER_NAME --namespace=kube-system --name=aws-load-balancer-controller2 --role-name AmazonEKSLoadBalancerControllerRole2 \
--attach-policy-arn=arn:aws:iam::$ACCOUNT_ID:policy/AWSLoadBalancerControllerIAMPolicy2 --override-existing-serviceaccounts --approve
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-controller2
위와 같은 과정으로 생성한 NLB 로드밸런싱 테스트 작업을 배포 및 테스트 해보겠습니다.
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
# 분산 접속 확인
NLB=$(kubectl get svc svc-nlb-ip-type -o jsonpath={.status.loadBalancer.ingress[0].hostname})
curl -s $NLB
NLB 엔드포인트를 획득해 curl 명령어로 응답받는 pod ip들을 확인해보았습니다.
AWS LB Controller의 Pod Readiness Gate
https://kubernetes-sigs.github.io/aws-load-balancer-controller/v2.7/deploy/pod_readiness_gate/
LB Controller의 doc을 읽어보면 흥미로운 파라미터가 있습니다.
기존에 쿠버네티스에서 자체적으로 관리하는 Service 와 Endpoint 항목에 대해서
AWS VPC CNI를 이용할경우, LB → POD IP 로 직접 트래픽이 인입되는경우가 발생하기때문에
kube-proxy의 서비스 엔드포인트를 타고 들어가는것과는 사뭇 다른 방향으로 흘러가기 때문입니다.
이때 리소스들은 AWS에서 LB의 타겟그룹에서 관리되는데, Elastic Load Balancing과 통합하여 쿠버네티스 서비스에 대한 로드밸런싱을 자동으로 관리합니다.
이 컨트롤러는 ELB의 설정을 자동으로 업데이트하여, 새로운 파드가 생성될 때 이를 로드밸런서의 대상 그룹에 추가하거나, 파드가 종료될 때 제거합니다.
별도로 HealthCheck 로직을 구현하여 LB의 Healthcheck를 통해서도 probe에 의한 pod lifecycle 관리를 가능하게 연결해줍니다.
그중에서도 target-type가 IP 일때와 Instance 모드일때의 동작이 조금 다릅니다.
Instance 모드의 경우 노드를 거쳐 트래픽이 인입되기 때문에, 저런 Healthcheck 로직을 타고 들어 갈 수 있지만, IP mode를 이용할경우 LB 에서 VPC CNI를 통해 직접 pod의 EIP로 트래픽이 인입되기때문에, HealthCheck을 할 수 없다는 단점이 있습니다.
여기서 나오는 개념이 바로 pod-readiness-gate 입니다.
https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-readiness-gate
LoadBalancer Controller에 pod readiness gate를 활성화 할 경우
pod의 status.conditions 상태를 통해 파드를 ready 상태로 표시하고, 트래픽을 받기 시작합니다.
도전과제
NLB로 UDP 통신하기
요새 유명한 palworld 라는 게임의 게임서버를 열어보겠습니다.
특이한점은 게임서버는 속도때문인지 UDP로 통신하게 설정이되어있는데
해당 Yaml 파일을 올려보겠습니다.
배포에 필요한 기본파일은 다음 git에서 배포중입니다.
https://github.com/thijsvanloef/palworld-server-docker/tree/main/k8s
필요한 파일들만 두개로 분리해서 보겠습니다.
---
apiVersion: v1
kind: ConfigMap
metadata:
name: palworld-cm
data:
PUID: "1000"
PGID: "1000"
PORT: "8211" # Optional but recommended
PLAYERS: "16" # Optional but recommended
SERVER_PASSWORD: "worldofpals" # Optional but recommended
MULTITHREADING: "true"
RCON_ENABLED: "true"
RCON_PORT: "25575"
TZ: UTC
COMMUNITY: "false" # Enable this if you want your server to show up in the community servers tab, USE WITH SERVER_PASSWORD!
SERVER_NAME: "World of Pals"
SERVER_DESCRIPTION: ""
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: palworld-server
name: palworld-server
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: palworld-server
template:
metadata:
labels:
app: palworld-server
spec:
containers:
- name: palworld-server
image: thijsvanloef/palworld-server-docker
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8211
name: server
protocol: UDP
- containerPort: 27015
name: query
protocol: UDP
env:
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: palworld-secrets
key: rconPassword
envFrom:
- configMapRef:
name: palworld-cm
volumeMounts:
- name: palworld-volume
mountPath: /palworld
volumes:
- name: palworld-volume
emptyDir: {}
---
apiVersion: v1
kind: Secret
metadata:
name: palworld-secrets
type: Opaque
stringData:
rconPassword: yourRconPassword
서버 배포에 필요한 yaml 파일입니다. 별도의 PVC를 사용해 EBS 등을 이용할 수 있겠지만, 현재 과정에서 크게 중요한부분이 아니라 ephemeral한 emptydir을 이용해 프로비저닝했습니다.
---
apiVersion: v1
kind: Service
metadata:
name: palworld-server
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "external" # 외부 NLB를 사용합니다.
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip" # 타겟 타입을 IP로 설정합니다.
service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "8211" # 헬스 체크 포트를 지정합니다.
service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol: "TCP" # TCP를 사용한 헬스 체크 프로토콜입니다.
service.beta.kubernetes.io/aws-load-balancer-healthcheck-healthy-threshold: "3"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-unhealthy-threshold: "3"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-timeout: "10"
service.beta.kubernetes.io/aws-load-balancer-healthcheck-interval: "10"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing" # 인터넷 페이싱 설정입니다.
spec:
selector:
app: palworld-server
ports:
- name: server
protocol: UDP
port: 8211
targetPort: server
- name: query
protocol: UDP
port: 27015
targetPort: query
type: LoadBalancer
기존의 service 파일을 수정해 다음과 같이 프로비저닝을 진행했습니다.
로드 밸런서 타입 어노테이션: service.beta.kubernetes.io/aws-load-balancer-type
을 "external"
로 설정하여 외부 NLB를 사용하도록 지정했습니다.
UDP 프로토콜에서는 헬스체크를 진행할 수 없어, TCP 타입의 헬스체크로직을 별도로 수정했습니다.
배포가 되는 파일을 확인해보겠습니다.
AWS 콘솔의 로드밸런서 영역에서도 NLB / UDP 타입으로 오픈된것을 확인 할 수 있습니다.
생성된 DNS 이름으로 접속을 해보겠습니다.
신규 계정생성을 완료해 NLB로 오픈한 UDP 게임서버에 접속한 모습입니다.