[CloudNeta] EKS 워크샵 스터디 (1) - EKS 소개 #2

이번 게시글에서는 EKS 워크샵 스터디 제 1주차 내용을 작성합니다.

이 글은 2부로 나누어집니다.

  1. 1주차 - EKS 소개 #1
  2. 1주차 - EKS 소개 #2 (현재 보고계신 글)

EKS Cluster Endpoint Access에 대해

Cluster Endpoint Access는 EKS 생성 시 선택할 수 있는 옵션입니다. 이는 쿠버네티스 API서버(kube-apiserver)에 접근가능 제어를 어디까지 열어둘 것인지 지정하는 설정을 의미합니다. 서비스 구성에 앞서 두 가지 주체를 생각해야합니다.

  1. 사람의 kubectl 트래픽. aws eks update-kubeconfig 으로 kubeconfig 을 수신한 후에 API서버의 엔드포인트 URL이 추가되는 것은 확인하셨습니다. 그렇다면 이 Endpoint Access의 종류에 따라 퍼블릭 인터넷으로도 도달 가능한지, VPC 내부에서만 도달 가능한지 정하는 것입니다.

  2. 워커 노드의 kubelet 트래픽. 노드 등록 시 VPC 외부의 퍼블릭 엔드포인트를 쓸지, VPC 내부의 프라이빗 엔드포인트를 쓸지 결정합니다.

쿠버네티스 클러스터 관리에 대해

Cluster Endpoint Access 모드를 살펴보기 전에, EKS가 쿠버네티스 클러스터 운영의 일부를 어떻게 관리하는지 살펴봅시다.

AWS가 운영하는 컨트롤 플레인 살펴보기

먼저 사진을 봅시다.

001-eks-control-plane

그럼 AWS managed 컨트롤 플레인과는 어떻게 통신해요?

[2]앞서 말씀드렸듯, AWS 가 운영하는 컨트롤 플레인은 고가용성을 위해 여러 AZ에 배포해두었습니다. 아예 별도의 VPC에 구성된다는 뜻이지요. 어쨌거나 별도의 VPC니까, 후술할 어떤 모드를 사용하든간에 워커 노드와 컨트롤플레인이 통신하기 위한 별도의 EKS owned ENI(NIC라고 생각하면 됨)를 클러스터에 구성해줍니다.

ENI를 별도로 달아줍니다

그럼 두 가지 경로가 있습니다. 이 둘은 이렇게 쓰입니다.

  1. 퍼블릭 엔드포인트(NLB): VPC에서 NAT Gateway를 타고 인터넷을 경유. 이후 NLB를 타고 API 서버로 접근
  2. EKS owned ENI: AWS가 관리하는 계정과 연결된 ENI를 통해 API 서버로 접근

그렇다면, 실제로 유저의(AWS 입장에서는 고객의) 데이터 플레인은 이렇게 구성됩니다.

002-eks-data-plane

그림에서 녹색(AWS 책임)과 주황색(고객 책임)이 구분되어 있습니다. 컨트롤 플레인과 X-ENI는 AWS가 관리하고, Customer VPC 안의 노드 운영(kubelet, kube-proxy, Pod)은 고객이 관리합니다. Endpoint Access는 이 두 영역 사이의 네트워크 도달 경로를 퍼블릭/프라이빗 중 어떻게 열 것인가를 결정하는 설정입니다.

Cluster Endpoint Access 모드에 대해

상술한 두 가지 경로가 있으니, 운영하는 요구사항에 따라 각각 다르게 구성할 수 있습니다.

Public Only

003-public-only

유저의 kubectl 트래픽은 public ip 로 공개된 주소에 붙고, 데이터 플레인도 igw 를 타고 public 로 향하는 것을 보실 수 있습니다.

Public + Private

004-public-private

유저의 kubectl 트래픽은 public ip로 공개된 주소에, 데이터 플레인에서는 생성된 EKS owned ENI로 통신하는 것을 보실 수 있습니다.

Private Only

005-private-only

유저의 kubectl 트래픽도 VPN을 사용하거나 bastion 에서 구동하도록 해야하고, 데이터 플레인에서도 마찬가지로 생성된 EKS owned ENI로 통신합니다.

실습해보기 (1) - Endpoint Access 설정 살펴보기

기존 코드에서 아래와 같이 변경되면 public and private 옵션으로 배포됩니다.

endpoint_public_access = true

# 아래 코드들을 살펴보고 재구동을 하면 public, public+private 옵션 구동을 확인할 수 있습니다.
endpoint_private_access = true
endpoint_public_access_cidrs = [
  var.ssh_access_cidr
]

내부에서 직접 변경 후 스크립트를 구동해보면 아래와 같이 변경되었음을 확인할 수 있습니다:

# kubelet 은 노드에서 systemctl restart kubelet으로 적용해보자 : ss에 kubelet peer IP 변경 확인
for i in $NODE1 $NODE2; do echo ">> node $i <<"; ssh ec2-user@$i sudo systemctl restart kubelet; echo; done
ssh ec2-user@$NODE1 sudo ss -tnp | grep -v ssh
State Recv-Q Send-Q Local Address:Port   Peer Address:Port Process
ESTAB 0      0      192.168.1.212:49442    3.37.98.54:443   users:(("aws-k8s-agent",pid=2456,fd=7))
ESTAB 0      0      192.168.1.212:42834 192.168.2.185:443   users:(("kubelet",pid=19469,fd=30))
ESTAB 0      0      192.168.1.212:37782 192.168.1.202:443   users:(("kube-proxy",pid=19048,fd=6))

# aws-node
kubectl rollout restart ds/aws-node  -n kube-system
ssh ec2-user@$NODE1 sudo ss -tnp | grep -v ssh
State Recv-Q Send-Q Local Address:Port   Peer Address:Port Process
ESTAB 0      0      192.168.1.212:32912 192.168.2.185:443   users:(("aws-k8s-agent",pid=20094,fd=7))
ESTAB 0      0      192.168.1.212:48058  52.95.193.80:443   users:(("aws-k8s-agent",pid=20094,fd=8))
ESTAB 0      0      192.168.1.212:42834 192.168.2.185:443   users:(("kubelet",pid=19469,fd=30))
ESTAB 0      0      192.168.1.212:37782 192.168.1.202:443   users:(("kube-proxy",pid=19048,fd=6))

실습해보기 (2) - Fully private cluster 설정 살펴보기

기존 리포지토리의 eks-private 디렉터리를 구동하시고 작업해주세요.

Fully private cluster 구동 시 필수적으로 오픈해야하는 VPCE가 있는데요. 그 목록은 여기서 확인하실 수 있습니다.

Fully private cluster에서 서비스 하나를 배포해보기

외부로 통신하기 위해서는 NLB를 사용하고, 라우팅을 하기 위한 태그값을 올바르게 추가해주어야 합니다. AWS 링크

# (실습을 위해) NAT Gateway로 외부 컨테이너 레지스트리(public ECR도 포함)에서 컨테이너를 pull할 수 있는 루트를 만듭니다. 
enable_nat_gateway = true
single_nat_gateway = true

private_subnet_tags = {
  "kubernetes.io/role/internal-elb" = 1
}

# 라우팅을 위한 태그값은 이렇게 기재합니다.
public_subnet_tags = {
  "kubernetes.io/role/elb" = 1
}

복잡한 운영을 위해 Ingress 형태로 사용하려면 ALB 배포를, 간단히 nginx 하나만 노출하기 위해선 NLB만으로도 충분하죠. 그러니 이번 글에서는 NLB 배포를 테스트해보고자 합니다.

또한 Fully private cluster에서 외부 배포를 하려면 AWS Load Balancer Controller를 사용합니다. 여기서는 직접 bastion에 붙어서 배포작업을 수행할 예정입니다.

사전준비

먼저 현재 클러스터가 fully private cluster인지 확인해봅시다.

006-fully-private-status-check

root@bastion-EC2:~# aws eks --region ap-northeast-2 update-kubeconfig --name eks-private

# 이 경우에는
#   AWS 권한을 bastion에 부여합니다. 키를 부여하는 것보다 권한을 부여하는 쪽이 좋겠지요.

```shell
aws: [ERROR]: An error occurred (NoCredentials): Unable to locate credentials. You can configure credentials by running "aws login".
root@bastion-EC2:~# k get nodes
E0318 18:54:45.320947    2113 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"http://localhost:8080/api?timeout=32s\": dial tcp 127.0.0.1:8080: connect: connection refused"
E0318 18:54:45.321221    2113 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"http://localhost:8080/api?timeout=32s\": dial tcp 127.0.0.1:8080: connect: connection refused"
E0318 18:54:45.322620    2113 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"http://localhost:8080/api?timeout=32s\": dial tcp 127.0.0.1:8080: connect: connection refused"
E0318 18:54:45.322888    2113 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"http://localhost:8080/api?timeout=32s\": dial tcp 127.0.0.1:8080: connect: connection refused"
E0318 18:54:45.324235    2113 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"http://localhost:8080/api?timeout=32s\": dial tcp 127.0.0.1:8080: connect: connection refused"
The connection to the server localhost:8080 was refused - did you specify the right host or port?

Bastion에 "테스트용으로" IAM 규칙을 굉장히 넓게 부여하고

######################
# Bastion IAM Role   #
######################

resource "aws_iam_role" "bastion" {
  name = "bastion-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachment" "bastion_admin" {
  role       = aws_iam_role.bastion.name
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

resource "aws_iam_instance_profile" "bastion" {
  name = "bastion-ec2-profile"
  role = aws_iam_role.bastion.name
}

권한을 붙여줍니다.

# EKS 클러스터 관리용 Bastion Host EC2 인스턴스를 생성.
resource "aws_instance" "eks_bastion" {
  ami                         = data.aws_ssm_parameter.ami.value
  ...
  iam_instance_profile        = aws_iam_instance_profile.bastion.name

  ...이하 생략
}
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 20.11"

  cluster_name    = local.name
  cluster_version = "1.34"

  # Optional: Adds the current caller identity as an administrator via cluster access entry
  enable_cluster_creator_admin_permissions = true

  access_entries = {
    bastion = {
      principal_arn = aws_iam_role.bastion.arn
      policy_associations = {
        admin = {
          policy_arn = "arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy"
          access_scope = {
            type = "cluster"
          }
        }
      }
    }
  }
  ... 이하 생략
}
root@bastion-EC2:~# aws eks --region ap-northeast-2 update-kubeconfig --name eks-private
Added new context arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private to /root/.kube/config

다시호출하니 또 안됩니다. 클러스터 SG에 bastion을 허용해주어야 하죠. public -> private 경로를 의미합니다.

# 이 경우에는
#   bastion이 public subnet에 있고,
#   EKS API 엔드포인트에 붙으려면 sg를 열어줘야 통신이 되겠죠.
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# k get nodes
E0318 18:58:36.100432    2237 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"https://<REDACTED>.gr7.ap-northeast-2.eks.amazonaws.com/api?timeout=32s\": dial tcp 10.0.41.251:443: i/o timeout"
E0318 18:59:06.101499    2237 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"https://<REDACTED>.gr7.ap-northeast-2.eks.amazonaws.com/api?timeout=32s\": dial tcp 10.0.22.205:443: i/o timeout"

main.tf 에서 아래코드로 재배포합니다.

# Bastion(public subnet)에서 EKS API endpoint 접근 허용
cluster_additional_security_group_ids = []
cluster_security_group_additional_rules = {
  bastion_ingress = {
    description              = "Bastion to EKS API"
    protocol                 = "tcp"
    from_port                = 443
    to_port                  = 443
    type                     = "ingress"
    source_security_group_id = aws_security_group.eks_sec_group.id
  }
}

좋습니다, 잘 나오는군요. 그럼 본격적으로 배포해볼까요.

(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# k get nodes
NAME                                             STATUS   ROLES    AGE   VERSION
ip-10-0-45-219.ap-northeast-2.compute.internal   Ready    <none>   35m   v1.34.4-eks-f69f56f
ip-10-0-6-231.ap-northeast-2.compute.internal    Ready    <none>   35m   v1.34.4-eks-f69f56f

배포해보기

IAM policy와 IRSA를 만듭시다. 이때 eksctl을 활용합니다.

(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# aws iam create-policy \
>   --policy-name AWSLoadBalancerControllerIAMPolicy \
>   --policy-document file://iam_policy.json
{
    "Policy": {
        "PolicyName": "AWSLoadBalancerControllerIAMPolicy",
        "PolicyId": "ANPATQGTWHICHXQQAJYPS",
        "Arn": "arn:aws:iam::<REDACTED>:policy/AWSLoadBalancerControllerIAMPolicy",
        "Path": "/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "CreateDate": "2026-03-18T10:07:51+00:00",
        "UpdateDate": "2026-03-18T10:07:51+00:00"
}
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# export AWS_DEFAULT_REGION=ap-northeast-2
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# eksctl create iamserviceaccount   --cluster=eks-private   --namespace=kube-system   --name=aws-load-balancer-controller   --role-name AmazonEKSLoadBalancerControllerRole   --attach-policy-arn=arn:aws:iam::${ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy   --approve
2026-03-18 19:10:48 [ℹ]  1 iamserviceaccount (kube-system/aws-load-balancer-controller) was included (based on the include/exclude rules)
2026-03-18 19:10:48 [!]  serviceaccounts that exist in Kubernetes will be excluded, use --override-existing-serviceaccounts to override
2026-03-18 19:10:48 [ℹ]  1 task: {
    2 sequential sub-tasks: {
        create IAM role for serviceaccount "kube-system/aws-load-balancer-controller",
        create serviceaccount "kube-system/aws-load-balancer-controller",
    } }2026-03-18 19:10:48 [ℹ]  building iamserviceaccount stack "eksctl-eks-private-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2026-03-18 19:10:48 [ℹ]  deploying stack "eksctl-eks-private-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2026-03-18 19:10:48 [ℹ]  waiting for CloudFormation stack "eksctl-eks-private-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2026-03-18 19:11:18 [ℹ]  waiting for CloudFormation stack "eksctl-eks-private-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2026-03-18 19:11:18 [ℹ]  created serviceaccount "kube-system/aws-load-balancer-controller"

이어서 helm으로 AWS Load balancer를 설치해봅시다.

(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# helm repo add eks https://aws.github.io/eks-charts
"eks" has been added to your repositories
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "eks" chart repository
Update Complete. ⎈Happy Helming!⎈
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
  -n kube-system \
  --set clusterName=eks-private \
  --set serviceAccount.create=false \
  --set serviceAccount.name=aws-load-balancer-controller
NAME: aws-load-balancer-controller
LAST DEPLOYED: Wed Mar 18 19:13:16 2026
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
AWS Load Balancer controller installed!

좋습니다. 그럼 저는 아래 웹 사이트를 배포하려고 해요.

배포되어있나 확인해볼까요?

(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# k apply -f nginx.yaml
deployment.apps/nginx created
service/nginx created

# 이건 바로 올라옵니다.
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# k get deploy -n kube-system aws-load-balancer-controller
NAME                           READY   UP-TO-DATE   AVAILABLE   AGE
aws-load-balancer-controller   2/2     2            2           47s

그럼 bastion 쉘 대신 로컬 쉘에서 확인해봅시다.

# 하지만 AWS의 NLB가 올라오기까지는 2~3분이 걸리니, 이후 확인해보세요.
 nslookup k8s-default-nginx-5c2c562c82-42b9509722fa43c1.elb.ap-northeast-2.amazonaws.com
Server:         10.255.255.254
Address:        10.255.255.254#53

** server can't find k8s-default-nginx-5c2c562c82-42b9509722fa43c1.elb.ap-northeast-2.amazonaws.com: NXDOMAIN

잠시 기다리는동안 콘솔에 들어가서 상황을 봅시다.

007-deploy-wip-console

이제 될 것 같은데요? 그럼 커맨드로 때려보고 화면도 봅시다.

# 2~3분을 기다리면 이렇게 뜹니다.
 curl k8s-default-nginx-5c2c562c82-42b9509722fa43c1.elb.ap-northeast-2.amazonaws.com
curl: (6) Could not resolve host: k8s-default-nginx-5c2c562c82-42b9509722fa43c1.elb.ap-northeast-2.amazonaws.com

~
 curl k8s-default-nginx-5c2c562c82-42b9509722fa43c1.elb.ap-northeast-2.amazonaws.com
<!DOCTYPE html>
<html>
<head><title>s3ich4n rules!</title>

성공적으로 배포했음을 알 수 있습니다!

008-deploy-done

삭제하기

  1. Nginx와 NLB를 먼저 지웁니다
  2. Helm 삭제를 통해 LB Controller를 지웁니다
  3. IRSA를 지웁니다
  4. IAM Policy를 지웁니다
# 1번 과정
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# k delete -f nginx.yaml
deployment.apps "nginx" deleted from default namespace
service "nginx" deleted from default namespace

# 2번 과정
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# helm uninstall aws-load-balancer-controller -n kube-system
release "aws-load-balancer-controller" uninstalled

# 3번 과정
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# eksctl delete iamserviceaccount \
>   --cluster=eks-private \
>   --namespace=kube-system \
>   --name=aws-load-balancer-controller
2026-03-18 19:30:14 [ℹ]  1 iamserviceaccount (kube-system/aws-load-balancer-controller) was included (based on the include/exclude rules)
2026-03-18 19:30:14 [ℹ]  1 task: {
    2 sequential sub-tasks: {
        delete IAM role for serviceaccount "kube-system/aws-load-balancer-controller" [async],
        delete serviceaccount "kube-system/aws-load-balancer-controller",
    } }2026-03-18 19:30:14 [ℹ]  will delete stack "eksctl-eks-private-addon-iamserviceaccount-kube-system-aws-load-balancer-controller"
2026-03-18 19:30:15 [ℹ]  deleted serviceaccount "kube-system/aws-load-balancer-controller"
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# echo $ACCOUNT_ID
<REDACTED>

# 4번 과정
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~# aws iam delete-policy --policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/AWSLoadBalancerControllerIAMPolicy
(arn:aws:eks:ap-northeast-2:<REDACTED>:cluster/eks-private:N/A) root@bastion-EC2:~#

이후 배포한 eks-privatetf destroy -auto-approve 로 삭제합니다.


  1. ALB대신 NLB와 CLB를 쓰는 이유는 이 영상을 참고해주세요. ↩︎

  2. 현재 내용의 원문이 필요하시면 이 링크를 참고해주세요.
    추가로 이 링크도 좋습니다. ↩︎