Terraform AWS VPC 만드는 법

도은호·2025년 9월 22일

terraform

목록 보기
10/32

VPC + 퍼블릭 서브넷(IGW) + (선택) NAT + 프라이빗 서브넷까지 한 번에.

0) 준비물

  • Terraform 1.4+ 설치
  • AWS 자격증명 설정: aws configure (또는 AWS_PROFILE, AWS_REGION)
  • 새 폴더 만들고 아래 파일들 생성

1) 버전/프로바이더 고정 (versions.tf)

terraform {
  required_version = "~> 1.9"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}
provider "aws" { region = var.region }

➡️Terraform의 버전 제약에서 ~>는 “pessimistic(호환 범위)” 연산자

  • ~> 1.9 ⇒ 1.9.0 이상, 2.0.0 미만

  • ~> 1.9.0 ⇒ 1.9.0 이상, 1.10.0 미만(= 1.9 패치만 허용)


2) 변수 정의 (variables.tf)

variable "project" { type = string,  default = "jj" }
variable "env"     { type = string,  default = "dev" }
variable "region"  { type = string,  default = "ap-northeast-2" } # 서울
variable "vpc_cidr" {
  type    = string
  default = "10.0.0.0/16"
  validation {
    condition     = can(cidrnetmask(var.vpc_cidr))
    error_message = "유효한 CIDR을 입력하세요. 예: 10.0.0.0/16"
  }
}
variable "az_count" {
  type    = number
  default = 2 # 1~3 권장
  validation {
    condition     = var.az_count >= 1 && var.az_count <= 3
    error_message = "az_count는 1~3 범위여야 합니다."
  }
}
# 서브넷 마스크: /16 VPC에서 /24 서브넷으로 쪼개려면 8비트
variable "public_subnet_bits"  { type = number, default = 8 }
variable "private_subnet_bits" { type = number, default = 8 }

3) AZ/네이밍/서브넷 계산 (data&locals.tf)

data "aws_availability_zones" "available" {
  state = "available"
}

locals {
  name_prefix = "${var.project}-${var.env}"
  common_tags = { Project = var.project, Env = var.env }

  # 사용할 AZ 목록 (앞에서부터 az_count개)
  azs = slice(data.aws_availability_zones.available.names, 0, var.az_count)

  # 서브넷 CIDR 자동 계산: public은 0.., private은 100..(충돌 방지)
  public_subnets = {
    for idx, az in local.azs :
    az => cidrsubnet(var.vpc_cidr, var.public_subnet_bits, idx)
  }
  private_subnets = {
    for idx, az in local.azs :
    az => cidrsubnet(var.vpc_cidr, var.private_subnet_bits, idx + 100)
  }
}

4) VPC + IGW + 퍼블릭 라우팅 (vpc_public.tf)

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = merge(local.common_tags, { Name = "${local.name_prefix}-vpc" })
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id
  tags   = merge(local.common_tags, { Name = "${local.name_prefix}-igw" })
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  tags   = merge(local.common_tags, { Name = "${local.name_prefix}-rt-public" })
}

resource "aws_route" "public_default" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

resource "aws_subnet" "public" {
  for_each                = local.public_subnets
  vpc_id                  = aws_vpc.main.id
  availability_zone       = each.key
  cidr_block              = each.value
  map_public_ip_on_launch = true   # 퍼블릭 서브넷 필수 옵션
  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-pub-${replace(each.key, var.region, "")}"
  })
}

resource "aws_route_table_association" "public" {
  for_each       = aws_subnet.public
  subnet_id      = each.value.id
  route_table_id = aws_route_table.public.id
}

5) (선택) NAT + 프라이빗 라우팅/서브넷 (vpc_private.tf)

NAT 게이트웨이는 유료입니다. 실습 후 꼭 destroy 할것.

# NAT용 탄력적 IP
resource "aws_eip" "nat" {
  domain = "vpc"
  tags   = merge(local.common_tags, { Name = "${local.name_prefix}-eip-nat" })
}

# 퍼블릭 서브넷 중 첫 번째에 NAT 게이트웨이 배치
resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat.id
  subnet_id     = values(aws_subnet.public)[0].id
  tags          = merge(local.common_tags, { Name = "${local.name_prefix}-nat" })
  depends_on    = [aws_internet_gateway.igw] # IGW 먼저
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id
  tags   = merge(local.common_tags, { Name = "${local.name_prefix}-rt-private" })
}

resource "aws_route" "private_default" {
  route_table_id         = aws_route_table.private.id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.nat.id
}

resource "aws_subnet" "private" {
  for_each          = local.private_subnets
  vpc_id            = aws_vpc.main.id
  availability_zone = each.key
  cidr_block        = each.value
  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-pri-${replace(each.key, var.region, "")}"
  })
}

resource "aws_route_table_association" "private" {
  for_each       = aws_subnet.private
  subnet_id      = each.value.id
  route_table_id = aws_route_table.private.id
}

6) 출력 (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 = try([for s in aws_subnet.private : s.id], []) }
output "igw_id"             { value = aws_internet_gateway.igw.id }
output "natgw_id"           { value = try(aws_nat_gateway.nat.id, null) }

7) 실행 명령어

terraform init
terraform validate
terraform plan -out=plan.bin
terraform apply plan.bin
# 출력 확인
terraform output

8) 검증 팁

  • 콘솔: VPC, Subnets, Route tables, IGW/NAT 존재/연결 상태 확인
  • CLI(예시):
aws ec2 describe-vpcs --filters Name=cidr,Values=10.0.0.0/16
aws ec2 describe-route-tables --filters Name=vpc-id,Values=$(terraform output -raw vpc_id)

9) 비용 주의 & 정리

  • NAT 게이트웨이 + EIP는 과금됩니다. 실습 끝나면:
terraform destroy
  • 퍼블릭만 필요하면 ⑤ 파일을 빼고(또는 주석) 퍼블릭 VPC로만 운영해도 됩니다.

10) 자주 하는 실수 🤦

  • 퍼블릭 서브넷인데 map_public_ip_on_launch를 빼먹음 → EC2가 공인 IP 없음
  • NAT를 프라이빗에 만들거나 IGW 라우트 없는 서브넷에 만듦 → 인터넷 불통
  • 서브넷 CIDR 겹침 → cidrsubnet()로 자동 계산 추천

요약

VPC → IGW → 퍼블릭 RT/서브넷 → (선택) NAT → 프라이빗 RT/서브넷 → apply → output 확인.

profile
`•.¸¸.•´´¯`••._.• 🎀 𝒸𝓇𝒶𝓏𝓎 𝓅𝓈𝓎𝒸𝒽💞𝓅𝒶𝓉𝒽 🎀 •._.••`¯´´•.¸¸.•`

0개의 댓글