본문 바로가기
IaC/Terraform

[Terraform] 3-1. For each와 Map을 활용하여 리소스 관리하기

by okms1017 2024. 6. 30.
728x90

✍ Posted by Immersive Builder  Seong

 

 

1. 실습 소개 

이번 실습에서는 for_eachmap 타입 변수를 활용하여 웹 프로젝트를 배포합니다. 

for_each 구문은 반복되는 유사한 리소스 또는 모듈 집합을 구성하기 위한 메타 인수로서,

each object는 key, value 2개의 속성을 가지며 주로 key-value 형태의 map(또는 set) 타입 변수와 함께 활용됩니다. 

 

키 값 기반으로 리소스에 접근하기 때문에 잘못된 Count 사용으로 인한 장애 상황을 예방할 수 있습니다. 

실습 진행 순서는 하기와 같습니다. 

 

  • Step1. 외부 공개 모듈(module) 사용하기  
  • Step2. map 타입 변수 선언하기 
  • Step3. 변수 파일(terraform.tfvars) 작성하기
  • Step4. for_each 구문 작성하기
  • Step5. 로컬 모듈(module) 사용하기 
  • Step6. 웹 프로젝트 배포 및 접속 테스트 

 


2. For each와 Map을 활용하여 리소스 관리하기  

Step1. 외부 공개 모듈(module) 사용하기 

웹 프로젝트를 배포하기 위한 네트워크 구성 환경 및 리소스를 처음부터 작성할 필요는 없습니다. 

테라폼 레지스트리에 공개된 AWS 모듈을 사용하여 VPC, Security Group, LoadBalancer를 구성할 예정입니다. 

 

  • 모듈 호출

모듈을 선언할 때에는 source와 version을 지정합니다. 

source는 모듈이 위치한 레지스트리 경로이며, version은 모듈의 버전을 의미합니다. 

 


        module "vpc" {
        source  = "terraform-aws-modules/vpc/aws"
        version = "3.14.2"
        '''
        }
 
        module "app_security_group" {
        source  = "terraform-aws-modules/security-group/aws//modules/web"
        version = "4.9.0"
        '''
        }
 
        module "lb_security_group" {
        source  = "terraform-aws-modules/security-group/aws//modules/web"
        version = "4.9.0"
        '''
        }
 
        module "elb_http" {
        source  = "terraform-aws-modules/elb/aws"
        version = "3.0.1"
        '''
        }
 

 

위와 같이 VPC, Security Group, LoadBalancer 모듈을 각각 선언하여 호출합니다. 

 

Step2. map 타입 변수 선언하기 

개발 환경(dev-vpc)과 스테이징 환경(stg-vpc)을 격리하여 2개의 웹 프로젝트를 배포할 예정입니다. 

따라서 동일한 리소스 구성을 가지는 웹 프로젝트를 변수로 선언하고 map 타입으로 지정합니다. 

 


        variable "project" {
          description = "Map of project names to configuration."
          type = map(object({
            vpc_cidr_block = string
            public_subnet_cidr_blocks = list(string)
            private_subnet_cidr_blocks = list(string)
            public_subnets_per_vpc  = number
            private_subnets_per_vpc = number
            instances_per_subnet    = number
            instance_type           = string
            environment             = string
          }))
        }
 

 

프로젝트(project) 변수는 값으로 object 타입을 가지며, 각 object는 8개의 인수로 구성됩니다. 

 

  • vpc_cidr_block : vpc 네트워크 대역 
  • public_subnet_cidr_blocks : 퍼블릭 서브넷 리스트 
  • private_subnet_cidr_blocks : 프라이빗 서브넷 리스트 
  • public_subnets_per_vpc : vpc 당 구성할 퍼블릭 서브넷 개수
  • private_subnets_per_vpc : vpc 당 구성할 프라이빗 서브넷 개수 
  • instances_per_subnet : 서브넷마다 배포할 인스턴스 개수 
  • instance_type : 인스턴스 타입  ex) t3.medium, c4.large 
  • environment : 인스턴스 태그 (tag:Name=Environment)

이후 해당 변수는 var.project 형식으로 참조하게 됩니다. 

 

Step3. 변수 파일(terraform.tfvars) 작성하기 

VPC, Security Group, LoadBalancer, Instance 등 리소스를 프로비저닝할 때 해당 프로젝트(project) 변수 값을 전달하기 위해 변수 파일(terraform.tfvars)을 작성합니다. 

 

  • terraform.tfvars

프로젝트(project) 변수는 map 타입으로 key와 value 값을 가집니다. 

'dev-webapp'과 'stg-webapp'이 key에 해당하고, 개발 환경과 스테이징 환경에 적용될 인수들을 각각 object 형태의 value 값으로 전달합니다. 

 


        project = {
          dev-webapp = {
            vpc_cidr_block = "192.168.0.0/16"
            public_subnet_cidr_blocks = ["192.168.1.0/24","192.168.2.0/24","192.168.3.0/24","192.168.4.0/24"]
            private_subnet_cidr_blocks = ["192.168.101.0/24","192.168.102.0/24","192.168.103.0/24","192.168.104.0/24"]
            public_subnets_per_vpc  = 2,
            private_subnets_per_vpc = 2,
            instances_per_subnet    = 1,
            instance_type           = "t3.large",
            environment             = "dev"
          },
          stg-webapp = {
            vpc_cidr_block = "10.10.0.0/16"
            public_subnet_cidr_blocks = ["10.10.1.0/24","10.10.2.0/24","10.10.3.0/24","10.10.4.0/24"]
            private_subnet_cidr_blocks = ["10.10.101.0/24","10.10.102.0/24","10.10.103.0/24","10.10.104.0/24"]
            public_subnets_per_vpc  = 2,
            private_subnets_per_vpc = 2,
            instances_per_subnet    = 1,
            instance_type           = "t3.medium",
            environment             = "stg"
          }
        }
 

 

위와 같이 작성하면 개발(dev-vpc, 192.168.0.0/16) 및 스테이징(stg-vpc, 10.10.0.0/16) 2개의 VPC를 구성하고, 

VPC마다 퍼블릭/프라이빗 서브넷을 2개씩 구성한 다음 서브넷마다 EC2 인스턴스를 배포할 것을 예상할 수 있습니다. 

 

Step4. for_each 구문 작성하기 

  • VPC 모듈

다수의 VPC를 구성하고자 할 때 for_each 메타 인수를 사용할 수 있습니다. 

다음 코드는 var.project map의 key-value 쌍을 각각 each.key, each.value에 할당합니다. 

 


        module "vpc" {
          source  = "terraform-aws-modules/vpc/aws"
          version = "3.14.2"

          for_each = var.project

          cidr = each.value.vpc_cidr_block

          azs             = data.aws_availability_zones.available.names
          public_subnets  = slice(each.value.public_subnet_cidr_blocks, 0, each.value.public_subnets_per_vpc)
          private_subnets = slice(each.value.private_subnet_cidr_blocks, 0, each.value.private_subnets_per_vpc)

          enable_nat_gateway = true
          enable_vpn_gateway = false

          map_public_ip_on_launch = false
        }
 

 

  • for_each : key-value 형태의 반복 메타 인수
  • each.value.vpc_cidr_block : 변수 값에서 vpc_cidr_block에 해당하는 네트워크 대역 값
  • slice(list, startindex, endindex) : list[startindex] ≤ list[x] < list[endindex], 인덱스 시작 값은 포함하고 끝 값은 제외함
  • slice(each.value.public_subnet_cidr_blocks, 0, each.value.public_subnets_per_vpc) : slice(["192.168.1.0/24","192.168.2.0/24","192.168.3.0/24","192.168.4.0/24"],0,2) => [ "192.168.1.0/24","192.168.2.0/24"]  # dev-public-subnet
    slice(["10.10.1.0/24","10.10.2.0/24","10.10.3.0/24","10.10.4.0/24"],0,2) => ["10.10.1.0/24","10.10.2.0/24"]  # stg-public-subnet
  • slice(each.value.private_subnet_cidr_blocks, 0, each.value.private_subnets_per_vpc) : slice(["192.168.101.0/24","192.168.102.0/24","192.168.103.0/24","192.168.104.0/24"], 0, 2) => ["192.168.101.0/24","192.168.102.0/24"]  # dev-private-subnet
    slice(["10.10.101.0/24","10.10.102.0/24","10.10.103.0/24","10.10.104.0/24"], 0, 2) => ["10.10.101.0/24","10.10.102.0/24"]  # stg-private-subnet

 

  • Security Group 모듈

Security Group name과 vpc_id, ingress_cidr_blocks에서 each.key를 통해 for_each로 구성된 리소스를 구별할 수 있습니다. 

 


        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
        }

        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"]
        }
 

 

  • LoadBalancer(ELB) 모듈

마찬가지로 각 VPC에 배포되는 로드밸런서 또한 each.key를 통해 name과 Security Group ID, Subnet, 인스턴스 ID를 구별합니다.  

 


        module "elb_http" {
          source  = "terraform-aws-modules/elb/aws"
          version = "3.0.1"

          for_each = var.project
 
          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
 
        '''
        }
 

 

Step5. 로컬 모듈(module) 사용하기

웹 프로젝트를 배포하기 위한 인스턴스 리소스 블록은 아래와 같이 Count가 적용되어 있습니다. 

 

       
        resource "aws_instance" "app" {
          count = var.instance_count

          ami           = data.aws_ami.amazon_linux.id
          instance_type = var.instance_type

          subnet_id              = var.subnet_ids[count.index % length(var.subnet_ids)]
          vpc_security_group_ids = var.security_group_ids

          user_data = <<-EOF
            #!/bin/bash
            sudo yum update -y
            sudo yum install httpd -y
            sudo systemctl enable httpd
            sudo systemctl start httpd
            echo "<html><body><div>Welcome to Seong's skill builder!</div></body></html>" > /var/www/html/index.html
            EOF

          tags = {
            Terraform   = "true"
            Project     = var.project_name
            Environment = var.environment
          }
        }
 

 

하지만 Count와 for_each는 동일한 블록에 같이 사용할 수 없습니다. 

이를 해결하기 위해 aws_instance 리소스를 자식 모듈로 구성하여 호출하겠습니다. 

 

├── examples
│   ├── for_each_webapp
│        ├── main.tf         자식 모듈 호출
│        ├── outputs.tf
│        ├── terraform.tf
│        ├── terraform.tfvars
│        └── variables.tf
├── modules
     └── aws-web-instance
          ├── main.tf        ≪  여기로 aws_instance 리소스 블록 이동 ! 
          ├── outputs.tf
          └── variables.tf

 

  • 자식 모듈 호출 

모듈을 호출하는 방법은 Step1과 동일하나, source에 로컬 디렉토리 경로를 지정하는 점이 다릅니다. 

루트 모듈을 기준으로 상대 경로("../../modules/aws-web-instance")를 지정합니다. 

 


        module "ec2_instances" {
          source     = "../../modules/aws-web-instance"
          '''   
        }
 

 

Step6. 웹 프로젝트 배포 및 접속 테스트 

웹 프로젝트를 배포하여 결과를 확인합니다. 

 

$ terraform init && terraform apply -auto-approve

 

배포가 완료되기까지 약 6분 정도 소요됩니다. 

 

VPC 배포 확인
서브넷 배포 확인
LB 배포 확인
LB Security Group 배포 확인
LB 대상 인스턴스 확인
인스턴스 배포 확인
dev web 인스턴스 1
dev web 인스턴스 2
stg web 인스턴스 1
stg web 인스턴스 2
Security Group 배포 확인

 

  • WEB 서비스 테스트

LB DNS로 접속을 시도할 때 WEB 서비스가 정상적으로 제공됨을 확인할 수 있습니다. 

 

$ DEV_LBDNS=$(aws elb describe-load-balancers --load-balancer-names lb-gwB9-dev-webapp-dev | jq -r .LoadBalancerDescriptions[0].DNSName)

$ echo $DEV_LBDNS

lb-gwB9-dev-webapp-dev-894199057.ap-northeast-2.elb.amazonaws.com

 

$ while true; do curl --connect-timeout 1  http://$DEV_LBDNS; echo "------------------------------"; date; sleep 1; done
------------------------------
Sun Jun 30 02:32:25 KST 2024
<html><body><div>Welcome to Seong's skill builder!</div></body></html>
------------------------------
Sun Jun 30 02:32:26 KST 2024
<html><body><div>Welcome to Seong's skill builder!</div></body></html>

 

$ STG_LBDNS=$(aws elb describe-load-balancers --load-balancer-names lb-gwB9-stg-webapp-stg | jq -r .LoadBalancerDescriptions[0].DNSName)

$ echo $STG_LBDNS

lb-gwB9-stg-webapp-stg-232613450.ap-northeast-2.elb.amazonaws.com

 

$ while true; do curl --connect-timeout 1  http://$STG_LBDNS; echo "------------------------------"; date; sleep 1; done

------------------------------
Sun Jun 30 02:36:13 KST 2024
<html><body><div>Welcome to Seong's skill builder!</div></body></html>
------------------------------
Sun Jun 30 02:36:14 KST 2024
<html><body><div>Welcome to Seong's skill builder!</div></body></html>

 

 

실습을 완료하고 리소스를 반납합니다. 

 

$ terraform destroy -auto-approve

 


▶ 소스코드: 개인 GitHub

 


[출처]

1) https://developer.hashicorp.com/terraform/tutorials/configuration-language/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

2) https://developer.hashicorp.com/terraform/language/meta-arguments/for_each

 

The for_each Meta-Argument - Configuration Language | Terraform | HashiCorp Developer

The for_each meta-argument allows you to manage similar infrastructure resources without writing a separate block for each one.

developer.hashicorp.com

728x90