Fargate 교과서 구성: ALB와 NAT Gateway는 언제 필요할까?
AWS 비용 대시보드를 열었는데 NAT Gateway가 Fargate보다 비싼 경우가 있습니다. 앱을 돌리는 값보다 앱이 인터넷으로 나가는 통로 값이 더 나가는 겁니다. 트래픽이 거의 없는 서비스에서도 그렇습니다.
이게 이상하게 보인다면, 교과서 구성의 각 조각이 무슨 일을 하는지가 아직 정리되지 않은 겁니다.
AWS에 컨테이너 앱을 올리는 방법을 검색하면 거의 같은 그림이 나옵니다. VPC를 만들고, 퍼블릭 서브넷에 ALB와 NAT Gateway를 두고, 프라이빗 서브넷에 Fargate 태스크를 넣고, 격리된 서브넷에 RDS를 둡니다. 이 글에서는 이걸 교과서 구성이라고 부르겠습니다.
흔히 이렇게 이해합니다
교과서 구성을 두고 가장 자주 보는 설명은 이겁니다.
컴퓨트를 프라이빗 서브넷에 두는 것이 보안의 기본이고, ALB와 NAT Gateway는 그걸 성립시키는 필수 부품입니다.
앞 절반은 맞습니다. 뒤 절반이 문제입니다. 이 설명대로라면 두 부품은 보안을 위해 존재하므로 빼는 건 곧 보안을 포기하는 것이 됩니다. 그래서 사용자가 세 명인 사내 도구에도 그대로 붙습니다.
실제로는 두 부품이 하는 일이 각각 명확하게 정해져 있습니다. 그 일이 필요 없는 환경에서는 돈만 씁니다. 그리고 NAT Gateway를 정말로 뺄 수 없게 만드는 이유가 하나 있는데, 그건 보안이 아닙니다. 마지막에 다루겠습니다.
퍼블릭 서브넷과 프라이빗 서브넷의 차이는 라우트 테이블뿐입니다
먼저 정리하고 갈 것이 있습니다. 서브넷 자체에 "퍼블릭"이라는 속성이 붙어 있는 게 아닙니다. AWS 콘솔에서 서브넷을 만들 때 종류를 고르는 항목이 없는 것도 그래서입니다.
퍼블릭 서브넷 라우트 테이블
10.0.0.0/16 → local
0.0.0.0/0 → igw-xxxx (인터넷 게이트웨이)
프라이빗 서브넷 라우트 테이블
10.0.0.0/16 → local
0.0.0.0/0 → nat-xxxx (NAT Gateway)
DB 서브넷 라우트 테이블
10.0.0.0/16 → local
(기본 라우트 없음)세 서브넷의 차이는 0.0.0.0/0이 어디를 가리키느냐, 또는 아예 없느냐입니다. DB 서브넷에 기본 라우트가 없다는 게 마지막 층의 핵심입니다. RDS는 인터넷으로 나갈 일이 없고, 나갈 길 자체가 없으면 설정 실수로 열릴 일도 없습니다.
가용 영역을 둘 이상 쓰는 것은 선택이 아닙니다. ALB는 최소 두 개 AZ의 서브넷을 요구하고, RDS 서브넷 그룹도 마찬가지입니다. 단일 AZ로 만들고 싶어도 만들어지지 않습니다.
ALB가 실제로 하는 일은 넷입니다
TLS를 종료합니다. ACM 인증서를 붙이면 갱신을 AWS가 알아서 하고, 앱은 평문 HTTP로 받습니다. 컨테이너 안에서 인증서를 다루지 않아도 됩니다.
헬스체크로 죽은 태스크를 갈아냅니다. 지정한 경로를 주기적으로 찔러 응답이 없으면 타깃에서 빼고, ECS가 새 태스크를 띄웁니다.
여기서 실무적으로 중요한 함정이 하나 있습니다. 헬스체크 경로가 데이터베이스를 확인하면 안 됩니다.
GET /api/health → 앱 프로세스가 살아 있는가 (라이브니스)
GET /api/ready → DB까지 준비됐는가 (레디니스)둘을 한 경로에 합치면, DB가 잠깐 흔들릴 때 ALB가 모든 태스크를 비정상으로 판정합니다. ECS가 전부 내리고 새로 띄우는데, 새 태스크도 DB를 못 보니 또 비정상이 됩니다. DB 장애가 앱 장애로 끝나지 않고 앱 전멸로 번집니다. 헬스체크는 라이브니스만 봐야 하고, 레디니스는 별도 경로에 두고 배포 판정에만 씁니다.
나머지 둘은 짧습니다. 태스크가 여럿일 때 요청을 나눠 보내고, AWS WAF를 붙이는 자리가 됩니다. 봇 차단이나 지역 차단을 앱 코드 밖에서 하려면 여기가 그 자리입니다.
태스크가 하나뿐이고 WAF를 쓰지 않는다면, ALB가 하는 일 중 실제로 남는 것은 TLS 종료와 헬스체크 두 가지입니다.
Fargate에서 헷갈리는 두 가지
Fargate는 네트워크 모드가 awsvpc 하나뿐입니다. 태스크마다 ENI가 붙고 VPC 안의 사설 IP를 하나씩 갖습니다. 그래서 ALB 타깃 그룹 타입이 instance가 아니라 ip가 되고, 보안 그룹도 인스턴스가 아니라 태스크 단위로 붙습니다.
더 자주 걸리는 건 IAM 역할이 두 개라는 점입니다.
실행 역할은 ECS가 태스크를 띄우려고 씁니다. ECR에서 이미지를 받고, CloudWatch Logs에 쓰고, SSM이나 Secrets Manager에서 환경변수를 읽는 권한이 여기 들어갑니다.
태스크 역할은 앱 코드가 씁니다. S3를 읽거나 SQS에 넣는 권한이 여기입니다.
이름이 비슷해서 헷갈리는데, 증상이 나는 시점이 다릅니다. 이미지를 못 받아와서 태스크가 아예 안 뜨면 실행 역할 문제이고, 태스크는 잘 떴는데 앱에서 S3 접근이 거부되면 태스크 역할 문제입니다.
NAT Gateway는 나가는 문입니다
프라이빗 서브넷의 태스크는 인터넷에서 닿을 수 없습니다. 그런데 나가야 할 일은 많습니다. 외부 API 호출, 패키지 다운로드, 웹훅 전송이 전부 아웃바운드입니다.
NAT Gateway가 그 통로입니다. 나가는 패킷의 출발지 주소를 자기 것으로 바꿔 내보내고 응답을 원래 태스크로 되돌립니다. 나가는 건 되고 들어오는 건 안 되는 단방향 문입니다.
요금 구조를 보면 왜 이게 최대 항목이 되는지 알 수 있습니다. AWS 공식 요금 기준으로 us-east-1에서 시간당 $0.045이고, 여기에 처리한 GB당 요금이 또 붙습니다. 시간당 요금이 붙는다는 건 트래픽이 0이어도 월 30달러대가 나간다는 뜻입니다.
교과서대로 AZ마다 하나씩 두면 이게 두 배가 됩니다. AZ당 하나를 두는 이유는 둘인데, NAT가 있는 AZ가 죽으면 다른 AZ 태스크도 인터넷이 끊기기 때문이고, AZ를 넘는 트래픽에는 교차 AZ 요금이 또 붙기 때문입니다.
S3와 DynamoDB만 쓴다면 NAT를 거칠 이유가 없습니다. VPC 게이트웨이 엔드포인트는 시간당 요금도 GB 요금도 없습니다. S3 트래픽이 많은데 NAT로 나가고 있다면 그냥 돈을 버리는 중입니다.
RDS에서 못 하는 것
관리형 데이터베이스의 값어치는 자동 백업과 시점 복구입니다. 이건 요금표의 인스턴스 값에 포함되어 있어서 눈에 잘 안 띄는데, 직접 구성하려면 시간이 꽤 듭니다.
보안 그룹을 쓸 때 요령이 하나 있습니다. 인바운드 소스를 CIDR이 아니라 보안 그룹 ID로 지정하는 겁니다. 태스크 IP가 바뀌어도 규칙을 고칠 필요가 없습니다.
Multi-AZ에 대한 오해도 흔합니다. 스탠바이는 트래픽을 받지 않습니다. 동기 복제만 받다가 장애 시 승격됩니다. 읽기 성능이 아니라 가용성을 위한 것이고, 요금은 두 배가 됩니다. 읽기 부하를 나누려면 리드 리플리카를 따로 만들어야 합니다.
그리고 온프레미스에서 옮겨 올 때 걸리는 지점이 있습니다. RDS는 superuser를 주지 않고 rds_superuser만 줍니다. ALTER SYSTEM이 막히고 파라미터는 파라미터 그룹으로 바꿉니다. 지원 목록에 없는 확장은 설치할 수 없습니다. 쓰던 확장 하나가 RDS 목록에 없으면 그 시점에 이전이 멈춥니다. 옮기기 전에 확장 목록부터 대조하는 게 순서입니다.
요금은 어디에 몰려 있나
작은 서비스 기준으로 항목을 세워 보면 대략 이런 모양이 됩니다. Fargate를 0.5 vCPU에 1GB로 상시 돌리고, RDS는 가장 작은 버스트 인스턴스를 씁니다. 여기에 ALB 하나와 NAT Gateway 하나를 붙입니다.
이때 NAT Gateway 하나가 Fargate와 RDS를 합친 것보다 비싸집니다. 앱도 아니고 DB도 아닌, 나가는 길을 뚫어 주는 부품이 가장 큰 항목이 됩니다. ALB가 그다음입니다.
두 부품을 빼면 전체가 절반 아래로 떨어집니다. 방법은 세 가지입니다. 태스크를 퍼블릭 서브넷에 두고 공인 IP를 부여해서 NAT 없이 나가게 하고, ALB 대신 CloudFront를 쓰거나 앞에 아무것도 두지 않고, RDS를 퍼블릭 엔드포인트로 두되 보안 그룹으로 막습니다.
마지막 항목은 오해를 사기 쉬워서 짚고 갑니다. "퍼블릭 접근 허용"은 인터넷에 열어둔다는 뜻이 아닙니다. 퍼블릭 서브넷에 엔드포인트를 둬서 DNS가 공인 IP로 해석된다는 뜻이고, 실제 접근 허용은 보안 그룹이 결정합니다. 인바운드를 태스크 보안 그룹 하나로만 한정하면 인터넷의 누구도 붙지 못합니다.
빼면 무엇을 잃는가
싸진 만큼 잃는 게 있습니다. ALB를 빼면 TLS 종료를 CloudFront로 옮겨야 하고, 헬스체크 기반 자동 교체가 약해지고, WAF를 붙일 자리가 바뀝니다.
실질적으로 가장 아픈 건 진입 주소입니다. 태스크를 다시 띄우면 공인 IP가 바뀝니다. 앞에 CloudFront를 두거나, 배포할 때마다 Route 53 레코드를 갱신하는 자동화가 필요합니다.
NAT를 빼면 태스크가 공인 IP를 갖게 되므로 노출면이 늘어납니다. 보안 그룹으로 막긴 하지만, "닿을 수 없음"과 "닿을 수 있으나 거부됨"은 다릅니다. 감사에서 컴퓨트를 프라이빗 서브넷에 두라고 요구하면 이 구성은 통과하지 못합니다.
진짜 이유는 고정 아웃바운드 IP입니다
여기가 이 글에서 가장 하고 싶은 이야기입니다.
NAT Gateway를 뺄 때 뒤늦게 발견하는 것이 있습니다. NAT를 쓰면 밖으로 나가는 모든 트래픽의 출발지 IP가 NAT의 Elastic IP 하나로 고정됩니다. 태스크가 몇 개든, 재시작을 몇 번 하든 같습니다.
NAT를 빼면 태스크마다 다른 공인 IP를 갖고, 재시작할 때마다 바뀝니다.
이게 문제가 되는 곳이 있습니다. 외부 API가 IP 화이트리스트를 요구하는 경우, 상대가 IP 기반으로 레이트 리밋을 거는 경우, 결제사나 금융 API처럼 출발지 IP 등록이 필수인 경우입니다.
그런 API를 하나라도 부른다면 NAT는 비용이 아니라 요건입니다. 뺄 수 있느냐 없느냐의 문제가 아니라 뺄 선택지가 아예 없습니다.
반대로 공개 API를 키로만 인증해서 쓴다면, 그리고 대부분의 지도 API와 LLM API가 그렇습니다, 이 문제는 생기지 않습니다.
처음에 말한 오해로 돌아가면, 사람들이 NAT를 두는 이유로 보안을 말하는데 실제로 뺄 수 없게 만드는 건 고정 IP 요건입니다. 그리고 이건 보안 요구사항이 아니라 상대방 시스템의 제약입니다.
판단 기준은 하나면 충분합니다
교과서 구성이 틀린 게 아닙니다. 외부에 공개된 서비스, 여러 태스크, 규정 준수가 걸린 환경에서는 저 조각들이 전부 제 일을 합니다.
문제는 그 조건이 아닌 곳에 그대로 복사할 때입니다. 사용자가 세 명인 사내 도구에 로드밸런서를 붙이면 트래픽을 나눌 대상이 없고, 외부 API를 키로만 부르는데 NAT를 두면 고정 IP를 아무도 쓰지 않습니다.
저는 이럴 때 질문 하나로 좁힙니다.
이 조각이 없으면 무엇이 실제로 깨지는가?
답이 나오지 않으면 아직 필요 없는 겁니다. ALB를 나중에 붙이는 건 하루 일이고, NAT를 나중에 붙이는 건 라우트 테이블 한 줄입니다. 처음부터 다 갖추는 것보다 왜 필요한지 알고 붙이는 편이 낫다고 봅니다.
다만 두 가지는 나중이 없습니다. 데이터베이스 확장 목록과 IP 화이트리스트 요건은 옮기기 전에 확인해야 합니다. 앞의 것은 이전을 막고, 뒤의 것은 구성 선택지를 지웁니다.
여기까지가 확실한 부분
요금 수치는 AWS 공식 요금 페이지 기준이고 리전마다 다릅니다. 특히 서울 리전은 us-east-1보다 높은데, 제3자 자료마다 값이 갈려서 정확한 액수가 필요하면 AWS Pricing Calculator로 확인하시는 게 맞습니다. 이 글에서 말하고 싶은 건 절대 액수가 아니라 항목 사이의 비율입니다. 그 비율은 리전이 바뀌어도 크게 흔들리지 않습니다.
구성 선택은 트래픽 규모와 팀 상황에 따라 달라집니다. 저는 작게 시작하는 쪽을 선호하지만, 운영 인력이 있고 장애 대응 기준이 정해진 조직이라면 처음부터 교과서 구성으로 가는 판단도 충분히 합리적이라고 봅니다.
참고 자료는 AWS Fargate 요금, Amazon RDS 요금, Amazon VPC 요금 페이지입니다.
함께 읽기
- AWS 소규모 배포 3가지: EC2, Lightsail, App Runner 비교앞 글에서 Fargate + RDS + ALB + NAT로 짠 교과서 구성을 뜯어봤습니다. ALB와 NAT Gateway를 빼면 요금이 절반 아래로 떨어진다는 것까지 봤습니다.
- Zoho Mail 발송 도메인 구성: SMTP·IMAP 연동에서 막힌 지점들애플리케이션에서 메일을 자동으로 보내려고 발송 전용 도메인을 하나 세웠습니다. DNS 레코드를 넣고 인증을 붙이는 작업 자체는 30분쯤 걸렸습니다. 나머지 세 시간은 전부 다른 데서 썼습니다.
- 커널 패치는 왜 재부팅해야 적용될까? Livepatch 와 자동 재부팅새로 만든 서버를 점검하다가 이걸 봤습니다.
- ingress 와 egress: 터널 설정과 청구서에서 만난 두 방향하루에 두 번 이 단어들을 만났습니다. 오전에는 터널 설정 화면에서 ingress 를 봤고, 오후에는 데이터베이스 청구 항목에서 egress 를 봤습니다. 몇 년째 둘 다 쓰고 있었는데, 그날에서야 같은 축의 양쪽이라는 게 눈에 들어왔습니다.
- Cloudflare 무료 플랜과 한국 엣지: TTFB 1초의 원인 분해데모 사이트 하나가 느렸습니다. 첫 바이트까지 1초가 걸렸습니다. 대시보드를 여는 화면이라 무겁긴 한데, 그래도 1초는 이상했습니다. 앱을 뜯어볼 생각으로 시작했다가, 결국 앱은 한 줄도 안 고쳤습니다.