테라폼 실전 강좌 #2 VPC 네트워크 코드화: 서브넷 2 AZ, 라우팅, NAT 게이트웨이
인프라의 첫 리소스는 언제나 네트워크입니다. 이후의 모든 것(EC2, RDS, ECS)이 서브넷 위에 놓이므로, 여기서 정한 구조가 시리즈 전체의 뼈대가 됩니다. 이번 글의 목표는 교과서적 2계층 네트워크입니다. 인터넷에서 접근 가능한 퍼블릭 서브넷과 격리된 프라이빗 서브넷을 두 가용 영역(AZ)에 걸쳐 만들고, 라우팅과 NAT까지 연결합니다. VPC 개념 자체가 낯설다면 AWS 중급 강좌를 먼저 보고 오는 것을 권하고, 여기서는 그 개념을 코드로 옮기는 데 집중하겠습니다.
주소 설계: 변수로 잡아 두기 #
CIDR와 AZ 구성을 variables에 정의합니다. 기초 #6에서 다룬 for_each를 쓰기 좋게, 서브넷 구성을 맵으로 표현합니다.
# variables.tf에 추가
variable "vpc_cidr" {
type = string
default = "10.0.0.0/16"
}
variable "public_subnets" {
type = map(string)
default = {
"ap-northeast-2a" = "10.0.1.0/24"
"ap-northeast-2c" = "10.0.2.0/24"
}
}
variable "private_subnets" {
type = map(string)
default = {
"ap-northeast-2a" = "10.0.11.0/24"
"ap-northeast-2c" = "10.0.12.0/24"
}
}AZ를 키로, CIDR를 값으로 잡았습니다. 퍼블릭은 1번대, 프라이빗은 11번대로 띄워 두면 IP만 보고도 어느 계층인지 읽을 수 있고, 사이에 서브넷을 추가할 여지도 남습니다.
VPC와 서브넷 #
# network.tf (새 파일)
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = { Name = "myapp-${var.env}" }
}
resource "aws_subnet" "public" {
for_each = var.public_subnets
vpc_id = aws_vpc.main.id
availability_zone = each.key
cidr_block = each.value
map_public_ip_on_launch = true
tags = { Name = "myapp-${var.env}-public-${each.key}" }
}
resource "aws_subnet" "private" {
for_each = var.private_subnets
vpc_id = aws_vpc.main.id
availability_zone = each.key
cidr_block = each.value
tags = { Name = "myapp-${var.env}-private-${each.key}" }
}count가 아니라 for_each를 쓴 이유는 기초 #6에서 본 그대로입니다. 나중에 AZ를 추가하거나 빼도 다른 서브넷이 재생성되지 않습니다. 주소가 aws_subnet.public["ap-northeast-2a"]처럼 AZ 이름으로 잡히니 plan을 읽기도 쉽습니다. enable_dns_hostnames는 기본값이 꺼짐인데, 뒤에서 RDS 엔드포인트 같은 DNS 이름을 쓰려면 켜 두어야 합니다.
인터넷 게이트웨이와 퍼블릭 라우팅 #
퍼블릭 서브넷이 퍼블릭인 이유는 이름이 아니라 라우팅 테이블에 인터넷 게이트웨이(IGW)로 가는 경로가 있기 때문입니다. 그 정의를 코드로 그대로 옮깁니다.
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
subnet_id = each.value.id
route_table_id = aws_route_table.public.id
}연결(association)의 for_each에 변수 맵이 아니라 리소스 자체(aws_subnet.public)를 넘긴 것에 주목합니다. 리소스를 for_each로 만들면 그 결과도 맵이라서, 그대로 다른 for_each의 입력이 됩니다. each.value가 서브넷 리소스이므로 .id로 참조합니다.
NAT 게이트웨이: 비용과 구조의 절충 #
프라이빗 서브넷의 인스턴스도 아웃바운드는 필요합니다(패키지 설치, 외부 API 호출). 그 통로가 NAT 게이트웨이이고, 퍼블릭 서브넷에 놓여 프라이빗의 트래픽을 대신 내보냅니다. 설계 결정이 하나 있습니다. NAT는 시간당 요금이 나가는 비용 상위 단골이라, AZ마다 둘 것인지 하나만 둘 것인지 골라야 합니다. AZ마다 두면 한 AZ 장애 때도 다른 AZ의 아웃바운드가 살지만 비용이 두 배입니다. 실습(그리고 많은 dev 환경)은 하나로 충분하므로 하나로 갑니다. 이 선택을 #9에서 환경별 변수로 승격하겠습니다.
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public["ap-northeast-2a"].id
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
}
resource "aws_route_table_association" "private" {
for_each = aws_subnet.private
subnet_id = each.value.id
route_table_id = aws_route_table.private.id
}depends_on이 등장했습니다. NAT 게이트웨이는 IGW가 있어야 실제로 동작하는데 둘 사이에 직접 참조가 없으므로, 기초 #6에서 정리한 “참조 없는 순서 의존"의 드문 정당한 사례입니다. AWS provider 문서가 권장하는 패턴이기도 합니다.
output과 검증 #
다음 편들이 가져다 쓸 값을 출력으로 정리합니다.
# outputs.tf에 추가
output "vpc_id" {
value = aws_vpc.main.id
}
output "public_subnet_ids" {
value = [for s in aws_subnet.public : s.id]
}
output "private_subnet_ids" {
value = [for s in aws_subnet.private : s.id]
}apply 하면 VPC 1, 서브넷 4, IGW 1, NAT 1, EIP 1, 라우팅 테이블 2와 연결 4까지 열네 개 리소스가 만들어집니다. plan에서 개수를 확인하고 apply 후 terraform output으로 ID들이 나오는지 보면 이번 편의 검증은 끝입니다. NAT가 켜져 있다는 것을 기억하고, 오늘 실습을 마친다면 destroy를 잊지 않습니다.
정리 #
이번 글에서 다룬 내용입니다.
- 서브넷 구성을 AZ 키의 맵 변수로 정의하고 for_each로 만들었습니다. AZ 추가·제거가 다른 서브넷을 건드리지 않습니다
- 퍼블릭 서브넷의 정의는 IGW로 가는 라우트입니다. 라우팅 테이블과 연결까지 코드로 명시했습니다
- for_each로 만든 리소스는 맵이므로 그대로 다른 리소스의 for_each 입력이 됩니다
- NAT 게이트웨이는 비용 때문에 실습·dev에서는 1개, 이 선택은 #9에서 환경별 변수가 됩니다. IGW와의 관계는 depends_on의 정당한 사례입니다
- vpc_id와 서브넷 ID 리스트를 output으로 정리해 다음 편의 입력을 준비했습니다
다음 글(#3 EC2와 보안 그룹)에서는 이 네트워크 위에 첫 컴퓨팅을 올립니다. 보안 그룹 체이닝 설계와 user_data로 웹 서버를 부팅까지 시키겠습니다.