Published on

EKS Pod DNS는 되는데 HTTPS만 실패할 때 점검

Authors

서버리스나 온프레미스에서는 잘 되던 코드가 EKS로 올라오자마자 이상해지는 순간이 있습니다. 특히 Pod에서 DNS는 정상적으로 되는데 외부 HTTPS만 실패하는 케이스는, 애플리케이션 문제가 아니라 **네트워크 경로의 특정 계층(특히 egress/NAT/TLS 구간)**에서 끊기는 경우가 대부분입니다.

이 글은 “nslookup/dig는 되는데 curl https://...만 타임아웃/리셋/handshake 실패” 상황에서, VPC CNI의 SNAT 동작, NACL/보안그룹/라우팅, NAT Gateway/Instance 상태, MTU(특히 1500↔9001/터널링), 프록시/IPv6까지 포함해 재현 가능한 방식으로 원인을 좁혀갑니다.

> EKS 운영 중 인증/권한 이슈로 이미지가 안 받아지는 경우는 네트워크와 증상이 섞여 보일 수 있습니다. 별도 케이스는 Kubernetes ImagePullBackOff 401 - ECR·IRSA·imagePullSecrets도 참고하세요.

증상 정의: “DNS OK, HTTPS FAIL”을 정확히 분류하기

먼저 실패 형태를 분류해야 합니다. DNS는 되는데 HTTPS가 안 되는 증상은 크게 4종류로 갈립니다.

  1. TCP 연결 자체가 안 됨: connect timeout / SYN이 나가고 응답이 없음
  2. TCP는 되는데 TLS 핸드셰이크 실패: SSL_ERROR_SYSCALL, handshake timeout, unexpected EOF
  3. 특정 도메인/특정 CDN만 실패: MTU, SNI, 경로 기반 필터링, 프록시 정책 가능성
  4. 간헐적: SNAT 포트 고갈, conntrack 이슈, NAT GW/instance 부하

아래 커맨드로 “어디까지 되는지”를 먼저 찍습니다.

# 1) DNS 확인
kubectl exec -it deploy/myapp -- sh -c 'nslookup example.com || true'

# 2) TCP 레벨(443) 연결 확인
kubectl exec -it deploy/myapp -- sh -c 'nc -vz -w 3 example.com 443 || true'

# 3) TLS 핸드셰이크까지 확인
kubectl exec -it deploy/myapp -- sh -c 'curl -Iv --connect-timeout 3 https://example.com/ || true'

# 4) SNI/인증서 교섭 확인(openssl)
kubectl exec -it deploy/myapp -- sh -c 'openssl s_client -servername example.com -connect example.com:443 -brief < /dev/null || true'
  • nc도 안 되면 라우팅/NACL/SG/NAT/방화벽 쪽 가능성이 큽니다.
  • nc는 되는데 curl -Iv에서 TLS가 깨지면 MTU/프록시/중간장비/SSL inspection 쪽을 의심합니다.

1단계: Pod egress 경로를 그림으로 고정하기 (가장 중요)

EKS에서 Pod가 외부로 나갈 때 기본 경로는 대개 아래 중 하나입니다.

  • (A) Pod IP(Secondary IP) → 노드 ENI → (퍼블릭 서브넷이면) IGW → 인터넷
  • (B) Pod IP → 노드 ENI → (프라이빗 서브넷이면) NAT Gateway/Instance → IGW → 인터넷
  • (C) Pod IP → 노드 ENI → 사내 프록시/방화벽(Transit Gateway, GWLB 등) → 인터넷

여기서 “DNS는 된다”는 것은 보통 UDP/TCP 53은 열려 있다는 뜻이지, 443 egress가 정상이라는 뜻이 아닙니다. 또한 CoreDNS가 VPC 내부(예: Route 53 Resolver)로 질의하는 구조라면, DNS만 내부에서 해결되고 외부 443은 막혀도 이상하지 않습니다.

2단계: VPC CNI의 SNAT 동작 점검 (특히 private subnet)

SNAT이 왜 핵심인가

Pod가 VPC의 Secondary IP를 직접 쓰는 AWS VPC CNI 환경에서, 프라이빗 서브넷 Pod가 인터넷으로 나가려면 일반적으로 노드에서 SNAT 또는 NAT Gateway를 통해 소스가 변환되어야 합니다.

  • 노드/Pod가 프라이빗 서브넷에 있고, 라우팅 테이블이 0.0.0.0/0 -> NAT Gateway라면 보통은 NAT GW가 처리합니다.
  • 하지만 CNI 설정/라우팅/보안 정책에 따라 **노드에서 SNAT(ip-masq)**가 관여하거나, 특정 CIDR은 SNAT 제외(excluded)될 수 있습니다.

aws-node 설정에서 확인할 것

aws-node(aws-vpc-cni) 데몬셋의 환경변수를 확인합니다.

kubectl -n kube-system get ds aws-node -o yaml | sed -n '/env:/,/image:/p'

특히 아래 항목을 봅니다(클러스터/버전에 따라 존재 여부는 다름).

  • AWS_VPC_K8S_CNI_EXTERNALSNAT
    • true: CNI가 SNAT을 하지 않음(외부 NAT에 맡김). 프라이빗 서브넷이라면 NAT GW 경로가 확실해야 합니다.
    • false: 노드에서 iptables로 SNAT할 수 있습니다.
  • AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS
    • 여기에 외부로 나가야 할 대역이 들어가 있으면, SNAT이 빠져서 응답이 돌아오지 않는 문제가 납니다.

> 실무에서 자주 보는 패턴: 사내망/피어링/VPN CIDR을 exclude로 넣어둔 뒤, 실제로는 해당 경로가 없거나 NACL에서 막혀서 HTTPS만 실패.

노드에서 iptables/라우팅 확인

문제가 난 Pod가 올라간 노드로 들어가 확인하는 게 가장 빠릅니다.

# 노드에 SSM 또는 SSH로 접속 후
sudo iptables -t nat -S | sed -n '1,200p'

ip route

# conntrack 상태(간헐적/대량 트래픽에서 유용)
sudo conntrack -S || true
  • NAT 테이블에 MASQUERADE/SNAT 규칙이 어떻게 잡혀 있는지
  • 0.0.0.0/0 디폴트 라우트가 NAT GW/IGW 방향으로 제대로 잡혀 있는지

3단계: NACL/보안그룹에서 “리턴 트래픽”까지 확인

DNS는 되는데 HTTPS가 안 되는 대표 원인 중 하나가 NACL의 ephemeral port 허용 누락입니다.

  • 클라이언트(Pod) → 서버(443)로 나갈 때
  • 서버 → 클라이언트로 돌아오는 응답은 **클라이언트의 ephemeral port(대개 1024–65535)**로 들어옵니다.

체크리스트

  • Private Subnet NACL
    • Outbound: 443 허용
    • Inbound: 1024-65535 허용(응답 트래픽)
  • Public Subnet(NAT GW가 있는 서브넷) NACL
    • NAT GW는 상태 저장이 아니므로, NACL이 빡빡하면 HTTPS만 끊길 수 있습니다.

보안그룹(SG)은 stateful이라 보통 “나가는 443 허용”이면 돌아오는 응답은 자동 허용이지만, NACL은 stateless라 양방향 규칙이 필요합니다.

4단계: NAT Gateway/Instance 및 라우팅 테이블 검증

프라이빗 서브넷에서 외부 HTTPS만 실패한다면 NAT 경로를 의심해야 합니다.

빠른 검증

  • 해당 노드가 속한 서브넷의 라우팅 테이블에서
    • 0.0.0.0/0 -> nat-xxxx (프라이빗)
    • NAT GW가 속한 퍼블릭 서브넷은 0.0.0.0/0 -> igw-xxxx

NAT Gateway 상태

  • NAT GW가 Available인지
  • CloudWatch ErrorPortAllocation(포트 고갈), PacketsDropCount가 튀는지

간헐적 HTTPS 실패는 NAT GW 포트 고갈로도 나타납니다. 특히 많은 Pod가 짧은 커넥션을 대량으로 생성하면(HTTP keep-alive 미사용, 재시도 폭증 등) 443만 문제처럼 보일 수 있습니다.

5단계: MTU 문제로 TLS 핸드셰이크가 깨지는 케이스

DNS(작은 UDP 패킷)는 잘 되는데, TLS는 ClientHello/Certificate 등으로 패킷이 커지면서 PMTUD가 막히거나 MTU mismatch가 있으면 실패할 수 있습니다.

증상

  • curlConnected까지 갔다가 멈춤
  • openssl s_client가 중간에 타임아웃
  • 특정 사이트(특히 인증서 체인/확장 큰 곳)만 실패

Pod에서 MTU/PMTUD 테스트

# 인터페이스 MTU 확인
kubectl exec -it deploy/myapp -- sh -c 'ip link show eth0'

# DF 비트로 MTU 탐색(예: 1472 payload => 1500 MTU 기준)
# ping이 없다면 busybox/ubuntu 디버그 파드로 수행
kubectl run -it --rm netdebug --image=ubuntu:24.04 -- bash
apt-get update && apt-get install -y iputils-ping

ping -M do -s 1472 1.1.1.1
ping -M do -s 1372 1.1.1.1
  • 큰 사이즈에서 깨지면 MTU 이슈 가능성이 큽니다.
  • VPC 내부에서 TGW, VPN, GWLB, 프록시 장비를 거치면 권장 MTU가 달라지기도 합니다.

6단계: 프록시/SSL Inspection/네트워크 정책(egress) 확인

조직 환경에 따라 EKS 노드가 인터넷으로 직접 나가지 못하고 HTTP(S) 프록시를 반드시 거치게 구성된 경우가 있습니다.

  • Pod에 HTTP_PROXY/HTTPS_PROXY/NO_PROXY 환경변수가 필요한데 빠져 있거나
  • 반대로 프록시가 설정되어 있는데, 프록시가 443 CONNECT를 차단하거나 SNI 기반으로 제한

확인:

kubectl exec -it deploy/myapp -- sh -c 'env | egrep -i "proxy|no_proxy" || true'

# 프록시 우회 테스트
kubectl exec -it deploy/myapp -- sh -c 'HTTPS_PROXY= HTTP_PROXY= NO_PROXY=* curl -Iv https://example.com --connect-timeout 3 || true'

또한 Calico/Cilium 같은 CNI/NetworkPolicy를 쓰는 경우, DNS(53)만 허용하고 443 egress를 막아둔 정책이 흔합니다.

kubectl get networkpolicy -A
kubectl describe networkpolicy -n myns

7단계: IPv6/Happy Eyeballs로 “DNS는 되는데 연결은 실패”

example.com이 AAAA 레코드를 반환하고, Pod/노드가 IPv6 라우팅이 불완전하면 애플리케이션은 먼저 IPv6로 붙으려다 실패 후 IPv4로 폴백하면서 지연/실패가 발생할 수 있습니다.

테스트:

kubectl exec -it deploy/myapp -- sh -c 'getent ahosts example.com || true'

# curl에서 IPv4 강제
kubectl exec -it deploy/myapp -- sh -c 'curl -4Iv https://example.com --connect-timeout 3 || true'

-4로는 되는데 기본이 실패하면 IPv6 경로/정책을 점검해야 합니다.

8단계: 실전 트러블슈팅 런북(10분 컷)

아래 순서대로 하면 “어디가 문제인지”가 대부분 10~20분 내에 좁혀집니다.

  1. Pod에서 nc -vz host 443
    • 안 되면 L3/L4(라우팅/SG/NACL/NAT)로 이동
  2. Pod에서 curl -Ivopenssl s_client
    • TCP는 되는데 TLS가 깨지면 MTU/프록시/inspection 의심
  3. 서브넷 라우팅 테이블
    • 프라이빗: 0.0.0.0/0 -> NAT GW
    • 퍼블릭: 0.0.0.0/0 -> IGW
  4. NACL 양방향 규칙(특히 ephemeral port)
  5. aws-node(CNI) SNAT 관련 env
    • EXTERNALSNAT, EXCLUDE_SNAT_CIDRS
  6. NAT GW CloudWatch 지표(PortAllocation/Drop)
  7. MTU ping DF 테스트
  8. NetworkPolicy egress

재현용 디버그 Pod 매니페스트 (도구 포함)

운영 파드 이미지에 curl, openssl, dig가 없는 경우가 많으니 디버그 파드를 하나 만들어 두면 좋습니다.

apiVersion: v1
kind: Pod
metadata:
  name: netshoot
  namespace: default
spec:
  restartPolicy: Never
  containers:
    - name: netshoot
      image: nicolaka/netshoot:latest
      command: ["sleep", "36000"]

적용 후:

kubectl apply -f netshoot.yaml
kubectl exec -it netshoot -- bash

dig example.com
nc -vz example.com 443
curl -Iv https://example.com/
openssl s_client -servername example.com -connect example.com:443 -brief < /dev/null

자주 나오는 원인 TOP 5와 처방

  1. NACL에서 ephemeral port 인바운드 미허용
    • 처방: 프라이빗/퍼블릭(NAT GW) 서브넷 NACL에 1024-65535 허용
  2. 프라이빗 서브넷에 NAT GW 경로 없음(라우팅 테이블 오류)
    • 처방: 0.0.0.0/0 -> NAT GW 연결 확인
  3. CNI SNAT 제외 CIDR 설정 실수
    • 처방: AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS 재검토, 실제 라우팅 존재 여부 확인
  4. MTU mismatch/PMTUD 차단
    • 처방: 경유 장비(TGW/VPN/GWLB) 포함 경로에서 MTU 조정, ICMP 차단 정책 점검
  5. NetworkPolicy에서 443 egress 누락
    • 처방: DNS(53)뿐 아니라 필요한 FQDN/IP/포트에 대한 egress 추가

마무리: “DNS는 된다”는 신호를 과신하지 말기

DNS 성공은 단지 이름 해석이 가능한 상태일 뿐, HTTPS는 TCP 3-way handshake + TLS 핸드셰이크 + (종종) MTU/프록시/검사 장비까지 통과해야 합니다. EKS에서는 특히 VPC CNI의 SNAT 경계, NACL의 stateless 특성, NAT GW 경로, MTU가 얽히면서 “443만” 실패하는 모양이 자주 만들어집니다.

다음 글로는, 위 점검을 했는데도 간헐적으로만 깨지는 경우(재시도 폭증, 커넥션 재사용 실패, 타임아웃 설계 미흡)까지 포함해 애플리케이션 레벨에서 안정화하는 방법도 함께 다뤄볼 예정입니다. 비슷한 맥락의 운영 디버깅 글로는 EKS IRSA 설정 후 WebIdentityToken 오류 해결도 같이 보면 “EKS에서 생기는 문제를 계층별로 분리해서 보는 감각”을 잡는 데 도움이 됩니다.