- Published on
EKS Pod DNS는 되는데 HTTPS만 실패할 때 점검
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스나 온프레미스에서는 잘 되던 코드가 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종류로 갈립니다.
- TCP 연결 자체가 안 됨:
connect timeout/ SYN이 나가고 응답이 없음 - TCP는 되는데 TLS 핸드셰이크 실패:
SSL_ERROR_SYSCALL,handshake timeout,unexpected EOF - 특정 도메인/특정 CDN만 실패: MTU, SNI, 경로 기반 필터링, 프록시 정책 가능성
- 간헐적: 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_EXTERNALSNATtrue: 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가 있으면 실패할 수 있습니다.
증상
curl이Connected까지 갔다가 멈춤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분 내에 좁혀집니다.
- Pod에서
nc -vz host 443- 안 되면 L3/L4(라우팅/SG/NACL/NAT)로 이동
- Pod에서
curl -Iv와openssl s_client- TCP는 되는데 TLS가 깨지면 MTU/프록시/inspection 의심
- 서브넷 라우팅 테이블
- 프라이빗:
0.0.0.0/0 -> NAT GW - 퍼블릭:
0.0.0.0/0 -> IGW
- 프라이빗:
- NACL 양방향 규칙(특히 ephemeral port)
- aws-node(CNI) SNAT 관련 env
EXTERNALSNAT,EXCLUDE_SNAT_CIDRS
- NAT GW CloudWatch 지표(PortAllocation/Drop)
- MTU ping DF 테스트
- 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와 처방
- NACL에서 ephemeral port 인바운드 미허용
- 처방: 프라이빗/퍼블릭(NAT GW) 서브넷 NACL에 1024-65535 허용
- 프라이빗 서브넷에 NAT GW 경로 없음(라우팅 테이블 오류)
- 처방:
0.0.0.0/0 -> NAT GW연결 확인
- 처방:
- CNI SNAT 제외 CIDR 설정 실수
- 처방:
AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS재검토, 실제 라우팅 존재 여부 확인
- 처방:
- MTU mismatch/PMTUD 차단
- 처방: 경유 장비(TGW/VPN/GWLB) 포함 경로에서 MTU 조정, ICMP 차단 정책 점검
- 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에서 생기는 문제를 계층별로 분리해서 보는 감각”을 잡는 데 도움이 됩니다.