EKS Week 1 — Endpoint에 대하여

Sigrid Jin
20 min readMar 9, 2024

--

우리가 EKS를 처음 설정하면 AWS가 관리해주는 리소스들의 집합이 있는 VPC와 고객이 직접 매니징을 해주어야 하는 VPC로 총 2개로 나누어서 생각해볼 수 있다. 먼저 EKS의 컨트롤 플레인은 AWS Managed VPC로서 3개의 AZ 가용 영역과 API NLB 그리고 etcd ELB로 구성되도록 한다.

이 때 우리가 API의 부하 분산을 ALB가 아니라 NLB로 하는 이유는 무엇일까. 본인의 생각으로는 대량의 요청을 처리해야 한다는 점에서 Layer 4 계층의 NLB가 ALB에 비해 오버헤드를 최소화할 수 있는 방법이라고 생각한다. 기본적으로 NLB는 여러 가용 영역에 대하여 자동 확장이 가능하기 때문에 중단 없는 운영도 보장할 수 있다.

특히 가시다님의 의견에 따르면 NLB 아키텍처는 Hyperplane 기반이기 때문에 ALB의 처리가 VM 기반으로 동작하는 구조보다 훨씬 안정적이라고 한다. kubernetes의 EKS API에서 etcd 사이의 헬스 체크에는 Classic Load Balancer (CLB) 가 사용되는 것으로 추정되는데 정확한 이유는 알 수 없으나 NLB에 비하여 latency가 압도적으로 빠른 것을 알 수 있겠다.

관리형 노드 그룹에서 Pod, kubelet 까지가 AWS의 책임 영역이고 kube-proxy는 이와 달리 고객의 책임 영역이다. AWS는 EKS에서 파드의 스케줄링과 배포 및 관리를 담당하기 때문에 사용자는 파드를 정의하기만 하고 AWS는 해당 파드가 클러스터 노드에 적절히 배치되도록 하는 것을 보증하는 역할이다. 또한 kubelet은 각 노드에서 실행되는 에이전트이기 때문에 파드의 상태 관리와 파드 내부의 컨테이너의 정상적 실행을 보장하기 때문에 이 역시도 AWS가 EKS 대상 노드에 kubelet을 설치하고 구성하는 역할을 담당하게 될 것이다.

AWS EKS 배포 중…

이와 달리 kube-proxy는 노드 수준에서 실행되는 서비스인데 클러스터 내부에서 네트워크 통신을 담당하는 의무를 띈다. 고객은 필요에 따라 kube-proxy의 설정을 조정하고 관리할 수 있다. 이는 고객마다 서로 다른 네트워크 요구사항과 토폴로지를 가질 수 있을텐데 이러한 설정을 사용자가 직접 지정하도록 허용하는 것이 필요하다고 판단했을 것이다.

또한 Custom AMI를 이용해서 고객이 직접 OS의 기본 구성과 패치를 할 수 있는 custom-managed nodes도 있다. 일반적으로 managed node groups는 최신의 EKS optimized된 AMI를 사용하고 있으며 이는 AMI 관리를 AWS에서 spot성 혹은 on-demand 형식으로 구축할 수 있게 된다.

카타 컨테이너 환경 vs 기존의 일반 컨테이너 환경

물론 노드 자체도 AWS Fargate 환경에서 제공하는 MicroVM을 이용하여 Pod 별로 VM을 할당하게 되면 모두 AWS가 담당하게 된다. 이 때 Kata Container라는 것을 사용하게 되는데 이는 컨테이너처럼 느껴지도록 작동하는 경량 VM을 의미한다. 하드웨어를 통해 강력하게 워크로드를 격리함으로서 안전한 컨테이너를 제공하려는 기술이다. 또한 별도의 네트워크 I/O Memory와 전용 커널을 제공하여 하드웨어 측면에서 보안성을 제고하고 VM처럼 성능의 소모 없이 분리된 환경을 제공할 수 있을 것이다. 지원하는 하이퍼바이저는 여러 가지가 있는데 그 중에서도 Firecraker가 가장 유명하다.

이제 EKS에 연결하는 엔드포인트에 대해 살펴보자. 먼저 Public이다. Public으로 열어 주게 되면 kubectl 명령어를 실행하는 노드/외부 머신에서 Internet Gateway를 타고 API 서버로 도착하게 된다. 이제 API 서버는 다중 VPC 간의 통신을 해야 한다. 이 때 AWS Managed VPC는 사전에 만들어둔 EKS owned ENI를 통해서 통신을 한다. 하지만 워커노드의 kube-proxy나 kubelet에서 health check 보고 등에 대하여 스펙을 맞춰보니 퍼블릭 도메인으로 나갔다가 들어가는 것을 볼 수 있었다.

아래 내용을 보고 시스템 소켓 정보에 주목해보도록 하자.

(iam-root-account@sigrid-myeks-240309:default) [root@myeks-host ~]# for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo ss -tnp; echo; done
>> node 192.168.1.241 <<
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 192.168.1.241:34760 10.100.0.1:443 users:(("aws-k8s-agent",pid=3401,fd=7))
ESTAB 0 0 192.168.1.241:54530 52.95.194.54:443 users:(("ssm-agent-worke",pid=2459,fd=10))
ESTAB 0 56 192.168.1.241:22 192.168.1.100:58636 users:(("sshd",pid=17132,fd=3),("sshd",pid=17100,fd=3))
ESTAB 0 0 192.168.1.241:52580 3.38.228.17:443 users:(("kubelet",pid=2936,fd=25))
ESTAB 0 0 192.168.1.241:49488 52.95.195.99:443 users:(("ssm-agent-worke",pid=2459,fd=14))
ESTAB 0 0 192.168.1.241:45074 13.124.205.250:443 users:(("kube-proxy",pid=3152,fd=7))
ESTAB 0 0 192.168.1.241:59982 10.100.0.1:443 users:(("controller",pid=3557,fd=11))

>> node 192.168.2.163 <<

State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 192.168.2.163:47996 10.100.0.1:443 users:(("controller",pid=3806,fd=11))
ESTAB 0 0 192.168.2.163:54176 10.100.0.1:443 users:(("aws-k8s-agent",pid=3399,fd=7))
ESTAB 0 0 192.168.2.163:48128 13.124.205.250:443 users:(("kube-proxy",pid=3146,fd=7))
ESTAB 0 0 192.168.2.163:22 192.168.1.100:54116 users:(("sshd",pid=17484,fd=3),("sshd",pid=17452,fd=3))
ESTAB 0 0 192.168.2.163:53578 52.95.194.54:443 users:(("ssm-agent-worke",pid=2466,fd=10))
ESTAB 0 0 192.168.2.163:51614 3.38.228.17:443 users:(("kubelet",pid=2931,fd=11))
ESTAB 0 0 192.168.2.163:53572 52.95.195.109:443 users:(("ssm-agent-worke",pid=2466,fd=14))

이제 여기서 주목해야 할 부분은 kubelet과 kubeproxy이다. 해당 서비스들의 outbound로 향하는 사항을 보면 Public IP로 통신하고 있음을 알 수 있었다. 즉 워커 노드 내부의 kubelet과 kube-proxy의 경우 DNS의 public ip를 통해서 접근하고 있음이 발각된 것이다.

192.168.1.241:52580    3.38.228.17:443   users:(("kubelet",pid=2936,fd=25))
192.168.1.241:45074 13.124.205.250:443 users:(("kube-proxy",pid=3152,fd=7))

(iam-root-account@sigrid-myeks-240309:default) [root@myeks-host ~]# APIDNS=$(aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint | cut -d '/' -f 3)

(iam-root-account@sigrid-myeks-240309:default) [root@myeks-host ~]# dig +short $APIDNS
3.38.228.17
13.124.205.250

ENI와 통신하는 부분을 살펴보기 위하여 임의의 워커 노드에 간단한 DaemonSet 파드를 만들고 그 파드에 대하여 bash로 들어가서 로그를 관찰해보자. 우리가 한 것은 어떤 더미의 서비스를 만들었다는 것이다. 그런데 아래 로그를 보면 재미있는 것이 발견된다.

kubectl exec daemonsets/aws-node -it -n kube-system -c aws-eks-nodeagent -- bash

# for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo ss -tnp; echo; done

(iam-root-account@sigrid-myeks-240309:default) [root@myeks-host ~]# for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo ss -tnp; echo; done
>> node 192.168.1.241 <<
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 192.168.1.241:34760 10.100.0.1:443 users:(("aws-k8s-agent",pid=3401,fd=7))
ESTAB 0 56 192.168.1.241:22 192.168.1.100:34096 users:(("sshd",pid=18056,fd=3),("sshd",pid=18024,fd=3))
ESTAB 0 0 192.168.1.241:52580 3.38.228.17:443 users:(("kubelet",pid=2936,fd=25))
ESTAB 0 0 192.168.1.241:49488 52.95.195.99:443 users:(("ssm-agent-worke",pid=2459,fd=14))
ESTAB 0 0 192.168.1.241:41646 52.95.198.157:443 users:(("ssm-agent-worke",pid=2459,fd=10))
ESTAB 0 0 192.168.1.241:45074 13.124.205.250:443 users:(("kube-proxy",pid=3152,fd=7))
ESTAB 0 0 192.168.1.241:59982 10.100.0.1:443 users:(("controller",pid=3557,fd=11))

>> node 192.168.2.163 <<
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
ESTAB 0 0 192.168.2.163:56740 52.95.194.65:443 users:(("ssm-agent-worke",pid=2466,fd=10))
ESTAB 0 0 192.168.2.163:47996 10.100.0.1:443 users:(("controller",pid=3806,fd=11))
ESTAB 0 0 192.168.2.163:54176 10.100.0.1:443 users:(("aws-k8s-agent",pid=3399,fd=7))
ESTAB 0 0 192.168.2.163:48128 13.124.205.250:443 users:(("kube-proxy",pid=3146,fd=7))
ESTAB 0 0 127.0.0.1:36930 127.0.0.1:40731 users:(("kubelet",pid=2931,fd=25))
ESTAB 0 56 192.168.2.163:22 192.168.1.100:43796 users:(("sshd",pid=18457,fd=3),("sshd",pid=18425,fd=3))
ESTAB 0 0 127.0.0.1:40731 127.0.0.1:36930 users:(("containerd",pid=2793,fd=15))
ESTAB 0 0 192.168.2.163:51614 3.38.228.17:443 users:(("kubelet",pid=2931,fd=11))
ESTAB 0 0 192.168.2.163:53572 52.95.195.109:443 users:(("ssm-agent-worke",pid=2466,fd=14))
ESTAB 0 0 [::ffff:192.168.2.163]:10250 [::ffff:192.168.1.136]:53944 users:(("kubelet",pid=2931,fd=22))

주목할 것은 위 로그의 가장 아랫 부분이다. 위 로그에 따르면 192.168.2.163에서 들어와서 192.168.1.136으로 나간다. 여기서 192.168.2.163의 경우 노드 그룹에 존재하는 인스턴스 Private IP 중 하나였다. 그런데 192.168.1.63은 무엇이냐고 여쭤보니, ENI에 해당하는 IP 주소인데 문제는 Owner와 Register의 상호 ID가 다르다는 것이다.

AWS Managed VPC의 API 서버는 Customer VPC의 Pod, Kubelet, kube-proxy와 통신하기 위해서 EKS owned ENI를 통해서 통신해야만 한다. 그래서 오너와 요청자가 다르다. 보안적인 측면에서 클러스터의 엔드포인트를 Public에서 다른 것으로 바꾸는 것이 훨씬 나아보인다. 우리가 고객의 VPC에서 pod, kubelet, kube-proxy를 IP만 알면 터질 수 있는 것이니까요.

대신 EKS Cluster Endpoint를 public + private으로 관리가 된다는 것이고, 사용자는 여전히 kubectl로 접근하는 것이 public하지만 가장 간단한 방법이다. 이와 달리 워커 노드 내부의 kubelet, kube-proxy는 이제 private hosted zone을 통해 eni를 거쳐 AWS Managed VPC와 통신하게 된다.

APIDNS=$(aws eks describe-cluster --name $CLUSTER_NAME | jq -r .cluster.endpoint | cut -d '/' -f 3)
dig +short $APIDNS
while true; do dig +short $APIDNS ; echo "------------------------------" ; date; sleep 1; done

클러스터의 엔드포인트를 Public + Private으로 전환하고 kubelet과 kube-proxy를 재시작해주어야 한다. kube-proxy의 경우에는 homebrew와 같은 CLI 도구로 재시작 가능하고 kubel의 경우에는 직접 ssh에 들어가서 serviced 재시작으로 대응하였다.

  • EKS Cluster Endpoint — Public : 제어부 → (EKS owned ENI) 워커노드 kubelet, 워커노드 → (퍼블릭 도메인) 제어부, 사용자 kubectl → (퍼블릭 도메인) 제어부
  • EKS Cluster Endpoint — Public Private : 제어부 → (EKS owned ENI) 워커노드 kubelet, 워커노드 → (프라이빗 도메인, EKS owned ENI) 제어부, 사용자 kubectl → (퍼블릭 도메인) 제어부
  • EKS Cluster Endpoint — Private : 제어부 → (EKS owned ENI) 워커노드 kubelet, 워커노드,사용자 kubectl → (프라이빗 도메인, EKS owned ENI) 제어부

특히 kube-proxy의 경우 내가 별도의 조치를 하지 않았는데도 private에서 public으로 갔다. 또한 워커 노드에서 접근할 때와 개인 PC에서 접근할 때의 차이이다.

워커 노드에서 접근할 때
$ dig +short xxx.gr7.ap-northeast-2.eks.amazonaws.com
192.168.1.136
192.168.2.112
----------
개인 PC에서 접근할 때
$ dig +short xxxxx.gr7.ap-northeast-2.eks.amazonaws.com
3.38.228.xx
13.124.205.xxx

kubelet은 serviced restart를 해서 private ip 변경 사항을 만들어서 준다.

# 모니터링 : tcp peer 정보 변화 확인
while true; do ssh ec2-user@$N1 sudo ss -tnp | egrep 'kubelet|kube-proxy' ; echo ; ssh ec2-user@$N2 sudo ss -tnp | egrep 'kubelet|kube-proxy' ; echo "------------------------------" ; date; sleep 1; done

# kube-proxy rollout
kubectl rollout restart ds/kube-proxy -n kube-system
kubectl rollout restart ds/kube-proxy -n kube-system

# kubelet 은 노드에서 systemctl restart kubelet으로 적용해보자
for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo systemctl restart kubelet; echo; done

for i in $N1 $N2; do echo ">> node $i <<"; ssh ec2-user@$i sudo systemctl restart kubelet; echo; done

사용자 PC에서는 Cluster 3 Oups Endpoint 관련 도메인 질의 시에는 public ip 정보를 따로 리턴해주지는 않는다. 오히려 Cluster Endpoint Private이면 완전 보안이고 root에서 config.json 등이 털려도 일단 노이슈이기 때문에 필요한 작업일 수 있겠다.

아무래도 Full Private Endpoint 가 아니더라도 접근 편리성을 위해서 Public + Private 를 설정하고, Public API 사용하는 사용자의 IP 대역 통제 설정하는 것이 필요하다고 생각한다.

마지막으로, 노드에 배포된 컨테이너 정보를 확인하려면 containerd clients 3종 중 하나를 택일하여 이용해야 한다. 이 중에서 ctr은 containerd 프로젝트의 일부로 제공되는 커맨드라인 클라이언트이고 그래서 눈여겨 볼만 하다.

  • containerd가 실행 중인 머신에 ctr 바이너리도 함께 존재할 가능성이 높다.
  • ctr 클라이언트는 Docker CLI와 유사하지만, 명령어와 플래그는 종종 (일반적으로 더 사용자 친화적인) docker 에 상응하는 것과 다르다.
  • 그러나 ctr은 containerd API 바로 위에서 작동하므로, 사용 가능한 명령을 살펴보면 containerd가 무엇을 할 수 있고 무엇을 할 수 없는지 잘 파악할 수 있어 일반 사용자들에게 훌륭한 탐색 수단이 될 수 있다.

이 외에도 etcd의 데이터베이스 크기를 EKS cluster로 동적 지정이 가능하다.

# monitor the metric etcd_db_total_size_in_bytes to track the etcd database size
kubectl get --raw /metrics | grep "apiserver_storage_size_bytes"
# HELP apiserver_storage_size_bytes [ALPHA] Size of the storage database file physically allocated in bytes.
# TYPE apiserver_storage_size_bytes gauge
apiserver_storage_size_bytes{cluster="etcd-0"} 2.957312e+06


# How do I identify what is consuming etcd database space?
kubectl get --raw=/metrics | grep apiserver_storage_objects |awk '$2>10' |sort -g -k 2
# HELP apiserver_storage_objects [STABLE] Number of stored objects at the time of last check split by kind.
# TYPE apiserver_storage_objects gauge
apiserver_storage_objects{resource="flowschemas.flowcontrol.apiserver.k8s.io"} 13
apiserver_storage_objects{resource="roles.rbac.authorization.k8s.io"} 15
apiserver_storage_objects{resource="rolebindings.rbac.authorization.k8s.io"} 16
apiserver_storage_objects{resource="apiservices.apiregistration.k8s.io"} 31
apiserver_storage_objects{resource="serviceaccounts"} 40
apiserver_storage_objects{resource="clusterrolebindings.rbac.authorization.k8s.io"} 67
apiserver_storage_objects{resource="events"} 67
apiserver_storage_objects{resource="clusterroles.rbac.authorization.k8s.io"} 81

신기한 EKS의 세상 앞으로 8주 동안 흠뻑 빠져보도록 하자!

--

--

No responses yet