2026-04-09 Terraform AWS VPC IaC 네트워크

Terraform으로 AWS VPC Public/Private 서브넷 구성하기

Terraform으로 AWS VPC, 퍼블릭/프라이빗 서브넷, 인터넷 게이트웨이, 보안 그룹, EC2 인스턴스를 코드로 자동 확장하는 방법을 단계별로 정리했습니다.

개요

AWS에서 보안이 강화된 네트워크 아키텍처를 구성할 때 가장 기본이 되는 패턴은 퍼블릭 서브넷(Public Subnet)과 프라이빗 서브넷(Private Subnet)의 분리입니다. 웹 서버·로드 밸런서는 퍼블릭 서브넷에, 데이터베이스·내부 서버는 인터넷에 직접 노출되지 않는 프라이빗 서브넷에 배치함으로써 기본적 보안요소인 3-티어 어플리케이션을 구현합니다.

이 포스트에서는 Terraform(Infrastructure as Code)을 사용해 이 구조를 코드 한 번의 실행으로 자동 확장하는 방법을 설명합니다.

아키텍처

AWS VPC 퍼블릭/프라이빗 서브넷 아키텍처

AWS VPC 내 퍼블릭 서브넷(웹 서버)과 프라이빗 서브넷(DB 서버) 구성

이번 실습에서 생성하는 리소스는 다음과 같습니다:

  • VPC — CIDR: 172.16.0.0/16
  • 퍼블릭 서브넷 — CIDR: 172.16.1.0/24, 인터넷 접근 허용
  • 프라이빗 서브넷 — CIDR: 172.16.2.0/24, 인터넷 직접 접근 차단
  • 인터넷 게이트웨이(IGW) — 퍼블릭 서브넷의 외부 통신 담당
  • 라우트 테이블 — 퍼블릭 서브넷 트래픽을 IGW로 라우팅
  • 보안 그룹(Security Group) — 서브넷별 인바운드/아웃바운드 규칙
  • EC2 인스턴스 — 퍼블릭(웹 서버), 프라이빗(DB 서버) 각 1대

사전 준비

  • AWS 계정 및 IAM 권한 (VPC, EC2 생성 권한)
  • AWS Access Key / Secret Key
  • Amazon Linux EC2 인스턴스 (Terraform 실행 서버)
  • EC2 키 페어(Key Pair)

1단계 — Terraform 설치

Amazon Linux 환경에서 (혹은 Windows WSL (Windows Subsystem for Linux)) Terraform을 설치합니다:

sudo yum install -y yum-utils
sudo yum-config-manager \
  --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum -y install terraform

2단계 — AWS CLI 구성

Terraform이 AWS API를 호출할 수 있도록 자격 증명을 설정합니다:

aws configure
  • AWS Access Key ID 입력
  • AWS Secret Access Key 입력
  • Default region: us-east-1 (또는 원하는 리전)
  • Default output format: table

3단계 — Terraform 구성 파일 작성 (main.tf)

아래 내용을 main.tf 파일로 저장합니다. 각 리소스 블록을 순서대로 살펴봅니다.

프로바이더 설정

provider "aws" {
  region = "us-east-1"
}

VPC 생성

resource "aws_vpc" "main" {
  cidr_block = "172.16.0.0/16"
  tags = {
    Name = "main-vpc"
  }
}

인터넷 게이트웨이(IGW)

퍼블릭 서브넷이 인터넷과 통신하기 위한 게이트웨이입니다.

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "main-gw"
  }
}

퍼블릭 서브넷

map_public_ip_on_launch = true로 설정하면 이 서브넷에 생성된 EC2 인스턴스에 자동으로 퍼블릭 IP가 할당됩니다.

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "172.16.1.0/24"
  map_public_ip_on_launch = true
  tags = {
    Name = "public-subnet"
  }
}

프라이빗 서브넷

퍼블릭 IP를 할당하지 않아 인터넷에서 직접 접근할 수 없습니다.

resource "aws_subnet" "private" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "172.16.2.0/24"
  tags = {
    Name = "private-subnet"
  }
}

퍼블릭 서브넷 라우트 테이블

퍼블릭 서브넷의 모든 트래픽(0.0.0.0/0)을 인터넷 게이트웨이로 라우팅합니다.

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table_association" "a" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}
프라이빗 서브넷에는 별도의 라우트 테이블을 연결하지 않습니다. 인터넷으로 나가는 경로가 없어야 외부에서의 직접 접근이 차단됩니다. 프라이빗 서브넷에서 인터넷 아웃바운드가 필요한 경우(패치, 업데이트 등)에는 NAT 게이트웨이를 퍼블릭 서브넷에 추가하고 프라이빗 라우트 테이블에서 NAT로 라우팅하면 됩니다.

퍼블릭 서브넷 보안 그룹

웹 서버용 — SSH(22)와 HTTP(80)를 외부에서 허용하고 아웃바운드는 전체 허용합니다.

resource "aws_security_group" "public_sg" {
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "public-sg"
  }
}

프라이빗 서브넷 보안 그룹

DB 서버용 — 퍼블릭 서브넷(172.16.1.0/24)에서 오는 MySQL(3306)과 ICMP(핑)만 허용합니다.

resource "aws_security_group" "private_sg" {
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = ["172.16.1.0/24"]
  }

  ingress {
    from_port   = -1
    to_port     = -1
    protocol    = "icmp"
    cidr_blocks = ["172.16.1.0/24"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "private-sg"
  }
}

퍼블릭 서브넷 EC2 인스턴스 (웹 서버)

resource "aws_instance" "public_server" {
  ami                         = "ami-0195204d5dce06d99"  # Amazon Linux 2 (us-east-1)
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.public.id
  vpc_security_group_ids      = [aws_security_group.public_sg.id]
  associate_public_ip_address = true
  key_name                    = "your_key_pair"

  tags = {
    Name = "public-server"
  }
}

프라이빗 서브넷 EC2 인스턴스 (DB 서버)

resource "aws_instance" "private_server" {
  ami                    = "ami-0195204d5dce06d99"  # Amazon Linux 2 (us-east-1)
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.private.id
  vpc_security_group_ids = [aws_security_group.private_sg.id]
  key_name               = "your_key_pair"

  tags = {
    Name = "private-db-server"
  }
}
ami ID는 리전마다 다릅니다. 서울 리전(ap-northeast-2) 사용 시 AWS 콘솔 EC2 → AMI 카탈로그에서 Amazon Linux 2의 최신 AMI ID를 확인하세요.

4단계 — 배포 실행

# 초기화 (프로바이더 플러그인 다운로드)
terraform init

# 변경 사항 미리 확인
terraform plan

# 리소스 프로비저닝
terraform apply

terraform apply 실행 후 yes를 입력하면 모든 리소스가 자동으로 생성됩니다.

5단계 — 연결 확인

  1. AWS 콘솔 → EC2 인스턴스 목록에서 public-serverprivate-db-server 생성 확인
  2. 퍼블릭 서버에 SSH 접속:
ssh -i your_key_pair.pem ec2-user@<public-server-public-ip>
  1. 퍼블릭 서버에서 프라이빗 서버로 핑 테스트:
ping 172.16.2.x   # private-db-server의 프라이빗 IP

ICMP 응답이 돌아오면 두 서브넷 간 통신이 정상적으로 구성된 것입니다.

6단계 — 리소스 삭제

실습 완료 후 불필요한 비용이 발생하지 않도록 모든 리소스를 삭제합니다:

terraform destroy

보안 고려사항

  • 퍼블릭 서버의 SSH(22) 접근을 0.0.0.0/0으로 열어두면 위험합니다. 실제 운영 환경에서는 회사 IP 또는 VPN IP로 제한하세요.
  • 프라이빗 서버는 퍼블릭 IP가 없으므로 인터넷에서 직접 접근이 불가합니다. 관리 접속이 필요하면 퍼블릭 서버를 배스천 호스트(Bastion Host)로 활용합니다.
  • 프라이빗 서브넷에서 OS 패치 등 아웃바운드 인터넷이 필요한 경우 NAT 게이트웨이를 추가해 인바운드는 차단하면서 아웃바운드만 허용하는 구조로 설계합니다.
  • AMI ID는 주기적으로 업데이트되므로 최신 Amazon Linux 2023 AMI를 사용하는 것을 권장합니다.

전체 main.tf 파일

provider "aws" {
  region = "us-east-1"
}

resource "aws_vpc" "main" {
  cidr_block = "172.16.0.0/16"
  tags = { Name = "main-vpc" }
}

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = { Name = "main-gw" }
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "172.16.1.0/24"
  map_public_ip_on_launch = true
  tags = { Name = "public-subnet" }
}

resource "aws_subnet" "private" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "172.16.2.0/24"
  tags = { Name = "private-subnet" }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  tags = { Name = "public-rt" }
}

resource "aws_route_table_association" "a" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

resource "aws_security_group" "public_sg" {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "public-sg" }
}

resource "aws_security_group" "private_sg" {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port   = 3306
    to_port     = 3306
    protocol    = "tcp"
    cidr_blocks = ["172.16.1.0/24"]
  }
  ingress {
    from_port   = -1
    to_port     = -1
    protocol    = "icmp"
    cidr_blocks = ["172.16.1.0/24"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "private-sg" }
}

resource "aws_instance" "public_server" {
  ami                         = "ami-0195204d5dce06d99"
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.public.id
  vpc_security_group_ids      = [aws_security_group.public_sg.id]
  associate_public_ip_address = true
  key_name                    = "your_key_pair"
  tags = { Name = "public-server" }
}

resource "aws_instance" "private_server" {
  ami                    = "ami-0195204d5dce06d99"
  instance_type          = "t2.micro"
  subnet_id              = aws_subnet.private.id
  vpc_security_group_ids = [aws_security_group.private_sg.id]
  key_name               = "your_key_pair"
  tags = { Name = "private-db-server" }
}

참고 자료