🔥 NAT Gateway: Private Subnet의 외부 연결

1648자
18분

큰 푸른 사각형 VPC 안에 두 개의 subnet 박스가 놓여 있고, 왼쪽 어두운 'private subnet'에는 서버 두 대가, 오른쪽 밝은 'public subnet'에는 주황색 'NAT GW' 박스 한 개가 들어 있다. VPC 위쪽 경계에는 얇은 주황색 Internet Gateway 줄이 가로지르고, 그 위로 'Internet'이라는 회색 클라우드가 떠 있다. private subnet의 서버에서 시작해 NAT GW 박스를 통과하고 IGW 줄을 지나 인터넷 클라우드로 향하는 한 줄짜리 화살표가 그려져, 사설 인스턴스의 모든 outbound 트래픽이 NAT Gateway라는 좁은 통로 한 곳을 거쳐야 한다는 사실을 한 장으로 보여 주는 강의 표지

처음 private subnet에 EC2 한 대를 띄우고 apt-get update를 친 순간, 명령은 그냥 멈춰 있었다. 라우팅 테이블도 보안 그룹도 다 맞춰 둔 줄 알았는데. 그제야 지난 편 Internet Gateway: 외부로 나가는 유일한 길에서 IGW는 경계 한 줄짜리 객체이고, public IPv4가 없으면 패킷이 멈춘다 라고 정리해 둔 게 떠올랐다. private subnet 인스턴스에는 그 public IPv4가 없다. 그래서 다른 박스가 하나 더 필요하다.

그 박스가 NAT Gateway다.

박스 한 개의 위치

NAT Gateway는 public subnet 안에 연결되는 매니지드 NAT 서비스다. AZ에 묶인 자원이고, 기본적으로 EIP 한 개를 붙여 둔다. 그 EIP가 곧 바깥에서 본 한 인스턴스의 얼굴이다. 같은 NAT GW를 가리키는 private 인스턴스가 100대든 1만 대든 인터넷 쪽에서는 그 한 개의 IP만 노출한다 (필요하면 secondary IP를 더 붙일 수 있는데, 그 얘기는 뒤에서 따로 한다).

Internet Gateway: 외부로 나가는 유일한 길에서 IGW는 logically one-to-one NAT라고 적었다. NAT Gateway는 거기에서 한 글자만 다르다. N:1 NAT. NAT는 여러 인스턴스의 사설 IP를 자기 EIP 하나로 합친다. 그래서 인스턴스마다 EIP를 붙여 줄 필요가 없고, 그래서 한 박스가 바깥으로 나가는 좁은 통로 역할을 한다.

두 개의 패널이 가운데 세로 줄로 나뉜 비교 다이어그램. 왼쪽 패널 'IGW: 1:1 NAT' 아래에는 EC2 인스턴스 '10.0.0.5' 한 대, 가운데 주황색 IGW 줄, 오른쪽 'public 54.x.x.10' 클라우드가 한 줄로 늘어서 있고 캡션이 '1 private IP ↔ 1 public IP'를 적어 둔다. 오른쪽 패널 'NAT Gateway: N:1 NAT' 아래에는 EC2 세 대 '10.0.1.5/6/7'이 왼쪽에 쌓여 있고 가운데에 주황색 NAT GW 한 박스 "EIP 54.x.x.99"가 놓여 세 개의 화살표가 그 박스로 모인 뒤 한 개의 화살표가 오른쪽 'Internet'으로 나간다. 캡션이 'N private IPs share 1 EIP'를 적어 두어, IGW와 NAT Gateway가 똑같이 NAT라고 불리지만 매핑 방식이 1:1과 N:1로 다르다는 사실을 한 장으로 보여 주는 비교 다이어그램

"단방향"이라는 말의 정확한 뜻

다이어그램과 자료에서는 outbound only나 단방향 이라는 표현을 자주 쓴다. 처음 봤을 때 나도 이 부분에서 한참 헤맸다. 응답 패킷도 못 들어오는 줄 알았다. 그게 아니다.

NAT Gateway는 stateful한 connection tracking 테이블을 유지한다. 안쪽 인스턴스가 바깥 80번 포트에 SYN을 던지면, NAT는 (인스턴스 사설 IP, 포트) ↔ (NAT EIP, 포트) 한 항목을 만들어 둔다. 그 항목 위에서 오는 응답 패킷은 거꾸로 매핑되어 인스턴스로 잘 돌아온다. 막히는 것은 바깥에서 먼저 시작하는 새 연결 이다. 인터넷의 어떤 호스트도 NAT Gateway의 EIP에 처음으로 손을 뻗어서 안쪽 인스턴스에 닿을 수 없다. 그게 보안 속성이고, 단방향 이라는 말의 정확한 의미다.

패킷 한 줄의 여행

private 인스턴스가 https://example.com을 부르면 패킷이 가는 길은 이렇다.

  1. 인스턴스의 라우트 테이블이 0.0.0.0/0 → NAT Gateway 한 줄을 본다. (라우팅 테이블: 패킷이 어디로 가는지에서 이 매칭 규칙을 봤다.)
  2. 패킷은 같은 VPC 안 NAT Gateway까지 간다. NAT는 출발지 IP를 자기 사설 IPv4로 바꾸고 conntrack 항목을 적어 둔다. (이 단계는 처음 마주치면 헷갈리기 쉽다. NAT GW 안에서 사설 IP가 곧장 EIP로 가지 않는다.)
  3. NAT Gateway가 속한 public subnet의 라우트 테이블이 다시 0.0.0.0/0 → IGW를 본다.
  4. IGW가 1:1 NAT 동작으로 NAT의 사설 IPv4를 NAT에 연결된 EIP로 매핑해 바깥 인터넷에 흘려보낸다.

응답은 같은 길을 거꾸로. NAT의 conntrack 항목이 응답을 보고 인스턴스 사설 IP로 다시 매핑한다. 한 흐름에 NAT GW 한 번, IGW 한 번. AWS는 NAT를 시간당+GB당으로 청구하고, IGW 자체는 처리 요금이 0이지만 AWS는 그 EIP의 PublicIPv4 시간당 비용을 별도 항목으로 청구한다.

VPC 컨테이너 안에 위·아래로 두 subnet이 쌓여 있다. 아래는 어두운 'Private Subnet'이고 그 안에 'EC2 (10.0.1.5)' 라벨의 서버 한 대가, 위는 밝은 'Public Subnet'이고 그 안에 주황색 'NAT GW (priv 10.0.10.7, EIP 54.x.x.x)' 박스가 들어 있다. VPC 윗 경계에는 주황색 IGW 줄이, 그 위로 'Internet (203.0.113.10)' 회색 클라우드가 있다. 네 개의 번호 화살표가 EC2에서 출발해 NAT GW로 들어가고, NAT 안에서 src를 10.0.10.7으로 바꾸고 conntrack 항목을 적은 뒤, IGW 줄로 올라가 EIP 54.x.x.x로 매핑돼 인터넷으로 나간다. 아래쪽 캡션이 응답이 같은 길을 거꾸로 거치며 conntrack이 다시 10.0.1.5로 매핑한다는 점을 적어 둔, NAT Gateway outbound 한 흐름의 시퀀스 다이어그램

AZ에 갇힌 박스

여기가 처음 NAT를 그릴 때 가장 자주 헛디디는 대목이다. NAT Gateway는 AZ 자원 이다. 한 NAT GW는 한 AZ 안에서만 살고, 그 AZ가 죽으면 같이 죽는다.

그래서 진짜 운영 토폴로지는 AZ당 NAT GW 한 개 + AZ당 RT 한 개다. ap-northeast-2-a의 private subnet은 ap-northeast-2-a의 NAT를 가리키고, ap-northeast-2-c의 private subnet은 ap-northeast-2-c의 NAT를 가리킨다. 한 AZ가 죽어도 다른 AZ는 자기 NAT로 계속 나간다. 단일 NAT GW로 cross-AZ 트래픽까지 빼면, AZ 간 data transfer 비용이 한 번 더 붙고, 단일 장애점도 같이 생긴다.

두 패널이 가운데 세로 줄로 나뉜 토폴로지 비교 다이어그램. 왼쪽 패널 빨간 헤더 'Single NAT spans 3 AZs (anti-pattern)' 아래에 푸른 VPC 컨테이너가 'AZ-a / AZ-b / AZ-c' 세 줄로 가로 분할돼 있고, 각 AZ의 왼쪽에 어두운 private subnet 박스가 놓여 있다. 주황색 NAT GW 아이콘은 AZ-a 한 줄에만 들어 있고, 세 AZ의 private subnet에서 출발한 가는 화살표 세 개가 모두 그 NAT GW로 모이는 모양에 빨간 점선이 둘러져 있다. 캡션은 'AZ-a fails so all three AZs lose internet'. 오른쪽 패널 초록 헤더 'Per-AZ NAT (recommended)' 아래에는 같은 VPC가 같은 세 AZ로 나뉘어 있고, 이번에는 각 AZ 안에 자기만의 NAT GW가 들어 있어 짧고 곧은 화살표 세 개가 AZ 경계를 넘지 않은 채 같은 AZ 안에서 닫힌다. 캡션은 'AZ failure isolated. Bill = 3 times NAT GW hourly'. 단일 NAT GW로 cross-AZ를 빼는 패턴이 단일 장애점을 만들고, 권장 토폴로지는 AZ당 NAT GW 한 개라는 사실을 한 장에 담은 비교 다이어그램

2025-11에 Regional NAT Gateway라는 변형이 새로 나왔다. AZ에 묶지 않고 워크로드가 있는 AZ로 자동으로 퍼지는 모드다. 옵트인이고, 기본은 여전히 AZ-scoped(이제 Zonal 이라고 부른다). 새 토폴로지를 짜고 단순화가 우선이면 후보지만, 이 글의 기본 그림은 Zonal 쪽이다.

다시 비용

비용 구조는 Subnet: Public과 Private의 진짜 차이에서 본 그대로다. ap-northeast-2 기준 시간당 $0.045 + 처리 GB당 $0.045, 두 줄. 여기서는 한 줄만 더 박아 둔다. AWS는 그 비용을 AZ 수만큼 그대로 곱해 청구한다. 3-AZ 권장 토폴로지를 따르면 NAT GW 시간당 비용도 그대로 3배다.

첫 글에서 다룬 "관리형이 감추는 비용" 흐름에서 NAT는 IGW의 PublicIPv4 시간당 $0.005보다 한 자릿수 더 무거운 항목이다.

55,000이라는 숫자

운영에서 의외로 자주 부딪히는 한도가 IPv4 한 개와 unique destination 한 쌍당 동시 55,000 연결이라는 한도다. 같은 외부 endpoint(외부 OAuth provider, 외부 결제 게이트웨이, 외부 DB) 한 곳으로 fan-out 하는 워크로드라면 가까워지는 숫자다. 한도를 넘기면 새로 시도한 connection이 silent하게 실패한다.

탈출구 두 가지: secondary IPv4 주소를 NAT GW에 최대 7개까지 더 붙여 1+7=8개로 8 × 55,000 = 440,000까지 늘리거나, NAT GW 자체를 분할한다 (Public NAT의 EIP 연관 한도는 기본 2개라, 8개까지 채우려면 service quota 증액 요청이 같이 필요하다). AWS가 secondary IP attach 기능을 따로 둔 건 이 한도 때문이다.

대역폭 자체는 5 Gbps에서 시작해 AWS가 100 Gbps까지 자동 확장한다. 패킷 처리량은 1M PPS에서 10M PPS까지. 워크로드 패턴에 따라 다르긴 하지만, 내가 본 한도 이슈는 대역폭보다 connection 쪽이 압도적으로 많았다.

350초 RST: silent failure 지점

idle 350초가 지난 TCP connection을 NAT Gateway는 자기 conntrack 테이블에서 지운다. 다음 패킷이 오면 RST 한 번 돌려보내고 끝. FIN이 아니다. application이 350초 idle을 모르고 그 위에 메시지를 다시 쓰면 connection reset 한 번 맞고 그대로 종료한다. TCP keepalive를 350초보다 짧게 두는 게 표준 처방이다.

한 가지 헷갈리는 대목이 있다. 2023-11에 AWS가 도입한 configurable connection tracking idle timeout은 NAT GW의 350초 자체를 바꾸는 기능이 아니다. 그쪽은 ENI 단위, Nitro 인스턴스의 보안 그룹 conntrack을 다루는 별도 기능이다. NAT GW의 350초는 여전히 고정값이다.

언제 NAT GW를 쓰지 말아야 하는가

두 번째 짚을 큰 흐름. 답은 Gateway Endpoint다.

S3와 DynamoDB는 Gateway Endpoint라는 별도의 경로를 제공한다. private subnet에서 S3/DynamoDB로 가는 트래픽을 VPC 라우팅 테이블에 한 줄 추가하는 것만으로 NAT Gateway를 거치지 않게 만들 수 있다. 시간당 비용 0, 처리 GB당 비용 0. AWS의 흔치 않은 무료 경로.

private subnet 워크로드의 outbound가 주로 S3 다운로드(데이터 처리 잡, 컨테이너 이미지 캐시, 정적 자산 빌드 같은 케이스)인 경우, Gateway Endpoint 없이 NAT만 쓰면 청구서가 그대로 NAT 처리 GB당 $0.045 항목으로 차곡차곡 쌓는다. Gateway Endpoint를 같이 그어 두면 그 비용이 0이 된다.

다만 Gateway Endpoint를 지원하는 서비스는 S3와 DynamoDB 단 둘 이다. 다른 AWS 서비스에 사설로 닿고 싶으면 Interface Endpoint(PrivateLink)인데, AWS는 그쪽에 시간당 + GB당 두 항목을 따로 청구한다.

푸른 VPC 컨테이너 안 'Private Subnet'에 EC2 인스턴스 한 대가 놓여 있고, 거기서 두 갈래의 길이 오른쪽 Amazon S3 버킷으로 향한다. 위쪽 길은 빨간 달러 칩이 붙어 EC2에서 주황색 'NAT Gateway' 박스, 주황색 Internet Gateway 줄, 회색 'Internet' 클라우드를 차례로 지나 S3 버킷으로 가며 캡션이 '$0.045/hr (NAT GW) + $0.045/GB (NAT processing) + standard data transfer'를 보여 준다. 아래쪽 길은 초록 free 칩이 붙어 EC2에서 VPC 경계에 붙은 초록색 'S3 Gateway Endpoint' 박스를 거쳐 곧장 S3 버킷으로 간다. 캡션이 'Free. No NAT, no IGW. Route table entry only. S3 + DynamoDB only.'를 적는다. 아래쪽 하단 띠에 "Same destination, two paths, very different bills"가 적혀, 같은 S3 목적지에 두 개의 길이 있고 청구서가 한쪽에서 0이라는 사실을 한 장으로 정리한 비교 다이어그램

NAT Instance 한 줄

옛 이름으로 NAT Instance가 있었다. EC2 인스턴스 한 대에 NAT 라우팅을 직접 돌리는 방식이다. AWS는 deprecated 처리는 안 했지만 NAT Gateway로 마이그레이션을 권장하는 입장이고, 운영 환경에서 NAT Instance를 새로 만드는 사례는 거의 보이지 않는다. 포트 포워딩이 꼭 필요하거나 dev 환경 비용을 마지막 한 푼까지 깎는 경우 정도가 예외다.

마치며

프로덕션에서는 매번 같은 세 줄을 함께 적는다. AZ당 NAT GW 한 개. AZ당 라우트 테이블. S3/DynamoDB Gateway Endpoint 한 줄. 단일 NAT으로 cross-AZ를 빼지 않고, S3 트래픽은 NAT 청구서에서 빼낸다. 그 세 줄이 NAT Gateway라는 박스가 청구서 1등으로 올라가는 가장 흔한 흐름을 미리 차단한다.

참고 자료

YouTube 영상

채널 보기
AI는 데이터를 어떻게 분류할까? 벡터의 거리와 KNN 알고리즘 | 선형대수학
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학
우리가 매일 쓰는 맞춤법 검사기와 라우터 속에 숨겨진 알고리즘은? | Trie 자료구조 이야기
행렬의 기본 연산 - 행렬 덧셈, 스칼라 곱, 전치 | 선형대수학
Trie 자료구조 파이썬 구현: Search와 Starts With 연산 | Trie 자료구조 이야기
AI 추천 시스템의 원리, 벡터 사이의 각도와 코사인 유사도 | 선형대수학
스칼라 곱셈과 내적의 기하학적 의미 | 선형대수학
AI는 왜 수백 차원의 벡터를 사용할까? 고차원 공간과 행렬 | 선형대수학