테라폼 기초 강좌 #2 HCL 핵심 문법: 블록, 인수, 리소스 주소와 변경의 세 가지 모양

5 분 소요

지난 글에서 main.tf 하나로 S3 버킷을 만들고 지웠습니다. 그때 “문법은 다음 편에서"라고 미뤄 둔 약속을 지킬 차례입니다. 테라폼의 코드는 HCL(HashiCorp Configuration Language)이라는 전용 언어로 작성합니다. 범용 언어보다 배울 것이 훨씬 적고, 구조를 한번 파악하면 처음 보는 리소스도 문서를 보며 바로 쓸 수 있습니다. 이번 글에서는 블록과 인수라는 뼈대, 리소스 주소와 참조, 그리고 plan 출력을 읽는 법을 손에 익히겠습니다.

HCL의 뼈대: 블록과 인수 #

HCL 파일은 전부 블록(block)인수(argument) 두 가지로 이루어집니다. 지난 글의 코드를 다시 봅니다.

main.tf
resource "aws_s3_bucket" "hello" {
  bucket = "my-terraform-hello-20260822"

  tags = {
    ManagedBy = "terraform"
  }
}
  • 블록블록타입 "라벨" { ... } 형태입니다. resource 블록은 라벨을 두 개(리소스 타입, 이름) 받고, provider 블록은 하나, terraform 블록은 라벨이 없습니다. 몇 개를 받는지는 블록 타입이 정합니다.
  • 인수는 블록 몸통 안의 이름 = 값 한 줄입니다. bucket = "..."이 인수입니다.
  • 블록 안에 블록이 또 들어갈 수 있습니다. 값이 단순한 tags는 맵 값을 가진 인수지만, 뒤에서 만날 lifecycle { ... }처럼 = 없이 중괄호가 바로 오는 중첩 블록도 있습니다.
  • 주석은 #으로 시작합니다.

문법 전체가 사실상 이것뿐입니다. 리소스마다 달라지는 것은 “어떤 인수를 받는가"이고, 그것은 provider 문서에 리소스별로 정리되어 있습니다. HCL을 안다는 것은 문법을 외우는 일이 아니라 문서에서 인수를 찾아 채우는 일에 가깝습니다.

리소스 주소: 타입과 이름 #

resource "aws_s3_bucket" "hello"에서 두 라벨을 합친 aws_s3_bucket.hello가 이 리소스의 주소입니다. 여기서 초심자가 가장 많이 헷갈리는 것이 두 번째 라벨입니다. hello는 AWS에 전달되는 값이 아니라 테라폼 코드 안에서만 쓰는 이름입니다. 실제 버킷 이름은 bucket 인수의 값이고, 콘솔 어디에도 hello라는 글자는 나타나지 않습니다. 그래서 같은 타입의 리소스를 여러 개 선언할 때 서로 다른 이름을 붙여 구분합니다.

주소는 세 곳에서 쓰입니다. plan 출력이 리소스를 가리킬 때, 다른 리소스가 참조할 때, 그리고 #5에서 다룰 state 명령이 대상을 지정할 때입니다. 코드를 읽는 사람이 역할을 알 수 있는 이름(logs, static_assets)을 붙이는 것이 좋고, bucket1 같은 이름은 한 달 뒤의 자신을 괴롭힙니다.

리소스끼리 참조하기 #

인프라는 리소스 하나로 끝나지 않습니다. 버킷에 버저닝 설정을 붙여 보겠습니다. AWS provider에서 버저닝은 별도 리소스입니다.

main.tf
resource "aws_s3_bucket" "hello" {
  bucket = "my-terraform-hello-20260822"
}

resource "aws_s3_bucket_versioning" "hello" {
  bucket = aws_s3_bucket.hello.id

  versioning_configuration {
    status = "Enabled"
  }
}

핵심은 bucket = aws_s3_bucket.hello.id입니다. 주소 뒤에 점을 찍고 속성 이름을 붙이면 그 리소스의 값을 가져옵니다. 버킷 이름을 문자열로 다시 적지 않고 참조로 연결했기 때문에, 버킷 이름이 바뀌어도 이 줄은 고칠 필요가 없습니다. 어떤 속성을 참조할 수 있는지도 provider 문서의 리소스 페이지 하단(Attribute Reference)에 정리되어 있습니다. 참조에는 문법 이상의 의미가 하나 더 있는데, 테라폼이 이 참조를 보고 생성 순서를 결정합니다. 그 이야기는 #6에서 하겠습니다.

변경의 세 가지 모양: create, update, replace #

이 코드로 terraform apply를 하고 나서, 태그를 하나 바꿔 다시 plan을 실행해 봅니다.

plan 출력
  # aws_s3_bucket.hello will be updated in-place
  ~ resource "aws_s3_bucket" "hello" {
      ~ tags = { ... }
    }

기호가 +가 아니라 ~입니다. 이미 있는 버킷을 그대로 두고 태그만 고치겠다는 update in-place입니다. 이번에는 bucket 인수의 이름 자체를 바꿔 봅니다.

plan 출력
  # aws_s3_bucket.hello must be replaced
-/+ resource "aws_s3_bucket" "hello" {
      ~ bucket = "my-terraform-hello-20260822" -> "new-name" # forces replacement
    }

-/+지우고 다시 만든다는 뜻입니다. S3 버킷 이름은 만든 뒤에 바꿀 수 없는 속성이라, 이름을 바꾸려면 교체밖에 없습니다. 안에 데이터가 있었다면 그대로 날아가는 작업이므로, plan에서 이 기호를 보면 반드시 멈춰서 확인해야 합니다. 정리하면 plan의 기호는 네 가지입니다.

기호의미
+새로 만듭니다
~그 자리에서 수정합니다
-/+지우고 다시 만듭니다(교체)
-지웁니다

어떤 속성이 교체를 일으키는지는 provider가 정하며, plan 출력의 # forces replacement 주석이 알려 줍니다. 교체를 제어하는 방법은 #7에서 자세히 다루겠습니다.

fmt와 validate: 커밋 전 습관 #

HCL에는 공식 포매터가 내장되어 있습니다.

실행
terraform fmt        # 현재 디렉터리의 .tf 파일 정렬·들여쓰기 정리
terraform validate   # 문법과 참조 오류 검사 (AWS 호출 없음)

fmt는 들여쓰기와 = 정렬을 표준형으로 맞추고, validate는 AWS에 연결하지 않고 코드 자체의 오류(오타 난 인수, 없는 참조)를 잡습니다. 둘 다 몇 초면 끝나므로 커밋 전에 돌리는 습관을 들이면 리뷰에서 스타일 지적이 사라집니다. 팀에서 이것을 CI로 강제하는 방법은 운영 시리즈에서 다루겠습니다.

정리 #

이번 글에서 다룬 내용입니다.

  • HCL은 블록과 인수 두 가지로 이루어집니다. 리소스별 인수는 외우는 것이 아니라 provider 문서에서 찾는 것입니다
  • aws_s3_bucket.hellohello는 테라폼 안에서만 쓰는 이름이며 AWS에는 전달되지 않습니다. 역할이 드러나는 이름을 붙입니다
  • 다른 리소스의 값은 주소.속성으로 참조합니다. 문자열을 복사하지 않고 참조로 연결하면 변경에 강해집니다
  • plan 기호 +, ~, -/+, - 중에서 -/+(교체)는 리소스를 지웠다 다시 만든다는 뜻이므로 반드시 확인합니다
  • terraform fmtvalidate는 커밋 전 습관입니다

다음 글(#3 변수·출력·로컬 값)에서는 지금까지 하드코딩해 온 값들을 변수로 빼내는 방법을 다룹니다. 같은 코드로 개발용과 운영용을 만들 수 있게 되는 첫걸음입니다.

X