본문 바로가기
IaC

[IaC] Terraform 작성 및 배포(2) - for_each, flatten, local values

by cloudraw 2024. 3. 5.

안녕하세요, 클라우드로입니다!

 

 이번 글에서는 테라폼 코드 작성 중 유용하게 사용할 수 있는 for_each, flatten, local values와 관련된 내용에 대해 알아보겠습니다. 이전 테라폼 글과 마찬가지로 ncloud에서 제공하는 리소스들의 테라폼 코드 중 일부를 같이 소개하며 설명하겠습니다. 

 

for_each

 이전 테라폼 글에서는 resource block 내부에 배포될 값을 직접 입력하는 방식으로 테라폼 코드를 작성했습니다. 이 방식은 직관적이고 테라폼 파일을 굳이 나누지 않는다는 이점이 있지만, 동일한 리소스를 여러번 배포하는 경우, 그 수에 맞게 resource block을 작성해야한다는 단점이 있습니다. 이에 관련하여, 동일한 리소스를 여러 번 배포할 때, 테라폼 공식 홈페이지에서는 count와 for_each를 적용하도록 안내하고 있습니다. count는 동일한 리소스끼리 내부 설정 값도 일치할 경우 사용하고, for_each의 경우, 내부 설정에 차이를 주며 배포할 수 있도록 도와줍니다. 이 글에서는 for_each에 대한 이야기를 해보겠습니다. 

for_each는 each 즉, object를 for문으로 반복하여 리소스를 배포하겠다는 의미로 설명할 수 있습니다. 여기서 each로 지정된 object는 key와 value로 구분되며 이를 each.key와 each.value로 표현합니다. 그렇다면 for_each에 필요한 object 형태는 어떻게 만들까요? 바로 variables.tf 에서 실제 배포할 내용을 object로 생성하는데, 이 때 map과 set 함수를 사용합니다. 여기부터는 코드를 통해 설명하겠습니다. 아래 코드는 vpc에 대한 variables.tf 입니다. 

variables.tf - map을 적용한 vpc

위 코드을 보면 vpc에 해당하는 block의 type이 map(object)로 감싸져있습니다. 이는 vpc에서 사용할 인자값들을 여러 object들의 map 형태로 구분하겠다는 의미입니다. default를 보면 좀 더 직관적으로 확인할 수 있습니다. 

default = {
    "vpc-test" = {
      "default_service_name" = "vpc-test"
      "ipv4_cidr_block" = "10.0.0.0/16"
    }, 
    "vpc-test2" = {
      "default_service_name" = "vpc-test2"
      "ipv4_cidr_block" = "10.0.1.0/16"
    }, 
    "vpc-test3" = {
      "default_service_name" = "vpc-test3"
      "ipv4_cidr_block" = "10.0.2.0/16"
    }
  }

 

default에 2개의 vpc object를 추가한 모습입니다. 위에서 볼 수 있듯, 각 object는 key와 value로 이루어져있습니다. resource block에서 사용할 each.key는 for문이 한 바퀴 돌 때마다 "vpc-test", "vpc-test2", "vpc-test3"이 될 것이고, 그에 맞는 each.value는 각각 키와 맵핑되는 값이 될 것입니다. 

each.key => "vpc-test" 
each.value => 
	{
    	"default_service_name" = "vpc-test"
        "ipv4_cidr_block" = "10.0.0.0/16"
    	} 
    
each.key => "vpc-test2"
each.value =>
	{
    	"default_service_name" = "vpc-test2"
        "ipv4_cidr_block" = "10.0.1.0/16"
    	}
    
each.key => "vpc-test3"
each.value => 
	{
    	"default_service_name" = "vpc-test3"
        "ipv4_cidr_block" = "10.0.2.0/16"
    	}

 

이제 main.tf을 확인해보겠습니다. 

main.tf - for_each을 적용한 vpc

variables.tf에서 생성된 map 속의 object들은 var.vpc로 들어옵니다. 따라서 k, v는 각각 each.key, each.value를 의미합니다. 그 뒷부분에 ": v.default_service_name => v" 라고 작성이 되어있는데, 이는 v.default_service_name 즉, each.value 중 default_service_name으로 받은 값을 해당 object의 key로, v를 지칭하는 each.value를 value로 사용하겠다는 의미입니다. resource block 내부 인자에서 사용할 때는, each.value 중에서 필요한 값을 선택하면 됩니다. 

 위와 같이 for_each를 사용하여 resource block을 배포 할 리소스의 수만큼 작성할 필요없이 한번에 작성 할 수 있습니다.

 

flatten

for_each를 사용할 때, variables.tf를 map(object)형태로 작성하는 방법 위에서 설명했습니다. 이 때, 간혹 variables.tf에서 한 리소스의 인자를 map 으로 설정해야할 때가 있습니다. 예시를 들어보겠습니다. ncloud 리소스 중 sourcedeploy라는 리소스는 여러 sourcedeploy_stage 리소스를 가질 수 있고, 각 sourcedeploy_stage는 여러 scenario를 가질 수 있습니다. 만약 stage에 해당하는 variable block을 작성한다면, 그 자체로 map을 형성하고, scenario에 대한 내용도 아래 코드처럼 map으로 설정할 수 있을 것입니다. 

variables.tf - stage에 대한 map(object) 내부에 존재하는 scenario map(object)

 위와 같은 형태로 구성된 상황을 resource block에서 for_each를 사용하다고 한 들, stage에 대한 each object는 생성되겠지만, scenario의 each 처리는 할 수 없습니다. 이럴 때 사용할 수 있는 함수가 flatten입니다. flatten함수는 리스트 내부의 값과, 그 리스트 내부에 중첩된 값을 1차원 리스트로 만들어주는 함수입니다.  

flatten([ ["a", "b"], [], ["c"] ])
> ["a", "b", "c"]

 

 위의 설명처럼 한 뎁스로 맞춰주는 flatten은 variables.tf에서 이중 map, 즉 map 형식 내부에 map이 또 있을 때 주로 사용합니다. 간단한 예시로, variables.tf 가 아래와 같이 구성되어 있다고 가정해봅시다.

variable "networks" {
  type = map(object({
    default_service_name = string
    subnets = map(object({
      cidr_block = string
    }))
  }))
  default = {
    "default-network" = {
      "default_service_name" = "default-network"
      "subnets" = {
        "default_subnet" = {
            "cidr_block" = "10.0.1.0/24"              
        },
        "default_subnet2" = {
            "cidr_block" = "10.0.2.0/24"              
        }
      }
    }
  }
}

 

variables.tf가 이렇게 map 내부에 map이 존재할 경우, 즉 한 개의 network 내부에 여러 subnets object들이 있을 경우, main.tf에서는 flatten 함수를 사용합니다. 

for network in flatten([
  for network_key, network_value in var.networks : [
    for subnet_key, subnet_value in network_value.subnets : {
      network_key = network_key
      subnet_key = subnet_key 
      cidr_block = subnet_value.cidr_block
      ...
      (flatten 이후, 사용할 변수명) = (할당할 값)
    }
  ]
])

 

먼저, 외부에 위치한 map에서 key(network_key)와 value(network_value)를 가져옵니다. 그 다음, 내부 map에서 사용될 key(subnet_key)와 value(subnet_value)를 앞서 설정한 value 중 map 부분(network_value.subnets)에서 가져옵니다. 그 이후, flatten함수를 적용한 후에 사용할 변수명과 값을 할당합니다. 즉, 위의 예시에서는 network_key, subnet_key, cidr_block 이라는 변수를 사용할 것이고, 그 값으로 각각 network_key, subnet_key, subnet_value.cidr_block을 사용한다는 의미입니다. 

flatten함수를 사용한 위 코드를 통해 다음과 같이 변환됩니다.

[
    {
        "network_key" : "default-network",
        "subnet_key" : "default_subnet",
        "cidr_block" : "10.0.1.0/24"
    },
    {
        "network_key" : "default-network",
        "subnet_key" : "default_subnet2",
        "cidr_block" : "10.0.2.0/24"
    }
]

 

이제 위에서 언급한 내용을 sourcedeploy stage, scenario 코드를 활용하여 확인해보겠습니다. 

main.tf - source_deploy_project_stage중 일부

 위 코드는 stage에 관련된 main.tf 중 일부입니다. 위 리소스 block을 보면, for_each를 사용한 것을 확인할 수 있습니다. 이렇게 되면 variables.tf에서 deploy stage에 해당하는 부분이 map(object) 형태로 작성될 것을 유추할 수 있으며, 실제로 확인해보면 아래와 같이 작성한 것을 확인할 수 있습니다. 

variables.tf - source_deploy_project_stage 중 일부

 

 그런데 ncloud 앞서 언급한 것처럼, 한 stage마다 여러 scenario라는 리소스를 가질 수 있습니다. 즉, 아래 코드처럼 variables.tf에서 map으로 설정된 stage 내부에 또다른 map scenario에 관한 값들이 들어갈 수 있다는 것입니다. 

variables.tf - source_deploy_stage에 scenario 추가

 

 main.tf 파일에서 flatten함수를 사용하여, deploy stage의 map에 속하는 값과, scenario의 map에 속하는 값들을 같은 차원으로 가져올 수 있습니다. 

main.tf - source_deploy_project_stage_scenario 중 일부

 

 위 코드을 보면, 외부 map에 해당하는 stage와 내부 map에 해당하는 scenario_value.scenario를 flatten을 통해 한 개의 object로 생성했습니다. 새로 생성된 object의 key 값으로는 "${scenario.stage_name}-${scenario.scenario_name}" 을 설정했습니다. 이를 테라폼 코드에서 사용할 때는 다른 리소스 때와 동일하게 each.value.scenario_name 과 같은 방식을 사용하면 됩니다. 결론적으로 기존 variables.tf의 형태는 다음과 같이 stage 내부의 여러 scenairo들이 존재하는 형태였다면,  

"example_stage" = {
  "default_service_name" = "example_stage"
  "included_source_deploy_name" = "deploy1"
  "target_type" = "Server"
  "linked_server_list" = ["server1"]
  "target_server_id_list" = ["12345678"]
  "linked_auto_scaling_group" = ""
  "linked_nks_cluster" = ""
  "linked_object_storage_name" = ""
  "scenario" = {
    "default_scenario1" = {
      "deploy_scenario_description" = ""
      "deploy_scenario_strategy" = "normal"
      "deploy_scenario_file_type" = "SourceBuild"
      "target_object_storage_bucket_name" = ""
      "object_storage_object_name" = ""
      "linked_source_build_project" = "build1"
      "rollback" = "false"
    }
  }
}

 

flatten 함수를 사용하여 만들어진 새로운 object는 다음과 같은 형태입니다.

"example_stage-default_scenario1" = {
  "scenario_name" = "default_scenario1"
  "stage_name" = "example_stage"
  "included_source_deploy_name" = "deploy1"
  "target_type" = "Server"
  "scenario_value" = {
      "deploy_scenario_description" = ""
      "deploy_scenario_strategy" = "normal"
      "deploy_scenario_file_type" = "SourceBuild"
      "target_object_storage_bucket_name" = ""
      "object_storage_object_name" = ""
      "linked_source_build_project" = "build1"
      "rollback" = "false"
  }
}

 

flatten을 사용하여 main.tf 와 variables.tf를 구성하는 것은 까다롭고 복잡할 뿐더러, 구성하는 방식도 다양하기 때문에 코드의 구성을 달리 해보고, 반복해서 작성해보는 것을 권장합니다. 

 

local values

 다음은 local values입니다. local values는 local block을 생성한 후, 내부에 값을 지정해두면, resource block에서 필요 시 사용할 수 있습니다. 

main.tf - source build

 위의 코드을 보면 runtime block이 dynamic block으로 설정된 것을 확인할 수 있습니다. 그 내부를 보면, id와 version block이 있습니다. id는 variables.tf의 env_platform_runtime_name으로 어떤 값이 오느냐에 따라 값이 결정되는 조건문 형식으로 작성되어 있습니다. 이 방식으로 작성할 경우, env_platform_runtime_name으로 올 수 있는 값의 종류가 적다면 문장이 짧게 끝나겠지만, 올 수 있는 값이 많을 경우, 한 줄에 작성되어 길게 이어지기 때문에 가독성이 떨어집니다. 이럴 때 local values를 사용합니다. locals 을 생성하여 그 내부에 값을 key, value 형태로 저장해두고, 필요할 때 사용합니다. 

main.tf - local values

 local values는 위 코드처럼 main.tf 파일에 locals 라는 이름으로 작성하고 저장할 내용은 key, value 형태로 작성합니다.  

id = local.platform_version_map[each.value.env.env_platform_runtime_version]

 

이 후, 위와 같이 사용합니다. 이렇게 되면 variables.tf 에서는 each.value.env.env_platform_runtime_version으로 "16.04-1.0.0" 혹은 "7-1.0.0"과 같은 값들을 받고, locals에서 해당 내용에 맞는 값을 불러오는 방식입니다. 만약 locals value를 사용하지 않았더라면, 아래처럼 모든 조건을 전부 작성해야하는 불편함을 겪어야합니다. 또 아래와 같은 코드를 여러 줄 반복했다면 더욱 불편했을텐데, locals에 한번 저장해둔 값은 여러 번 사용할 수 있기에 그런 불편함도 방지할 수 있습니다. 

id = each.value.env.env_platform_runtime_version == "16.04-1.0.0" ? "1" :each.value.env.env_platform_runtime_version == "7-1.0.0" ? "2" : ...

 

 

 

 이렇게 for_each, flatten 그리고 local values에 대한 소개와 코드 작성 방법을 알아봤습니다. 이전 테라폼 글과 이번 글에서 소개한 개념들은 개별적으로, 혹은 상황에 따라 혼합하여 사용될 수 있는 개념들입니다. 따라서 각 개념들을 잘 숙지하고, 다양한 방법으로 실습을 진행해보시길 바랍니다.

 

 

감사합니다. 

 

 


 

    

 

Cloudraw는 쉽게 클라우드 인프라를 그리고 사용할 수 있는 서비스를 제공하기 위해 노력하고 있습니다.

 

클라우드가 있는 곳 어디든 Cloudraw가 함께합니다.

 

📨 help@cloudraw.kr