🔥 VPC Endpoint: S3·DynamoDB에 나가지 않고 접근하기
강의 목차

한 달치 NAT Gateway: Private Subnet의 외부 연결의 청구서를 뜯어 본 적이 있다. 시간당 비용은 그러려니 했다. 처리 GB 비용 줄에서 손이 멈췄다. 절반이 S3 트래픽이었다. private subnet 안의 워커들이 같은 리전 S3 버킷에 객체를 올리고 내리는 그 트래픽이 NAT Gateway를 한 번, IGW를 한 번 거치고 인터넷 경계를 빙 돌아 다시 같은 리전으로 들어오고 있었다.
같은 리전이라도 S3 endpoint가 퍼블릭 도메인이라는 한 가지 사실 때문에 트래픽이 무조건 인터넷을 한 번 거친다. NAT Gateway가 그 인터넷 길에 깔린 통로니까, AWS는 S3로 가는 GB를 전부 NAT 청구서에 합친다. 의도한 적이 없는데 그렇게 되어 있었다.
VPC Endpoint는 그 우회를 끊는 도구다. VPC 안에서 S3, DynamoDB, KMS, STS 같은 AWS 서비스로 인터넷을 한 번도 거치지 않고 사설로 닿는 입구를 한 개 그어 둔다. 트래픽은 VPC를 떠나지 않는다. 정확히는 AWS 백본 안에서 끝난다.

두 type, Gateway와 Interface
VPC Endpoint는 흔히 쓰는 두 가지 형태로 나누어 쓴다. 한 가지는 Gateway Endpoint, 다른 한 가지는 Interface Endpoint(PrivateLink). 그 외에 GatewayLoadBalancer endpoint, 그리고 최근 추가된 Resource나 Service Network 같은 type도 있긴 한데, 일반 워크로드에서 매일 마주하는 형태는 이 두 가지다. 같은 메뉴 아래에 들어 있고 하는 일도 같은 줄에 있지만, 어디에 연결되는지가 다르다.
Gateway Endpoint는 라우팅 테이블에 한 줄로 들어간다. endpoint를 만들면 AWS가 prefix list ID(pl-xxxxxxxx)를 destination으로 하고 endpoint ID(vpce-xxxxxxxxxxxxxxxxx)를 target으로 하는 행을 자동으로 라우팅 테이블에 적어 준다. 그 행 하나가, 그 라우팅 테이블에 묶인 모든 인스턴스의 S3와 DynamoDB 트래픽을 가로채 백본으로 흘려 보낸다. 인스턴스 쪽 설정도, ENI 추가도 없다. 라우팅 한 줄이 전부다.
Interface Endpoint는 라우팅 테이블 대신 subnet에 ENI 한 장을 꽂아 둔다. AWS는 그 ENI에 subnet의 사설 IP 한 개를 부여하고, AWS가 만들어 두는 private hosted zone 한 칸이 원래 퍼블릭 service endpoint 도메인을 그 사설 IP로 해석한다. 예를 들어 인스턴스가 kms.ap-northeast-2.amazonaws.com을 부르면, DNS는 퍼블릭 IP가 아니라 그 ENI의 사설 IP를 반환한다. 코드는 같은 SDK 호출, 도메인은 같은 도메인이고 응답 IP만 사설 IP다. (S3는 historical 이유로 private DNS 동작이 다른 서비스와 살짝 다르고 별도 옵션이 붙는다.)

왜 Gateway는 S3와 DynamoDB 둘만일까
이 부분이 처음에는 비대칭으로 다가온다. AWS 서비스가 200개 가까이 있는데 Gateway Endpoint가 지원하는 건 S3(2015년 출시)와 DynamoDB(2017년 출시) 둘뿐이다. 다른 모든 서비스(KMS, STS, ECR, SecretsManager, SQS, SNS, 그리고 2021년부터는 S3까지도)는 Interface Endpoint로 따로 만들어야 한다.
이유를 단정할 수 있는 공식 한 줄을 못 찾았지만, 맥락을 따라가 보면 두 가지가 떠오른다. 첫째, Gateway Endpoint는 라우팅 테이블에 prefix list 한 줄을 쓰는 방식이라 destination이 AWS가 관리하는 IP 대역 집합이어야 한다. AWS가 그 대역을 prefix list로 publish하고 그 안에서 다시 갱신하는 메커니즘은 가벼운 구조라, 모든 서비스로 무한정 확장하기에는 그 구조가 맞지 않는다. 둘째, 2017년 PrivateLink 출시 이후 AWS는 신규 서비스를 거의 전부 Interface 쪽으로만 추가한다. S3가 2021-02에 Interface Endpoint도 받았지만, Gateway 쪽은 그대로 살려 둔 채로 둘 다 가능한 상태다. 이미 깔려 있는 라우팅 한 줄을 깨지 않으려는 보존 의도로 다가온다.
실무에서는 한 가지 규칙으로 굳어 있다. Gateway Endpoint가 가능하면 무조건 그걸 쓴다. S3와 DynamoDB는 둘 다 Interface Endpoint도 가능하지만, 비용 0과 시간당+GB당 두 줄을 비교할 사안이 아니다. 같은 리전 안에서 S3와 DynamoDB로 가는 길은 Gateway Endpoint가 기본값이고, Interface Endpoint는 온프레미스에서 사설로 닿아야 하는 하이브리드 시나리오나 cross-region 사설 접근처럼 Gateway가 못 닿는 경우에만 꺼낸다.
prefix list와 라우팅 평가
Gateway Endpoint를 만들 때 종종 헷갈리는 부분이 있다. 기존에 0.0.0.0/0이 NAT Gateway 대상으로 적힌 라우팅 테이블에 prefix list 한 줄이 더 들어오면 우선순위는 어떻게 되는가. 답은 라우팅 테이블: 패킷이 어디로 가는지에서 본 longest prefix match를 그대로 적용한다는 것이다.
S3의 prefix list가 publish하는 IP 대역은 0.0.0.0/0보다 훨씬 좁다. 그래서 AWS는 그 대역 안에 들어오는 트래픽에 0.0.0.0/0 → NAT보다 prefix list → vpce-xxxx 행을 우선 적용한다. 같은 리전 안 S3 트래픽은 endpoint를 거치고, 다른 모든 인터넷 트래픽은 그대로 NAT를 거친다. 한 줄을 더 적었을 뿐인데 청구서가 두 갈래로 나뉜다.
예외도 하나 알아 둔다. static CIDR 라우트가 prefix list 라우트와 겹치면, AWS는 static CIDR을 우선한다. 예를 들어 누가 52.219.0.0/16 → ...라는 한 줄을 따로 적어 두면, 그 대역에 한해서는 AWS가 endpoint 행을 무시한다. 그래서 endpoint를 깔고도 간헐적으로 S3 트래픽이 NAT로 새는 사례가 가끔 있다. 그 의심이 들면 라우팅 테이블을 한 번 전체로 보고, 누가 prefix list 위에 더 좁은 행을 박지 않았는지를 확인한다.
두 정책의 교집합
VPC Endpoint를 깔면 끝이 아니다. 누가 그 endpoint로 무엇에 닿을 수 있는지를 정하는 단계가 한 칸 더 생긴다. 그게 Endpoint Policy다. resource-based JSON으로 endpoint 자체에 매달리고, default는 "*"라서 모든 principal이 모든 action으로 닿을 수 있다. 거의 항상 좁혀 두는 게 맞다.
자주 헷갈리는 건 이 부분이다. Endpoint policy가 깔려도 IAM과 bucket policy는 같이 본다. endpoint를 통과한다고 해서 bucket policy의 deny가 사라지지 않고, IAM이 막은 action을 endpoint policy가 풀어 주지도 않는다. 세 정책 모두를 통과해야 AWS가 요청을 처리한다. 즉 교집합이다. AWS 공식 문서가 강조하는 한 줄("does not override or replace identity-based or resource-based policies")이 이 부분에서 매번 다시 확인하게 된다.
거꾸로도 마찬가지다. S3 bucket을 우리 VPC endpoint를 거친 요청에만 열고 싶을 때는, bucket policy 쪽에 aws:SourceVpce 조건을 박아 둔다.
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": { "aws:SourceVpce": "vpce-1a2b3c4d" }
}
}{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"StringNotEquals": { "aws:SourceVpce": "vpce-1a2b3c4d" }
}
}이 한 줄이 추가되면 그 bucket은 우리 endpoint를 통과한 요청에만 응답한다. AWS Console에서의 직접 접근도 함께 끊는다 (Console은 우리 endpoint로 안 오니까). 의도된 부작용이고, 그게 내부망 한정 bucket의 그림자 출구를 닫는 효과다.

Interface Endpoint의 가격, AZ가 곱셈 단위
Interface Endpoint는 지금까지 이어진 "관리형이 감추는 비용" 흐름이 가장 조용히 새는 항목 중 하나다. 가격 모델은 두 줄이고, 둘 다 AZ 단위다.
- 시간당 $0.01 / AZ. endpoint를 한 AZ에 enable해 두면 그 AZ에 ENI가 한 장 꽂히고, 그 ENI가 살아 있기만 해도 AWS는 시간당 비용을 청구한다. 트래픽이 0이어도 청구한다.
- 처리 GB당 $0.01. endpoint를 통과하는 모든 GB. 1 PB 이후로 단가가 내려가는 tier가 있긴 한데, 일반 워크로드 규모에서는 그 줄을 마주칠 일이 드물다.
3-AZ 표준 토폴로지(앞서 NAT Gateway: Private Subnet의 외부 연결에서 본 그 그림) 위에 Interface Endpoint를 한 개 깔면 3 × $0.01/h = $0.03/h가 빠져나간다. 한 달이면 약 $22, 1년이면 약 $263을 AWS가 청구한다. 한 개 endpoint 기준이다. KMS, STS, SecretsManager, CloudWatch Logs, ECR(api와 dkr 두 개), SSM 3종까지 흔히 깔리는 일고여덟 개가 곱셈으로 누적되면 1년에 $2,000 가까이가 그냥 살아 있는 ENI 값으로 빠져나간다. 처리 GB 비용은 그 위에 더 붙는다.
이 부분에서 두 가지를 의심한다. 첫째, 모든 AZ에 다 enable해 둘 필요가 있는지. 트래픽이 한 AZ에서 발생하면 그 AZ에만 켜 두면 된다(단, AZ 장애 격리 효과는 그만큼 줄어든다). 둘째, 멀티 계정에서 같은 endpoint를 곱셈으로 만들고 있는지. 계정마다 다 따로 만들면 AWS가 그 곱셈을 그대로 청구한다. cross-account 공유는 VPC Peering과 Transit Gateway: VPC를 잇는 두 방법에서 본 TGW를 끼고 공통 endpoint VPC에 endpoint를 한 번만 두는 패턴이 흔하다.
Gateway Endpoint와 비교하면 이 두 줄이 더 또렷하게 드러난다. S3와 DynamoDB는 Gateway 한 줄로 비용 0. 다른 모든 AWS 서비스는 Interface로 AZ × 시간 + GB. 같은 회사 안에 두 가지 가격 모델이 같은 메뉴 아래에 같이 살고 있다.
PrivateLink, 자기 서비스도 사설로 노출한다
VPC Endpoint의 두 type을 다 봤지만, PrivateLink는 그 위에 한 단계가 더 있다. AWS 서비스가 아니라, 내가 만든 서비스를 다른 사람의 VPC에 사설로 노출하는 길이다.
작동 그림은 거꾸로다. 내 VPC에 Network Load Balancer 또는 Gateway Load Balancer를 한 대 두고(NLB와 GWLB는 섹션 4에서 자세히 본다), 그걸 Endpoint Service로 publish한다. AWS가 service name(com.amazonaws.vpce.<region>.vpce-svc-xxxxxxxx)을 발급한다. 다른 계정이나 다른 VPC에 있는 consumer는 그 service name으로 자기 VPC에 Interface Endpoint 한 개를 만들면, 자기 subnet의 ENI에서 출발한 트래픽이 사설로 내 NLB에 닿는다.
VPC Peering과 Transit Gateway: VPC를 잇는 두 방법에서 본 도구들과 동작이 다르다. Peering이나 TGW는 양쪽 VPC를 같은 사설 망으로 묶는 도구다. 양방향, 라우팅 노출, 그리고 Peering의 경우 CIDR 겹침이 즉시 걸리는(TGW는 라우트 propagate가 안 되는) 제약이 따라온다. PrivateLink는 한쪽 방향, 한 서비스 endpoint만 노출한다. 양쪽 VPC의 라우팅이 서로에게 보이지 않으니 CIDR 겹침 문제에서 훨씬 자유롭다. SaaS 회사가 고객 VPC에 자기 API를 노출하는 길로 자주 쓰는 패턴이고, 멀티 계정 아키텍처에서 공통 서비스 계정을 묶는 길로도 흔히 쓴다.

언제 깔지 말아야 하는가
VPC Endpoint는 해 두면 무조건 좋은 도구가 아니다.
- 트래픽이 작은 dev 환경. Interface Endpoint 한 개를 3 AZ에 enable해 두면 트래픽 0이어도 한 달 $22가 빠져나간다. NAT Gateway를 거쳐서 처리되는 같은 트래픽이 한 달 $1도 안 되는 환경이라면, endpoint가 손해다. 같은 셈을 dev 계정마다 곱하면 격차도 더 커진다.
- cross-account 곱셈. endpoint는 VPC당이고 가격은 AZ당이다. 계정 30개에 같은 endpoint를 다 따로 깔면 30 × 3 × $0.01/h를 AWS가 청구서에 쌓는다. 공통 endpoint VPC와 TGW 패턴을 미리 정해 두지 않으면 이 곱셈이 조용히 새 나간다.
- 같은 리전 S3나 DynamoDB가 아닌데 Gateway를 찾는 경우. Gateway는 두 서비스 전용이다. 다른 서비스에는 Interface 한 가지뿐이고, 그건 가격 모델이 다르다. 둘이 비슷하게 생겼으니 비용도 비슷하겠지가 가장 비싸지는 함정.
Gateway는 항상, Interface는 필요한 곳에만
Gateway Endpoint는 거의 항상 깔아 둔다. 청구서에 한 줄도 더하지 않는데 NAT 청구서의 한 항목을 통째로 줄여 준다. Interface Endpoint는 그 endpoint 하나가 매달 $22 × AZ 수만큼을 켜 두는 일이라는 사실을 잊지 않은 채, 필요한 곳에만 깔아 둔다. 두 줄을 같이 머리에 두면 endpoint 콘솔에서 손이 너무 가벼워지지도, 너무 무거워지지도 않는다.
참고 자료
- Gateway endpoints (AWS Documentation): Gateway Endpoint 동작과 prefix list 메커니즘
- Configure an interface endpoint (AWS Documentation): Interface Endpoint 설정과 private DNS 동작
- AWS PrivateLink Pricing: Interface Endpoint 시간당과 GB당 가격 모델
- Control access to VPC endpoints using endpoint policies: endpoint policy와 IAM, bucket policy 평가 관계
- Controlling access from VPC endpoints with bucket policies:
aws:SourceVpce조건 사용 예시 - How route priority works: longest prefix match와 prefix list 우선순위
- AWS PrivateLink for Amazon S3 is Now Generally Available (2021-02): S3 Interface Endpoint 출시 발표
- Share your services through AWS PrivateLink: Endpoint Service로 자기 서비스 노출하기










