Terraform 을 이용하여 로컬 리소스를 다루는것도 의미가 있겠으나, 내가 Terraform 을 알아보기 시작한 이유는 클라우드 인프라를 재사용가능한 효율적인 형태로 다루고 싶기 때문이었다.
그 첫번째로 AWS 의 기본 리소스중 하나인 VPC 를 만들어보겠다.
AWS VPC
우선 지금부터 만들려고하는 AWS VPC 에 대한 개념과 이를 구성하는 세부 요소에 대해서 알아보고 왜 많고 많은 AWS 인프라 중에서 VPC 를 먼저 만들려고하는지 알아보자
VPC(Virtual Private Cloud) 란 AWS 클라우드 내에서 자신의 네트워크를 "논리적" 으로 격리하여 정의할 수 있는 가상 네트워크이다. 인터넷에 노출되면 안되는 리소스를 격리하여 보안에 필수적이며, 보안 그룹과 네트워크 ACL 를 활용하여 더욱 보안성을 높일 수 있다.
VPC 는 가용 영역(AZ)이 아닌 Region 에 속하므로 여러 가용 영역에 걸쳐 네트워크를 확장할 수도 있다.
여러 서비스를 하나의 네트워크 망에서 다룰게 아니라면 만들려고 하는 AWS 리소스의 네트워크를 먼저 정의하는 것은 필수 과정일 것이다.
VPC의 구성 요소
1. 서브넷 (Subnet)
서브넷은 VPC 내에서 IP 주소 범위를 더 세분화한 논리적 단위이다. VPC는 여러 서브넷으로 나뉠 수 있으며, 각 서브넷은 특정 가용 영역에 연결된다.
- 특징:
- 퍼블릭 서브넷: 인터넷 게이트웨이와 연결되어 외부 인터넷과 통신 가능.
- 프라이빗 서브넷: NAT 게이트웨이 등을 통해 간접적으로 인터넷에 액세스 가능.
- 용도:
- 퍼블릭 서브넷: 웹 서버, 로드 밸런서와 같이 외부 인터넷과 통신이 필요한 리소스 배치.
- 프라이빗 서브넷: 데이터베이스, 애플리케이션 서버와 같이 내부적으로만 접근 가능한 리소스 배치.
2. 라우팅 테이블 (Route Table)
라우팅 테이블은 네트워크 트래픽의 경로를 정의하는 규칙의 집합이다. 서브넷은 반드시 라우팅 테이블과 연결되어야 하며, 이를 통해 트래픽이 어디로 전달될지 결정된다.
- 구성:
- 기본 라우트: local로 설정된 라우트는 VPC 내의 모든 서브넷 간 통신을 허용.
- 추가 라우트: 특정 목적지(CIDR)로 트래픽을 보내기 위해 IGW, NGW, VPN, Direct Connect 등을 지정.
- 용도:
- 퍼블릭 라우팅 테이블: 트래픽이 인터넷 게이트웨이(IGW)를 통해 외부 인터넷으로 이동하도록 설정.
- 프라이빗 라우팅 테이블: 트래픽이 NAT 게이트웨이(NGW)를 통해 인터넷에 간접적으로 연결되도록 설정.
3. 인터넷 게이트웨이 (Internet Gateway, IGW)
인터넷 게이트웨이는 VPC와 외부 인터넷 간의 통신을 가능하게 하는 네트워크 컴포넌트이다.
- 특징:
- VPC당 하나의 IGW만 연결 가능.
- 퍼블릭 서브넷과 연결된 리소스가 인터넷에서 액세스 가능하도록 지원.
- 용도:
- 퍼블릭 서브넷의 리소스(예: EC2 인스턴스)가 인터넷과 양방향으로 통신할 수 있도록 설정.
- 일반적으로 웹 서버, API Gateway 등 외부와 직접 통신해야 하는 리소스에서 사용.
4. NAT 게이트웨이 (Network Address Translation Gateway, NGW)
NAT 게이트웨이는 프라이빗 서브넷의 리소스가 인터넷과 통신할 수 있도록 하지만, 외부에서 프라이빗 리소스를 직접적으로 액세스하지 못하도록 하는 역할을 한다.
- 특징:
- 퍼블릭 서브넷에 배치되며, Elastic IP를 사용.
- 프라이빗 서브넷에서 오는 요청을 NAT 게이트웨이가 대행하여 외부와 통신.
- 용도:
- 프라이빗 서브넷의 리소스(예: 데이터베이스, 애플리케이션 서버)가 소프트웨어 업데이트나 외부 API 호출을 위해 인터넷과 통신할 때 사용.
- 외부로부터의 직접적인 트래픽은 차단.
Terraform 으로 VPC 만들기
1. AWS CLI 준비
근본적인 질문이 있다. Terraform 은 내 컴퓨터에서 돌아가고 결국 AWS 에서 제공하는 API 를 호출할텐데 이러한 API 호출에 대한 인증,인가를 어떻게 이뤄지는 걸까? Terraform 안에 AWS credential 정보를 넣어야 하는걸까?
물론 Terraform 스크립트 안에 AWS credential 정보를 넣을 수도 있을 것이다. 하지만 그렇게 한다면 과연 이 스크립트를 팀내에서, 혹은 외부와 공유할 수 있을까?
그래서 등장하는게 AWS CLI 이다. AWS CLI 를 이용하면 터미널에서 AWS API 를 이용하여 클라우드 인프라를 생성, 삭제, 조회 할 수 있으며, 무엇보다 aws configure 명령어를 통해 지금부터 요청을 보내려고하는 IAM role 을 정해줄 수 있다.
굳이 로컬 작업 환경이 아니더라도 github action 에서는 별도 설치없이 AWC CLI 를 이용할 수 있는 등 CI/CD 에서 자주 이용하게 된다.
2. 기본 Terraform 구조
어떤 변수 없이, 그저 static 하게 main.tf 하나로도 AWS VPC와 subent 을 public 과 private 로 나누고 라우트 테이블을 분리하여 인터넷 게이트웨이와 네트워크 게이트웨이로 라우팅하도록 만들 수 있다.
main.tf 는 다음과 같다.
provider "aws" {
region = "ap-northeast-2"
}
# VPC 생성
resource "aws_vpc" "my_vpc" {
cidr_block = "10.0.0.0/16"
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "my-vpc"
}
}
# 인터넷 게이트웨이 생성
resource "aws_internet_gateway" "my_igw" {
vpc_id = aws_vpc.my_vpc.id
tags = {
Name = "my-igw"
}
}
# 퍼블릭 서브넷 생성
resource "aws_subnet" "public_subnets" {
count = 3
vpc_id = aws_vpc.my_vpc.id
cidr_block = cidrsubnet("10.0.0.0/16", 4, count.index) # /20 서브넷 생성
availability_zone = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"][count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
# 프라이빗 서브넷 생성
resource "aws_subnet" "private_subnets" {
count = 3
vpc_id = aws_vpc.my_vpc.id
cidr_block = cidrsubnet("10.0.0.0/16", 4, count.index + 3) # /20 서브넷 생성
availability_zone = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"][count.index]
tags = {
Name = "private-subnet-${count.index + 1}"
}
}
# NAT 게이트웨이 생성
resource "aws_eip" "ngw_eip" {
domain = "vpc"
tags = {
Name = "ngw-eip"
}
}
resource "aws_nat_gateway" "nat_gateway" {
allocation_id = aws_eip.ngw_eip.id
subnet_id = aws_subnet.public_subnets[0].id # 첫 번째 퍼블릭 서브넷에 배치
tags = {
Name = "nat-gateway"
}
}
# 퍼블릭 라우팅 테이블 생성
resource "aws_route_table" "public_route_table" {
vpc_id = aws_vpc.my_vpc.id
tags = {
Name = "public-route-table"
}
}
# 퍼블릭 라우팅 테이블에 IGW 추가
resource "aws_route" "public_route" {
route_table_id = aws_route_table.public_route_table.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.my_igw.id
}
# 퍼블릭 서브넷과 퍼블릭 라우팅 테이블 연결
resource "aws_route_table_association" "public_associations" {
count = 3
subnet_id = aws_subnet.public_subnets[count.index].id
route_table_id = aws_route_table.public_route_table.id
}
# 프라이빗 라우팅 테이블 생성
resource "aws_route_table" "private_route_table" {
vpc_id = aws_vpc.my_vpc.id
tags = {
Name = "private-route-table"
}
}
# 프라이빗 라우팅 테이블에 NAT 게이트웨이 추가
resource "aws_route" "private_route" {
route_table_id = aws_route_table.private_route_table.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat_gateway.id
}
# 프라이빗 서브넷과 프라이빗 라우팅 테이블 연결
resource "aws_route_table_association" "private_associations" {
count = 3
subnet_id = aws_subnet.private_subnets[count.index].id
route_table_id = aws_route_table.private_route_table.id
}
3. static한 방식의 문제점, cidr 중복
하지만 이렇게 VPC 리소스를 생성하면 결정적인 문제가 생긴다. 우선 재사용이 불가능하다는 것이다. 왜일까?
AWS CLI 가 연결되어 있다면 이런 커맨드를 입력해보자
1. VPC 조회
aws ec2 describe-vpcs --query "Vpcs[*].[VpcId, CidrBlock, Tags[?Key=='Name'].Value | [0]]" --output table
이런 결과를 반환한다. 원래 AWS 에서 기본으로 제공해주던 VPC 외에 위의 스크립트 때문에 생성된 my-vpc 가 보인다. 이 vpc 의 id를 이용하여 subent 들을 조회할 수 있다.
2. 특정 VPC 의 subent 조회
새롭게 생성한 my-vpc 의 subent 들을 조회해보겠다.
aws ec2 describe-subnets \
--filters "Name=vpc-id,Values={VPC_ID}" \
--query "Subnets[*].[SubnetId, AvailabilityZone, MapPublicIpOnLaunch, CidrBlock]" \
--output table
문제점이 보이는가? 바로 위 스크립트에 명시했던 subent 이 규칙에 따라 생성된것이 확인된다. subent 리소스를 생성한 규칙을 다시 보겠다.
resource "aws_subnet" "public_subnets" {
count = 3
vpc_id = aws_vpc.my_vpc.id
cidr_block = cidrsubnet("10.0.0.0/16", 4, count.index) # /20 서브넷 생성
availability_zone = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"][count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
resource "aws_subnet" "private_subnets" {
count = 3
vpc_id = aws_vpc.my_vpc.id
cidr_block = cidrsubnet("10.0.0.0/16", 4, count.index + 3) # /20 서브넷 생성
availability_zone = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"][count.index]
tags = {
Name = "private-subnet-${count.index + 1}"
}
}
이 리소스 생성의 행심은 cidrsubent 이다. cidrsubent 함수는 CIDR 블록을 분할 및 하위 네트워크 생성에 사용된다. 여기서는 VPC 의 cidr_block 을 "10.0.0.0/16" 로 정했기 때문에 이를 기준으로 새로운 하위 네트워크를 생성하도록 하였다. 즉, 다음에 다른 Terraform 작업 공간에서 이 스크립트를 다시 이용할때 겹치지 않는 cidr 을 subent 에 부여한다는 것을 보장할 수도 없으며 무엇보다 VPC 의 cidr_block 이 겹치는 문제가 발생한다.
3.1 cidr 중복 문제의 해결
기존 vpc 와 subent 의 cidr 을 읽어와서 처리하면 해결될 수 있는 문제이다. main.tf 제일 윗 부분을 이렇게 바꾸로
# 기존 VPC 확인
data "aws_vpcs" "existing_vpcs" {}
# 기존 VPC CIDR 블록 가져오기
data "aws_vpc" "vpc_details" {
count = length(data.aws_vpcs.existing_vpcs.ids)
id = data.aws_vpcs.existing_vpcs.ids[count.index]
}
# 기존 서브넷 CIDR 가져오기
data "aws_subnets" "existing_subnets" {}
data "aws_subnet" "subnet_details" {
count = length(data.aws_subnets.existing_subnets.ids)
id = data.aws_subnets.existing_subnets.ids[count.index]
}
# 동적 CIDR 계산
locals {
base_cidr = "10.0.0.0/8" # CIDR 블록 기본 값
existing_cidrs = concat(
[for vpc in data.aws_vpc.vpc_details : vpc.cidr_block],
[for subnet in data.aws_subnet.subnet_details : subnet.cidr_block]
)
new_vpc_cidr = cidrsubnet(local.base_cidr, 8, length(local.existing_cidrs)) # CIDR 범위 /16~ 사용
}
aws_vpc 와 aws_subent 을 생성하는 블록을 변경하자
resource "aws_vpc" "my_vpc" {
cidr_block = local.new_vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "my-vpc"
}
}
# 퍼블릭 서브넷 생성
resource "aws_subnet" "public_subnets" {
count = 3
vpc_id = aws_vpc.my_vpc.id
cidr_block = cidrsubnet(local.new_vpc_cidr, 4, count.index) # /20 서브넷 생성
availability_zone = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"][count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
# 프라이빗 서브넷 생성
resource "aws_subnet" "private_subnets" {
count = 3
vpc_id = aws_vpc.my_vpc.id
cidr_block = cidrsubnet(local.new_vpc_cidr, 4, count.index + 3) # /20 서브넷 생성
availability_zone = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"][count.index]
tags = {
Name = "private-subnet-${count.index + 1}"
}
}
여기까지 수정된 파일은 이다.
4. static 한 방식의 문제점, resoucre 이름 및 개수
다른 문제점은 리소스 개수가 하드코딩 되어있다는 점이다. 가용영역과 리소스의 이름을 변수화해서 이러한 문제를 해소할 수 있다.
4.1 variable 을 이용한 해결
# variable.tf
variable "availability_zones" {
description = "Availability zones to use for subnets"
type = list(string)
default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}
variable "resource_name_prefix" {
description = "Prefix for resource names"
type = string
default = "my"
}
variable "base_cidr" {
description = "Base CIDR for VPC"
type = string
default = "10.0.0.0/16"
}
# main.tf
# 추가
resource "random_string" "resource_suffix" {
length = 6
upper = false
special = false
}
# ... 기존 스크립트
이상의 스크립트를 이용하면 이렇게 유연하게 VPC 를 생성할 수 있겠다.
관련코드는 깃헙 (https://github.com/jts8257/terraform-aws/tree/main/vpc-section) 에 올려두었다.
'탐구 생활 > Terraform' 카테고리의 다른 글
Terraform 기초: 리소스 생명 주기 및 데이터 소스 관리하기 (0) | 2024.11.22 |
---|---|
Terraform 기초: commands (0) | 2024.11.21 |
Terraform 기초: State (0) | 2024.11.21 |
Terraform 기초: depends_on, output (0) | 2024.11.19 |
Terraform 기초: HCL 기본기 (0) | 2024.11.17 |