AWS EKS Fully Private EKS 클러스터 - Nginx 배포와 외부 통신 라우팅 실습
서종호(가시다)님의 AWS EKS Workshop Study(AEWS) 1주차 학습 내용을 기반으로 합니다.
- 개요: Fully Private EKS 환경이란?
- K8s Service 모델 이해 (ClusterIP / NodePort / LoadBalancer)
- AWS Load Balancer Controller (LBC) 배포 및 인증 구성
- 실습: Nginx 배포 및 L4 (NLB) IP 모드 아키텍처
- 심화: L7 (ALB) Ingress 라우팅 구조 분석
1. 개요: Fully Private EKS 환경이란?
최근 엔터프라이즈 환경에서는 보안과 규정 준수를 위해 워커 노드(EC2)와 API 서버 엔드포인트를 모두 Private Subnet에 격리하는 Fully Private EKS 아키텍처를 표준으로 채택하고 있다.
이러한 폐쇄망 구조에서 내부의 파드(Pod)를 외부 인터넷에 안전하게 서비스하려면 어떻게 해야 할까?
정답은 Public Subnet에 AWS Load Balancer(NLB/ALB)를 배치하고, 이 LB가 Private Subnet의 파드로 트래픽을 넘겨주도록(Routing) 구성하는 것이다.
본 포스팅에서는 이를 구현하기 위한 쿠버네티스의 코어 네트워크 리소스(Service)의 동작 원리와, AWS 인프라를 동적으로 제어하는 AWS Load Balancer Controller(LBC)의 딥한 아키텍처까지 살펴본다.
2. K8s Service 모델 이해
EKS 환경에서 외부 통신을 논하려면 먼저 순수 K8s의 Service 리소스 구조를 명확히 이해해야 한다. K8s의 서비스 리소스는 완전히 분리된 3가지 타입이 존재하는 것이 아니라, 하위 타입을 포함하며 확장해 나가는 중첩(Nesting) 구조를 띈다.

2.1. ClusterIP
- 역할: ClusterIP는
Endpoints리소스를 통해 흩어진 파드들의 실제 프라이빗 IP(10.x.x.x)를 하나로 묶고, 클러스터 내부에서만 통용되는 가상의 논리적 IP(172.20.x.x)를 부여한다. - 중요성: 추후 외부망 연동 시 우회되기도 하지만, 클러스터 내 파드 간의 동서(East-West) 통신과 K8s 내부 DNS 리졸빙을 담당하는 생략할 수 없는 필수 코어 리소스다.
2.2. NodePort (노드 레벨 노출)
- 역할:
ClusterIP를 감싸는 껍데기. 모든 워커 노드(EC2)에 특정 포트(예: 30000~32767)를 개방한다. - 흐름:
클라이언트 ➡️ 노드 IP : NodePort ➡️ kube-proxy(iptables) ➡️ ClusterIP ➡️ 파드순으로 트래픽을 라우팅하는 노드 레벨의 간소화된 로드밸런서다.
2.3. LoadBalancer
- 역할:
NodePort를 다시 한번 감싸며, AWS NLB/ALB와 같은 클라우드 제공자의 물리적 로드밸런서를 생성한다. 여러 워커 노드를 타겟 그룹으로 묶고 단일 DNS 주소로 클라이언트의 요청을 받아낸다. 클라우드 제공 LB를 사용하지 않는 경우 MetalLB와 같은 구현체를 통해 구현 할 수 있다.
3. AWS Load Balancer Controller (LBC) 배포 및 인증 구성
AWS 환경에서 type: LoadBalancer를 선언했을 때 물리적인 NLB를 생성해주기 위해서는 AWS Load Balancer Controller (LBC) 설치가 필수적이다.
3.1. AWS LBC의 본질 (Operator 패턴)
LBC는 단순한 애드온이 아니라 K8s의 오퍼레이터(Operator) 패턴을 구현한 커스텀 컨트롤러다. 쉽게 말해 해당 파드가 리소스의 변화를 감지(reconcile)하여 각 변화에 알맞은 AWS 리소스를 생성 및 매핑해주는 역할을 수행한다.
- 감시 (Watch): K8s API 서버 내부의
Service,Ingress,Endpoints객체를 모니터링한다. - 배포 (Provision): 변화가 감지되면 부여받은 IAM 권한으로 AWS API를 직접 호출하여 리소스를 프로비저닝한다.
- 매핑 (Mapping): 대상 파드의 IP가 변경될 때마다
TargetGroupBinding이라는 CRD를 통해 AWS 타겟 그룹을 동적으로 동기화한다.
3.2. LBC 설치 및 IRSA 구성
IAM OIDC 공급자를 통해 K8s ServiceAccount에 AWS IAM 권한을 부여(IRSA)하고 Helm으로 배포한다.
# 1. 클러스터 OIDC 공급자 활성화
$ eksctl utils associate-iam-oidc-provider --cluster <CLUSTER_NAME> --approve
# 2. IAM 정책/Role 생성 및 ServiceAccount 맵핑
$ 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 \
--approve
# 3. Helm 차트를 이용한 배포
$ helm repo add eks https://aws.github.io/eks-charts
$ helm install aws-load-balancer-controller eks/aws-load-balancer-controller -n kube-system ...
Troubleshooting: 403 AccessDenied Error
– 문제: 컨트롤러 설치 후 매니페스트를 배포했으나EXTERNAL-IP가 계속<pending>상태에 머무름. 로그 확인 시DescribeListenerAttributes ... 403 AccessDenied에러 발생.– 원인: LBC 버전은 최신이나, 연결된 IAM 정책 파일(JSON)이 구버전이어서 최신 API 제어 권한이 누락됨.– 해결: 공식 GitHub에서 최신iam_policy.json을 다운로드해aws iam create-policy-version으로 정책을 업데이트한 뒤 파드를 재시작(rollout restart)하여 해결.
4. 실습: Nginx 배포 및 L4 (NLB) IP 모드 아키텍처
LBC가 정상적으로 구동되었다면, Nginx를 배포하고 외부로 노출시켜보자.
4.1. 사전 구성 (Subnet 태깅 및 Security Group)
LBC가 로드밸런서를 프로비저닝할 위치를 식별할 수 있도록 VPC 서브넷에 아래 태그를 반드시 확인/추가해야 한다. 워커 노드와 파드는 Private Subnet에 격리되어 있지만, 외부 클라이언트의 트래픽을 최초로 받아낼 Internet-facing NLB를 프로비저닝하기 위해 클러스터와 연결된 Public Subnet에 태깅을 진행한다. 이번 예제에서 사용하는 NLB 실습이 Public 서브넷을 활용하는 예시이다.
- Public 서브넷:
kubernetes.io/role/elb = 1

추가적으로 구동되어 있는 워커 노드들의 SG에 대한 설정도 필요하다. NLB에서 해당 노드의 80포트로의 Inbound 규칙이 허용이 되어야 정상적으로 파드 내의 nginx에 트래픽을 받을 수 있다. IP 전부 허용보다는 NLB의 Security Group 자체를 열어주면 된다.
추가적으로 Internal LB를 사용해야 하는 경우가 있다면 아래처럼 private 서브넷에도 태그를 관리해줘야한다.
- Private 서브넷:
kubernetes.io/role/internal-elb = 1

4.2. 매니페스트 작성 및 배포
K8s 1.24+ 및 AWS EKS 모범 사례를 모두 적용한 Service 매니페스트는 아래와 같다.
apiVersion: v1
kind: Service
metadata:
name: nginx-nlb-svc
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
service.beta.kubernetes.io/aws-load-balancer-type: external
# 핵심 1: 파드로 직접 라우팅하는 IP 모드 적용
service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
spec:
type: LoadBalancer
# 핵심 2: K8s 1.24+ 무의미한 NodePort 할당 차단
allocateLoadBalancerNodePorts: false
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
4.3. IP Mode의 동작 원리 분석
위 야믈 파일에서 가장 주목해야 할 점은 target-type: ip 와 allocateLoadBalancerNodePorts: false 다.
순수 K8s 관점에서는 LoadBalancer 타입을 선언하면 무조건 NodePort와 ClusterIP가 할당되어야 한다. 하지만 AWS LBC는 이 원칙을 우회한다.
- LBC는 kube-proxy 기반의 기본 라우팅을 우회하고, K8s의 EndpointSlice(또는 Endpoints) 리소스를 직접 참조하여 파드들의 실제 프라이빗 IP를 추출해낸다.
- 파드들이 가진 진짜 프라이빗 IP(
10.x.x.x)를 추출하여 AWS NLB 대상 그룹에 다이렉트로 등록해버린다. - 결과적으로 트래픽은
클라이언트 ➡️ NLB ➡️ (NodePort/iptables 우회) ➡️ Nginx 파드 IP로 직행한다.

이 구조는 네트워크 홉(Hop)을 줄여 지연 시간을 낮추고, 클라이언트의 원본 IP를 파드 레벨까지 온전히 보존할 수 있게 해준다. 실제로 생성된 NLB를 콘솔 상에서 확인보면 리소스 맵에서 target group에 실제 파드들의 private ip가 할당 되어 있음을 확인할 수 있다.
그 다음으로 살펴본 부분은 allocateLoadBalancerNodePorts 옵션이다. 해당 옵션을 false로 설정하고 생성된 서비스의 정보를 확인해보면, 의미 없는 NodePort가 할당되지 않고(unset) 비워져 있는 것을 확인할 수 있다. 만약 이를 기본값 true를 사용할 경우는 노드포트 리소스가 할당됨을 확인할 수 있다.

5. 참고 : L7 (ALB) Ingress 라우팅 구조 분석
L4 계층의 NLB가 'IP와 포트'만 보고 통신한다면, 마이크로서비스(MSA) 환경에서는 도메인 주소(Host)나 URL 경로(/api, /web)를 뜯어보고 트래픽을 정해진 규칙에 의해서 분산시켜주는 L7 계층의 Ingress(ALB) 아키텍처가 필수적이다.
5.1. Ingress와 ALB의 관계
K8s의 Ingress는 그 자체로 서비스 객체가 아니라 **'라우팅 규칙(Rules)의 집합'**이다.
사용자가 클러스터에 Ingress 리소스를 배포하면, AWS LBC는 이 규칙을 물리적으로 수행할 수 있는 AWS ALB (Application Load Balancer) 를 프로비저닝한다. K8s의 추상적인 Ingress 라우팅 규칙을 AWS 환경에서 물리적으로 구현해 내는 주체가 바로 ALB라고 이해할 수 있다.