Skip to main content

확장 가능한 테라폼 코드 관리

  • "조직이 커지면서 관리하는 테라폼 코드가 점점 복잡해지고 있어요"
  • "무슨 테라폼 모듈을 만들고 어떻게 관리해야할지 감이 안와요"
  • "도대체 하나의 테라폼 워크스페이스에 담아야 하는 리소스의 경계는 어디까지일까요?"
  • "중복되는 테라폼 코드가 너무 많아지고 있어요"

위 4가지 질문에 대한 인사이트를 얻고자 유튜브 영상에 나온 내용을 글로 정리해보았습니다. 테라폼을 활용한 인프라 리소스의 관한 꿀팁들이 모두 담겨져 있어 꼭 영상을 한 번 보시는 것을 추천드립니다.

목차

  • 테라폼 모듈을 사용하라
  • 외부 모듈을 사용하지 마라
  • 모듈의 버전을 관리하라
  • 두 종류의 테라폼 모듈을 관리하라
  • 코드와 데이터를 분리하라
  • 하나의 워크스페이스에 모든 것을 담지 마라
  • 워크스페이스 간의 의존성을 관리하라
  • 모든 것을 테라폼으로 관리하려 하지마라

확장가능한 코드란 ?

  • 읽어야 하는 코드의 양이 적다 (가독성)
  • 수정해야 하는 코드의 양이 적다 (유지보수)

테라폼 모듈을 사용하라

모듈(Module)

  • 여러 테라폼 리소스를 하나의 논리적 그룹으로 관리하기 위해 사용하며 하나의 디렉토리 내에 .tf 혹은 .tf.json 파일로 구성된 콜렉션

루트 모듈 (Root Module)

  • 테라폼 CLI가 plan / apply 등과 같이 실제로 수행하게 되는 작업 디렉토리의 테라폼 코드 모음

차일드 모듈 (Child Module)

  • 다른 모듈의 테라폼 코드 내에서 호출(참조)하기 위한 목적으로 작성된 테라폼 코드 모음

테라폼 모듈

다른 모듈의 테라폼 코드 내에서 호출(참조)하기 위한 목적으로 작성된 테라폼 코드 모음이며 테라폼 문법에서 module 블록을 통해 호출하게 되는 테라폼 코드

resource "aws_iam_user" "claud" {
name = "claud"
}

resource "aws_iam_user_group_membership" "claud" {
user = aws_iam_user.claud.name
groups = ["sre", "backend"]
}

resource "aws_iam_user_policy_attachment" "claud_1" {
user = aws_iam_user.claud.name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

resource "aws_iam_user" "albert" {
name = "albert"
}
...

위와 같이 반복되는 코드를 for_each문을 사용하여 아래와 같이 간단하게 표시할 수 있습니다.

locals {
users = [
{ name = "claud", groups = ["sre", "backend"] },
{ name = "tomas", groups = ["security", "intern"] },
{ name = "jeremy", groups = ["backend"] },
]
}

resource "aws_iam_user" "this" {
for_each = {
for user in local.users:
user.name => user
}

name = each.key
}

resource "aws_iam_user_group_membership" "this" {
for_each = {
for user in local.users:
user.name => user
}

user = each.key
groups = each.value.groups
}

하지만 1:N 관계에서는 상당히 복잡해지므로 모듈을 사용하는 것이 효율적입니다. 모듈을 사용하게 되면 복잡한 객체(리소스 집합)을 단순하게 관리할 수 있게 됩니다.

locals {
users = [
{ name = "claud", groups = ["sre", "backend"], polices = [...] },
{ name = "tomas", groups = ["security", "intern"], polices = [...] },
{ name = "jeremy", groups = ["backend"], polices = [...] },
]
}

module "user" {
source = "tedilabs/account/aws//modules/iam-user"
version = "~> 0.19.0"

for_each = {
for user in local.users :
user.name => user
}

name = each.key
groups = each.value.groups
policies = each.value.policies
}

테라폼 모듈을 사용해야 하는 이유

캡슐화 (Encapsulation)

  • 객체지향 프로그램의 핵심 개념 중 하나로 객체의 응집도와 독립성을 높이기 위해 객체의 모듈화를 지향하는 것이며 객체의 모듈화가 잘 이루어지면 모듈 단위의 재사용이 매우 용이해지며 간편하게 유지보수가 가능하다.
  • 관련된 리소스를 하나의 모듈로 묶어서 캡슐화

외부 모듈을 사용하지 마라

테라폼 레지스트리 (Terraform Registry)

  • 하시코프에서 공식적으로 운영하는 테라폼 프로바이더 및 모듈저장소, 공개된 테라폼 모듈을 쉽게 찾아 활용할 수 있음

모듈에 심각한 보안 문제점을 찾아 당장 고쳐야 한다면?

  • AWS 프로바이더 신규 버전에서 추가된 기능을 적용하고 싶은데 모듈 내에 추상화되어 건들 수가 없다면 ?
  • 리소스에서는 지원해주는 기능인데 모듈에서 해당 기능에 대한 인터페이스를 제공해주지 않고 있다면 ?

이러한 문제등이 발생할 수 있기 때문에 외부 모듈을 사용하는 것보다 아래 두 가지 방법이 권고됩니다.

  • DIY (Do It Your Self): 조직 내에서 직접 테라폼 모듈을 설계하고 작성하여 관리

  • Use After Fork: 외부 모듈을 사용하고자 한다면 조직 내부에 복제 후 사용

모듈의 버전을 관리하라

테라폼 모듈을 로컬 파일 경로를 통해 사용한다면 편리하지만 모듈의 버전을 선택할 수는 없습니다.

여러 워크스페이스에서 사용중인 로컬 모듈에 잘못된 수정을 한다면 어떻게 될까요?

  • Git, 테라폼 레지스트리, 테라폼 클라우드 등 원격 모듈을 사용한다면 특정 버전의 모듈을 사용하도록 지정할 수 있습니다.
  • 각 워크스페이스에서 원격 모듈의 특정 버전을 사용한다면 모듈을 변경하더라도 영향을 받지 않도록 할 수 있습니다.

모듈 블록 내 version은 버전 제약 조건 식을 지원합니다. 이를 잘 이용하면 불필요한 버전 값 업데이트 빈도수를 줄일 수 있습니다.

  • 1.2.0 1.2.0 버전
  • = 1.2.0 1.2.0 버전
  • ≥ 1.2.0 1.2.0 버전 이상 중 최신 버전
  • ≥ 1.2.0, < 2.0.0 1.2.0 버전 이상 2.0.0 버전 미만 중 최신 버전
  • ~> 1.2.0 1.2.x 버전 중 최신 버전
  • ~> 1.0 1.x 버전 중 최신 버전

두 종류의 테라폼 모듈을 관리하라

모듈을 사용하기에 앞서, 어떠한 모듈들을 만들어 운용할지 잘 고민해야 합니다. A: 우리 조직의 표준과 컨벤션이 적용된 S3 버킷 모듈을 만들자, 앞으로 S3 버킷이 필요하면 이 모듈을 가지고 동일한 표준과 컨벤션을 적용할 수 있을거야, 굳이 S3 버킷의 모든 옵션을 설정할 수 있도록 자유도를 줄 필요 없잖아 ?

B: 나중에 예외케이스가 발생했을 때 대응하기 어려워지면 어떻게 해? 모듈이 기존 리소스의 기능을 제한시키면 안된다고 생각해. 기존 리소스 모음을 유연하게 사용할 수 있도록 도와줘야지! 우선 S3 버킷과 관련된 리소스들의 관계를 파악해서 잘 추상화된 하나의 객체로 표현할 수 있도록 모듈을 만드는 게 어때?

모듈 (Module)

기존 리소스들을 유의미한 객체 단위로 다시 추상화한 리소스 모음, 조직의 정보를 담고 있지 않도록 한다. 어떠한 조직에서든 용도에 무관하게 재사용하기 쉽도록 하는 것이 목적

스택 (Stack)

조직의 기술 표준과 컨벤션이 적용되어 제한된 인터페이스만 제공하는 테라폼 모듈, 스택은 리소스와 모듈의 모음으로 구성되며 사용 용도와 옵션을 제한하여 사용성을 향상시키고 기술 표준을 강제 적용하는 것이 목적

코드와 데이터를 분리하라

다음은 데이터가 하드코딩된 코드입니다.

module "s3_bucket_1" {
source = "tedilabs/data/aws//modules/s3-bucket"
version = "0.0.1"

name = "my-bucket-1"
}

module "s3_bucket_2" {
source = "tedilabs/data/aws//modules/s3-bucket"
version = "0.0.2"

name = "my-bucket-2"
}

module "s3_bucket_3" {
source = "tedilabs/data/aws//modules/s3-bucket"
version = "0.0.3"

name = "my-bucket-3"
}

다음은 반복된 코드를 없애기 위해 for_each문을 사용한 코드입니다.

locals {
s3_buckets = ["my-bucket-1", "my-bucket-2", "my-bucket-3"]
}

module "s3_bucket" {
source = "tedilabs/data/aws//modules/s3-bucket"
version = "0.0.1"

for_each = toset(local.s3_buckets)
name = each.value
}

locals문 말고도 variable을 사용해서 외부로부터 변수를 입력받는다고 하더라도 워크스페이스에서 관리하는 리소스가 많아질수록 variable을 통한 데이터 주입에 한계를 느낍니다.

모든 개발자들에게 친숙하고 가독성이 좋은 YAML로 데이터를 관리해볼 수는 없을까요?

variable "config_file" {
description = "The path of configuration YAML file."
type = string
default = "./config.yaml"
}

locals {
config = yamldecode(file(var.config_file))
}
secrets:
- name: "eks/apne2-test/addons/argo-cd"
description: "Secrets for Argo CD addon in apne2-test EKS cluster."
kms_key: aws-secrets-manager/tedilabs/common
replicas: []
type: KEY_VALUE
- name: "eks/apne2-test/addons/argo-workflow"
description: "Secrets for Argo Workflow addon in apne2-test EKS cluster."
kms_key: aws-secrets-manager/tedilabs/common
replicas: []
type: KEY_VALUE
- name: "eks/apne2-test/apps/grafana"
description: "Secrets for Grafana app in apne2-test EKS cluster."
kms_key: aws-secrets-manager/tedilabs/common
replicas: []
type: KEY_VALUE

적용 코드

module "secret" {
source = "tedilabs/secret/aws//modules/secrets-manager-secret"
version = "~> 0.2.0"

for_each = {
for secret in local.config.secrets:
secret.name => secret
}

name = each.key
description = try(each.value.description, "Managed by Terraform.")

type = try(each.value.type, "TEXT")
value = try(each.value.value, null)
versions = try(each.value.versions, [])

kms_key = try(local.kms_keys[each.value.kms_key].arn, null)
policy = try(each.value.policy, null)
block_public_policy = true

deletion_window_in_days = try(each.value.deletion_window_in_days, 7)

replicas = [
for replica in try(each.value.replicas, []) : {
region = replica.region
kms_key = try(local.kms_keys[replica.kms_key].arn, null)
}
]
overwrite_in_replicas = try(each.value.overwrite_in_replicas, false)

rotation_lambda_function = try(each.value.rotation.lambda_fuinction, null)
rotation_duration_in_days = try(each.value.rotation.duration, null)

tags = merge(
try(each.value.tags, {}),
)
}

try(arg1, arg2, ..., argN)

  • 임의 길이의 인자 전달 가능
  • 순서대로 인자 표현식(argument expression)을 해석
  • 오류가 발생하지 않는 첫 번째 인자 값을 반환

lookup(map, key, default)

  • map 데이터에서 특정 key에 해당하는 value를 반환하며 해당 key가 없을 경우 기본값(default) 반환

try 함수가 lookup보다 가독성이 좋고 더 다양한 유즈케이스에 적용이 가능합니다.

테라폼의 templatefile 함수를 이용하여 YAML 파일 내 템플릿 기능을 활용할 수 있습니다.

locals {
context = yamldecode(file(var.config_file)).context
config = yamldecode(templatefile(var.config_file, local.context))
}
context:
account: tedilabs
cluster: apne2-test

secrets:
- name: "eks/${cluster}/addons/argo-cd"
description: "Secrets for Argo CD addon in ${cluster} EKS cluster."
kms_key: aws-secrets-manager/${account}/common
replicas: []
type: KEY_VALUE
- name: "eks/${cluster}/addons/argo-workflow"
description: "Secrets for Argo Workflow addon in ${cluster} EKS cluster."
kms_key: aws-secrets-manager/${account}/common
replicas: []
type: KEY_VALUE
- name: "eks/${cluster}/apps/grafana"
description: "Secrets for Grafana app in ${cluster} EKS cluster."
kms_key: aws-secrets-manager/${account}/common
replicas: []
type: KEY_VALUE

위 기능을 통해 개발자의 요청 사항에 대하여 데브옵스 및 SRE는 씨크릿을 추가 및 제거를 보다 자유롭게 진행할 수 있게 된다.

하나의 워크스페이스에 모든 것을 담지 마라

간혹 특정 서비스 배포를 위한 테라폼 코드를 살펴보면 네트워크, 도메인, 서버, 데이터 저장소, 로드밸런서 등 인프라 전반적인 리소스를 모두 포함하고 있는 경우를 보곤 합니다. 조직의 구성이나 일하는 방식에 따라서 워크스페이스별로 나눌 수 있습니다.

워크스페이스 간의 의존성을 관리하라

데이터소스 (Data Source)

  • 프로바이더에서 제공하는 데이터 조회 목적의 리소스 (GET / LIST)
  • data 블록을 통해 정의

terraform_remote_state 리소스

  • 테라폼 프로바이더를 통해 제공하는 데이터소스, 외부 테라폼 워크스페이스 상태 데이터 조회 목적

YAML 설정 파일의 remote_states 항목을 정의하는 것으로 필요한 의존 워크스페이스를 주입할 수 있습니다.

remote_states:
"network-vpc":
organization: "tedilabs"
workspace: "aws-network-vpc-tedilabs-apne2-playground"
"domain-cert":
organization: "tedilabs"
workspace: "aws-domain-cert-tedilabs-apne2"
locals {
remote_states = {
for name, remote_state in data.terraform_remote_state.this :
name => remote_state.outputs
}
}

data "terraform_remote_state" "this" {
for_each = local.config.remote_states

backend = "remote"

config = {
organization = each.value.organization
workspaces = {
name = each.value.workspace
}
}
}

여러 워크스페이스로 분리하여 관리하면 당연히 각 워크스페이스 간의 의존성이 발생하게 됩니다. 각 워크스페이스가 어떠한 의존도를 가지고 있는지 관리할 필요가 있습니다. 워크스페이스 간 관계에서 순환 참조가 발생하면 관리가 복잡해지고, 예상치 못한 이슈를 만날 수 있으니 조심해야 합니다.

모든 것을 테라폼으로 관리하려 하지마라

스스로에게 질문해보기

  • “동일한 작업에 대해 기존 작성된 테라폼 코드가 있는가?”
  • “새롭게 테라폼 코드를 작성할만큼 시간적 여유가 있는가?”
  • “추후에 동일한 작업이 발생했을 때 소요 시간을 단축시켜주는가?”
  • “IaC의 장점(Audit, Code Review, Documentation 등)이 해당 코드에 대해 유효한가?”
  • “이미 다른 방법으로 만들어진 동일 리소스가 있다면 모두 테라폼으로 마이그레이션을 진행할 수 있는가?”

팀 내에 테라폼으로 관리하는 리소스 종류에 대한 합의를 만드는 것이 중요합니다.

내가 테라폼 코드로 작성하기 시작하면, 동료도 동일하게 작업을 해야 한다는 것을 잊지 말아야 합니다.


테라폼 관리 커버리지 측정

driftctl

  • 테라폼 상태 데이터와 실제 인프라 리소스를 비교하여 IaC 커버리지 측정, 테라폼 관리 리소스 중 외부 변경사항 추적
  • AWS / Azure / GCP / GitHub 프로바이더 지원
  • IaC 커버리지: 전체 리소스 중 IaC (테라폼 코드)로 관리되고 있는 리소스의 비중

여러 테라폼 워크스페이스를 관리한다면 테라폼 버전 관리자는 필수입니다.

  • tfswitch: 가장 활발하게 관리되고 있는 테라폼 버전 관리자, tfenv보다 다양한 기능을 제공
  • tfenv:가장 많이 사용되는 테라폼 버전 관리자, rbenv에 영감을 받아 개발

Terraform Cloud

프라이빗 프로바이더 / 모듈 저장소 무료 제공

  • 상태 저장소 무료 제공 (Lock 기능 제공)
  • 팀원 5명까지 무료
  • Remote 실행 모드: 테라폼 클라우드에서 관리하는 에이전트에서 테라폼 코드 원격 실행
  • Local 실행 모드: 사용자 랩탑에서 테라폼 코드 실행 (상태저장소만 테라폼 클라우드 이용)