스타일 가이드

스타일 가이드 (Style Guide)

조직의 Terraform 코드 스타일 가이드를 만들 때 고려할 권장 사항을 정리해 드릴게요. 코드 스타일과 운영·워크플로 두 부분으로 나뉘어 있어요.

출처: 문서

본문

Terraform 구성 언어의 유연성은 코드를 작성하고, 디렉터리를 구성하고, 구성을 테스트할 때 선택할 수 있는 많은 옵션을 제공해요. 일부 설계 결정은 조직의 요구나 선호에 달려 있지만, 채택을 권장하는 몇 가지 공통 패턴이 있어요. 스타일 가이드를 채택하고 지키면 Terraform 코드를 읽기 쉽고, 확장 가능하며, 유지보수하기 쉽게 유지할 수 있어요.

이 문서는 조직의 스타일 가이드를 개발하면서 명심해야 할 모범 사례와 고려 사항을 다뤄요. 두 섹션으로 나뉘어 있어요. 첫 번째 섹션은 포맷팅과 리소스 구성 같은 코드 스타일 권장 사항을, 두 번째 섹션은 메타-인자를 통한 라이프사이클 관리, 버전 관리, 민감 데이터 관리 같은 운영 및 워크플로 권장 사항을 다뤄요.

코드 스타일 (Code style)

일관된 스타일로 Terraform 코드를 작성하면 읽고 유지보수하기 쉬워져요. 다음 섹션에서 코드 스타일 권장 사항을 다뤄요:

  • 코드를 버전 관리에 커밋하기 전에 terraform fmtterraform validate를 실행해요.
  • TFLint 같은 린터를 사용해 조직만의 코딩 모범 사례를 강제해요.
  • 단일 및 여러 줄 주석에 #을 사용해요.
  • 리소스 이름에 명사를 사용하고 이름에 리소스 유형을 포함하지 마세요.
  • 이름에서 여러 단어를 구분할 때 밑줄을 사용해요. 리소스 정의에서 리소스 유형과 이름을 큰따옴표로 감싸요.
  • 코드가 자기 위에 쌓이도록 해요: 참조하는 리소스 다음에 의존 리소스를 정의해요.
  • 모든 변수에 유형과 설명을 포함해요.
  • 모든 출력에 설명을 포함해요.
  • 변수와 로컬 값의 과도한 사용을 피해요.
  • 항상 기본 프로바이더 구성을 포함해요.
  • countfor_each를 아껴서 사용해요.

코드 포맷팅 (Code formatting)

Terraform 파서는 구성 파일에서 요소를 배치하는 방법에 약간의 유연성을 허용하지만, Terraform 언어에는 다른 팀이 작성한 파일과 모듈 간 일관성을 위해 항상 따르길 권장하는 관용적 스타일 관례도 있어요.

  • 중첩 레벨마다 두 칸을 들여써요.
  • 한 줄 값이 있는 여러 인자가 같은 중첩 레벨의 연속된 줄에 나타나면 등호를 정렬해요:
ami           = "abc123"
instance_type = "t2.micro"
  • 블록 본문 안에 인자와 블록이 함께 나타나면 모든 인자를 위에 모으고 중첩 블록을 그 아래에 배치해요. 인자와 블록을 구분하는 데 빈 줄 한 개를 사용해요.
  • 빈 줄을 사용해 블록 안의 인자 논리 그룹을 구분해요.
  • 인자와 "메타-인자"(Terraform 언어 의미론이 정의하는)를 모두 포함하는 블록에서 메타-인자를 먼저 나열하고 다른 인자와 빈 줄 한 개로 구분해요. 메타-인자 블록을 마지막에 배치하고 다른 블록과 빈 줄 한 개로 구분해요.
resource "aws_instance" "example" {
  # 메타-인자 먼저
  count = 2

  ami           = "abc123"
  instance_type = "t2.micro"

  network_interface {
    # ...
  }

  # 메타-인자 블록 마지막
  lifecycle {
    create_before_destroy = true
  }
}
  • 최상위 블록은 항상 서로 빈 줄 한 개로 구분해야 해요. 중첩 블록도 빈 줄로 구분해야 하지만, 같은 유형의 관련 블록을 모을 때(리소스의 여러 provisioner 블록처럼)는 예외예요.
  • 블록 유형이 의미론상 한 가족을 형성하지 않는 한, 같은 유형의 여러 블록을 다른 유형의 블록과 함께 묶는 것을 피해요. (예: aws_instanceroot_block_device, ebs_block_device, ephemeral_block_device는 AWS 블록 디바이스를 설명하는 블록 유형 한 가족을 형성하므로 함께 묶고 섞을 수 있어요.)

terraform fmt 명령은 Terraform 구성을 위 권장 사항의 일부로 포맷팅해요. 기본적으로 terraform fmt는 실행하는 디렉터리의 Terraform 코드만 수정하지만, -recursive 플래그를 포함하면 모든 하위 디렉터리의 코드도 수정할 수 있어요. 버전 관리에 커밋하기 전마다 terraform fmt를 실행하는 것을 권장해요. Git pre-commit hooks 같은 메커니즘을 사용해 코드를 커밋할 때마다 이 명령을 자동 실행할 수 있어요.

Microsoft VS Code를 사용한다면 Terraform VS Code 확장을 사용해 구문 강조와 검증, 자동 코드 포맷팅, HCP Terraform 통합 같은 기능을 활성화해요. 개발 환경이나 텍스트 편집기가 Language Server Protocol을 지원한다면 Terraform Language Server를 사용해 VS Code 확장 기능 대부분에 접근할 수 있어요.

코드 검증 (Code validation)

terraform validate 명령은 구성이 구문적으로 유효하고 내부적으로 일관적인지 확인해요. validate 명령은 인자 값이 특정 프로바이더에 유효한지는 확인하지 않지만 올바른 유형인지는 검증해요. 기존 state는 평가하지 않아요. 자동으로 자주 실행해도 안전하므로, 텍스트 편집기에서 저장 후 검사로 실행하거나, Git 저장소의 pre-commit 훅으로 정의하거나, CI/CD 파이프라인의 단계로 실행할 수 있어요.

파일 이름 (File names)

다음 파일 명명 관례를 권장해요:

  • backend.tf: 백엔드 구성을 포함. 구성에 여러 terraform 블록을 정의해 백엔드 구성을 Terraform·프로바이더 버전 구성과 분리할 수 있어요.
  • main.tf: 모든 리소스와 데이터 소스 블록을 포함.
  • outputs.tf: 모든 출력 블록을 알파벳순으로 포함.
  • providers.tf: 모든 provider 블록과 구성을 포함.
  • terraform.tf: required_versionrequired_providers를 정의하는 단일 terraform 블록을 포함.
  • variables.tf: 모든 변수 블록을 알파벳순으로 포함.
  • locals.tf: 로컬 값을 포함.
  • override.tf: 구성의 오버라이드 정의를 포함. Terraform은 이 파일과 _override.tf로 끝나는 모든 파일을 마지막에 로드해요. 이들은 코드를 이해하기 어렵게 만들므로 아껴 쓰고 원본 리소스 정의에 주석을 추가해요.

코드베이스가 커지면 이 파일들만으로 유지보수하기 어려워질 수 있어요. 크기 때문에 코드 탐색이 어려워진다면 논리 그룹별로 리소스와 데이터 소스를 별도 파일로 구성하는 것을 권장해요. 예를 들어 웹 애플리케이션이 네트워킹, 스토리지, 컴퓨트 리소스를 필요로 한다면 다음 파일을 만들 수 있어요:

  • network.tf: VPC, 서브넷, 로드 밸런서 및 기타 모든 네트워킹 리소스를 포함.
  • storage.tf: 객체 스토리지와 관련 권한 구성을 포함.
  • compute.tf: 컴퓨트 인스턴스를 포함.

어떻게 코드를 나누든, 유지보수 담당자가 특정 리소스나 데이터 소스 정의를 즉시 찾을 수 있어야 해요. 구성이 커지면 여러 state 파일로 분리해야 할 수도 있어요. HashiCorp Well-Architected Framework는 구성 구조와 범위에 대한 더 많은 지침을 제공해요.

린팅과 정적 코드 분석 (Linting and static code analysis)

Terraform에는 내장 린터가 없지만, 많은 조직이 TFLint 같은 타사 린팅 도구에 의존해 코드 표준을 강제해요. 린터는 정적 코드 분석을 사용해 Terraform 코드를 규칙 집합과 비교해요. 대부분의 린터는 기본 규칙 집합과 함께 제공되지만 직접 규칙을 작성할 수도 있어요.

주석 (Comments)

코드를 이해하기 쉽게 작성하고, 필요할 때만 다른 유지보수 담당자를 위해 복잡성을 명확히 하는 주석을 사용해요. 단일 및 여러 줄 주석 모두에 #을 사용해요. ///* */ 주석 문법은 관용적이지 않지만, Terraform은 HCL의 이전 버전과의 하위 호환성을 위해 이를 지원해요.

# 각 터널은 연관된 게이트웨이를 거치거나 떠나는 트래픽을 암호화·복호화하는 역할을 한다.
resource "google_compute_vpn_tunnel" "tunnel1" {
  ## ...
}

리소스 이름 (Resource naming)

구성 안의 모든 리소스는 고유한 이름을 가져야 해요. 일관성과 가독성을 위해 설명적인 명사를 사용하고 단어를 밑줄로 구분해요. 리소스 주소가 이미 포함하므로 리소스 식별자에 리소스 유형을 포함하지 마세요. 리소스 유형과 이름을 큰따옴표로 감싸요.

❌ 나쁨:

resource aws_instance webAPI-aws-instance {...}

✅ 좋음:

resource "aws_instance" "web_api" {...}

리소스 순서 (Resource order)

코드에서 리소스와 데이터 소스의 순서는 Terraform이 이를 만드는 방식에 영향을 주지 않으므로 가독성을 위해 리소스를 구성해요. Terraform은 리소스 간 의존성을 기반으로 생성 순서를 결정해요. 리소스 순서는 코드의 크기와 복잡성에 크게 달려 있지만, 데이터 소스를 이를 참조하는 리소스와 함께 정의하는 것을 권장해요. 가독성을 위해 Terraform 코드는 "자기 위에 쌓여야" 해요 — 데이터 소스를 이를 참조하는 리소스보다 먼저 정의해야 해요.

다음 예시는 aws_amiaws_availability_zone 두 데이터 소스에 의존하는 aws_instance를 정의해요. 가독성과 연속성을 위해 데이터 소스를 aws_instance 리소스보다 먼저 정의해요:

data "aws_ami" "web" {
  ##...
}

data "aws_availability_zones" "available" {
  ##...
}

resource "aws_instance" "web" {
  ami               = data.aws_ami.web.id
  availability_zone = data.aws_availability_zones.available.names[0]
  ##...
}

리소스 파라미터에 대해 일관된 순서를 따르는 것을 권장해요:

  • 있으면 count 또는 for_each 메타-인자.
  • 리소스별 비블록 파라미터.
  • 리소스별 블록 파라미터.
  • 필요하면 lifecycle 블록.
  • 필요하면 depends_on 파라미터.

변수 (Variables)

변수는 모듈을 더 유연하게 만들지만, 변수를 과도하게 사용하면 코드를 이해하기 어렵게 할 수 있어요. 리소스 설정에 변수를 노출할지 결정할 때, 이 파라미터가 배포 사이에 변할지 고려해요.

  • 모든 변수에 typedescription을 정의해요.
  • 변수가 선택적이면 합리적인 default를 정의해요.
  • 비밀번호와 개인 키 같은 민감 변수는 sensitive 파라미터를 true로 설정해요. Terraform이 이 값을 여전히 state에 평문으로 저장한다는 것을 기억하세요. 하지만 terraform plan이나 terraform apply 실행 시 표시하지는 않아요. 민감 값의 안전한 처리는 시크릿 관리를 참고해요.
  • 입력 변수 검증을 사용해 Terraform의 타입 검증에 더해 변수 값에 대한 추가 규칙을 만들 수 있어요. 변수 값에 유일하게 제한적인 요구사항이 있을 때만 변수 검증을 사용해요. 예를 들어 Terraform 구성이 웹 인스턴스 두 개를 요구한다면 validation 블록을 추가해 강제할 수 있어요:
variable "web_instance_count" {
  type        = number
  description = "배포할 웹 인스턴스 수. 이 애플리케이션은 인스턴스가 두 개 이상 필요합니다."

  validation {
    condition     = var.web_instance_count > 1
    error_message = "이 애플리케이션은 웹 인스턴스가 두 개 이상 필요합니다."
  }
}

변수 파라미터에 대해 일관된 순서를 따르는 것을 권장해요: 타입, 설명, 기본값(선택), 민감(선택), 검증 블록.

출력 (Outputs)

출력 값은 명령줄에서 인프라에 대한 데이터를 노출하고 다른 Terraform 구성에서 쉽게 참조하게 해요. 변수처럼 각 출력에 typedescription을 제공해요. 출력 파라미터 순서: 타입, 설명, 값, 민감(선택).

모든 변수와 출력은 고유한 이름을 요구해요. 일관성과 가독성을 위해 설명적인 명사와 단어 사이 밑줄 구분을 권장해요.

variable "db_disk_size" {
  type        = number
  description = "API 데이터베이스의 디스크 크기"
  default     = 100
}

variable "db_password" {
  type        = string
  description = "데이터베이스 비밀번호"
  sensitive   = true
}

output "web_public_ip" {
  type        = string
  description = "웹 인스턴스의 공용 IP"
  value       = aws_instance.web.public_ip
}

로컬 값 (Local values)

로컬 값은 표현식이나 값을 여러 번 참조하게 해요. 과도하게 쓰면 코드를 이해하기 어렵게 만들 수 있으므로 아껴서 사용해요. 예를 들어 로컬 값으로 지역·환경 접미사(development 또는 test 같은)를 만들고 여러 리소스에 덧붙일 수 있어요:

locals {
  name_suffix = "${var.region}-${var.environment}"
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t3.micro"

  tags = {
    Name = "web-${local.name_suffix}"
  }
}

로컬 값은 두 곳 중 하나에 정의해요:

  • 여러 파일에서 로컬 값을 참조한다면 locals.tf라는 파일에 정의해요.
  • 로컬 값이 특정 파일에만 해당한다면 해당 파일 맨 위에 정의해요.

다른 Terraform 객체처럼 로컬 값 이름에도 설명적인 명사를 사용하고 여러 단어를 밑줄로 구분해요.

프로바이더 별칭 (Provider aliasing)

프로바이더 별칭을 사용하면 같은 Terraform 프로바이더에 대해 여러 provider 블록을 정의할 수 있어요. 별칭의 잠재적 사용 사례는 단일 구성에서 여러 지역에 리소스를 프로비저닝하는 것이에요. 리소스의 provider 메타-인자와 모듈의 providers 메타-인자가 사용할 프로바이더를 지정해요.

providers.tf:

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "west"
  region = "us-west-2"
}

main.tf:

resource "aws_instance" "example" {
  provider = aws.west
  # ...
}

module "aws_vpc" {
  source = "./aws_vpc"
  providers = {
    aws = aws.west
  }
}
  • alias 파라미터를 정의하지 않는 프로바이더 블록은 기본 프로바이더 구성이에요.
  • 항상 기본 프로바이더 구성을 포함하고 모든 프로바이더를 같은 파일에 정의해요.
  • 프로바이더의 여러 인스턴스를 정의하면 기본을 먼저 정의해요.
  • 기본이 아닌 프로바이더는 provider 블록의 첫 번째 파라미터로 alias를 정의해요.

동적 리소스 수 (Dynamic resource count)

for_eachcount 메타-인자는 런타임 조건에 따라 단일 resource 블록에서 여러 리소스를 만들 수 있게 해요. 이 메타-인자로 코드를 유연하게 만들고 중복 리소스 블록을 줄일 수 있어요. 리소스가 거의 동일하면 count를 사용하고, 일부 인자에 정수에서 유도할 수 없는 서로 다른 값이 필요하면 for_each를 사용해요.

for_each 메타-인자는 map 또는 set 값을 받으며, Terraform은 제공한 값의 각 요소에 대해 그 리소스의 인스턴스를 만들어요. 다음 예시에서 Terraform은 web_instances 변수에 정의된 문자열 "ui", "api", "db", "metrics" 각각에 대해 aws_instance를 만들어요. 예시는 each.key를 사용해 각 인스턴스에 고유한 이름을 부여해요. web_private_ips 출력은 for 표현식을 사용해 인스턴스 이름과 개인 IP 주소의 맵을 만들고, web_ui_public_ip 출력은 "ui" 키를 가진 인스턴스를 직접 주소 지정해요:

variable "web_instances" {
  type        = list(string)
  description = "웹 애플리케이션용 인스턴스 목록"
  default = [
    "ui",
    "api",
    "db",
    "metrics"
  ]
}

resource "aws_instance" "web" {
  for_each = toset(var.web_instances)
  ami           = data.aws_ami.webapp.id
  instance_type = "t3.micro"
  tags = {
    Name = "web_${each.key}"
  }
}

output "web_private_ips" {
  description = "웹 인스턴스의 개인 IP"
  value = {
    for k, v in aws_instance.web : k => v.private_ip
  }
}

output "web_ui_public_ip" {
  description = "웹 UI 인스턴스의 공용 IP"
  value       = aws_instance.web["ui"].public_ip
}

위 예시는 다음 출력을 만들 거예요:

web_private_ips = {
  "api" = "172.31.25.29"
  "db" = "172.31.18.33"
  "metrics" = "172.31.26.112"
  "ui" = "172.31.20.142"
}
web_ui_public_ip = "18.216.208.182"

count 메타-인자는 단일 리소스 블록에서 리소스의 여러 인스턴스를 만들 수 있게 해요. 조건부로 리소스를 만들고자 할 때 일반적으로 count 메타-인자를 조건 표현식과 함께 사용해요. 다음 예시에서 Terraform은 var.enable_metricstrue일 때만 aws_instance를 만들어요:

variable "enable_metrics" {
  description = "메트릭 서버를 배포해야 하면 true"
  type        = bool
  default     = true
}

resource "aws_instance" "web" {
  count = var.enable_metrics ? 1 : 0

  ami           = data.aws_ami.webapp.id
  instance_type = "t3.micro"
  ##...
}

메타-인자는 코드를 단순화하지만 복잡성을 더하므로 적당히 사용해요. 메타-인자의 효과가 바로 명확하지 않으면 설명을 위해 주석을 사용해요.

.gitignore

저장소에 .gitignore 파일을 정의해서 state 파일처럼 버전 관리에 게시하지 말아야 할 파일을 제외해요. 다음은 커밋하지 마세요:

  • terraform.tfstate state 파일(백업 state 파일 terraform.tfstate.* 포함). state 파일은 시크릿과 기타 민감 정보를 포함할 수 있어요. 또한 버전 관리 시스템은 한 번에 하나의 Terraform 인스턴스만 수정할 수 있도록 state 파일 잠금을 지원하지 않아요.
  • .terraform.tfstate.lock.info 파일. Terraform이 terraform apply 명령을 실행할 때 자동으로 만들고 삭제하며 state 잠금에 대한 정보를 포함해요.
  • .terraform 디렉터리. Terraform이 프로바이더와 자식 모듈을 다운로드하는 곳이에요.
  • terraform plan 실행 시 -out 플래그를 포함할 때 만드는 저장된 plan 파일.
  • 민감 정보를 포함하는 모든 .tfvars 파일.

항상 커밋해요:

  • 모든 Terraform 코드 파일
  • .terraform.lock.hcl 의존성 잠금 파일
  • 아래 나열된 파일을 제외하는 .gitignore 파일
  • 코드, 입력 변수, 출력을 설명하는 README.md

워크플로 스타일 (Workflow style)

이 섹션은 예측 가능하고 안전한 Terraform 워크플로를 가능하게 하는 기준을 검토해요:

  • Terraform, 프로바이더, 모듈 버전을 고정해요.
  • HCP Terraform 레지스트리를 사용할 때 모듈 저장소 이름을 terraform--라는 세 부분으로 된 이름으로 지정해요.
  • 로컬 모듈을 ./modules/에 저장해요.
  • 두 state 파일 간 state를 공유하려면 tfe_outputs 데이터 소스나 프로바이더별 데이터 소스를 사용해요.
  • 동적 프로바이더 자격 증명이나 HashiCorp Vault 같은 시크릿 관리자로 자격 증명을 보호해요.
  • 모듈용 테스트를 작성해요.
  • 인프라 운영에 가드레일을 설정하려면 HCP Terraform에서 정책 적용을 사용해요.

버전 고정 (Version pinning)

프로바이더와 모듈 업그레이드가 인프라에 의도하지 않은 변경을 도입하지 않도록 버전 고정(version pinning)을 사용해요. required_providers 블록으로 프로바이더 버전을 지정해요. Terraform 버전 제약은 허용되는 버전 범위를 지원해요. 아래 예시처럼 모듈을 특정 주요·부 버전으로 고정해 안정성을 보장해요. 모듈이 주요 버전 업데이트 밖에서 호환성을 깨는 변경을 도입하지 않는다고 확신하면 더 느슨한 제약을 사용할 수 있어요. 또한 terraform 블록의 required_version으로 Terraform 바이너리의 최소 필수 버전을 설정하는 것을 권장해요. 이렇게 하면 모든 운영자가 구성의 모든 필수 기능을 가진 Terraform 버전을 사용하게 돼요.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.34.0"
    }
  }

  required_version = ">= 1.7"
}

위 예시는 hashicorp/aws 프로바이더의 버전을 5.34.0으로 고정하고 운영자가 Terraform 1.7 이상을 사용하도록 요구해요. 레지스트리에서 소싱한 모듈은 module 블록의 version 파라미터로 버전을 고정해요. 로컬 모듈의 경우 Terraform은 version 파라미터를 무시해요.

module "vault_starter" {
  source  = "hashicorp/vault-starter/aws"
  version = "1.0.0"
  ##...
}

모듈 저장소 이름 (Module repository names)

Terraform 레지스트리는 레지스트리에 게시하는 모든 모듈이 명명 관례와 일치하기를 요구해요. 모듈 저장소는 terraform--라는 세 부분으로 된 이름을 사용해야 해요. 여기서 는 모듈이 관리하는 인프라 유형을, 는 모듈이 사용하는 주요 프로바이더를 반영해요. `` 부분은 추가 하이픈을 포함할 수 있어요. 예: terraform-google-vault 또는 terraform-aws-ec2-instance.

모듈 구조 (Module structure)

Terraform 모듈은 자체 포함(self-contained)되고 재사용 가능한 infrastructure-as-code 조각을 정의해요. 함께 프로비저닝해야 하는 논리적으로 관련된 리소스를 모으려면 모듈을 사용해요. 예:

  • VPC와 서브넷, 게이트웨이, 보안 그룹을 정의하는 네트워킹 모듈.
  • 각 배포에 필요한 모든 리소스를 정의하는 애플리케이션 모듈. 이 스택은 웹 서버, 데이터베이스, 스토리지, 지원 네트워킹을 포함할 수 있어요.

모듈 구성 방법에 대한 지침은 모듈 생성 권장 패턴 문서표준 모듈 구조를 참고해요.

로컬 모듈 (Local modules)

로컬 모듈은 원격 모듈 레지스트리가 아니라 로컬 디스크에서 소싱돼요. 조직 전반에서 모듈을 쉽게 버전 관리하고 공유·재사용하려면 HCP Terraform 프라이빗 레지스트리 같은 모듈 레지스트리에 모듈을 게시하는 것을 권장해요. 모듈 레지스트리를 사용할 수 없다면 로컬 모듈이 코드 유지보수와 업데이트를 단순화할 수 있어요. 자식 모듈을 ./modules/ 디렉터리에 정의하는 것을 권장해요.

저장소 구조 (Repository structure)

버전 관리에서 모듈과 Terraform 구성을 구성하는 방식은 버전 관리와 운영에 큰 영향을 미쳐요. 실제 인프라 구성을 모듈 코드와 별도로 저장하는 것을 권장해요. 각 모듈을 개별 저장소에 저장해요. 이렇게 하면 각 모듈을 독립적으로 버전 관리할 수 있고 Terraform 프라이빗 레지스트리에 모듈을 게시하기 쉬워져요. 논리적으로 관련된 리소스를 모은 저장소로 인프라 구성을 구성해요. 예를 들어 컴퓨트, 네트워킹, 데이터베이스 리소스를 요구하는 웹 애플리케이션을 위한 단일 저장소가 그래요. 리소스를 그룹으로 분리하면 어떤 작업의 실패에 영향을 받을 수 있는 리소스 수를 제한할 수 있어요.

또 다른 접근 방식은 모든 모듈과 인프라 구성을 단일 모놀리식 저장소(monorepo)로 묶는 것이에요. 예를 들어 모노레포는 인프라 스택의 각 컴포넌트에 대한 로컬 모듈 모음을 정의하고 루트 모듈에서 배포할 수 있어요:

.
├── modules
│   ├── function
│   │   ├── main.tf      # aws_iam_role, aws_lambda_function 포함
│   │   ├── outputs.tf
│   │   └── variables.tf
│   ├── queue
│   │   ├── main.tf      # aws_sqs_queue 포함
│   │   ├── outputs.tf
│   │   └── variables.tf
│   └── vpc
│       ├── main.tf      # aws_vpc, aws_subnet 포함
│       ├── outputs.tf
│       └── variables.tf
├── main.tf
├── outputs.tf
└── variables.tf

모놀리식 저장소의 장점은 모든 인프라 변경을 추적하는 단일 진실 공급원이 있다는 것이에요. 그러나 모놀리식 저장소는 CI/CD 자동화를 복잡하게 만들 수 있어요. 어떤 코드 변경이든 전체 저장소에 대해 작동하는 배포를 트리거하므로, 워크플로가 수정된 디렉터리만 대상으로 해야 해요. 또한 저장소에 대한 접근 권한이 있는 사람이라면 누구나 안의 어떤 파일도 수정할 수 있으므로 세밀한 접근 제어를 잃게 돼요. 조직이 모놀리식 접근을 요구한다면 HCP Terraform과 Terraform Enterprise가 워크스페이스를 저장소의 특정 디렉터리로 범위 지정하게 해서 워크플로를 단순화해요.

브랜칭 전략 (Branching strategy)

Terraform 코드에서 협업하려면 GitHub flow를 사용하는 것을 권장해요. 이 접근은 단기 브랜치를 사용해 팀이 코드 변경을 빠르게 검토·테스트·병합하게 해요. 코드를 변경하려면: 메인 브랜치에서 새 브랜치를 만들고, 변경 사항을 새 브랜치에 작성·커밋·푸시하고, 풀 리퀘스트를 만들고, 팀과 변경을 검토하고, 풀 리퀘스트를 병합하고, 브랜치를 삭제해요.

HCP Terraform과 Terraform Enterprise는 풀 리퀘스트에 대한 추측 plan을 실행할 수 있어요. 이 추측 plan은 풀 리퀘스트를 만들거나 업데이트할 때 자동으로 실행되며, 메인 브랜치에 병합하기 전에 변경이 인프라에 미칠 영향을 확인할 수 있어요. 풀 리퀘스트를 병합하면 HCP Terraform이 이 변경을 적용하기 위해 새 run을 시작해요.

여러 환경 (Multiple environments)

저장소의 main 브랜치가 모든 환경의 진실 공급원이 되기를 권장해요. HCP Terraform과 Terraform Enterprise 사용자라면 환경마다 별도 워크스페이스를 사용하는 것을 권장해요. 더 큰 코드베이스에서는 여러 워크스페이스에 리소스를 분할해 큰 state 파일을 방지하고 변경의 의도하지 않은 결과를 제한하는 것을 권장해요. 예를 들어 코드를 이렇게 구성할 수 있어요:

.
├── compute
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
├── database
│   ├── main.tf
│   ├── outputs.tf
│   └── variables.tf
└── networking
    ├── main.tf
    ├── outputs.tf
    └── variables.tf

이 시나리오에서는 환경당 워크스페이스 3개를 만들 거예요. 예를 들어 프로덕션 환경은 prod-compute, prod-database, prod-networking 워크스페이스를 갖게 돼요. HCP Terraform이나 Terraform Enterprise를 사용하지 않는다면, 모듈로 구성을 캡슐화하고 환경마다 디렉터리를 사용해 각각 별도 state 파일을 갖도록 하는 것을 권장해요. 이 디렉터리의 구성은 각각 환경별 파라미터로 로컬 모듈을 호출해요. 이렇게 하면 환경마다 별도의 변수와 백엔드 구성을 유지할 수도 있어요.

├── modules
│   ├── compute
│   │   └── main.tf
│   ├── database
│   │   └── main.tf
│   └── network
│       └── main.tf
├── dev
│   ├── backend.tf
│   ├── main.tf
│   └── variables.tf
├── prod
│   ├── backend.tf
│   ├── main.tf
│   └── variables.tf
└── staging
    ├── backend.tf
    ├── main.tf
    └── variables.tf

State 공유 (State sharing)

state에 민감 정보가 포함되므로 가능하면 전체 state 파일 공유를 피해요. HCP Terraform이나 Terraform Enterprise를 사용하며 워크스페이스 간 리소스를 참조해야 한다면 tfe_outputs 데이터 소스를 사용해요. HCP Terraform이나 Terraform Enterprise를 사용하지 않지만 여전히 다른 인프라 리소스의 데이터를 참조해야 한다면 데이터 소스를 사용해 프로바이더를 조회해요. 예를 들어 aws_instance 데이터 소스로 AWS EC2 인스턴스를 ID나 태그로 조회할 수 있어요.

시크릿 관리 (Secrets management)

원격 state 저장을 구성하지 않으면 Terraform CLI는 전체 state를 로컬 디스크에 평문으로 저장해요. state는 비밀번호와 개인 키 같은 민감 데이터를 포함할 수 있어요. HCP Terraform과 Terraform Enterprise는 HashiCorp Vault를 통해 state 암호화를 제공해요.

HCP Terraform이나 Terraform Enterprise를 사용한다면 다음을 권장해요:

  • Terraform Enterprise를 사용할 때 local_exec 프로비저너나 외부 데이터 소스의 사용을 방지하는 Sentinel 정책을 정의하고 강제해요.
  • HCP Terraform이나 Terraform Enterprise를 사용할 때 동적 프로바이더 자격 증명을 사용해 장기 유지되는 정적 자격 증명 사용을 피해요.

Terraform Community Edition을 사용한다면 다음을 권장해요:

  • 프로바이더별 환경 변수로 프로바이더 자격 증명을 구성해요.
  • Terraform Vault 프로바이더로 HashiCorp Vault 같은 시크릿 관리 시스템에서 시크릿에 접근해요. Terraform이 여전히 이 값을 state 파일에 평문으로 기록한다는 점을 인지해요.

커스텀 CI/CD 파이프라인을 사용한다면 CI/CD 도구의 민감 값 관리 모범 사례를 검토해요. 대부분의 도구는 민감 값을 환경 변수로 접근할 수 있게 해요.

통합 및 단위 테스트 (Integration and unit testing)

Terraform 테스트는 모듈을 검증하고 호환성을 깨는 변경을 잡아냅니다. Terraform 모듈용 테스트를 작성하고, 풀 리퀘스트의 pre-merge 검사나 자동화된 CI/CD 파이프라인의 전제 단계처럼 애플리케이션 코드의 테스트를 실행하듯 실행하는 것을 권장해요. 테스트는 변수 검증, precondition, postcondition, check 블록 같은 검증 방법과 다르다는 점을 기억해요. 이런 기능은 코드가 배포한 인프라 검증에 초점을 맞추는 반면, 테스트는 코드 자체의 동작과 로직을 검증해요.

정책 (Policy)

정책은 HCP Terraform이 Terraform run에 적용하는 규칙이에요. 정책으로 Terraform plan이 조직의 모범 사례를 준수하는지 검증할 수 있어요. 예를 들어 다음을 수행하는 정책을 작성할 수 있어요:

  • 웹 인스턴스의 크기 제한
  • 필수 리소스 태그 확인
  • 금요일 배포 차단
  • 보안 구성과 비용 관리 강제

정책을 Terraform 코드와 별도의 VCS 저장소에 저장하는 것을 권장해요. 자세한 내용은 정책 적용 문서Sentinel로 정책 적용, 인프라 드리프트 감지와 정책 적용 튜토리얼을 참고해요.

다음 단계 (Next steps)

이 문서는 조직의 Terraform 스타일 가이드를 표준화할 때 명심할 몇 가지 고려 사항을 소개해요. 조직 전반에서 Terraform 코드 작성·구성 표준 방식을 강제하면 코드가 읽기 쉽고 유지보수 가능하며 공유 가능하게 보장할 수 있어요. 더 많은 Terraform 도입 모범 사례는 Terraform 도입 단계를 참고해요.

더 알아보기 (Learn more)