Table of contents
- 재사용이 시작되는 자리
- variable — 외부 입력
- 타입 제약
- validation — 값 검증
- sensitive — 민감한 값
- 값을 주는 여러 방법과 우선순위
- output — 값 내보내기
- locals — 중간 계산
- 셋을 엮은 전형 패턴
- 자주 하는 실수
- 다음으로
재사용이 시작되는 자리
지금까지의 코드는 값이 전부 하드코딩이었다. ap-northeast-2, t3.micro, terraform-first-deploy 같은 값들이 리소스 안에 직접 박혀 있었다. 실습으로는 문제없지만, 실무에선 금방 한계가 온다. 같은 구성을 개발/스테이징/운영에 각각 만들고 싶다면? 리전을 바꿔가며 돌리고 싶다면? 전체 코드를 복사해서 값만 바꾸는 건 재앙이다.
이 문제를 푸는 장치가 변수(variable), 출력(output), 로컬(locals) 세 가지다. 이름만 보면 단순하지만 역할은 분명히 다르다.
- variable: 모듈 바깥에서 값을 주입받는다
- output: 모듈 바깥으로 값을 내보낸다
- locals: 모듈 안에서 중간 계산 결과를 정의한다
이번 편은 이 세 가지를 하나씩 풀어본다.
variable — 외부 입력
가장 많이 쓰는 블록이다. variables.tf 파일을 관례적으로 만들고 거기에 모든 변수를 모은다.
# variables.tf
variable "region" {
description = "리소스를 배포할 AWS 리전"
type = string
default = "ap-northeast-2"
}
variable "environment" {
description = "환경 이름 (dev, staging, prod)"
type = string
}
각 요소를 살펴보자.
description: 변수의 설명.terraform plan출력이나 문서 자동 생성에 쓰인다type: 타입 제약. 틀린 타입이 들어오면 에러로 막힌다default: 기본값. 지정하지 않으면 필수 변수가 된다
environment는 default가 없으니 반드시 값을 줘야 한다. 값을 안 주고 apply하면 Terraform이 대화형으로 물어보거나, 비대화형 환경에선 에러로 실패한다.
변수를 참조할 땐 var. 접두사를 붙인다.
provider "aws" {
region = var.region
}
resource "aws_instance" "web" {
# ...
tags = {
Environment = var.environment
}
}
타입 제약
HCL은 JavaScript처럼 값을 암묵적으로 넣어도 어느 정도 돌아가지만, 변수엔 타입을 명시하는 게 원칙이다. 타입 제약은 실수를 컴파일 타임에 잡아준다.
기본 타입
variable "name" {
type = string
}
variable "count" {
type = number
}
variable "enabled" {
type = bool
}
컬렉션 타입
# 리스트 (같은 타입만 허용)
variable "availability_zones" {
type = list(string)
default = ["ap-northeast-2a", "ap-northeast-2b"]
}
# 맵 (같은 타입 값만)
variable "instance_types" {
type = map(string)
default = {
dev = "t3.micro"
prod = "t3.large"
}
}
# 셋
variable "allowed_ports" {
type = set(number)
default = [80, 443]
}
구조화 타입
여러 속성을 묶은 구조화 타입도 있다. object와 tuple이다.
# object — 이름 있는 속성을 가진 구조체
variable "web_server" {
type = object({
ami = string
instance_type = string
count = number
enable_logs = bool
})
default = {
ami = "ami-xxx"
instance_type = "t3.micro"
count = 2
enable_logs = true
}
}
# tuple — 순서 있는 여러 타입 모음
variable "location" {
type = tuple([string, number, number])
default = ["Seoul", 37.5665, 126.9780]
}
실무에선 object가 훨씬 자주 쓰인다. 관련된 여러 값을 한 덩어리로 묶고 싶을 때 유용하다. tuple은 거의 안 쓴다.
optional — 선택 속성
object 안의 속성을 선택적으로 만들 수 있다(Terraform 1.3+).
variable "web_server" {
type = object({
ami = string
instance_type = string
enable_logs = optional(bool, false) # 기본값 false
backup = optional(string) # 기본값 null
})
}
“대부분의 경우 기본값이면 충분한데, 가끔 덮어쓰고 싶은 속성”에 쓴다. 모듈을 만들 때 사용자 경험이 크게 좋아진다.
any — 타입 추론에 맡김
variable "tags" {
type = any
default = {}
}
타입을 강제하지 않고 싶을 때 any를 쓴다. 유연하지만 위험하기도 하니, 정말 타입을 못 정할 때만 쓰자.
validation — 값 검증
타입만으론 부족한 검증이 있다. 예를 들어 “environment는 dev/staging/prod 중 하나여야 한다” 같은 규칙이다. validation 블록으로 해결한다.
variable "environment" {
description = "환경 이름"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment는 dev, staging, prod 중 하나여야 한다."
}
}
variable "instance_type" {
type = string
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "비용 절감을 위해 t3 계열만 허용한다."
}
}
condition은 true가 나와야 통과한다. can()은 “에러 없이 실행되면 true”를 반환하는 함수라 regex 같은 검증에 자주 쓴다. 여러 validation 블록을 중첩해서 여러 규칙을 걸 수도 있다.
팀 단위로 규칙을 코드에 박아두면, 실수로 설정을 넣어도 plan 단계에서 잡힌다. 리뷰어가 잡는 것보다 효율적이다.
sensitive — 민감한 값
데이터베이스 비밀번호, API 키 같은 값은 plan/apply 로그에 찍히면 안 된다. sensitive = true로 표시하면 Terraform이 로그에서 값을 (sensitive value)로 가린다.
variable "db_password" {
description = "데이터베이스 마스터 비밀번호"
type = string
sensitive = true
}
resource "aws_db_instance" "main" {
# ...
password = var.db_password
}
이 변수가 들어간 리소스의 output도 자동으로 민감하게 처리된다. 단, 주의할 게 있다. Sensitive 표시는 로그에서만 가릴 뿐, tfstate 파일엔 평문으로 저장된다. 그래서 State 파일은 암호화된 원격 백엔드(S3 with SSE, Terraform Cloud 등)에 보관해야 한다.
더 나은 방식은 변수에 비밀번호를 직접 넣지 않고, AWS Secrets Manager나 SSM Parameter Store에서 읽어오는 것이다.
data "aws_secretsmanager_secret_version" "db" {
secret_id = "myapp/db/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db.secret_string
}
이러면 tfstate엔 시크릿 ARN만 남고, 실제 값은 AWS 쪽에서 관리된다.
값을 주는 여러 방법과 우선순위
변수에 값을 넣는 경로는 여러 가지다. 같은 변수가 여러 경로로 지정되면 우선순위가 적용된다.
flowchart LR
D[기본값<br/>default] -->|낮은 우선순위| E
E[환경 변수<br/>TF_VAR_foo] -->|덮어씀| F
F[terraform.tfvars] --> G
G[*.auto.tfvars<br/>알파벳순] --> H
H[-var-file= 옵션] --> I
I[-var 옵션<br/>최고 우선순위]
우선순위가 낮은 것부터 높은 것 순으로 정리하면 이렇다.
1. default (가장 낮음)
variable 블록의 default 속성. 다른 값이 없을 때 쓰인다.
2. 환경 변수
TF_VAR_<이름> 형태의 환경 변수를 읽어온다.
export TF_VAR_region="us-east-1"
terraform apply
CI 파이프라인에서 민감한 값을 넣을 때 유용하다.
3. terraform.tfvars 또는 terraform.tfvars.json
같은 디렉토리에 이 이름의 파일이 있으면 자동으로 읽는다. 관례적으로 프로젝트별 기본값을 여기에 적는다.
# terraform.tfvars
region = "ap-northeast-2"
environment = "dev"
4. *.auto.tfvars
terraform.tfvars 외에도 <이름>.auto.tfvars 파일이 있으면 모두 자동으로 읽는다. 알파벳 순으로 읽혀서 뒤에 오는 파일이 앞을 덮어쓴다.
# dev.auto.tfvars
instance_type = "t3.micro"
# prod.auto.tfvars (이 파일은 보통 prod 배포용 브랜치에만 둔다)
instance_type = "t3.large"
5. -var-file= 옵션
파일을 명시적으로 지정하고 싶을 땐 CLI 옵션을 쓴다.
terraform apply -var-file="environments/prod.tfvars"
환경별 tfvars를 디렉토리 구조로 나눠 관리할 때 이 방식이 편하다.
environments/
dev.tfvars
staging.tfvars
prod.tfvars
6. -var 옵션 (가장 높음)
한 번 실행하면서 일회성으로 값을 덮어쓰고 싶을 때.
terraform apply -var="environment=staging" -var="instance_type=t3.small"
CI에서 태그 값만 일시적으로 바꿀 때 유용하다.
실무 권장 패턴
한 프로젝트에서 환경별로 관리하는 전형적인 구성은 이렇다.
terraform/
main.tf
variables.tf
outputs.tf
environments/
dev.tfvars
staging.tfvars
prod.tfvars
배포 시엔 환경별 tfvars를 명시적으로 지정한다.
terraform workspace select dev
terraform apply -var-file="environments/dev.tfvars"
민감값(API 키 등)은 tfvars에 넣지 말고 환경 변수나 Secrets Manager로 분리한다.
output — 값 내보내기
output 블록은 apply가 끝난 뒤 “이 값을 밖으로 꺼내놓겠다”는 선언이다. 세 군데에서 쓰인다.
- apply 후 콘솔 출력
terraform output <이름>으로 조회- 모듈을 썼을 때, 부모 모듈에서 자식 모듈의 값을 받기
# outputs.tf
output "instance_id" {
description = "EC2 인스턴스 ID"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "EC2 퍼블릭 IP"
value = aws_instance.web.public_ip
}
output "db_endpoint" {
description = "RDS 엔드포인트"
value = aws_db_instance.main.endpoint
sensitive = false
}
output "db_password" {
description = "DB 비밀번호 (로그에 안 찍힘)"
value = var.db_password
sensitive = true
}
sensitive = true가 붙은 output은 plan/apply 로그에서 (sensitive value)로 가려진다. 단, terraform output <이름>으로 직접 조회하면 보이긴 한다. 가리기보다 “실수로 로그에 남지 않게” 하는 장치다.
output은 JSON 형태로도 뽑을 수 있다.
terraform output -json > outputs.json
CI 파이프라인에서 “apply 후 생성된 값을 다음 스텝으로 넘겨야 할 때” 이 형태가 요긴하다.
locals — 중간 계산
locals는 모듈 안에서만 쓰는 중간 변수다. 같은 값을 여러 곳에서 쓰고 있을 때, 또는 복잡한 표현식을 한 번만 계산하고 싶을 때 쓴다.
# locals.tf
locals {
# 공통 태그
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
CostCenter = "platform-team"
}
# 조건에 따른 계산
is_prod = var.environment == "prod"
instance_type = local.is_prod ? "t3.large" : "t3.micro"
backup_enabled = local.is_prod
# 이름 규칙
name_prefix = "${var.project_name}-${var.environment}"
}
참조할 땐 local. 접두사를 쓴다(locals. 아님).
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = local.instance_type
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web"
Role = "web"
})
}
variable과 locals의 차이를 헷갈리기 쉬운데, 역할이 분명히 다르다.
- variable: 바깥에서 들어오는 입력. 사용자가 값을 바꿀 수 있다
- locals: 안에서 만드는 파생값. 사용자가 건드릴 수 없다
둘 다 같은 용도로 쓰지 말자. “파라미터”는 variable, “내부 계산”은 locals다.
셋을 엮은 전형 패턴
변수, 출력, locals가 실전에서 어떻게 엮이는지 하나의 흐름으로 보자. 환경별로 EC2를 띄우는 작은 모듈이라고 치자.
# variables.tf — 외부 입력
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment는 dev, staging, prod 중 하나여야 한다."
}
}
variable "project_name" {
type = string
}
# locals.tf — 내부 파생값
locals {
is_prod = var.environment == "prod"
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
}
instance_type = local.is_prod ? "t3.large" : "t3.micro"
replicas = local.is_prod ? 3 : 1
name_prefix = "${var.project_name}-${var.environment}"
}
# main.tf — 리소스 정의
resource "aws_instance" "web" {
count = local.replicas
ami = data.aws_ami.amazon_linux.id
instance_type = local.instance_type
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-${count.index}"
})
}
# outputs.tf — 바깥으로 내보낼 값
output "instance_ids" {
value = aws_instance.web[*].id
}
output "public_ips" {
value = aws_instance.web[*].public_ip
}
한 프로젝트의 Terraform 디렉토리는 대체로 이런 모양을 유지한다. variable은 파라미터, locals는 공유 계산, resource는 실제 선언, output은 결과 노출. 역할이 분명하면 코드가 10배로 늘어도 정리된다.
자주 하는 실수
- 민감한 값에
sensitive를 안 붙인다. 로그와 CI 히스토리에 남는다. 시크릿 쪽은 일단sensitive = true를 기본으로 variables.tf에default를 남용한다. 필수 파라미터에 default를 넣으면 실수로 빈 문자열이 지나갈 수 있다. 진짜 필수라면 default를 빼서 “꼭 넣어라”라고 강제하는 편이 낫다locals로 해결할 걸variable로 선언한다. 사용자 입력이 아닌데 variable로 두면 불필요하게 넓어진다. 반대의 실수도 흔하다- tfvars를 Git에 커밋한다. 민감값이 들어있지 않은지 반드시 확인한다.
.gitignore에 기본적으로*.tfvars를 넣어두고, 정말 커밋해야 하는 환경별 파일만 명시적으로 추가하는 편이 안전하다
다음으로
변수와 출력을 다루면 Terraform 코드에 숨이 트인다. 다음 편에서는 Terraform이 클라우드 API와 대화하는 창구인 프로바이더를 파고든다. required_providers 블록의 버전 제약을 제대로 거는 법, 여러 리전/계정을 한 프로젝트에서 다루는 법(alias), 공식 프로바이더와 커뮤니티 프로바이더의 차이까지 본다.


Loading comments...