이론을 넘어 실전으로
VPC 세팅과 Peering vs TGW 글 내용을 기반으로, 2AZ VPC 를 terraform 으로 구성해 보겠습니다.
1. 변수 정의와 리전 설정
먼저 확장성을 위해 AZ 목록을 변수화하고 리전을 정의합니다.
variable "region" {
default = "ap-northeast-2"
}
variable "azs" {
description = "사용할 가용 영역 리스트"
type = list(string)
default = ["ap-northeast-2a", "ap-northeast-2c"] # 2-AZ 전략
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
2. VPC와 서브넷: cidrsubnet과 count의 활용
서브넷을 하드코딩하는 것은 가장 피해야 할 습관입니다. 테라폼의 내장 함수를 사용하여 자동으로 계산되게 구성합니다.
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = { Name = "main-vpc" }
}
# Public Subnets
resource "aws_subnet" "public" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index) # 10.0.0.0/24, 10.0.1.0/24 ...
availability_zone = var.azs[count.index]
map_public_ip_on_launch = true
tags = { Name = "public-sn-${var.azs[count.index]}" }
}
# Private Subnets
resource "aws_subnet" "private" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10) # 10.0.10.0/24, 10.0.11.0/24 ...
availability_zone = var.azs[count.index]
tags = { Name = "private-sn-${var.azs[count.index]}" }
}
HCL Tip: cidrsubnet 함수
cidrsubnet(prefix, newbits, netnum) 함수는 메인 CIDR을 쪼개는 데 매우 유용합니다. newbits를 8로 주면 /16을 /24로 쪼개게 되며, netnum을 통해 각 서브넷에 순차적인 번호를 부여할 수 있습니다.
3. IGW(Internet Gateway)와 라우팅
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.main.id
tags = { Name = "main-igw" }
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = { Name = "public-rt" }
}
resource "aws_route_table_association" "public" {
count = length(var.azs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
4. NAT Gateway와 Private 라우팅 (High Availability 구성)
Private 서브넷의 인스턴스들이 외부 인터넷과 통신(업데이트, 패치 등)하기 위해서는 NAT Gateway가 필요합니다. 여기서는 높은 가용성(HA)을 위해 각 가용영역(AZ)마다 별도의 NAT Gateway를 생성하는 구성을 사용합니다.
# NAT Gateway를 위한 Elastic IP
resource "aws_eip" "nat" {
count = length(var.azs)
domain = "vpc"
tags = { Name = "nat-eip-${var.azs[count.index]}" }
}
# 각 Public 서브넷에 NAT Gateway 생성
resource "aws_nat_gateway" "main" {
count = length(var.azs)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = { Name = "main-nat-${var.azs[count.index]}" }
}
# Private 서브넷을 위한 개별 라우트 테이블
resource "aws_route_table" "private" {
count = length(var.azs)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
tags = { Name = "private-rt-${var.azs[count.index]}" }
}
# Private 서브넷 - 라우트 테이블 연결
resource "aws_route_table_association" "private" {
count = length(var.azs)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
비용 vs 가용성 (2-AZ의 기준)
- Dual NAT (권장): AZ를 2개로만 가져가는 구성에서는 Dual NAT 배치가 필수적이라고 생각됩니다. 한쪽 AZ의 NAT Gateway에 문제가 생기거나 해당 AZ 자체가 점검에 들어갈 때, 연쇄적인 통신 두절을 막기 위한 최소한의 안전장치입니다.
- Single NAT: 비용 절감을 위해 하나의 NAT Gateway만 두고 모든 Private 서브넷이 공유할 수 있습니다. 하지만 이는 관리형 VPC 엔드포인트를 충분히 활용하거나, 일시적인 통신 중단이 허용되는 테스트/개발 환경에서만 고려하는 것이 좋을듯 합니다.
5. 멀티 VPC 확장: Module의 필요성
만약 main-vpc 외에 환경 격리를 위해 dev-vpc(동일 구성, 단일 NAT)를 추가해야 한다면 어떻게 해야 할까요? 위에서 작성한 코드를 복사해서 파일 하나를 더 만드는 것은 가장 피해야 할 방식입니다.
이때가 바로 **모듈(Module)**을 본격적으로 도입해야 할 시점입니다.
모듈화를 통한 재사용 전략
VPC 생성 로직을 ./modules/vpc 폴더로 옮기고, main.tf에서 이를 호출하는 방식으로 구조를 바꿉니다. 이때 enable_dual_nat 같은 변수를 추가하면 환경별로 NAT 구성을 손쉽게 바꿀 수 있습니다.
# main-vpc: 프로덕션용, 듀얼 NAT 사용
module "main_vpc" {
source = "./modules/vpc"
vpc_name = "main-vpc"
vpc_cidr = "10.0.0.0/16"
enable_dual_nat = true # 2-AZ 듀얼 NAT 필수
}
# dev-vpc: 개발용, 단일 NAT로 비용 절감
module "dev_vpc" {
source = "./modules/vpc"
vpc_name = "dev-vpc"
vpc_cidr = "10.1.0.0/16"
enable_dual_nat = false # 개발용이므로 싱글 NAT로 타협
}
왜 모듈인가?
- 일관성:
main과dev의 서브넷 마스크 비트(cidrsubnet)나 태깅 규칙을 동일하게 유지할 수 있습니다. - 가독성: 수백 줄의 리소스 코드가 단 몇 줄의 모듈 호출 코드로 압축됩니다.
- 유연성: 위 예시처럼 변수 하나(
enable_dual_nat)로 인프라의 가용성 수준을 코드 레벨에서 결정할 수 있습니다.
예제 코드: GitHub Repository
(Module 구조, VPC Peering, Variables 활용이 포함된 테라폼 예제코드 입니다.)
결론: 코드가 곧 아키텍처다
단순히 클릭 몇 번으로 끝낼 수 있는 VPC 생성이지만, 테라폼의 count와 cidrsubnet 그리고 Module을 활용하면 리전이 바뀌거나 환경이 추가될 때 최소한의 수정으로 완벽하게 대응할 수 있는 유연한 인프라를 갖추게 됩니다.