테라폼

4-2. 테라폼 - VPC, Subnet, Route Table 구현하기

pininini 2026. 5. 12. 12:44

테라폼 - VPC, Subnet, Route Table 구현하기

AWS 네트워크 기본 구조를 Terraform 코드로 이해하기


이전 글에서는 Terraform으로 AWS 기본 프로젝트 템플릿을 만들었다.

그 템플릿에서는 VPC, Public Subnet, Internet Gateway, Route Table, Security Group, EC2를 한 번에 구성했다.

이번 글에서는 그중에서 가장 기반이 되는 네트워크 리소스를 더 자세히 정리해보려 한다.

VPC
Subnet
Internet Gateway
Route Table
Route Table Association
NAT Gateway

AWS에서 EC2, RDS, ECS, Lambda VPC 연결 같은 리소스를 제대로 구성하려면 네트워크 구조를 먼저 이해해야 한다.

VPC는 대부분의 AWS 리소스가 배치되는 네트워크 기반이고, Subnet과 Route Table은 트래픽 흐름을 결정한다.

목차

  • 1. 이번 글에서 만들 구조
  • 2. VPC란 무엇인가
  • 3. CIDR 블록 이해하기
  • 4. Subnet이 필요한 이유
  • 5. Public Subnet과 Private Subnet
  • 6. Internet Gateway
  • 7. Route Table
  • 8. Route Table Association
  • 9. NAT Gateway가 필요한 경우
  • 10. Terraform 코드 전체 구성
  • 11. 의존성 흐름
  • 12. 자주 하는 실수
  • 13. 마무리

1. 이번 글에서 만들 구조

이번 글에서는 다음과 같은 네트워크 구조를 만든다.

VPC
├── Public Subnet
│   └── Internet Gateway를 통해 인터넷 접근 가능
│
└── Private Subnet
    └── 기본적으로 외부에서 직접 접근 불가

조금 더 구체적으로 보면 다음과 같다.

Internet
  ↓
Internet Gateway
  ↓
Public Route Table
  ↓
Public Subnet

Private Subnet
  ↓
Private Route Table
  ↓
필요 시 NAT Gateway를 통해 외부로 나감

이번 글에서는 기본적으로 VPC, Public Subnet, Private Subnet, Internet Gateway, Route Table을 만든다.

NAT Gateway는 비용이 발생하므로 선택적으로 설명한다.


2. VPC란 무엇인가

VPC는 Virtual Private Cloud의 약자다.

쉽게 말하면 AWS 안에 만드는 나만의 네트워크 공간이다.

EC2, RDS, ECS 같은 리소스들은 보통 VPC 안에 배치된다.

VPC
→ AWS 안에 만드는 논리적인 네트워크 공간

Terraform으로 VPC를 만들면 다음과 같다.

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-vpc"
  })
}

여기서 중요한 값은 cidr_block이다.

cidr_block = "10.0.0.0/16"

이 값은 VPC가 사용할 IP 범위를 의미한다.


3. CIDR 블록 이해하기

CIDR은 네트워크 IP 범위를 표현하는 방식이다.

예를 들어 다음과 같은 CIDR이 있다고 하자.

10.0.0.0/16

이것은 10.0.0.0부터 시작하는 큰 네트워크 대역을 의미한다.

VPC에는 보통 큰 CIDR을 할당하고, 그 안에서 Subnet을 더 작은 범위로 나눈다.

VPC CIDR
10.0.0.0/16

Public Subnet CIDR
10.0.1.0/24

Private Subnet CIDR
10.0.2.0/24

즉, VPC는 전체 네트워크 범위이고, Subnet은 그 안의 일부 구간이다.

VPC = 큰 네트워크
Subnet = VPC 안의 작은 네트워크 구역

초보 단계에서는 다음처럼 이해해도 충분하다.

/16 → 큰 네트워크
/24 → 작은 네트워크

4. Subnet이 필요한 이유

Subnet은 VPC 안에서 리소스를 배치하는 실제 네트워크 구역이다.

EC2를 만들 때도 Subnet을 지정해야 한다.

resource "aws_instance" "app" {
  subnet_id = aws_subnet.public.id
}

즉, VPC만 만들었다고 EC2를 바로 배치할 수 있는 것은 아니다.

EC2, RDS, ECS Task 같은 리소스는 실제로 Subnet 안에 배치된다.

VPC는 네트워크 공간이고, Subnet은 리소스가 들어가는 구역이다.

Terraform으로 Public Subnet을 만들면 다음과 같다.

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidr
  availability_zone       = var.public_availability_zone
  map_public_ip_on_launch = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-public-subnet"
  })
}

여기서 vpc_id를 보면 Subnet이 VPC를 참조하고 있다는 것을 알 수 있다.

vpc_id = aws_vpc.main.id

따라서 Subnet은 VPC가 먼저 있어야 생성할 수 있다.


5. Public Subnet과 Private Subnet

Subnet은 보통 Public Subnet과 Private Subnet으로 나누어 구성한다.

구분 의미 예시 리소스
Public Subnet 인터넷과 직접 통신 가능한 Subnet ALB, Bastion, Public EC2
Private Subnet 외부에서 직접 접근하지 못하게 구성하는 Subnet RDS, ECS Task, 내부 EC2

Public Subnet이 되려면 보통 다음 조건이 필요하다.

1. Subnet이 VPC 안에 있어야 한다.
2. Internet Gateway가 VPC에 연결되어 있어야 한다.
3. Route Table에 0.0.0.0/0 → Internet Gateway 경로가 있어야 한다.
4. Subnet이 해당 Route Table과 연결되어 있어야 한다.
5. EC2에 Public IP가 있어야 한다.

반대로 Private Subnet은 외부 인터넷에서 직접 접근되지 않도록 구성한다.

Private Subnet의 리소스가 외부로 나가야 하는 경우에는 NAT Gateway를 사용한다.


6. Internet Gateway

Internet Gateway는 VPC가 인터넷과 통신할 수 있도록 해주는 리소스다.

Terraform 코드는 다음과 같다.

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-igw"
  })
}

Internet Gateway는 VPC에 연결된다.

Internet Gateway
→ VPC 참조

하지만 Internet Gateway를 만들었다고 해서 Public Subnet이 바로 인터넷과 통신할 수 있는 것은 아니다.

Route Table에 인터넷으로 나가는 경로를 추가해야 한다.


7. Route Table

Route Table은 네트워크 트래픽이 어디로 가야 하는지 정의하는 리소스다.

Public Subnet용 Route Table은 다음처럼 만든다.

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
  }

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-public-rt"
  })
}

여기서 중요한 부분은 route 블록이다.

route {
  cidr_block = "0.0.0.0/0"
  gateway_id = aws_internet_gateway.main.id
}

이 설정은 다음 의미다.

모든 외부 트래픽(0.0.0.0/0)을
Internet Gateway로 보낸다.

즉, 이 Route Table과 연결된 Subnet은 인터넷으로 나갈 수 있다.


8. Route Table Association

Route Table을 만들었다고 해서 자동으로 Subnet에 적용되는 것은 아니다.

Subnet에 Route Table을 연결해야 한다.

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

이 리소스는 두 가지 값을 참조한다.

subnet_id
route_table_id

즉, Public Subnet과 Public Route Table이 모두 있어야 생성할 수 있다.

Public Subnet + Route Table
→ Route Table Association

이 연결이 있어야 해당 Subnet이 Public Route Table의 규칙을 따른다.


9. NAT Gateway가 필요한 경우

Private Subnet에 있는 리소스는 외부에서 직접 접근할 수 없어야 한다.

하지만 Private Subnet 안의 리소스가 외부 인터넷으로 나가야 하는 경우는 많다.

예를 들어 다음과 같은 경우다.

패키지 다운로드
외부 API 호출
Docker 이미지 pull
보안 업데이트
외부 인증 서버 호출

이때 사용하는 것이 NAT Gateway다.

Private Subnet
→ NAT Gateway
→ Internet Gateway
→ Internet

주의할 점은 NAT Gateway는 비용이 발생한다는 것이다.

학습용 프로젝트에서는 꼭 필요한 경우가 아니라면 NAT Gateway를 만들지 않는 것이 좋다.

NAT Gateway는 편리하지만 비용이 발생하므로 학습용 환경에서는 주의해야 한다.

NAT Gateway를 만들려면 보통 Elastic IP가 필요하다.

resource "aws_eip" "nat" {
  domain = "vpc"

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-nat-eip"
  })
}

resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public.id

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-nat"
  })

  depends_on = [aws_internet_gateway.main]
}

그리고 Private Route Table에서 외부 트래픽을 NAT Gateway로 보내도록 설정한다.

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
  }

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-private-rt"
  })
}

이 글의 기본 코드에서는 NAT Gateway를 필수로 포함하지 않고, 선택 구현으로만 설명한다.


10. Terraform 코드 전체 구성

이제 VPC, Public Subnet, Private Subnet, Internet Gateway, Route Table을 구성하는 코드를 정리해보자.


10.1 variables.tf

variable "project_name" {
  description = "Project name"
  type        = string
}

variable "vpc_cidr" {
  description = "VPC CIDR block"
  type        = string
}

variable "public_subnet_cidr" {
  description = "Public subnet CIDR block"
  type        = string
}

variable "private_subnet_cidr" {
  description = "Private subnet CIDR block"
  type        = string
}

variable "public_availability_zone" {
  description = "Availability zone for public subnet"
  type        = string
}

variable "private_availability_zone" {
  description = "Availability zone for private subnet"
  type        = string
}

10.2 terraform.tfvars

project_name = "demo"

vpc_cidr             = "10.0.0.0/16"
public_subnet_cidr   = "10.0.1.0/24"
private_subnet_cidr  = "10.0.2.0/24"

public_availability_zone  = "ap-northeast-2a"
private_availability_zone = "ap-northeast-2a"

실제 운영에서는 Public Subnet과 Private Subnet을 여러 AZ에 나누는 것이 좋다.

하지만 초보자 예제에서는 구조를 이해하기 위해 하나의 AZ로 단순화한다.


10.3 locals.tf

locals {
  common_tags = {
    Project   = var.project_name
    ManagedBy = "terraform"
  }
}

10.4 main.tf

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-vpc"
  })
}

resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidr
  availability_zone       = var.public_availability_zone
  map_public_ip_on_launch = true

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-public-subnet"
  })
}

resource "aws_subnet" "private" {
  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidr
  availability_zone = var.private_availability_zone

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-private-subnet"
  })
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-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.main.id
  }

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-public-rt"
  })
}

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

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

  tags = merge(local.common_tags, {
    Name = "${var.project_name}-private-rt"
  })
}

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

위 코드에서 Private Route Table에는 인터넷으로 나가는 route를 추가하지 않았다.

따라서 Private Subnet은 기본적으로 외부 인터넷으로 직접 나갈 수 없다.

Private Subnet에서 외부 인터넷으로 나가야 한다면 NAT Gateway를 추가해야 한다.


10.5 outputs.tf

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "public_subnet_id" {
  description = "Public subnet ID"
  value       = aws_subnet.public.id
}

output "private_subnet_id" {
  description = "Private subnet ID"
  value       = aws_subnet.private.id
}

output "public_route_table_id" {
  description = "Public route table ID"
  value       = aws_route_table.public.id
}

output "private_route_table_id" {
  description = "Private route table ID"
  value       = aws_route_table.private.id
}

이 output 값들은 이후 EC2, RDS, ECS 같은 리소스에서 사용할 수 있다.

EC2 → public_subnet_id 사용
RDS → private_subnet_id 사용
ECS → public/private subnet IDs 사용

11. 의존성 흐름

이번 네트워크 구성의 의존성 흐름은 다음과 같다.

 

 

Terraform은 코드가 작성된 순서가 아니라 참조 관계를 기반으로 생성 순서를 계산한다.

Terraform에서 중요한 것은 파일에 적힌 순서가 아니라 리소스 간 참조 관계다.

12. 자주 하는 실수

12.1 Internet Gateway만 만들고 Route Table을 설정하지 않음

Internet Gateway를 만들었다고 해서 Public Subnet이 바로 인터넷과 통신할 수 있는 것은 아니다.

Route Table에 다음 경로가 있어야 한다.

0.0.0.0/0 → Internet Gateway

12.2 Route Table을 만들고 Subnet에 연결하지 않음

Route Table은 Subnet에 Association 되어야 적용된다.

Route Table 생성
→ Subnet에 연결 필요

이 연결이 없으면 원하는 라우팅 규칙이 Subnet에 적용되지 않는다.


12.3 Public IP만 있으면 인터넷이 된다고 생각함

EC2에 Public IP가 있어도 다음 조건이 없으면 인터넷 통신이 되지 않는다.

Internet Gateway
Public Route Table
Route Table Association
Security Group 허용

즉, Public IP 하나만으로는 충분하지 않다.


12.4 Private Subnet에서 바로 인터넷이 될 거라고 생각함

Private Subnet은 외부에서 직접 접근하지 못하도록 만드는 구역이다.

Private Subnet 리소스가 외부로 나가야 한다면 NAT Gateway가 필요하다.

Private Subnet
→ NAT Gateway
→ Internet Gateway
→ Internet

단, NAT Gateway는 비용이 발생하므로 학습용에서는 주의해야 한다.


12.5 CIDR 대역이 겹침

Subnet CIDR은 VPC CIDR 안에 있어야 하고, 서로 겹치면 안 된다.

잘못된 예시는 다음과 같다.

VPC: 10.0.0.0/16
Public Subnet: 10.0.1.0/24
Private Subnet: 10.0.1.0/24

Public Subnet과 Private Subnet의 CIDR이 같기 때문에 문제가 된다.

올바른 예시는 다음과 같다.

VPC: 10.0.0.0/16
Public Subnet: 10.0.1.0/24
Private Subnet: 10.0.2.0/24

12.6 운영 환경에서 단일 AZ만 사용

학습용 예제에서는 하나의 AZ만 사용해도 된다.

하지만 운영 환경에서는 여러 AZ에 Subnet을 나누는 것이 좋다.

ap-northeast-2a
ap-northeast-2c

예를 들어 ALB, RDS, ECS는 보통 Multi-AZ 구성을 고려한다.


13. 마무리

이번 글에서는 Terraform으로 AWS 네트워크 기본 리소스를 구현해보았다.

구성한 리소스는 다음과 같다.

VPC
Public Subnet
Private Subnet
Internet Gateway
Public Route Table
Private Route Table
Route Table Association

핵심은 다음이다.

VPC는 네트워크 공간이다.
Subnet은 리소스가 배치되는 구역이다.
Internet Gateway는 인터넷 연결 통로다.
Route Table은 트래픽 경로를 정의한다.
Route Table Association은 Subnet에 Route Table을 적용한다.

이 구조를 이해하면 이후 EC2, RDS, ECS, ALB 같은 리소스를 훨씬 쉽게 이해할 수 있다.


한 줄 정리

VPC는 공간을 만들고, Subnet은 구역을 나누며, Route Table은 트래픽 방향을 정한다.


다음 글에서는 Security Group을 다룬다. Security Group은 AWS 리소스 간 접근을 제어하는 핵심 보안 리소스다.