VPC 깊이 — Subnet 설계 · Peering · Transit Gateway · PrivateLink
8장에서 잡은 VPC 기초를 production 규모로 끌어올립니다. 3-tier / 4-tier Subnet 설계와 CIDR 계획, NAT · Egress-only IGW · VPC Endpoint로 인터넷 출입을 Terraform 코드와 비용 계산까지 다루고, VPC Peering과 Transit Gateway로 VPC를 잇는 법, PrivateLink, IPv6 dual-stack, 멀티 VPC 멘탈 모델까지 정리합니다.
5부 운영 · 보안 · 비용의 첫 챕터입니다. 4부까지 우리는 단일 VPC 안에서 ECS Fargate 위에 한 서비스를 올리고 운영했습니다. 8장 EC2와 VPC 기초에서 VPC · Subnet · 라우팅 테이블 · 인터넷 게이트웨이 · 보안 그룹의 기본을 잡았다면, 이 챕터는 그 위에서 production 규모의 네트워크를 어떻게 설계하는가를 다룹니다.
운영에 들어가면 단일 public Subnet 하나로는 부족합니다. DB는 인터넷에서 닿지 않아야 하고, 여러 환경과 여러 VPC가 생기며, 사설 구간에서도 AWS 서비스에 닿아야 합니다. 이 챕터에서 잡는 Subnet 계층 · VPC 간 연결 · 사설 출입 모델은 29장 보안 거버넌스의 멀티 어카운트와 6부 풀스택 앱 AWS 배포하기의 인프라 골격으로 그대로 이어집니다. 이 챕터의 코드는 25장 Terraform 입문의 HCL을 전제로 합니다.
8장에서 5부로 #
8장에서 다룬 것은 다음과 같습니다.
- VPC는 계정 안에 만드는 사설 네트워크이며, CIDR 블록(예:
10.0.0.0/16)으로 IP 범위를 정합니다. - Subnet은 VPC 안을 AZ 단위로 나눈 구획입니다.
- 인터넷 게이트웨이(IGW)에 라우팅이 연결된 Subnet이 public, 아닌 Subnet이 private입니다.
- 보안 그룹은 인스턴스 단위 방화벽, 네트워크 ACL은 Subnet 단위 방화벽입니다.
이 챕터는 그 기본 요소를 여러 계층 · 여러 AZ · 여러 VPC로 확장합니다.
Subnet 설계 — 3-tier와 4-tier #
production VPC의 기본 골격은 3-tier입니다. 역할이 다른 Subnet을 계층으로 나눕니다.
| 계층 | 인터넷 | 들어가는 것 | 기본 라우팅 |
|---|---|---|---|
| public | 인바운드 / 아웃바운드 | ALB, NAT Gateway, Bastion | 0.0.0.0/0 → IGW |
| private (app) | 아웃바운드만 | ECS / EC2 앱, Lambda(VPC) | 0.0.0.0/0 → NAT Gateway |
| isolated (data) | 없음 | RDS, ElastiCache | 로컬 + VPC Endpoint만 |
규제가 강한 환경에서는 여기에 관리 전용 계층(Bastion / 운영 도구)을 더해 4-tier로 나누기도 합니다. 핵심은 DB가 사는 isolated 계층은 인터넷으로 나가는 경로 자체가 없다는 것입니다. 앱은 private에서 NAT를 통해 나가고, DB는 어디로도 나가지 않습니다. 계층 사이의 접근은 보안 그룹으로 한 방향씩만 엽니다 — ALB → app(8000), app → DB(5432) 식으로 출발지 보안 그룹을 참조해 IP가 아닌 역할로 통제합니다.
resource "aws_security_group_rule" "app_from_alb" {
type = "ingress"
security_group_id = aws_security_group.app.id
source_security_group_id = aws_security_group.alb.id # ALB 에서 온 것만
from_port = 8000
to_port = 8000
protocol = "tcp"
}
resource "aws_security_group_rule" "db_from_app" {
type = "ingress"
security_group_id = aws_security_group.db.id
source_security_group_id = aws_security_group.app.id # app 에서 온 것만
from_port = 5432
to_port = 5432
protocol = "tcp"
}CIDR 계획 #
CIDR는 한 번 정하면 바꾸기 번거롭습니다. 처음에 넉넉히, 규칙적으로 자릅니다. 두 AZ × 3계층의 예시는 다음과 같습니다.
VPC 10.0.0.0/16 (65,536 IP)
AZ a
public 10.0.0.0/20 (4,096)
private-app 10.0.16.0/20 (4,096)
isolated-db 10.0.32.0/20 (4,096)
AZ c
public 10.0.128.0/20 (4,096)
private-app 10.0.144.0/20 (4,096)
isolated-db 10.0.160.0/20 (4,096)- VPC는
/16으로 크게 잡습니다. 나중에 계층이나 AZ를 추가할 여지를 둡니다. - 각 Subnet은
/20(4,096 IP) 정도면 ECS Task가 늘어나도 IP가 마르지 않습니다. Fargate Task 하나가 ENI 하나 = IP 하나를 쓰므로, 컨테이너 밀도가 높으면 IP 고갈이 실제 장애로 이어집니다. AWS가 예약하는 Subnet 당 5개(네트워크/브로드캐스트/게이트웨이 등)도 감안합니다. - AZ 끼리 상위 비트를 띄워(
10.0.0.xvs10.0.128.x) 나중에 읽기 쉽게 합니다. - 다른 VPC와 Peering 할 가능성이 있다면 CIDR가 겹치지 않게 합니다. 겹친 두 VPC는 Peering 할 수 없습니다. 조직 차원에서는 VPC 마다
10.0.0.0/16,10.1.0.0/16식으로 미리 번호를 떼어 둡니다.
라우팅 테이블 #
계층마다 라우팅 테이블을 따로 둡니다. 라우팅의 차이가 곧 계층의 정의입니다.
# public: 인터넷으로 양방향
resource "aws_route" "public_internet" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
# private-app: AZ 별 NAT 로 아웃바운드만
resource "aws_route" "private_nat" {
for_each = aws_nat_gateway.az # AZ 마다 하나
route_table_id = aws_route_table.private[each.key].id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = each.value.id
}
# isolated-db: 기본 경로 없음 — local 만. 인터넷 단절
# (aws_route_table.isolated 에 0.0.0.0/0 경로를 두지 않는다)isolated-db 라우팅 테이블에 0.0.0.0/0 경로를 두지 않는 것이 곧 DB를 인터넷에서 끊는 방법입니다.
인터넷 출입 — IGW · NAT · Egress-only IGW #
private 계층의 앱은 밖으로 나가야 할 때가 많습니다(패키지 설치, 외부 API 호출, ECR pull). 그런데 인바운드는 막아야 합니다. 이 비대칭을 푸는 것이 NAT입니다.
| 장치 | 방향 | 대상 | 비고 |
|---|---|---|---|
| Internet Gateway (IGW) | 양방향 | public Subnet | VPC 당 1개, 무료 |
| NAT Gateway | 아웃바운드만 (IPv4) | private Subnet | 시간당 + 처리 GB 당 과금. AZ 별 1개 권장 |
| Egress-only IGW | 아웃바운드만 (IPv6) | private Subnet | 무료. IPv6 전용 |
resource "aws_eip" "nat" {
for_each = toset(["a", "c"])
domain = "vpc"
}
resource "aws_nat_gateway" "az" {
for_each = aws_eip.nat
allocation_id = each.value.id
subnet_id = aws_subnet.public[each.key].id # NAT 는 public 에 산다
}NAT Gateway 비용 주의 #
NAT Gateway는 운영 청구서에서 의외로 큰 항목입니다. 시간당 요금(서울 기준 약 $0.059/h, NAT 하나당 한 달 약 $43)에 더해 처리한 데이터 GB 당(약 $0.059/GB) 과금됩니다. 그래서 private 앱이 S3 / ECR / DynamoDB처럼 트래픽이 많은 AWS 서비스를 NAT로 드나들면 비용이 빠르게 쌓입니다.
예를 들어 AZ 2개에 NAT를 두고 한 달 500GB를 처리하면 대략 다음과 같습니다.
시간당 $0.059 × 24h × 30일 × 2개 ≈ $85
처리량 $0.059 × 500GB ≈ $30
─────────
약 $115 / 월줄이는 방법은 다음과 같습니다.
- AZ 마다 NAT Gateway 하나 — AZ 간 데이터 전송 비용을 피합니다. 단일 NAT로 묶으면 싸 보이지만 AZ 장애 시 전 AZ의 아웃바운드가 끊기고, 다른 AZ의 앱이 NAT가 있는 AZ로 넘어오며 AZ 간 전송료가 붙습니다.
- VPC Endpoint로 NAT 우회 — S3와 DynamoDB는 Gateway Endpoint가 무료입니다. ECR · CloudWatch · Secrets Manager 트래픽도 Interface Endpoint로 보내면 NAT 처리 비용에서 빠집니다. 컨테이너 환경에서 ECR pull은 트래픽이 크므로 효과가 큽니다.
- 학습 / dev 환경은 NAT 대신 앱을 public Subnet에 두거나, 필요할 때만 NAT를 켜는 것도 방법입니다.
이 비용 관점은 27장 비용 최적화와 30장 재해 복구·백업의 멀티 AZ 설계와 함께 봅니다.
VPC Endpoint와 PrivateLink #
private / isolated 계층에서도 AWS 서비스(S3, Secrets Manager, ECR 등)에는 닿아야 합니다. 이때 인터넷으로 나갔다 들어오는 대신 VPC Endpoint로 사설 경로를 만듭니다.
| 종류 | 대상 | 동작 | 비용 |
|---|---|---|---|
| Gateway Endpoint | S3, DynamoDB | 라우팅 테이블에 경로 추가 | 무료 |
| Interface Endpoint (PrivateLink) | 대부분의 AWS 서비스, 직접 만든 서비스 | Subnet에 ENI 생성, 사설 DNS | 시간당 + GB 당 |
# S3 — 라우팅 테이블에 붙는 무료 Gateway Endpoint
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.ap-northeast-2.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [for rt in aws_route_table.private : rt.id]
}
# ECR — private subnet 에 ENI 가 생기는 Interface Endpoint
resource "aws_vpc_endpoint" "ecr_dkr" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.ap-northeast-2.ecr.dkr"
vpc_endpoint_type = "Interface"
subnet_ids = [for s in aws_subnet.private : s.id]
security_group_ids = [aws_security_group.endpoints.id]
private_dns_enabled = true # 기존 ECR 도메인이 사설 IP 로 해석됨
}private_dns_enabled = true 면 *.ecr.ap-northeast-2.amazonaws.com 같은 기존 도메인이 자동으로 Endpoint의 사설 IP로 해석되어, 앱 코드를 바꿀 필요가 없습니다.
PrivateLink는 한 VPC의 서비스를 다른 VPC나 계정에 인터넷 없이 노출하는 기술이기도 합니다. 한 팀이 만든 내부 API 앞에 NLB를 두고 aws_vpc_endpoint_service로 공개하면, 소비자 VPC는 Interface Endpoint로 그 서비스에 붙습니다. 두 VPC의 CIDR가 겹쳐도 사설로 연결되는 것이 Peering 대비 PrivateLink의 강점입니다. 20장 Secrets Manager / Parameter Store의 시크릿을 isolated 앱이 읽을 때도 Interface Endpoint가 깔끔한 경로입니다.
VPC 끼리 연결 — Peering과 Transit Gateway #
VPC가 둘 이상이 되면(환경 분리, 계정 분리, 인수한 시스템 등) 사설로 잇는 두 가지 방법이 있습니다.
VPC Peering #
두 VPC를 1:1로 직접 연결합니다. 연결을 만든 뒤 양쪽 라우팅 테이블에 상대 CIDR 경로를 직접 넣어야 통합니다.
resource "aws_vpc_peering_connection" "a_to_b" {
vpc_id = aws_vpc.a.id
peer_vpc_id = aws_vpc.b.id
auto_accept = true # 같은 계정/리전일 때
}
resource "aws_route" "a_to_b" {
route_table_id = aws_route_table.a.id
destination_cidr_block = aws_vpc.b.cidr_block # 10.1.0.0/16
vpc_peering_connection_id = aws_vpc_peering_connection.a_to_b.id
}
resource "aws_route" "b_to_a" {
route_table_id = aws_route_table.b.id
destination_cidr_block = aws_vpc.a.cidr_block # 10.0.0.0/16
vpc_peering_connection_id = aws_vpc_peering_connection.a_to_b.id
}- 설정이 단순하고, 같은 / 다른 계정 · 리전 모두 가능합니다.
- **비전이적(non-transitive)**입니다. A–B, B–C를 Peering 해도 A–C는 자동으로 통하지 않습니다. VPC가 N 개면 연결이 N(N-1)/2개로 늘어, 5개만 돼도 10개의 Peering과 그 두 배의 경로를 관리해야 합니다.
- 양쪽 CIDR가 겹치면 안 됩니다.
Transit Gateway #
여러 VPC를 하나의 허브에 붙이는 방식입니다.
resource "aws_ec2_transit_gateway" "hub" {
description = "org hub"
default_route_table_association = "enable"
default_route_table_propagation = "enable"
}
resource "aws_ec2_transit_gateway_vpc_attachment" "a" {
transit_gateway_id = aws_ec2_transit_gateway.hub.id
vpc_id = aws_vpc.a.id
subnet_ids = [for s in aws_subnet.a_private : s.id]
}
# VPC b, c... 도 같은 식으로 한 번씩만 붙인다- 각 VPC를 Transit Gateway에 한 번씩만 붙이면 됩니다(스타 구조). VPC가 늘어도 연결 수가 선형으로만 증가합니다.
- TGW 라우팅 테이블로 어떤 VPC 끼리 통신할지 통제합니다. 예를 들어 dev와 prod를 서로 못 보게 분리하면서 둘 다 공유 서비스 VPC에는 닿게 할 수 있습니다. 온프레미스(VPN / Direct Connect)도 같은 허브에 붙습니다.
- 시간당(부착당) + 처리 GB 당 과금이 있어, VPC가 2~3개로 적을 때는 Peering이 더 쌉니다.
결정 기준: VPC가 2~3개면 Peering, 그 이상으로 늘어날 조짐이 보이면 Transit Gateway입니다. 멀티 어카운트 거버넌스(29장)로 가는 조직은 처음부터 Transit Gateway를 깔아두는 편이 나중에 편합니다. 6부 캡스톤은 단일 VPC 라 둘 다 쓰지 않지만, 두 번째 환경 계정이 생기는 순간 이 결정이 등장합니다.
IPv6 도입 결정 가이드 #
IPv6는 모든 프로젝트에 필요하지는 않습니다. 다음일 때 검토합니다.
- 아웃바운드 트래픽이 많아 NAT Gateway 비용이 부담일 때. IPv6 아웃바운드는 무료인 Egress-only IGW로 처리되므로 NAT 비용을 줄일 수 있습니다.
- 공인 IPv4 주소가 유료(2024년 2월부터 사용 중인 공인 IPv4 하나당 시간당 과금)이므로, 대량의 공인 IP가 필요한 워크로드에서 비용 절감 여지가 있을 때.
- 외부에서 IPv6로 들어오는 클라이언트를 직접 받아야 할 때.
대부분은 dual-stack(IPv4 + IPv6 동시)으로 도입합니다.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
assign_generated_ipv6_cidr_block = true # AWS 가 /56 IPv6 할당
}
resource "aws_egress_only_internet_gateway" "main" {
vpc_id = aws_vpc.main.id # IPv6 아웃바운드 (무료)
}
resource "aws_route" "private_ipv6" {
route_table_id = aws_route_table.private["a"].id
destination_ipv6_cidr_block = "::/0"
egress_only_gateway_id = aws_egress_only_internet_gateway.main.id
}단순한 백엔드 한 대라면 IPv4 만으로 충분하니, IPv6는 비용이나 요구가 분명할 때 들입니다.
멀티 VPC 멘탈 모델 #
VPC를 언제 나누는지의 기준입니다.
- 단일 VPC + 계층 Subnet — 한 서비스, 한 팀, 한 환경이면 이 골격으로 충분합니다. 4부까지 우리가 쓴 모델입니다.
- 환경별 VPC — dev / staging / prod를 VPC로 나누면 사고의 폭발 반경이 환경 안에 갇힙니다. 다만 진짜 격리는 계정 분리(29장)가 더 강합니다.
- 계정 + VPC 조합 — 규모가 커지면 계정을 환경 / 팀별로 나누고, 각 계정에 VPC를 두며, Transit Gateway로 필요한 만큼만 잇습니다. 이것이 production 조직의 일반적인 모습입니다.
VPC를 나누는 1차 동기는 보통 보안 경계와 폭발 반경입니다. 그 깊은 이야기는 다음 29장 보안 거버넌스의 Organizations · SCP와 이어집니다.
연습문제 #
- 본인 서비스의 VPC를
10.0.0.0/16으로 두고 2 AZ × 3-tier(public / private-app / isolated-db)로 Subnet 6개의 CIDR를 직접 할당해 보세요. 각 Subnet의 라우팅 테이블이 IGW / NAT / 로컬 중 무엇을 기본 경로로 갖는지 한 줄씩 적고, isolated 계층에 왜0.0.0.0/0경로를 두지 않는지 설명해 보세요. 32장 풀스택 앱 AWS 배포하기의 Terraform VPC 모듈을 작성할 때 이 표가 그대로 입력이 됩니다. - private 앱이 ① S3, ② ECR(이미지 pull), ③ 외부 결제 API 세 곳으로 아웃바운드한다고 할 때, 각각을 NAT Gateway · Gateway Endpoint · Interface Endpoint 중 무엇으로 보내야 비용이 최소가 되는지와 그 이유를 §“NAT Gateway 비용 주의"의 개산을 근거로 적어 보세요.
- VPC가 현재 2개이고 1년 안에 5~6개로 늘어날 계획이라면, Peering과 Transit Gateway 중 무엇으로 시작할지 결정하고, 연결 수(N(N-1)/2 vs 선형)와 라우팅 관리 부담을 근거로 한 단락으로 적어 보세요.
한 줄 요약: production VPC는 public / private-app / isolated-db의 3-tier로 설계하고, DB 계층은 라우팅 테이블에
0.0.0.0/0경로 자체를 두지 않아 인터넷과 끊는다. 계층 간 접근은 보안 그룹 참조로 역할 기반으로 연다. private 앱의 아웃바운드는 AZ 별 NAT Gateway로 보내되 S3 · DynamoDB는 무료 Gateway Endpoint, ECR 등은 Interface Endpoint로 우회해 시간당 + GB 당 NAT 비용을 줄인다. VPC가 2~3개면 Peering(비전이적, 양쪽 경로 수동), 더 늘면 Transit Gateway(스타 허브, TGW 라우팅으로 통제)로 잇는다. PrivateLink는 CIDR가 겹쳐도 사설로 서비스를 노출하고, IPv6는 NAT/공인 IPv4 비용이 부담일 때 dual-stack + Egress-only IGW로 도입한다. VPC를 나누는 1차 동기는 보안 경계와 폭발 반경이다.
다음 챕터 #
다음 29장 보안 거버넌스에서는 단일 계정에서 멀티 어카운트로 넘어가는 시점을 다룹니다. AWS Organizations · SCP · Control Tower로 계정을 묶고 정책을 일괄 적용하는 모델, 그리고 GuardDuty · Security Hub · Config로 계정 전체를 감시하는 거버넌스를 정리합니다. 이 챕터의 멀티 VPC가 멀티 계정으로 확장되는 자연스러운 다음 단계입니다.