확장 가능한 테라폼 코드 관리
- "조직이 커지면서 관리하는 테라폼 코드가 점점 복잡해지고 있어요"
- "무슨 테라폼 모듈을 만들고 어떻게 관리해야할지 감이 안와요"
- "도대체 하나의 테라폼 워크스페이스에 담아야 하는 리소스의 경계는 어디까지일까요?"
- "중복되는 테라폼 코드가 너무 많아지고 있어요"
위 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을 통한 데이터 주입에 한계를 느낍니다.