인터넷 게이트웨이도 NAT 게이트웨이도 없는 VPC 에 EKS 클러스터를 올린다. 컨트롤 플레인 엔드포인트는 프라이빗만 열고, 노드는 VPC 엔드포인트만으로 컨트롤 플레인 · ECR · S3 에 닿는다. 스택을 네트워크 · 클러스터 · 노드 그룹 셋으로 나눠 만든다.
이 구성에서 가장 자주 나는 실패는 노드가 클러스터에 붙지 못하는 NodeCreationFailure: Instances failed to join the kubernetes cluster 이고, 원인은 거의 VPC 엔드포인트 누락이거나 보안 그룹이다.
EnableDnsSupport · EnableDnsHostnames 가 모두 true 인 VPC. 인터페이스 엔드포인트의 프라이빗 DNS 가 여기에 달려 있다.kubectl 이 닿지 않는다.프라이빗 서브넷 두 개, 라우트 테이블 하나, 그리고 엔드포인트를 만든다. 서브넷 태그 kubernetes.io/role/internal-elb=1 은 내부 로드밸런서를 자동으로 붙이기 위한 것이다.
AWSTemplateFormatVersion: '2010-09-09'
Description: Private VPC for private EKS (no IGW, no NAT)
Parameters:
VpcCidr:
Type: String
Default: 10.50.0.0/16
AZ1:
Type: AWS::EC2::AvailabilityZone::Name
AZ2:
Type: AWS::EC2::AvailabilityZone::Name
Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VpcCidr
EnableDnsSupport: true
EnableDnsHostnames: true
Rt:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
SubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.50.0.0/19
AvailabilityZone: !Ref AZ1
MapPublicIpOnLaunch: false
Tags:
- { Key: kubernetes.io/role/internal-elb, Value: '1' }
SubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.50.32.0/19
AvailabilityZone: !Ref AZ2
MapPublicIpOnLaunch: false
Tags:
- { Key: kubernetes.io/role/internal-elb, Value: '1' }
RtAssocA:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref Rt
SubnetId: !Ref SubnetA
RtAssocB:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref Rt
SubnetId: !Ref SubnetB
EndpointSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Interface endpoint SG
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: !Ref VpcCidr
VPCS3:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
RouteTableIds: [!Ref Rt]
Outputs:
VpcId:
Value: !Ref VPC
PrivateSubnetIds:
Value: !Join [",", [!Ref SubnetA, !Ref SubnetB]]
EndpointSGId:
Value: !Ref EndpointSG
인터페이스 엔드포인트는 서비스 이름만 바꿔 같은 모양으로 반복된다.
VPCEcrApi:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api
VpcEndpointType: Interface
SubnetIds: [!Ref SubnetA, !Ref SubnetB]
PrivateDnsEnabled: true
SecurityGroupIds: [!Ref EndpointSG]
| 서비스 이름 접미사 | 유형 | 없으면 생기는 일 |
|---|---|---|
eks |
인터페이스 | 노드가 클러스터 정보를 조회하지 못한다 |
sts |
인터페이스 | IRSA · 노드 인증 토큰 발급 실패 |
ecr.api · ecr.dkr |
인터페이스 | kube-proxy · coredns · VPC CNI 이미지를 못 받아 노드가 붙지 못한다 |
s3 |
게이트웨이 | ECR 이미지 레이어가 S3 에 있어 이미지 pull 이 실패한다 |
ec2 |
인터페이스 | VPC CNI 가 ENI · 보조 IP 를 만들지 못한다 |
logs |
인터페이스 | CloudWatch 로그 전송 실패 |
ssm · ssmmessages · ec2messages |
인터페이스 | 세션 매니저로 노드에 들어갈 수 없다 |
elasticloadbalancing |
인터페이스 | LoadBalancer 서비스 생성 실패 |
autoscaling |
인터페이스 | 관리형 노드 그룹 스케일링 실패 |
ecr.dkr 은 있는데 s3 게이트웨이가 없으면 이미지 매니페스트까지만 받고 레이어에서 멈춘다. 셋을 한 묶음으로 본다.
인터페이스 엔드포인트는 개수만큼 시간당 요금이 붙는다. NAT 게이트웨이를 둘 수 있는 환경이면 eks 와 sts 정도만 남기고 나머지는 NAT 경유로 내보내는 편이 싸다. 프라이빗 API 엔드포인트 자체는 NAT 유무와 무관하게 동작한다.
Resources:
ClusterRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal: { Service: eks.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEKSClusterPolicy
ClusterSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EKS cluster SG
VpcId: !Ref VpcId
EKSCluster:
Type: AWS::EKS::Cluster
Properties:
Name: !Ref ClusterName
Version: !Ref Version
RoleArn: !GetAtt ClusterRole.Arn
AccessConfig:
AuthenticationMode: API_AND_CONFIG_MAP
ResourcesVpcConfig:
EndpointPublicAccess: false
EndpointPrivateAccess: true
SecurityGroupIds: [!Ref ClusterSecurityGroup]
SubnetIds: !Ref PrivateSubnetIds
Outputs:
ClusterSecurityGroupId:
Value: !Ref ClusterSecurityGroup
클러스터 역할에는 AmazonEKSClusterPolicy 하나면 된다. 오래된 예제에 나오는 AmazonEKSServicePolicy 는 지금 필요하지 않고, AmazonEKSVPCResourceController 는 파드별 보안 그룹 기능을 쓸 때만 붙인다.
AccessConfig.AuthenticationMode 를 명시해 두면 접근 권한을 aws-auth ConfigMap 대신 액세스 엔트리(access entry) 로 관리할 수 있다. 신규 구축이면 API 로 두고 액세스 엔트리만 쓰는 편이 깔끔하다.
Version 은 지원 기간이 남은 버전을 골라 넣는다. 지원이 끝난 버전은 스택 생성 자체가 실패한다.
Resources:
NodeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal: { Service: ec2.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
- arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly
- arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
NodeGroup:
Type: AWS::EKS::Nodegroup
Properties:
ClusterName: !Ref ClusterName
NodeRole: !GetAtt NodeRole.Arn
Subnets: !Ref PrivateSubnetIds
AmiType: AL2023_x86_64_STANDARD
InstanceTypes: [m6i.large]
ScalingConfig:
DesiredSize: 2
MinSize: 1
MaxSize: 4
AMI 유형에 주의한다. EKS 는 2025-11-26 을 끝으로 Amazon Linux 2 기반 최적화 AMI 발행을 중단했고, AL2 를 지원한 마지막 쿠버네티스 버전은 1.32 다.[1] 1.33 이상에서는 AL2023_* 또는 Bottlerocket 계열을 쓴다. 오래된 템플릿의 AL2_x86_64 를 그대로 복사하면 노드 그룹 생성 단계에서 막힌다.
관리형 노드 그룹은 EKS 가 만들어 주는 클러스터 보안 그룹을 노드에 자동으로 붙이므로, 노드 보안 그룹을 따로 만들어 컨트롤 플레인과 양방향 규칙을 손으로 잇는 작업은 보통 필요 없다. 시작 템플릿으로 보안 그룹을 직접 지정하는 경우에만 노드 SG 와 클러스터 SG 사이를 서로 열어 준다.
SSH 키를 넣지 말고 AmazonSSMManagedInstanceCore 와 SSM 엔드포인트 세 개로 세션 매니저를 쓴다. 프라이빗 VPC 에 배스천을 두지 않아도 노드에 들어갈 수 있다.
aws eks update-kubeconfig --name private-eks --region ap-northeast-2 --alias private-eks
kubectl get nodes
이 명령은 VPC 에 네트워크 경로가 있는 곳에서 실행해야 한다. 권한은 액세스 엔트리로 준다.
aws eks create-access-entry \
--cluster-name private-eks \
--principal-arn arn:aws:iam::<ACCOUNT_ID>:role/<ROLE_NAME> \
--type STANDARD
aws eks associate-access-policy \
--cluster-name private-eks \
--principal-arn arn:aws:iam::<ACCOUNT_ID>:role/<ROLE_NAME> \
--access-scope type=cluster \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy
AuthenticationMode 를 CONFIG_MAP 으로 둔 기존 클러스터라면 kubectl -n kube-system edit configmap aws-auth 로 mapRoles 에 역할을 추가하는 옛 방식이 그대로 쓰인다.
로드밸런서는 내부용만 쓸 수 있다. 서브넷에 kubernetes.io/role/internal-elb=1 태그가 있고 서비스에 service.beta.kubernetes.io/aws-load-balancer-internal: "true" 를 주면 내부 NLB · ALB 가 만들어진다.
| 원인 | 확인 |
|---|---|
VPC 엔드포인트 누락 (ecr.api · ecr.dkr · s3 가 대표) |
노드는 부팅되지만 kube-system 이미지 pull 이 실패한다. 세션 매니저로 들어가 journalctl -u kubelet 를 본다 |
| 엔드포인트 보안 그룹이 443 을 막음 | 엔드포인트 SG 인바운드에 VPC 대역 443/tcp 가 있는지 본다 |
PrivateDnsEnabled: false 이거나 VPC 의 DNS 설정이 꺼짐 |
노드에서 nslookup api.ecr.ap-northeast-2.amazonaws.com 이 사설 IP 를 돌려줘야 한다 |
| 노드 역할 정책 누락 | AmazonEKSWorkerNodePolicy · AmazonEC2ContainerRegistryReadOnly · AmazonEKS_CNI_Policy 세 개가 모두 필요하다 |
| AMI 유형과 인스턴스 아키텍처 불일치 | Graviton 인스턴스에 x86_64 AMI 를 지정한 경우 |
| 서브넷 태그 누락 | 클러스터 이름 태그와 internal-elb 태그 |
EKS 최적화 AL2 AMI 지원 종료 2025-11-26, AL2 를 지원한 마지막 쿠버네티스 버전 1.32 — 2026-09-20 확인. https://docs.aws.amazon.com/eks/latest/userguide/eks-ami-deprecation-faqs.html ↩︎