Manage similar resources with for each(for each로 유사한 리소스 관리하기)
Terraform의 for_each 메타 인수를 사용하면 데이터 구조를 반복하여 데이터 구조의 각 항목에 대한 리소스 또는 모듈을 구성함으로써 유사한 리소스 집합(a set of similar resources)을 구성할 수 있습니다.
for_each를 사용하여 동일한 라이프사이클을 공유하는 유사한 리소스 집합(a set of similar resources)을 사용자 정의할 수 있습니다.
이 튜토리얼에서는 AWS에서 VPC, 로드 밸런서 및 EC2 인스턴스를 프로비저닝합니다.
그런 다음 for_each 인수와 데이터 구조로 여러 프로젝트를 프로비저닝하도록 구성을 리팩터링합니다.
그런 다음 for_each 인수와 데이터 구조로 여러 프로젝트를 프로비저닝하도록 구성을 리팩터링합니다.
1. 전제 조건(Prerequisites)
이 튜토리얼은 Terraform Community Edition 또는 HCP Terraform 2가지 방법을 통해 실습이 가능합니다.
HCP Terraform과 Terraform Community Edition의 주요 차이점
기능 | HCP Terraform | Terraform Community Edition |
상태 관리 | 원격 상태 파일 관리 및 저장소 제공 | 로컬 상태 파일 관리 (원격 저장소는 별도 설정 필요) |
명령어 실행 | 원격 실행 지원 | 로컬 실행 |
작업 공간 | 작업 공간(workspace) 관리 | 작업 공간 개념 존재, 그러나 고급 관리 기능 없음 |
계획 출력 및 요약 | 웹 인터페이스에서 구조화된 계획 출력 및 실행 요약 제공 | 로컬 출력 |
팀 협업 | 팀 단위 협업 및 역할/권한 관리 | 제한된 협업 기능 (버전 관리 시스템 필요) |
액세스 제어 | 사용자 관리 및 액세스 제어 기능 제공 | 사용자 관리 및 액세스 제어 기능 부재 |
사용 비용 | 유료 (기본 무료 플랜 존재) | 무료 (오픈 소스) |
해당 실습은 Terraform Community Edition을 기반으로 실습을 진행합니다.
2. GitHub Repository
https://github.com/hashicorp/learn-terraform-for-each
2-1. 파일 구조
.
├── modules
│ └── aws-instance
│ ├── main.tf
│ ├── outputs.tf
│ └── variables.tf
├── main.tf
├── outputs.tf
├── terraform.tf
└── variables.tf
3. Apply initial configuration
3-1. git clone
git clone https://github.com/hashicorp/learn-terraform-for-each.git
3-2. terraform.tf 파일 수정
terraform.tf > HCP Terraform 통합을 구성하는 클라우드 블록을 주석 처리
3-3. terraform init
terraform init
3-4. terraform apply
terraform apply -auto-approve
....
Terraform에서 서로 다른 환경(예: 개발, 테스트, 프로덕션)을 독립적으로 관리하려면 for_each 대신 별도의 Terraform 프로젝트나 작업 공간(workspaces)을 사용하는 것이 좋습니다. 이는 환경별로 리소스 수명 주기를 독립적으로 관리할 수 있기 때문입니다. 이 접근 방식은 특히 실수로 인한 전체 환경의 파괴를 방지하는 데 유용합니다.
예시 프로젝트 구조
.
├── dev
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
├── prod
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
├── staging
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
├── .terraform.lock.hcl
├── .gitignore
├── README.md
└── terraform.tfvars
3-5. AWS Web Console 리소스 배포 확인
4. Define a map to configure each project
4-1. variables.tf 코드 삭제
코드 삭제 후 variables.tf
4-2. project (map 변수) 추가
variable "project" {
description = "Map of project names to configuration."
type = map(any)
default = {
client-webapp = {
public_subnets_per_vpc = 2,
private_subnets_per_vpc = 2,
instances_per_subnet = 2,
instance_type = "t2.micro",
environment = "dev"
},
internal-webapp = {
public_subnets_per_vpc = 1,
private_subnets_per_vpc = 1,
instances_per_subnet = 2,
instance_type = "t2.nano",
environment = "test"
}
}
}
# 리스트 사용 예제
variable "subnet_names" {
description = "List of subnet names"
type = list(string)
default = ["subnet-1", "subnet-2", "subnet-3"]
}
resource "aws_subnet" "example" {
for_each = toset(var.subnet_names)
vpc_id = "vpc-12345678"
cidr_block = "10.0.0.0/24"
availability_zone = "us-east-1a"
tags = {
Name = each.key
}
}
# -------------------------------------
# Set 사용 예제
variable "subnet_names_set" {
description = "Set of subnet names"
type = set(string)
default = ["subnet-1", "subnet-2", "subnet-3"]
}
resource "aws_subnet" "example_set" {
for_each = var.subnet_names_set
vpc_id = "vpc-12345678"
cidr_block = "10.0.0.0/24"
availability_zone = "us-east-1a"
tags = {
Name = each.key
}
}
5. Add for_each to the VPC
5-1. main.tf - vpc (module)
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.2"
for_each = var.project
cidr = var.vpc_cidr_block
azs = data.aws_availability_zones.available.names
private_subnets = slice(var.private_subnet_cidr_blocks, 0, each.value.private_subnets_per_vpc)
public_subnets = slice(var.public_subnet_cidr_blocks, 0, each.value.public_subnets_per_vpc)
enable_nat_gateway = true
enable_vpn_gateway = false
map_public_ip_on_launch = false
}
5-2. main.tf - app_security_group (module)
module "app_security_group" {
source = "terraform-aws-modules/security-group/aws//modules/web"
version = "4.9.0"
for_each = var.project
name = "web-server-sg-${each.key}-${each.value.environment}"
description = "Security group for web-servers with HTTP ports open within VPC"
vpc_id = module.vpc[each.key].vpc_id
ingress_cidr_blocks = module.vpc[each.key].public_subnets_cidr_blocks
}
5-3. main.tf - lb_security_group (module)
module "lb_security_group" {
source = "terraform-aws-modules/security-group/aws//modules/web"
version = "4.9.0"
for_each = var.project
name = "load-balancer-sg-${each.key}-${each.value.environment}"
description = "Security group for load balancer with HTTP ports open within VPC"
vpc_id = module.vpc[each.key].vpc_id
ingress_cidr_blocks = ["0.0.0.0/0"]
}
5-4. main.tf - elb_http (module)
module "elb_http" {
source = "terraform-aws-modules/elb/aws"
version = "3.0.1"
for_each = var.project
# Comply with ELB name restrictions
name = trimsuffix(substr(replace(join("-", ["lb", random_string.lb_id.result, each.key, each.value.environment]), "/[^a-zA-Z0-9-]/", ""), 0, 32), "-")
internal = false
security_groups = [module.lb_security_group[each.key].security_group_id]
subnets = module.vpc[each.key].public_subnets
number_of_instances = length(aws_instance.app)
instances = aws_instance.app.*.id
listener = [{
instance_port = "80"
instance_protocol = "HTTP"
lb_port = "80"
lb_protocol = "HTTP"
}]
health_check = {
target = "HTTP:80/index.html"
interval = 10
healthy_threshold = 3
unhealthy_threshold = 10
timeout = 5
}
}
6. Move EC2 instance to a module
6-1. main.tf - resource "aws_instance" "app", data "aws_ami" "amazon_linux" 블럭 제거
6-2. main.tf - replace them with a reference to the aws-instance module
module "ec2_instances" {
source = "./modules/aws-instance"
depends_on = [module.vpc]
for_each = var.project
instance_count = each.value.instances_per_subnet * length(module.vpc[each.key].private_subnets)
instance_type = each.value.instance_type
subnet_ids = module.vpc[each.key].private_subnets[*]
security_group_ids = [module.app_security_group[each.key].security_group_id]
project_name = each.key
environment = each.value.environment
}
Terraform에서는 모듈 내에 count 또는 for_each를 사용하는 경우, 해당 모듈 내에 별도의 provider 블록을 포함할 수 없습니다. 대신, 이 모듈들은 루트 모듈(즉, 최상위 구성 파일)에서 제공하는 provider 구성을 상속받아야 합니다.
Terraform은 count와 for_each를 사용하는 모듈 내의 리소스를 생성할 때, 동일한 provider 설정을 사용해야 일관성을 유지할 수 있습니다. 만약 모듈 내에 별도의 provider 블록이 포함된다면, Terraform이 어떤 provider 설정을 사용할지 혼란이 생길 수 있습니다.
6-3. main.tf - elb_http (module) 수정
module "elb_http" {
source = "terraform-aws-modules/elb/aws"
version = "3.0.1"
for_each = var.project
# Comply with ELB name restrictions
name = trimsuffix(substr(replace(join("-", ["lb", random_string.lb_id.result, each.key, each.value.environment]), "/[^a-zA-Z0-9-]/", ""), 0, 32), "-")
internal = false
security_groups = [module.lb_security_group[each.key].security_group_id]
subnets = module.vpc[each.key].public_subnets
number_of_instances = length(module.ec2_instances[each.key].instance_ids)
instances = module.ec2_instances[each.key].instance_ids
listener = [{
instance_port = "80"
instance_protocol = "HTTP"
lb_port = "80"
lb_protocol = "HTTP"
}]
health_check = {
target = "HTTP:80/index.html"
interval = 10
healthy_threshold = 3
unhealthy_threshold = 10
timeout = 5
}
}
7. outputs.tf 수정
output "public_dns_names" {
description = "Public DNS names of the load balancers for each project."
value = { for p in sort(keys(var.project)) : p => module.elb_http[p].elb_dns_name }
}
output "vpc_arns" {
description = "ARNs of the vpcs for each project."
value = { for p in sort(keys(var.project)) : p => module.vpc[p].vpc_arn }
}
output "instance_ids" {
description = "IDs of EC2 instances."
value = { for p in sort(keys(var.project)) : p => module.ec2_instances[p].instance_ids }
}
Terraform에서 for_each와 for는 각각 다르게 사용되는 반복문 기능을 제공합니다. 이 두 기능은 유사해 보이지만, 실제 사용 방식과 목적이 다릅니다.
특징 | for_each | for (for expressions) |
목적 | 리소스 블록이나 모듈 블록 내에서 유사한 리소스를 반복적으로 생성 | 리스트나 맵을 반복하여 새로운 리스트나 맵을 생성 |
지원 데이터 타입 | 맵(map), 셋(set) | 리스트(list), 셋(set), 맵(map) |
사용 위치 | 리소스 블록, 모듈 블록 | 로컬 변수, 출력 값, 다른 표현식 |
주요 사용 사례 | 여러 인스턴스 생성, 여러 서브넷 정의 | 데이터 구조 변환, 필터링 |
# for_each를 사용한 EC2 인스턴스 생성
variable "instances" {
description = "Map of instances"
type = map(string)
default = {
instance1 = "ami-0c55b159cbfafe1f0",
instance2 = "ami-0c55b159cbfafe1f1",
}
}
resource "aws_instance" "example" {
for_each = var.instances
ami = each.value
instance_type = "t2.micro"
tags = {
Name = each.key
}
}
# for 표현식을 사용한 리스트 변환
variable "subnet_ids" {
description = "List of subnet IDs"
type = list(string)
default = ["subnet-1", "subnet-2", "subnet-3"]
}
locals {
subnet_tags = [for id in var.subnet_ids : "${id}-tag"]
}
output "subnet_tags" {
value = local.subnet_tags
}
8. Apply scalable configuration
terraform init
terraform apply -auto-approve
9. AWS Web Console 확인
10. EC2 Name 태그 추가
10-1. modules/aws-instance > main.tf
terraform apply -auto-approve
11. for_each - map 변수 추가
11-1. variables.tf - project 변수
terraform apply -auto-approve
12. 리소스 정리
terraform destroy -auto-approve