목차
28 장

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, Bastion0.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의 Subnet 분할 (2 AZ × 3 tier)
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.x vs 10.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 SubnetVPC 당 1개, 무료
NAT Gateway아웃바운드만 (IPv4)private Subnet시간당 + 처리 GB 당 과금. AZ 별 1개 권장
Egress-only IGW아웃바운드만 (IPv6)private Subnet무료. IPv6 전용
AZ 마다 NAT Gateway 하나
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를 처리하면 대략 다음과 같습니다.

NAT 비용 개산 (서울, 2 AZ, 월 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 EndpointS3, DynamoDB라우팅 테이블에 경로 추가무료
Interface Endpoint (PrivateLink)대부분의 AWS 서비스, 직접 만든 서비스Subnet에 ENI 생성, 사설 DNS시간당 + GB 당
Gateway Endpoint(S3, 무료) + Interface Endpoint(ECR)
# 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 경로를 직접 넣어야 통합니다.

VPC Peering + 양방향 경로
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를 하나의 허브에 붙이는 방식입니다.

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 동시)으로 도입합니다.

dual-stack — VPC/Subnet에 IPv6 추가 + Egress-only IGW
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와 이어집니다.

연습문제 #

  1. 본인 서비스의 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 모듈을 작성할 때 이 표가 그대로 입력이 됩니다.
  2. private 앱이 ① S3, ② ECR(이미지 pull), ③ 외부 결제 API 세 곳으로 아웃바운드한다고 할 때, 각각을 NAT Gateway · Gateway Endpoint · Interface Endpoint 중 무엇으로 보내야 비용이 최소가 되는지와 그 이유를 §“NAT Gateway 비용 주의"의 개산을 근거로 적어 보세요.
  3. 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가 멀티 계정으로 확장되는 자연스러운 다음 단계입니다.

X