🔥 Lambda와 VPC: Private 리소스에 접근하는 비용

1309자
15분

Lambda 함수가 어디서 도는지 보여 주는 표지 그림. 왼쪽 'Lambda Service VPC' 박스 안에 함수 캡슐들이 있고, 거기서 Hyperplane ENI 화살표가 오른쪽 'Your VPC' 박스로 이어진다. 오른쪽 박스 안에는 ENI 노드와 RDS 아이콘이 있다.

처음 배포할 때 한참을 헤맸다. RDS Proxy를 VPC private subnet에 깔아 두고, Lambda 함수가 거기에 닿게 하려고 콘솔에서 VPC 설정을 켰다. 함수가 RDS에는 무사히 닿았다. 그런데 같은 함수가 외부 API 한 곳에 보내는 HTTPS 요청이 전부 timeout으로 끝났다. 처음엔 보안 그룹 outbound를 잘못 잠갔나 싶었는데, 그게 아니었다. Lambda 함수를 VPC에 넣는 순간, AWS가 그 함수의 인터넷 egress 기본 경로를 차단해 둔다. NAT Gateway 같은 egress 자원을 따로 깔아 주지 않으면 다시 통하지 않는다. 그날 처음으로 "VPC 연결"이라는 콘솔 토글이 한 줄짜리 결정이 아니라는 걸 진지하게 짚었다.

Lambda란 무엇인가: 서버리스의 경계에서 관리형이 감추는 부담 네 갈래를 적어 두었는데, 셋째 갈래가 VPC connectivity였다. 거기서는 한 줄로만 약속을 해 두고 디테일은 이 글로 미뤘다. 그 약속을 짚는다.

함수는 처음부터 어떤 VPC 안에서 동작한다, 단지 내 VPC가 아닐 뿐

가장 먼저 깨야 할 그림이 있다. Lambda 함수가 평소에 "VPC 밖에 있다"는 표현을 자주 쓰는데, 사실 Lambda는 함수를 항상 어떤 VPC 안에서 돌린다. 단지 그 VPC가 내 계정 소유가 아닌, Lambda 서비스가 직접 운영하는 VPC일 뿐이다. AWS 공식 블로그가 "Every Lambda function runs inside a VPC owned and managed by the Lambda service"라고 적어 둔 이유다.

그러니까 함수에 VPC 설정을 추가한다는 행위는 함수를 VPC에 새로 넣는 게 아니다. 함수는 여전히 Lambda 서비스 VPC에서 돈다. 다만 그 함수가 내 VPC 안의 자원에 닿을 길을 하나 따로 뚫는다는 뜻이다. 그 길의 종점에 놓인 자원이 ENI(Elastic Network Interface)이고, 2019년 9월부터는 그 ENI가 Hyperplane ENI라는 공유 네트워크 인터페이스로 바뀌었다. Cold Start와 콜드스타트를 줄이는 길에서 이 ENI 도입이 VPC cold start를 한 자릿수 ms로 단축한 사건이라는 걸 한 줄 적어 두었는데, 이 글은 그 메커니즘 자체를 짚는다.

SG와 Subnet 한 짝당 ENI 한 개: 함수가 아니라 조합이 단위

여기서 가장 혼란스러웠던 사실이 ENI의 단위다. 옛날 모델에서는 함수가 동시 실행으로 새 환경을 띄울 때마다 ENI를 그 시점에 새로 붙이도록 짜여 있었다. 동시 실행이 늘면 ENI도 같이 늘어났고, 그 ENI 생성 시간이 cold start 안에 그대로 들어왔다. 그래서 VPC를 붙인 함수의 cold start가 여러 초씩 컸다.

지금은 단위가 아예 다르다. 내 계정 안에서 같은 (보안 그룹, Subnet) 짝을 공유하는 모든 함수가 ENI 한 개를 함께 쓴다. 함수 A가 (sg-app, subnet-a)로 VPC를 붙이고, 함수 B도 (sg-app, subnet-a)로 붙이면, 두 함수 뒤에 매달린 Hyperplane ENI는 같은 한 개다. 함수 C가 (sg-app, subnet-b)로 붙으면 그제서야 AWS가 새 ENI를 한 개 더 만든다.

Hyperplane ENI 공유 구조 그림. 같은 (SG, Subnet) 짝을 공유하는 함수들이 한 개 ENI를 함께 쓰는 모습이고, 짝이 다른 함수는 별도 ENI에 붙어 있다.

ENI 한 개당 동시 연결 65,000개까지 처리한다. 그 한도를 넘으면 Lambda 서비스가 자동으로 같은 짝에 ENI를 한 개 더 띄운다. 그러니까 함수 동시 실행이 1,000개로 늘어도 ENI는 그 비례만큼 늘지 않는다. 평소 운영 워크로드에서는 SG·Subnet 조합 수가 ENI 수의 사실상 상한이다.

ENI는 또 함수 호출 시점이 아니라 함수를 만들거나 VPC 설정을 바꿀 때 AWS가 미리 준비한다. 콘솔에서 VPC 토글을 켜고 저장 버튼을 누르면 새 ENI가 준비될 때까지 함수 상태가 잠깐 Pending에 머물고(드물게는 몇 분), 준비가 끝난 다음부터는 호출이 들어와도 그 ENI 위로 터널만 이으면 된다. ENI 생성 비용이 매 cold start에서 한 번씩 들었던 옛날과 달리, 지금은 함수 lifecycle의 한 번뿐인 비용으로 위치를 바꾼 셈이다. AWS는 이 변화로 cold start의 VPC 부담을 "극적으로 단축했다(dramatically reduces)"라고만 적었고 정확한 ms 숫자는 공식 문서에 안 적혀 있는데, 외부 측정으로는 한 자릿수 ms대까지 짧아진 결과를 보고한다.

ENI 라이프사이클 비교 그림. 위쪽은 2019년 이전으로 cold start마다 새 ENI를 만드는 흐름이고, 아래쪽은 Hyperplane ENI로 함수 생성 시점에 한 번만 ENI를 만들고 매 호출이 그 ENI를 다시 쓰는 흐름이다.

그래서 진짜 비용은 어디로 갔나: 세 갈래로 나누어 짚는다

cold start 비용이 거의 사라진 대신 다른 세 가지 비용이 들어섰다. 하나씩 짚는다.

VPC 연결을 켜면 따라오는 세 가지 비용을 보여 주는 그림. 가운데 'Lambda function attached to VPC' 박스에서 세 갈래가 뻗어 Subnet IP 소진, 인터넷 egress 단절, 운영 복잡도 카드 셋으로 이어진다.

첫째, Subnet IP 소진. Hyperplane ENI는 내 계정의 VPC 안에 매달리는 자원이다. 그러니까 내 subnet의 사설 IP를 한 개씩 차지한다. SG·Subnet 조합이 늘어나거나 한 짝의 동시 연결 수가 65,000을 넘어 ENI가 자동으로 추가되는 시점마다 IP가 한 개씩 더 줄어든다. /28 같은 작은 subnet(16개 IP, 그 중 5개는 예약 IP로 묶여 있어 11개만 사용 가능)을 Lambda용으로 잡아 두면, 다른 자원과 IP를 나눠 쓰다가 빠르면 한 자릿수 함수에서 SubnetIPAddressLimitReachedException이 뜬다. 운영에서는 IP 여유를 넉넉히 잡아 두는 게 안전한 default다. 우리 팀은 Lambda 전용 subnet을 /24 단위(251개 사용 가능)로 잡아 두는 편이고, 더 큰 함수 풀에서는 /22까지 올린 사례도 있다.

둘째, AWS가 인터넷 egress 경로를 차단한다. 이게 처음에 내가 한참 헤맸던 부분이다. Lambda를 VPC에 붙이는 순간 Lambda 서비스 VPC 쪽에서 자동으로 받던 인터넷 egress 경로가 사용자 영역에서 통하지 않는다. 함수에 외부 API 호출(예: Slack webhook, Stripe API, 외부 LLM endpoint)이 들어 있으면, 그 호출이 통하려면 Subnet: Public과 Private의 진짜 차이에서 본 두 길 중 하나를 직접 깔아야 한다.

길은 두 갈래로 나뉜다. 하나는 NAT Gateway다. 일반 인터넷 egress의 기본 답이다. AWS VPC pricing 페이지의 us-east-1 예시 기준 AZ당 시간당 약 $0.045 + 처리 GB당 약 $0.045가 매달 청구서에 자동으로 들어간다(서울 리전은 살짝 더 비싸다). 작은 워크로드에서도 한 NAT GW만 띄워 두면 가용성을 한 단계 낮추는 대신 월 $32 안팎에서 시작하고, 멀티 AZ로 안정성을 챙기면 AZ 수만큼 액수가 늘어난다.

다른 하나는 VPC Endpoint다. AWS 서비스 안에서 끝나는 트래픽이라면 NAT를 우회한다. S3와 DynamoDB는 Gateway Endpoint가 시간당·GB당 추가 요금이 0이라 제일 싼 길이고, 그 외 AWS 서비스(또는 PrivateLink로 노출된 SaaS)는 Interface Endpoint가 시간당 약 $0.01 + GB당 약 $0.01 선에서 시작하니 보통 NAT보다 싸다. 외부 SaaS API가 PrivateLink 카탈로그에 없는 경우(많은 일반 공개 API가 그렇다), 사실상 NAT GW가 유일한 답이 된다. IPv6를 쓸 수 있으면 egress-only IGW가 한 길 더 열리지만 운영에서 흔히 다루는 설정은 아니다.

셋째, 운영 복잡도. VPC에 들어간 함수는 보안 그룹·라우팅·NAT 헬스를 다 신경 써야 한다. 한 번은 새벽에 멀쩡히 돌던 함수가 갑자기 외부 API timeout만 뱉기 시작했는데, 원인이 NAT GW가 들어 있는 public subnet의 라우팅 테이블에서 IGW 행 한 줄이 빠진 것이었다. VPC를 안 쓰면 절대 부딪힐 일이 없는 종류의 사고다. 정책 평가 흐름에서 본 silent failure와 비슷한 패턴이다. 네트워크 한 줄이 빠지면 같은 식으로 조용히 멈춘다.

그래서 언제 VPC에 안 넣어도 되나

Lambda를 VPC에 안 붙여도 되는 경우를 정리한 결정 트리 그림. 첫 노드는 VPC 안 자원에 닿아야 하는지 묻고, 답이 No면 outside-VPC로 두고, Yes면 외부 API 호출 여부에 따라 VPC Endpoint 또는 NAT GW로 분기한다.

여기서 한 박자 멈추는 게 도움이 됐다. VPC 안의 자원에 직접 닿을 일이 없으면 함수를 VPC에 안 넣는 게 거의 항상 더 단순하고 싸다. 외부 API만 호출하는 함수, S3·DynamoDB만 읽고 쓰는 함수, EventBridge로 다른 서비스와 메시지만 주고받는 함수는 default outside-VPC로 두면 NAT GW도 IP 소진도 추가 ENI도 신경 쓸 일이 없다. 공식 가이드도 VPC 연결이라는 옵션은 정말 필요한 함수만 골라서 켜라는 입장으로 적혀 있다.

VPC를 반드시 써야 하는 경우가 두 갈래 정도 떠올랐다. 하나는 ElastiCache처럼 VPC 바깥에서는 닿을 길이 아예 없는 자원, 또는 RDS·Aurora를 일부러 private 모드로 두고 외부에 노출하지 않는 클러스터에 함수가 닿아야 할 때다. 다른 하나는 컴플라이언스 요구로 모든 트래픽이 내 VPC 안에 머물러야 할 때(보통 PrivateLink 조합으로 해결한다).

이 둘이 아닌데 VPC를 켜 둔 함수가 우리 코드베이스에도 한두 개 있어서 떼어 봤다. 외부 API만 호출하던 작은 ETL 함수 한 개를 VPC에서 빼니 NAT 처리량이 눈에 띄게 줄었다. 한 줄짜리 콘솔 토글이지만, 비용 곡선이 따라오는 토글이다.

참고 자료

YouTube 영상

채널 보기
트라이(Trie)에서 단어를 삭제하는 방법 | Trie 자료구조 이야기
직교성과 벡터 투영 | 선형대수학
스칼라 곱셈과 내적의 기하학적 의미 | 선형대수학
AI는 데이터를 어떻게 분류할까? 벡터의 거리와 KNN 알고리즘 | 선형대수학
Trie 자료구조 파이썬 구현: Search와 Starts With 연산 | Trie 자료구조 이야기
우리가 매일 쓰는 맞춤법 검사기와 라우터 속에 숨겨진 알고리즘은? | Trie 자료구조 이야기
트라이(Trie) 자료구조: 파이썬으로 삽입(Insert) 연산 구현하기 | Trie 자료구조 이야기
숫자 하나가 AI 모델의 운명을 바꾼다? | 선형대수학