서론

앞선 글(https://kintoun-blog.vercel.app/aws-팀원에게-iam-권한을-안전하게-위임하기-3bdd23c825c0802f96d2c5b9e2cfe3d2)에서 IAM 역할 생성을 PR 리뷰 기반으로 옮기기로 했다. 그러려면 먼저 Terraform이 굴러갈 토대부터 세워야 했다. 저장소 구조는 두 개의 독립된 루트 모듈로 나뉜다.

├── bootstrap/   # state 버킷 + GitHub Actions OIDC 롤 (사람이 1회 실행)
└── platform/    # 실제 인프라 (이후 CI가 관리)

본 글은 이 중 HCL을 처음 접하고 bootstrap을 처음부터 apply 진행한 내용이다.

HCL의 기본 구조

Terraform 설정은 HCL이라는 문법 위에 쓰인다. 정확히 하면 HCL이 정의하는 것은 아래의 블록 구조까지고, resource나 variable 같은 블록의 의미는 Terraform이 부여한다.

블록타입 "라벨" {
  인자 = 값
}
블록 역할 비유
variable 외부에서 받을 입력값 함수의 매개변수
resource 실제로 만들 인프라 함수 본문
output 실행 후 내보낼 값 함수의 반환값
provider / terraform 연결 설정, 버전 고정 환경 설정

파일 이름(variables.tf, main.tf, outputs.tf)은 관례일 뿐이고, Terraform은 디렉터리 안의 .tf 파일을 전부 합쳐서 하나로 읽는다. nginx의 conf와 유사하다고 느꼈다.

variable 블럭에서 default = null로 두면 기본 문자열이 생기는 것이 아니라, 기본적으로 그 기능을 쓰지 않게 된다. null을 리소스 인자에 넘기면 그 인자를 아예 설정하지 않은 것과 동일하게 취급된다.

variable "permissions_boundary_arn" {
  description = "CI 롤에 붙일 권한 경계 정책 ARN"
  type        = string
  default     = null # 값을 안 주면 경계를 안 붙인다
}

변수는 default가 없으면 필수, 있으면 선택으로 취급된다. tfvars 파일에 값을 주면 default를 덮어쓴다. 그래서 variables.tf는 값 저장소라기보다 모듈의 입력 인터페이스, 함수로 치면 매개변수 선언부처럼 느껴졌다.

실행 순서와 의존성 그래프

HCL은 React와 같이 선언형 시스템을 사용한다고 한다. 이 순서로 실행하라가 아니라, 최종적으로 기대하는 상태를 나타낸다. 그래서 Terraform은 코드에서 참조 관계를 읽어 의존성 그래프를 만들고, 참조가 없는 것들은 병렬로(기본 10개 동시) 생성한다고 한다. 생산성과 성능 두 마리를 잡은 기발한 설계라고 생각했다.

resource "aws_s3_bucket" "tfstate" {
  bucket = var.state_bucket_name
}

resource "aws_s3_bucket_versioning" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id # 참조가 있으니 버킷이 먼저 만들어진다

  versioning_configuration {
    status = "Enabled"
  }
}

우리의 경우 tfstate를 저장하기 위한 S3 버킷과 테라폼 배포를 위한 OIDC provider 설정을 동시에 진행해야 했는데, 둘은 서로 참조가 없어서 실제로도 병렬로 생성됐다.

루트 모듈 간 값 공유

조직 이름 같은 공유 값은 bootstrap 밖 상위 폴더에 있어야 하지 않나 생각했는데, Terraform에는 루트 모듈을 가로지르는 전역 변수라는 개념 자체가 없다. bootstrap과 platform은 각자 state를 가지고 각자 따로 apply되는 독립된 단위라고 한다.

여러 루트 모듈이 값을 공유해야 할 때는 -var-file=../common.tfvars로 공유 tfvars를 양쪽에서 명시적으로 로드하거나, 한쪽이 output으로 내보낸 값을 terraform_remote_state로 읽거나, 상수만 담은 작은 모듈을 양쪽에서 가져온다.

# platform 쪽에서 bootstrap의 output을 읽는 예
data "terraform_remote_state" "bootstrap" {
  backend = "s3"
  config = {
    bucket = "team-tfstate"
    key    = "bootstrap/terraform.tfstate"
    region = "ap-northeast-2"
  }
}