Published on

EKS에서 NodePort만 안 열릴 때 CNI·SG 점검

Authors

서론

EKS 운영 중 의외로 자주 겪는 장애가 있습니다. Pod 간 통신, ClusterIP, 심지어 LoadBalancer(Service)까지는 정상처럼 보이는데 NodePort만 외부에서 절대 안 열리는 상황입니다. 이때 문제를 “서비스가 안 떠서”라고 단정하면 시간을 크게 낭비합니다. NodePort는 kube-proxy(iptables/IPVS) + 노드 네트워크(ENI, 라우팅) + 보안 경계(SG/NACL) + CNI 동작이 맞물려 동작하기 때문에, 한 군데만 어긋나도 증상이 timeout 또는 connection refused로 나타납니다.

이 글은 EKS에서 NodePort만 막힐 때를 전제로, CNI(Amazon VPC CNI)와 보안그룹/네트워크 경로를 중심으로 가설을 빠르게 줄이는 체크리스트를 제공합니다. (kubectl/iptables/ss/tcpdump까지 포함)

관련 네트워크/제어 plane 진단 글로는 EKS에서 kubectl exec·logs가 안 될 때 진단법, CNI 초기화 문제는 EKS kubelet NotReady - CNI plugin not initialized 해결, 그리고 네트워크 타임아웃류는 EKS TLS handshake timeout 해결 - IRSA·VPC·CoreDNS도 함께 참고하면 좋습니다.

먼저 증상을 “정확히” 분류하기

NodePort 장애는 크게 2가지로 갈립니다.

  1. 노드까지는 도달하지만, 노드에서 Pod로 포워딩이 실패 (kube-proxy/iptables/IPVS, targetPort/selector, readiness 등)
  2. 노드 자체로 유입이 안 됨 (SG/NACL/라우팅/서브넷 경로/인스턴스 방화벽)

가장 먼저 “노드까지 패킷이 들어오는지”를 확인해야 합니다.

재현용 테스트 서비스 배포

아래는 가장 단순한 HTTP echo(NodePort) 예시입니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
        - name: echo
          image: hashicorp/http-echo:1.0
          args: ["-text=ok"]
          ports:
            - containerPort: 5678
---
apiVersion: v1
kind: Service
metadata:
  name: echo-np
spec:
  type: NodePort
  selector:
    app: echo
  ports:
    - name: http
      port: 80
      targetPort: 5678
      nodePort: 30080

적용 후 상태 확인:

kubectl apply -f echo-nodeport.yaml
kubectl get svc echo-np -o wide
kubectl get endpoints echo-np -o wide
kubectl get pod -l app=echo -o wide
  • endpoints가 비어 있으면 selector/labels 또는 readiness 문제입니다. 이 경우 NodePort 이전에 Service 자체가 백엔드를 못 찾는 상태입니다.

노드 내부에서 NodePort가 열렸는지 확인

노드에 SSM/SSH로 접속 가능하다면(또는 debug daemonset) 아래를 확인합니다.

# NodePort 리스닝은 "프로세스가 listen"하는 형태가 아니라 iptables/IPVS로 처리됩니다.
# 따라서 ss로 안 보일 수 있습니다.

sudo iptables -t nat -S | grep 30080 || true
sudo iptables -S | grep 30080 || true

# IPVS 모드라면
sudo ipvsadm -Ln | grep 30080 || true
  • 규칙이 아예 없다면 kube-proxy 문제(또는 serviceNodePortRange/정책) 가능성이 큽니다.

1단계: kube-proxy/Service 스펙 점검 (가장 빠른 로컬 원인 제거)

NodePort가 “노드에 도달했는데도” 안 되는 경우가 많습니다. 다음을 순서대로 확인합니다.

Service/Endpoint 매칭

kubectl describe svc echo-np
kubectl get endpointslice -l kubernetes.io/service-name=echo-np -o yaml | head
  • Endpoints:가 비어 있거나, 포트가 기대와 다르면 targetPort/컨테이너 포트 설정을 재확인합니다.

externalTrafficPolicy 영향

externalTrafficPolicy: Local이면 해당 노드에 Pod가 없을 때 NodePort가 실패합니다(특히 timeout).

kubectl get svc echo-np -o jsonpath='{.spec.externalTrafficPolicy}{"\n"}'
  • Local이면 테스트로 Cluster로 바꾸거나, 반드시 모든 노드에 Pod가 존재하도록 DaemonSet으로 재현해 보세요.

kube-proxy 상태

kubectl -n kube-system get ds kube-proxy -o wide
kubectl -n kube-system logs ds/kube-proxy --tail=200
  • iptables sync 에러, conntrack 관련 에러, 권한 문제 등이 없는지 확인합니다.

2단계: “노드로 들어오는 트래픽”이 막히는지 확인 (SG/NACL/라우팅)

NodePort는 노드 ENI로 들어오는 인바운드가 열려야 합니다. 특히 EKS에서는 “Control Plane SG”가 아니라 노드(Worker) SG가 핵심입니다.

가장 흔한 원인: 노드 SG에 NodePort 인바운드가 없음

  • 인바운드 규칙에 TCP 30080(또는 NodePort 범위)이 허용되어야 합니다.
  • 소스는 운영 정책에 맞게 제한하되, 디버깅 시에는 임시로 테스트 클라이언트 IP 또는 VPC CIDR로 좁혀 열어 확인합니다.

권장 방식(운영):

  • 특정 NodePort만 열기(예: 30080)
  • 또는 NodePort 범위(기본 30000-32767)를 필요한 최소로만
Security Group Inbound 예시
- TCP 30080 from <YourClientIP>/32
- (필요 시) ICMP from <YourClientIP>/32  # 경로 확인용

> 참고: NodePort 범위를 통째로 30000-32767 열어두면 편하지만, 노출면이 커집니다. 가능하면 NLB/ALB로 수렴시키고 NodePort는 내부용으로만 쓰는 것이 일반적입니다.

NACL(네트워크 ACL)에서 ephemeral 포트가 막히는 케이스

NACL은 stateless이므로 인바운드 NodePort 허용뿐 아니라 **아웃바운드 응답 트래픽(클라이언트 ephemeral 포트)**도 허용해야 합니다.

  • 클라이언트가 src: 49152~65535 같은 ephemeral 포트를 쓰면, 노드가 응답할 때 NACL 아웃바운드에서 차단될 수 있습니다.

점검 포인트:

  • Subnet NACL 인바운드: TCP 30080 허용
  • Subnet NACL 아웃바운드: TCP ephemeral(대개 1024-65535 또는 49152-65535) 허용

퍼블릭 서브넷/IGW/라우팅

외부(인터넷)에서 노드 퍼블릭 IP로 접근하는 테스트라면:

  • 노드가 퍼블릭 서브넷에 있고
  • 라우팅 테이블에 0.0.0.0/0 -> igw-...
  • 노드에 퍼블릭 IP(EIP 또는 auto-assign public IPv4)

이 중 하나라도 빠지면 외부에서 NodePort는 당연히 안 됩니다.

반대로 프라이빗 서브넷 노드라면:

  • 외부에서 직접 NodePort로 접근하는 모델 자체가 맞지 않습니다.
  • 이 경우는 NLB/ALB(LoadBalancer 타입) 또는 Bastion/VPN/Direct Connect 경유로 접근해야 합니다.

3단계: CNI(Amazon VPC CNI) 관점에서의 함정

NodePort 자체는 kube-proxy가 처리하지만, 노드↔Pod 경로SNAT/라우팅은 CNI 설정/동작에 의해 영향을 받습니다. 특히 다음 옵션들이 NodePort 장애를 “특정 조건에서만” 만들 수 있습니다.

(1) AWS_VPC_CNI_CUSTOM_NETWORK_CFG 사용 시 서브넷/SG 분리

Pod에 별도 서브넷/SG를 붙이는 커스텀 네트워크 구성을 사용하면:

  • Pod ENI 보안그룹이 별도로 적용되고
  • 노드 SG만 열어서는 해결이 안 되는 것처럼 보일 수 있습니다(특히 Pod로 직접 들어가는 트래픽 시나리오).

확인:

kubectl -n kube-system get ds aws-node -o yaml | grep -E 'CUSTOM_NETWORK_CFG|ENIConfig|POD_SECURITY_GROUP' -n
kubectl -n kube-system describe ds aws-node | sed -n '1,200p'

커스텀 네트워크 구성에서는 ENIConfig 리소스 및 서브넷 라우팅/SG를 함께 점검해야 합니다.

(2) Source IP 보존과 SNAT 설정

NodePort + externalTrafficPolicy: Local 조합은 소스 IP 보존을 위해 SNAT을 줄이지만, 그만큼 노드에 로컬 엔드포인트가 없으면 실패합니다. 또한 CNI의 SNAT 설정(AWS_VPC_K8S_CNI_EXTERNALSNAT)을 바꾸면 egress 경로가 달라져 “응답이 안 오는” 형태로 보일 수 있습니다.

CNI 환경 변수 확인:

kubectl -n kube-system exec ds/aws-node -c aws-node -- env | grep -E 'SNAT|EXTERNALSNAT|WARM|PREFIX|CUSTOM'
  • AWS_VPC_K8S_CNI_EXTERNALSNAT=true를 사용 중이면, NAT Gateway/외부 NAT 장비가 응답 경로를 제대로 처리하는지 확인해야 합니다.

(3) kubelet/CNI 이상 징후: ipamd/노드 라우팅 문제

CNI가 불안정하면 Pod IP 할당/라우팅이 꼬이고, 결과적으로 NodePort가 백엔드로 포워딩하지 못합니다.

kubectl -n kube-system logs ds/aws-node -c aws-node --tail=200
kubectl -n kube-system logs ds/aws-node -c aws-vpc-cni-init --tail=200

# 노드 상태에서 CNI 관련 이벤트
kubectl describe node <node-name> | sed -n '/Events:/,$p'

CNI plugin not initialized 같은 증상이 보이면 CNI 자체 복구가 우선이며, 이 경우는 NodePort만이 아니라 Pod 네트워크 전반에 문제가 동반됩니다. 이 케이스는 위의 CNI 초기화 글(EKS kubelet NotReady - CNI plugin not initialized 해결)의 흐름으로 접근하는 편이 빠릅니다.

4단계: 패킷이 어디서 끊기는지 “관측”으로 확정하기

가설을 끝내려면 관측이 필요합니다. 노드에서 tcpdump를 떠서 패킷이 노드에 들어오는지/나가는지를 보면 SG/NACL/iptables 중 어디가 문제인지 빠르게 확정됩니다.

노드에서 tcpdump

# 들어오는 NodePort 패킷이 보이는지
sudo tcpdump -ni any tcp port 30080

# 특정 인터페이스(대개 eth0)로 한정
sudo tcpdump -ni eth0 tcp port 30080

해석:

  • 아무 패킷도 안 보임: SG/NACL/라우팅/클라이언트 경로 문제
  • SYN은 들어오는데 SYN-ACK가 안 나감: 노드 로컬 방화벽/iptables/kube-proxy/커널 설정
  • SYN/SYN-ACK 이후 데이터가 끊김: MTU/경로 MTU, conntrack, 중간 장비

Pod까지 트래픽이 전달되는지

Pod가 떠 있는 노드에서 Pod IP로 직접 확인:

POD_IP=$(kubectl get pod -l app=echo -o jsonpath='{.items[0].status.podIP}')

# 노드에서 Pod로 직접 curl
curl -sS http://$POD_IP:5678
  • 노드→Pod가 안 되면 CNI/라우팅/보안그룹(커스텀 네트워크 시) 문제 가능성이 커집니다.

5단계: 자주 놓치는 설정/환경 체크리스트

노드 OS 레벨 방화벽

대부분 EKS AMI는 기본적으로 iptables를 kube-proxy가 사용하지만, 별도 hardening으로 firewalld/ufw가 켜져 있으면 인바운드가 막힐 수 있습니다.

sudo systemctl status firewalld || true
sudo ufw status || true

NodePort 범위 변경

클러스터가 기본 범위(30000-32767)가 아닌 경우도 있습니다. API 서버 플래그(--service-node-port-range)는 EKS 관리형이라 직접 보기 어렵지만, 최소한 Service의 nodePort가 기대 범위인지 확인하세요.

kubectl get svc echo-np -o jsonpath='{.spec.ports[0].nodePort}{"\n"}'

NLB/ALB 뒤에서 NodePort를 쓰는 경우

LoadBalancer 타입이 내부적으로 NodePort를 사용하기도 합니다(특히 instance target). 이때는 NLB SG(또는 NLB 소스 CIDR) → 노드 SG 인바운드가 열려야 합니다.

  • NLB target type이 ip인지 instance인지에 따라 열어야 하는 대상이 달라집니다.

결론: “NodePort만” 안 열릴 때의 최단 루트

정리하면, EKS에서 NodePort만 안 될 때 가장 짧은 진단 루트는 아래 순서입니다.

  1. endpoints 존재 여부로 Service 백엔드부터 확정
  2. externalTrafficPolicy: Local 여부 확인(특히 특정 노드에서만 실패하는지)
  3. 노드에서 iptables/IPVS 규칙 존재 확인(kube-proxy 정상 동작)
  4. tcpdump로 “노드에 패킷이 들어오는지”부터 관측
  5. 들어오지 않으면 SG/NACL/라우팅(퍼블릭/프라이빗 서브넷 모델 포함)
  6. 들어오는데 Pod로 안 가면 CNI/커스텀 네트워크/라우팅/SNAT 설정 점검

NodePort는 디버깅에 유용하지만, 운영에서 외부 노출은 보안/운영 부담이 큽니다. 외부 트래픽은 가능하면 NLB/ALB로 수렴시키고, NodePort는 내부 진단/특수 케이스로 제한하는 방향이 장애와 리스크를 함께 줄입니다.