고민의 시작... 모듈화와 Import의 우선순위
최근 GitLab의 계층적 그룹 구조를 테라폼으로 관리하는 방안을 고민하게 되었습니다.
먼저 GitLab Group의 계층적 구조는 상위 그룹과 하위 그룹(Sub Group) 간의 종속 관계를 다루게 됩니다. 예로 아래와 같은 화면은 GitLab의 Group 구조입니다.
처음 고민했던 부분은 Import를 먼저 진행하고 리팩토링할 것인지, 아니면 모듈화된 구조를 먼저 테라폼 코드로 짠 후 Import할 것인지에 대한 부분이었습니다. Import는 이미 생성된 리소스를 테라폼의 상태 파일로 가져오는 과정이지만, Import 이후의 코드화 과정에서 문제가 발생할 가능성이 있었습니다. 특히, 계층형 리소스 구조를 유지하면서도 기존 리소스의 설정을 무너뜨리지 않고 관리하려면 모듈화가 필수적이라는 판단을 했습니다.
향후 리소스 확장성을 높이면서도 기존 리소스들의 의존 관계를 명확히 관리할 수 있는 방식이기 때문에.. 고민 끝에 결국 모듈을 먼저 작성하고 Import하는 방식을 선택했습니다.
문제의 시작... YML로 리소스 관리와 모듈화 + Import
저희 팀에서는 여러 테라폼 리소스 생성을 YML파일 또는 CSV파일로 관리하고 있기에 GitLab 그룹과 프로젝트 등의 리소스 또한 YML 파일로 관리할 수 있도록 코드를 설계를 진행하였습니다.
기존에 사용중이던 GitLab 환경은 팀내에서 여러 하시코프제품 및 GitLab에 대한 PoC 및 데모를 위한 목적으로 이미 사용하고 있던 상황이였으며, 이미 구성된 환경 및 설정에 대한 Import 과정이 또한 필요했습니다.
GitLab 그룹의 이름, 설명, 경로, 상위 그룹 등을 YML로 정의하고 이를 Terraform으로 변환하여 사용하고, 모든 그룹을 하나의 YAML 파일로 정의, 이를 기반으로 단일 모듈 블록에서 계층형 리소스를 관리하려 했습니다.
이중에서 GitLab 그룹의 경우 서브그룹이 있는 계층 구조를 가지고 있어, Terraform으로 이를 관리하려면 parent_id (즉, 상위 그룹 참조) 의존성으로 인해 순환 참조 문제가 발생할 가능성이 있었습니다.
하지만, module block을 서브그룹의 뎁스가 추가될때마다 추가해야하는 번거로움이 존재했기에 어떻게든 순환참조를 해결할 수 있겠지?라는 생각으로 코드를 짜기 시작했습니다.
간단히 제가 초기에 사용한 테라폼 구조와 YML 형태를 보여드리면 다음과 같습니다.
- 테라폼 구조
- 그룹 관리 YML
groups:
- name: "SAML"
description: "SAML Group"
visibility_level: "private"
subgroups: []
- name: "MZC-Infra"
description: "MZC Infrastructure Group"
visibility_level: "private"
subgroups: []
- name: "GitLab"
description: "GitLab Group"
visibility_level: "private"
subgroups: []
- name: "Terraform"
description: "Terraform Group"
visibility_level: "private"
subgroups:
- name: "Internal"
description: "Internal Subgroup for Terraform"
visibility_level: "private"
subgroups:
- name: "kubernetes"
description: "Kubernetes Subgroup for Internal"
visibility_level: "private"
subgroups: []
- name: "packer"
description: "Packer Subgroup for Internal"
visibility_level: "private"
subgroups: []
- name: "RemoteState-test"
description: "Remote State Test Subgroup for Internal"
visibility_level: "private"
subgroups: []
- name: "PoC"
description: "PoC Group for Terraform"
visibility_level: "private"
subgroups:
- name: "PPPP"
description: "ZENT Subgroup for PoC"
visibility_level: "private"
subgroups: []
- name: "Vault"
description: "Vault Group"
visibility_level: "private"
subgroups: []
- name: "Nomad"
description: "Nomad Group"
visibility_level: "private"
subgroups: []
- name: "OIDC"
description: "OIDC Group"
visibility_level: "private"
subgroups: []
- 이 시점에서 import를 시도했던 코드
data "gitlab_group" "groups" {
for_each = { for group in local.groups_data : group["name"] => group }
full_path = each.value["name"]
}
import {
for_each = { for group in local.groups_data : group["name"] => group }
to = module.gitlab_groups[each.key].gitlab_group.group
id = data.gitlab_group.groups[each.key].id
}
locals {
groups_data = yamldecode(file("${path.module}/../resources_yaml/groups.yaml"))["groups"]
}
module "gitlab_groups" {
source = "../modules/group"
for_each = { for group in local.groups_data : group["name"] => group }
group_name = each.value["name"]
group_description = each.value["description"]
group_visibility_level = each.value["visibility_level"]
group_parent_id = lookup(each.value, "parent_group_name", null) != null ? module.gitlab_groups[lookup(each.value, "parent_group_name")].gitlab_group.id : null
group_path = lower(each.value["name"])
}
처음 시도한 모듈 코드에서는 모든 그룹을 하나의 모듈에서 처리하려 했던 이유로는 서브그룹의 뎁스가 추가될 때마다 모듈 블럭을 매번 추가해줘야 하는 번거로움이 존재했기에 초기에는 yml파일만을 통해 서브그룹간의 뎁스를 정의해놓고 사용하려했습니다.
처음에는 이 구조가 간단할 것이라고 생각했지만, 결국 우려했던... Import 과정에서 상위 그룹은 하위 그룹의 경로 정보를 참조하는 방식 때문에 순환 참조 문제가 발생했습니다.
해당 내용을 더 테스트하고 싶었지만, 다른 업무 일정 등으로 인해 상위 그룹과 하위 그룹을 분리하여 관리하는 구조로 계획을 변경하게 되었습니다. 결국 모듈 호출 방식 또한 이 구조에 맞게 수정하게 되었습니다.
Terraform에서 flatten은 중첩된 리스트를 하나의 평평한 리스트로 변환하는 함수입니다. YML에서 서브그룹이 중첩된 계층을 가지고 있기에 flatten을 사용하여 Terraform 동작을 일괄적으로 처리할 수 있도록 처리하였습니다.
locals {
# 최상위 그룹 처리
top_level_groups = [
for group in yamldecode(file("${path.module}/../resources_yaml/groups.yaml"))["groups"] : {
name = group.name,
description = group.description,
visibility_level = group.visibility_level,
full_path = lower(group.name),
}
]
# 2단계 서브그룹 처리
subgroups = flatten([
for group in yamldecode(file("${path.module}/../resources_yaml/groups.yaml"))["groups"] : [
for subgroup in lookup(group, "subgroups", []) : {
name = subgroup.name,
description = subgroup.description,
visibility_level = subgroup.visibility_level,
parent_group_name = group.name,
full_path = lower(join("/", [group.name, subgroup.name]))
}
]
])
# 3단계 서브그룹 처리
subsubgroups = flatten([
for group in yamldecode(file("${path.module}/../resources_yaml/groups.yaml"))["groups"] : [
for subgroup in lookup(group, "subgroups", []) : [
for subsubgroup in lookup(subgroup, "subgroups", []) : {
name = subsubgroup.name,
description = subsubgroup.description,
visibility_level = subsubgroup.visibility_level,
parent_group_name = subgroup.name,
full_path = lower(join("/", [group.name, subgroup.name, subsubgroup.name]))
}
]
]
])
}
# 상위 그룹 생성
module "gitlab_top_level_groups" {
source = "../modules/group"
for_each = { for group in local.top_level_groups : group["name"] => group }
group_name = each.value["name"]
group_description = each.value["description"]
group_visibility_level = each.value["visibility_level"]
group_parent_id = null
group_path = lower(each.value["name"])
}
# 2단계 서브그룹 생성
module "gitlab_subgroups" {
source = "../modules/group"
for_each = { for group in local.subgroups : group["name"] => group }
group_name = each.value["name"]
group_description = each.value["description"]
group_visibility_level = each.value["visibility_level"]
group_parent_id = module.gitlab_top_level_groups[each.value["parent_group_name"]].gitlab_group.id
group_path = lower(each.value["name"])
}
# 3단계 서브그룹 생성
module "gitlab_subsubgroups" {
source = "../modules/group"
for_each = { for group in local.subsubgroups : group["name"] => group }
group_name = each.value["name"]
group_description = each.value["description"]
group_visibility_level = each.value["visibility_level"]
group_parent_id = module.gitlab_subgroups[each.value["parent_group_name"]].gitlab_group.id
group_path = lower(each.value["name"])
}
import의 경우 Terraform v1.5.0 이상에서 나온 기능으로 일반적으로 사용하는 block 형태로 Import를 진행할 수 있습니다.
import를 하기 위해서는 import 대상의 id값이 필요하게 되는데 해당 id는 이미 그룹들이 GitLab에 생성되어 있고, 해당 그룹들을 YML 파일을 통해 관리할 것이기 때문에 YML에 선언된 이름을 통해 data block으로 id값을 가져와서 import를 할 수 있도록 선언하였습니다.
data "gitlab_group" "groups" {
for_each = { for group in concat(local.top_level_groups, local.subgroups, local.subsubgroups) : group["name"] => group }
full_path = each.value["full_path"]
}
# 상위 그룹 Import 블록
import {
for_each = { for group in local.top_level_groups : group["name"] => group }
to = module.gitlab_top_level_groups[each.key].gitlab_group.group
id = data.gitlab_group.groups[each.key].id
}
# 2단계 서브그룹 Import 블록
import {
for_each = { for group in local.subgroups : group["name"] => group }
to = module.gitlab_subgroups[each.key].gitlab_group.group
id = data.gitlab_group.groups[each.key].id
}
# 3단계 서브그룹 Import 블록
import {
for_each = { for group in local.subsubgroups : group["name"] => group }
to = module.gitlab_subsubgroups[each.key].gitlab_group.group
id = data.gitlab_group.groups[each.key].id
}
바뀌는 내용은 일단 제가 임의로 추가한 값이며, 실제 환경에는 영향이 없는 값들만 변경 되며 정상적으로 Import 됨을 확인!!!!!
마치며
해당 작업을 진행하며 얻은 교훈은 계층적 그룹 구조에서 Import를 수행할 때, 그룹 간의 의존성을 어떻게 관리할지가 핵심이라는 점입니다.
특히 순환 참조 문제를 피하기 위해서는 계층을 나누어 모듈화하는 작업이 필요했고, 이를 통해 Import 및 관리 작업을 성공적으로 마칠 수 있었습니다.
아마 AWS의 S3 버킷 정책과 같은 계층적 리소스를 관리하는 상황에서도 이와 같은 전략이 유용하게 적용될 수 있을 것 같습니다.
팀 내에서 테라폼 코드를 사용중인 상황이라면 새로운 환경에 대해서만 테라폼 코드화를 진행하는 것이 아닌 기존의 테라폼 코드화가 안되어 있는 환경 또한 코드화 진행이 있을 수 있습니다.
기존 환경을 테라폼으로 관리하려면 Import 과정이 필수적입니다. Import 시 어떤 리소스를 우선적으로 처리하고, 모듈화된 코드에서 순환 참조를 어떻게 방지할지에 대한 명확한 계획이 중요할 것 같습니다.
개인적으로, 이중 작업을 피하기 위해 Terraform 코드에 대한 설계를 먼저하고 Import를 진행하는 것이 좋아보이며, 리소스 확장성을 높이면서도 기존 리소스들의 의존 관계를 명확히 관리하기 위해 계층 구조에 맞춰 작업 우선순위를 설정하는 방식이 가장 합리적이라고 생각합니다.
참고한 stackoverflow
https://stackoverflow.com/questions/68130103/depending-on-another-element-from-a-list
'Terraform' 카테고리의 다른 글
AWS 트래픽 스파이크 처리 - Amazon Aurora와 Route 53 가중치 기반 라우팅 활용 (1) | 2024.11.03 |
---|---|
[Terraform] local-exec & remote-exec 프로비저너 (0) | 2024.05.26 |
[Terraform] terraform_data 리소스 (null_resource 비교) (0) | 2024.05.26 |
[Terraform] Moved Block을 통한 코드 리팩터링 & Provider alias (0) | 2024.05.26 |
[Terraform] count, for_each 등 실습예제 (0) | 2024.05.12 |