TFAS (TerraForm Addicted School)의 내용을 정리한 글입니다.
심각한 테라폼 중독입니다 - 책으로 스터디를 진행합니다.

다중 프로바이더를 사용한 vpc 피어링 모듈을 만들어보자. vpc 피어링은 서로 다른 vpc 사이의 프라이빗 통신을 가능하게 하는 네트워크 연결로 서로 다른 계정 또는 다른 리전의 vpc 와 피어링 연결이 가능하다. 따라서 단일 계정 혹은 다중 계정 중 어떤 상황에서도 사용 가능한 vpc 피어링 모듈을 설계하고 구현하자.
미리 고려해야할 점
테라폼의 AWS 프로바이더는 AWS API 를 사용하여 작동한다. 따라서 기능에 따라 콘솔에서 수행하는 작업 흐름과 API 를 직접 사용하는 작업이 서로 흐름이 다른 경우가 있을 수 있다. 대표적인 예가 VPC 피어링으로 콘솔 작업과는 다르게 다음과 같은 고려사항을 가진다.
다중 프로바이더 환경에서 피어링 요청 수락
콘솔에서 vpc 피어링을 맺는 경우 요청 받는 측의 계정이나 리전에서 연결 요청을 수락해야 한다. 이 과정을 테라폼에서 수행할 때는 자동으로 요청을 수락하도록 aws_vpc_peering_connection_accepter 리소스 블록의 auto_accept 설정값을 활용해야 한다.
aws_vpc_peering_connection 리소스에도 auto_accept 설정값이 존재한다. 해당 리소스는 앞서 언급한 리소스와 다르게 같은 계정과 같은 리전에 존재하는 단일 프로바이더 상황의 vpc 피어링만 사용할 수 있다. 즉, 다중 프로바이더 상황에서는 해당 리소스를 사용할 수 없다.
단일 또는 다중 프로바이더를 사용하는 상황에 사용할 수 있도록 aws_vpc_peering_connection_accepter 를 사용하자.
모듈로 두개의 프로바이더 전달하기
테라폼 프로바이더에는 반복문을 사용할 수 없다. 프로바이더 블록을 직접 선언한 경우 그렇게 정의한 모듈 또한 반복문을 사용할 수 없다. 따라서 모듈을 사용할 때 요청자와 수락자 프로바이더를 각각 입력받아 호출해야한다.
실행 환경 재구성하기
서로 다른 프로바이더의 vpc 와 나머리리소스를 만들어 관리해야 한다. 기본 리소스는 프로바이더 별로 실행 환경을 분리하고, vpc 피어링과 같은 다양한 프로바이더를 걸쳐 사용할 수 있는 네트워크 환경에 대해서는 추가로 실행 환경을 분리하는것이 좋다.
현재 앞선 모듈들의 구성에 대한 디렉토리 구조는 다음과 같다.
.
├── env
│ └── main.tf
├── info_files
│ └── production
│ ├── ec2
│ │ └── amazon-linux.yaml
│ ├── sg
│ │ └── mysql.csv
│ └── vpc.yaml
└── modules
├── ec2
│ ├── main.tf
│ ├── output.tf
│ └── variables.tf
├── sg
│ ├── main.tf
│ ├── output.tf
│ └── variables.tf
├── vpc
│ ├── gateway.tf
│ ├── main.tf
│ ├── output.tf
│ └── variables.tf
└── vpc_peering
└── output.tf
현재 디렉토리 구조는 다중 프로바이더 환경에 맞지 않다. 다중 프로바이더를 각각 실행할 수 있고 네트워크 실행환경도 분리하기 위해 다음과 같은 디렉토리 구조를 사용하는 것이 좋다.
.
├── env_network # 실행 환경: 네트워크 환경 관리
│ └── main.tf
├── env_seoul
│ ├── info_files # 실행 환경 : 서울 리전 기본 리소스 관리
│ │ ├── seoul-dev
│ │ │ └── vpc.yaml
│ │ └── seoul-prod
│ │ └── vpc.yaml
│ └── main.tf
└── env_virginia # 실행 환경 : 버지니아 리전 기본 리소스 관리
├── info_files
│ ├── virginia-dev
│ │ └── vpc.yaml
│ └── virginia-prod
│ └── vpc.yaml
└── main.t
요구사항 정리하기
네트워크 vpc 피어링 모듈을 만들 때 요구사항은 다음과 같다.
- topology.yaml 파일을 생성하여 네트워크 구성을 기술하고 이를 사용해 피어링 모듈 호출한다.
- env_seoul 과 env_virginia 실행 환경에서 만든 vpc 를 사용한다. 즉, 다른 실행 환경 상태에 있는 출력 값을 가지고 와서 사용해야 한다. 이를 위해 원격 상태 백엔드를 사용해야 한다.
- vpc 피어링을 맺는 경우 항상 dns resolution 을 활성화한다.
- vpc 피어링을 맺는 경우 상대 vpc 로 라우트를 각 vpc 에 속한 모든 라우트 테이블에 자동으로 추가한다.
먼저 원격의 상태 백엔드를 사용하는 구성에 대해서 알아보자.
원격 상태 설정하기
테라폼 상태 파일을 별도로 설정하지 않는 경우 로컬에서 tfstate 확장자를 가지는 로컬 파일이 생성된다. 그러나 두 명 이상의 협업, 다른 실행 환경의 출력값을 전달하려는 경우와 같은 상황에서는 상태 파일을 원격 저장소에 저장해야 한다.
테라폼 백엔드를 위한 s3 는 수동으로 만들어서 사용한다고 가정한다. 테라폼을 관리하기 위한 리소스를 테라폼으로 만들면 테라폼 상태 백엔드가 구성되어 있지 않은 상태에서 테라폼 상태 파일이 기록되어야 하기 때문에 부트스트래핑 문제가 발생한다. 생성할 s3 는 테라폼 상태 파일의 버전 관리를 위해 s3 버킷 버저닝을 활성화 하는 것이 좋다.
s3 를 사용하는 백엔드 블록
테라폼의 백엔드 블록엔 변수 처리가 불가능하다. 백엔드 블록은 terraform init 단계에서 필요한데, 테라폼 환경을 가장 먼저 만드는 명령어가 실행되는 시점에는 실행 환경에 존재하는 변수가 처리되어 있지 않다. 초기화가 된 후 변수 사용이 가능하기 때문에 초기화 단계에는 변수 사용이 되지 않는 것이다. 따라서 s3 버킷 이름이나 파일을 저장할 키-값을 실행 환경별로 직접 지정한다.
env_seoul/backend.tf
terraform {
backend "s3" {
bucket = "terraform-tfstate"
key = "seoul.tfstate"
region = "ap-northeast-2"
encrypt = true
profile = "terraform"
}
}
env_virginia/backend.tf
terraform {
backend "s3" {
bucket = "terraform-tfstate"
key = "virginia.tfstate"
region = "ap-northeast-2"
encrypt = true
profile = "terraform"
}
}
이후 terraform init 을 실행하고 상태 파일이 s3 에 잘 저장되는지 확인한다.
cd env_seoul && terraform init
cd env_virginia && terraform init
vpc 모듈의 출력을 원격 상태 파일로 전달
출력 블록을 사용하면 서로 다른 모듈간의 참조 뿐 아니라 서로 다른 실행 환경간 참조에도 사용된다. 각 실행환경의 결과물을 env_network 실행 환경에서 참조하기 위해 vpc 아이디에 대한 출력 블록을 설정한다.
env_seoul/output.tf, env_virginia/output.tf
output "vpc_id" {
description = "VPC ID"
value = {
for k, v in module.vpc : k => v.vpc_id
}
}
두 테라폼 실행 환경에 output.tf 를 생성하고 위 코드를 작성해준다. vpc 아이디만 있어도 데이터 블록을 사용하여 vpc 정보를 가지고 오고 라우팅을 위한 라우트 테이블 조회할 수 있다.
입력값과 전달 방식 정의하기
vpc 피어링을 구성하기 위한 topology.yaml 파일은 다음과 같은 형식을 가진다.
prod_peering:
requester:
tf_env: seoul
vpc: seoul-prod
accepter:
tf_env: virginia
vpc: virginia-prod
dev_peering:
requester:
tf_env: seoul
vpc: seoul-dev
accepter:
tf_env: virginia
vpc: virginia-dev
seoul_peering:
requester:
tf_env: seoul
vpc: seoul-prod
accepter:
tf_env: seoul
vpc: seoul-dev
vpc 의 피어링을 위한 요청자와 수락자의 역할을 다르게 명시하는 것이 좋다. 테라폼 실행 환경에 대한 정보를 각 요청마다 사용했으며, 이는 상태 백엔드의 key 로 사용한 이름과 매칭된다.
테라폼 상태 가져오기
s3 에 업로드 된 상태 파일을 가져와 정제하는 테라폼 코드를 작성하자.
env_network/main.tf
locals {
topology = yamldecode(file("./topology.yaml"))
tf_vpc_env_list = distinct(flatten([
for k, v in local.topology : [v.requester.tf_env, v.accepter.tf_env]
]))
}
data "terraform_remote_state" "vpc" {
for_each = toset(local.tf_vpc_env_list)
backend = "s3"
config = {
bucket = "terraform-tfstate"
key = "${each.key}.tfstate"
region = "ap-northeast-2"
profile = "terraform"
}
}
토폴로지 파일에 적혀있는 모든 tf_env 값을 모아 리스트로 만든다. 이를 데이터 블록에서 사용하여 원격 파일이 업로드되어 있는 s3 버킷 정보를 입력하여 원격의 상태 파일을 가지고 올 수 있다. 이렇게 가지고 온 상태 파일을 로컬 변수로 지정하여 vpc id 목록을 로컬 변수로 지정할 수 있다.
locals {
vpc_id = {
for k, v in data.terraform_remote_state.vpc : k => v.outputs.vpc_id
}
}
실행 환경에서 vpc 피어링 모듈 호출
# vpc peering
locals {
env_tags = {
tf_env = "env_network"
}
}
module "seoul_to_virginia_peering" {
source = "../../ch11/modules/vpc_peering"
for_each = {
for k, v in local.topology : k => v
if v.requester.tf_env == "seoul" && v.accepter.tf_env == "virginia"
}
providers = {
aws.requester = aws.seoul
aws.accepter = aws.virginia
}
name = each.key
requester_vpc_id = local.vpc_ids[each.value.requester.tf_env][each.value.requester.vpc]
accepter_vpc_id = local.vpc_ids[each.value.accepter.tf_env][each.value.accepter.vpc]
tags = local.env_tags
}
module "seoul_to_seoul_peering" {
source = "../../ch11/modules/vpc_peering"
for_each = {
for k, v in local.topology : k => v
if v.requester.tf_env == "seoul" && v.accepter.tf_env == "seoul"
}
providers = {
aws.requester = aws.seoul
aws.accepter = aws.seoul
}
name = each.key
requester_vpc_id = local.vpc_ids[each.value.requester.tf_env][each.value.requester.vpc]
accepter_vpc_id = local.vpc_ids[each.value.accepter.tf_env][each.value.accepter.vpc]
tags = local.env_tags
}
테라폼은 변수 연산보다 프로바이더 초기화가 선행되어야 하는 구조이므로 실행 시점에 결정되는 데이터에 따라 프로바이더를 동적으로 변경할 수 없다. 단일 반복문 내에서 서로 다른 리전 권한을 번갈아 쓸 수 없기에, 리전 조합(서울-서울, 서울-버지니아 등)별로 모듈 블록을 물리적으로 분리해서 선언해야한다.
결국 각 모듈 블록마다 if 조건으로 데이터를 필터링해 넣고, 해당 그룹에 맞는 프로바이더를 명시적으로 고정하여 각각 호출하는 방식을 필수적으로 사용해야 한다.
모듈 만들기
먼저 변수를 임시로 설정한다.
modules/vpc_peering/variables.tf
variable "name" {}
variable "requester_vpc_id" {}
variable "accepter_vpc_id" {}
variable "tags" {
default = {}
}
모듈 내부에서 사용할 vpc 에 대한 로컬 변수와 태그를 선언한다.
data "aws_vpc" "requester" {
provider = aws.requester
id = var.requester_vpc_id
}
data "aws_vpc" "accepter" {
provider = aws.accepter
id = var.accepter_vpc_id
}
locals {
requester_vpc = data.aws_vpc.requester
accepter_vpc = data.aws_vpc.accepter
module_tag = merge(
var.tags,
{
Name = var.name
tf_moudle = "vpc_peering"
Request_VPC = lookup(local.requester_vpc.tags, "Name", "네임태그없음")
Accepter_VPC = lookup(local.accepter_vpc.tags, "Name", "네임태그없음")
}
)
}
vpc 피어링 연결 생성 및 자동 수락
vpc 피어링 연결 리소스는 요청자 프로바이더에서 만들어야 하며, 생성 과정에서 수락자 계정의 아이디와 리전 정보가 필요하다. 이에 따라 수락자의 계정 정보와 리전을 local 로 선언하고 모듈을 만들어보자.
modules/vpc_peering/locals.tf
module "accepter" {
source = "../../../ch09/meta_module"
providers = {
aws = aws.accepter
}
}
locals {
accepter_account_id = module.accepter.account_id
accepter_region = module.accepter.region_name
}
modules/vpc_peering/main.tf
resource "aws_vpc_peering_connection" "this" {
provider = aws.requester
vpc_id = local.requester_vpc.id
peer_vpc_id = local.accepter_vpc.id
peer_owner_id = local.accepter_account_id
peer_region = local.accepter_region
auto_accept = false
tags = local.module_tag
}
수락자 리소스는 수락자 프로바이더에서 선언한다.
modules/vpc_peering/main.tf
resource "aws_vpc_peering_connection_accepter" "this" {
provider = aws.accepter
vpc_peering_connection_id = aws_vpc_peering_connection.this.id
auto_accept = true
tags = local.module_tag
}
DNS resolution
vpc 피어링에서 DNS Resolution 을 허용하는 것은 피어링된 VPC 리소스에 DNS 로 접근할 수 있게 한다는 의미이다.
modules/vpc_peering/main.tf
# VPC Peering Options : DNS Resolution
locals {
peering_id = aws_vpc_peering_connection_accepter.this.id
}
resource "aws_vpc_peering_connection_options" "requester" {
provider = aws.requester
vpc_peering_connection_id = local.peering_id
requester {
allow_dns_resolution_from_remote_vpc = true
}
}
resource "aws_vpc_peering_connection_options" "accepter" {
provider = aws.accepter
vpc_peering_connection_id = local.peering_id
accepter {
allow_dns_resolution_from_remote_vpc = true
}
}
요청자와 수락자 측에서 각각 선언해야 한다. 리소스간 종속성을 위해 aws_vpc_peering_connection_accepter 의 id 를 가지고 왔다. 피어링 연결 옵션은 피어링이 수락된 이후 생성되어야 하기 때문에 수락자 리소스로부터 암시적인 종속성을 설정해주기 위해 수락자의 출력값을 사용한다.
라우트 테이블에 라우트 추가
두 vpc 간 피어링을 생성한 후 라우트 테이블마다 해당 피어링 연결로 향하는 라우트 규칙을 추가해야 한다. 현재는 구현 편의를 위해 해당 vpc 에 속한 모든 라우트 테이블에 라우트를 모두 추가하는 방식으로 구현한다.
하나의 vpc 에 존재하는 모든 라우트 테이블을 데이터 블록으로 받아온 후 필요한 리소스 블록을 생성하자.
modules/vpc_peering/locals.tf
# route table
data "aws_route_tables" "requester" {
provider = aws.requester
vpc_id = var.requester_vpc_id
}
data "aws_route_tables" "accepter" {
provider = aws.accepter
vpc_id = var.accepter_vpc_id
}
locals {
requester_rtbs = data.aws_route_tables.requester.ids
accepter_rtbs = data.aws_route_tables.accepter.ids
}
modules/vpc_peering/main.tf
# VPC Route table
resource "aws_route" "requester_to_accepter" {
for_each = toset(local.requester_rtbs)
provider = aws.requester
route_table_id = each.key
destination_cidr_block = local.accepter_vpc.cidr_block
vpc_peering_connection_id = local.peering_id
}
resource "aws_route" "accepter_to_requester" {
for_each = toset(local.accepter_rtbs)
provider = aws.accepter
route_table_id = each.key
destination_cidr_block = local.requester_vpc.cidr_block
vpc_peering_connection_id = local.peering_id
}
요청자 vpc 라우트 테이블에서는 수락자 vpc cidr 로 vpc 라우트 테이블에서는 요청자 vpc cidr 로 향하게 한다.
유효성 검사
변수 타입 유효성 검사
variable "name" {
description = "VPC peering name"
type = string
}
variable "requester_vpc_id" {
description = "requester vpc id"
type = string
}
variable "accepter_vpc_id" {
description = "accepter vpc id"
type = string
}
variable "tags" {
description = "tag for all resources"
type = map(string)
default = {}
}
프로바이더 별명 지정
모듈 호출시 프로바이더를 전달할 떄 aws.requester 와 aws.accepter 로 지정하여 넘겨주어야 한다. 이를 검증하지 않는 경우 작동이 멈추지 않고 경고 메시지가 표시된다.
modules/vpc_peering/providers.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
configuration_aliases = [aws.requester, aws.accepter]
}
}
}
출력값 설정 및 더 고려해볼 만한 것
앞서 학습한 보안 그룹 모듈을 구현할때와 마찬가지로 외부에 출력할 만한 값은 피어링 연결 리소스의 아이디정도이다.
module/vpc_peering/output.tf
output "peering_id" {
description = "VPC Peering ID"
value = local.peering_id
}
이전에 다뤘던 모듈에 비해 간단한 모듈이기 때문에 고려할 수 있는 사항이 많지는 않다. dns resolution 을 선택적으로 허용할 수 있게 수정하거나 원하는 라우트 테이블에만 vpc 피어링 연결로 향하는 라우트를 추가하는 것들을 고려할 수 있다.
'School > TFAS' 카테고리의 다른 글
| [TFAS] 챕터 16 쿠버네티스 관련 프로바이더 (0) | 2025.12.14 |
|---|---|
| [TFAS] 챕터 15 하시코프 공식 유틸리티 프로바이더 (0) | 2025.12.10 |
| [TFAS] 챕터 13 VPC 와 보안 그룹 모듈의 출력값을 활용하는 EC2 모듈 만들기 (0) | 2025.12.06 |
| [TFAS] 챕터 12 CSV 파일로 관리하는 보안 그룹 모듈 만들기 (1) | 2025.12.06 |
| [TFAS] 챕터 11 YAML 파일로 관리하는 VPC 모듈 만들기 (0) | 2025.12.04 |