본문 바로가기
T101 4기

3-1. Manage similar resources with for each(for each로 유사한 리소스 관리하기)

by yeongki0944 2024. 6. 29.

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)을 사용자 정의할 수 있습니다.

이 튜토리얼에서는 AWS에서 VPC, 로드 밸런서 및 EC2 인스턴스를 프로비저닝합니다.
그런 다음 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 통합을 구성하는 클라우드 블록을 주석 처리

terraform.tf

 

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 리소스 배포 확인

EC2
LB

 

 

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 확인

SG
LB

 

EC2 Tag
EC2 Tag

 

 

10. EC2 Name 태그 추가

10-1. modules/aws-instance > main.tf

변경 전
변경 후

 

terraform apply -auto-approve

Plan
Apply

 

 

11. for_each - map 변수 추가

11-1. variables.tf  -  project 변수

 

변경 전
변경 후

 

terraform apply -auto-approve

 

 

12. 리소스 정리

terraform destroy -auto-approve