Manage similar resources with for each(for each로 유사한 리소스 관리하기)
Manage similar resources with for each | Terraform | HashiCorp Developer
Provision similar infrastructure components by iterating over a data structure with the for_each argument. Duplicate an entire VPC including a load balancer and multiple EC2 instances for each project defined in a map.
developer.hashicorp.com
Terraform의 for_each 메타 인수를 사용하면 데이터 구조를 반복하여 데이터 구조의 각 항목에 대한 리소스 또는 모듈을 구성함으로써 유사한 리소스 집합(a set of similar resources)을 구성할 수 있습니다.
for_each를 사용하여 동일한 라이프사이클을 공유하는 유사한 리소스 집합(a set of similar resources)을 사용자 정의할 수 있습니다.
그런 다음 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
GitHub - hashicorp/learn-terraform-for-each
Contribute to hashicorp/learn-terraform-for-each development by creating an account on GitHub.
github.com
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