IAC 01-1 테라폼 코드 한 벌 짜기 — 파일 배치·변수·출력·참조
고친 사람 github-actions[bot]
실측은 전부 라즈베리파이 5 · Terraform 1.16.4 · hashicorp/aws 6.67.0 에서 돌렸습니다. 실제 AWS(Amazon Web Services) 대신 AWS API(Application Programming Interface, 프로그램이 AWS 에 일을 시키는 창구)처럼 응답하는 모의 서버 moto 를 파이 안에 띄워 두고 거기에 요청을 보냈습니다. 자격 증명은 가짜 키
test만 썼습니다. 출력은 찍힌 것을 손대지 않고 붙였습니다. 길어서 자른 곳은 「…」로 표시했습니다. 몇몇 코드는 줄을 나눠 적었습니다(동작은 같습니다).
1. 빈 폴더에서 서버 한 대까지
빈 폴더에 .tf 파일을 하나씩 채워 보안 그룹 하나와 서버 한 대를 띄웠습니다. 그러는 동안 두 번 놀랐습니다.
terraform validate가Success! The configuration is valid.라고 한 코드가 바로 다음terraform plan에서Error를 냈습니다.- 같은 변수에 값을 세 길로 넣었더니 환경 변수로 넣은 값이 파일에 적힌 값에 졌습니다.
둘 다 테라폼 코드를 짜는 법을 알고 나면 당연해집니다. 이 편은 그 짜는 법을 파일 순서대로 따라갑니다.
01 편에서 가져올 말
이 편은 IAC 01 손으로 만든 서버는 무엇을 잃나 — 명령형 스크립트와 선언형 코드의 다음 편입니다. 거기서 푼 말은 한 줄씩만 되살립니다.
| 말 | 한 줄 |
|---|---|
| 테라폼 | 인프라의 최종 모양을 코드로 적으면 그 모양대로 만들어 주는 도구 |
| HCL | HashiCorp Configuration Language. .tf 파일의 언어. 타입 "레이블" { 이름 = 값 } 꼴의 블록이 기본 단위 |
| 프로바이더 | AWS 같은 바깥 서비스의 API 를 대신 불러 주는 테라폼 플러그인 |
| 리소스 | 테라폼이 만들고 관리하는 인프라 객체 하나(resource 블록) |
| plan · apply | plan 은 코드와 지금 인프라를 비교한 계획만 보여 줌. apply 는 그 계획을 실행 |
| 상태 파일 | 테라폼이 자기가 만든 것을 적어 두는 terraform.tfstate |
만들 것과 파일 여섯
만들 것은 두 자원입니다. 하나는 SSH(Secure Shell, 원격 접속 도구) 포트만 여는 보안 그룹(인스턴스 앞의 방화벽 규칙 묶음) demo-dev-web-sg 입니다. 다른 하나는 그 보안 그룹을 단 EC2(Elastic Compute Cloud) 인스턴스(AWS 가상 서버) demo-dev-web 입니다. 서버를 띄울 디스크 원본인 AMI(Amazon Machine Image, 머신 이미지)는 ID 를 박아 넣지 않고 Amazon Linux 2023 을 이름으로 찾아 씁니다.
다 채운 폴더는 이렇게 생겼습니다. 가운데 숫자는 그 파일을 채우는 절입니다.
demo-app/
├── versions.tf 2절 테라폼·프로바이더 버전
├── providers.tf 3절 AWS 에 어떻게 붙을지
├── variables.tf 4절 바깥에서 받을 입력
├── main.tf 5절 locals·data·resource
├── outputs.tf 6절 밖으로 내놓을 값
└── terraform.tfvars 7절 입력에 넣을 값
파일 이름은 테라폼에게 대개 아무 뜻이 없습니다. 공식 문서의 말로는 "Terraform evaluates all of the configuration files in a module, effectively treating the entire module as a single document."(테라폼은 한 폴더의 설정 파일을 전부 읽어 문서 하나처럼 다룬다)입니다. 블록을 여러 파일로 나누는 것은 사람이 읽기 편하라고 하는 일입니다.
그래도 다들 비슷하게 나눕니다. 공식 스타일 가이드가 권하는 이름이 있어서입니다. 가이드는 terraform 블록을 담는 파일을 terraform.tf 라고 부릅니다. 이름이 동작을 안 바꾸니 versions.tf 라고 해도 됩니다. 이 편은 versions.tf 로 갑니다.
이름이 동작을 바꾸는 예외도 있습니다. terraform.tfvars 와 *.auto.tfvars 는 테라폼이 이름을 보고 찾아 읽는 값 파일입니다. 다른 이름의 값 파일은 -var-file 로 따로 알려 줘야 합니다. override.tf 와 *_override.tf 는 다른 파일의 블록을 덮어쓰는 특별한 파일입니다. 이 편에서는 terraform.tfvars 만 씁니다.
locals·data 같은 블록 이름은 그 파일을 채울 때 하나씩 풉니다. 파일마다 전체를 그 절에서 한 번씩 보여 줍니다.
2. 첫 파일 — versions.tf
맨 먼저 넣는 것은 어떤 테라폼, 어떤 프로바이더로 돌릴지입니다. 이 정보는 terraform 블록에 적습니다.
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
required_version— 테라폼 본체의 버전 조건입니다.>= 1.6은 1.6 이상이면 됩니다source— 프로바이더를 받아 올 주소입니다. 왼쪽aws는 코드 안에서 부르는 이름입니다~> 6.0— 맨 오른쪽 숫자만 올라가도 된다는 조건입니다. 6.67 은 되고 7.0 은 안 됩니다. 공식 문서의 예로는~> 1.0.4가 1.0.10 은 허용하고 1.1.0 은 막습니다
공식 문서는 apply 를 직접 돌리는 폴더(루트 모듈)에서 프로바이더마다 ~> 로 아래와 위를 같이 막으라고 권합니다. 메이저 버전(맨 앞 숫자)이 오르면서 하위 호환이 깨지는 것을 막는 장치입니다.
terraform init 이 이 블록을 읽고 프로바이더를 내려받습니다.
실측 — terraform init (2026-10-05, 라즈베리파이 5, Terraform 1.16.4 + moto)
terraform init -no-color
…
- Installed hashicorp/aws v6.67.0 …
Terraform has created a lock file …
…
같은 ~> 6.0 으로 01 편(2026-09-30)에서는 6.66.0 이 받아졌습니다. 닷새 뒤인 이번에는 6.67.0 입니다. 조건이 범위라서 그 사이 새 버전이 나오면 새 버전이 들어옵니다.
그래서 init 은 고른 버전을 .terraform.lock.hcl(의존성 잠금 파일)에 version = "6.67.0" 으로 적어 둡니다. 공식 문서에 따르면 이 파일이 있으면 init 은 더 새 버전이 나와도 적힌 버전을 다시 고릅니다. 올리고 싶을 때만 terraform init -upgrade 를 씁니다. 출력이 시키는 대로 이 파일은 git 에 같이 올립니다. 그래야 동료와 CI(지속적 통합, 코드를 올릴 때마다 자동으로 검사·배포하는 파이프라인)가 같은 버전으로 돌립니다.
3. providers.tf — AWS 에 어떻게 붙나
provider "aws" 블록에는 프로바이더에게 넘길 설정을 적습니다. 이 실습에서 돌린 파일 전체입니다.
provider "aws" {
region = "us-east-1"
access_key = "test" # moto 전용
secret_key = "test" # moto 전용
# moto 전용: 자격 증명·계정 확인을 건너뛴다
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
# moto 전용: 요청을 moto 주소로 보낸다
endpoints {
ec2 = "http://127.0.0.1:4566"
s3 = "http://127.0.0.1:4566"
sts = "http://127.0.0.1:4566"
iam = "http://127.0.0.1:4566"
}
default_tags {
tags = {
Project = "demo"
ManagedBy = "terraform"
}
}
}
region— 자원을 만들 AWS 지역입니다access_key·secret_key— 자격 증명입니다. 실제 AWS 에서는 코드에 적지 않고 환경 변수나~/.aws설정에서 읽게 둡니다default_tags— 모든 자원에 붙일 태그입니다. 9절에서 풉니다
moto 전용 이라고 적은 줄은 실제 AWS 에 쓸 코드에서 지웁니다. 그러면 region 과 default_tags 만 남습니다. 이때 어긋나는 = 정렬은 10절의 terraform fmt 가 region = "us-east-1" 로 맞춰 줍니다.
4. variables.tf — 바뀔 값을 먼저 뽑는다
main.tf 를 쓰기 전에 환경마다 달라질 값부터 뽑아 둡니다. 환경 이름, 인스턴스 크기, SSH 포트, 덧붙일 태그(자원에 다는 키-값 이름표)입니다. 이 값들을 코드 곳곳에 글자로 박아 두면(하드코딩) 스테이징을 만들 때 코드를 고쳐야 합니다.
바깥에서 받는 값을 테라폼에서는 입력 변수(input variable)라 하고 variable 블록으로 선언합니다. 함수의 매개변수와 같은 구실입니다. 코드 안에서는 var.<이름> 으로 읽습니다.
variable "env" {
type = string
description = "배포 환경 이름"
default = "dev"
validation {
condition = contains(
["dev", "staging", "prod"], var.env
)
error_message = "env 는 dev, staging, prod 가운데 하나여야 합니다."
}
}
variable "instance_type" {
type = string
default = "t4g.micro"
}
variable "ssh_port" {
type = number
default = 22
}
variable "extra_tags" {
type = map(string)
default = {}
}
type— 받을 값의 타입 조건입니다.string·number·bool, 그리고map(string)(키와 값이 모두 문자열인 키-값 묶음) 같은 것이 있습니다default— 값을 안 넣으면 쓸 값입니다. 없으면 반드시 넣어야 하는 변수가 됩니다description— 사람이 읽을 설명입니다. 스타일 가이드는 변수마다type과description을 다 적으라고 권합니다validation— 값이 지켜야 할 규칙입니다.condition이 거짓이면error_message를 띄우고 멈춥니다contains(목록, 값)— 목록에 그 값이 있으면 참을 돌려주는 내장 함수입니다
type 은 값을 그 타입으로 변환할 수 있는지를 검사합니다. ssh_port 의 default 를 문자열 "22" 로 적어도 통과했습니다. plan 에는 보안 그룹의 포트 칸(5절)에 숫자 22 로 찍혔습니다. "twenty-two" 는 숫자로 못 바꾸니 거부됩니다. 이 거부가 언제 일어나는지는 8절에서 봅니다.
5. main.tf — locals · data · resource
이제 본체입니다. main.tf 에는 세 종류의 블록이 들어갑니다. 위에서부터 하나씩 채웁니다.
locals — 여러 번 쓸 식에 이름 붙이기
demo-dev 라는 접두어는 보안 그룹 이름에도, 인스턴스 이름에도 들어갑니다. 같은 식을 두 번 적으면 고칠 때 한쪽을 빼먹습니다. 이럴 때 쓰는 것이 로컬 값(local value)입니다. 공식 문서는 다른 언어의 함수 안 지역 변수와 비슷하다고 설명합니다. 바깥에서 넣는 값이 아니라 코드 안에서 계산해 이름만 붙인 값입니다.
locals {
name_prefix = "demo-${var.env}"
common_tags = merge(
{ Env = var.env },
var.extra_tags,
)
}
locals로 정의하고local.<이름>으로 읽습니다. 읽을 때는 s 가 빠집니다"demo-${var.env}"— 문자열 안에 값을 끼워 넣는 문법입니다.env가dev면demo-dev입니다merge(맵1, 맵2)— 키-값 묶음 여럿을 하나로 합칩니다. 키가 겹치면 뒤쪽 값이 이깁니다(10절 console 에서 확인합니다)
공식 문서는 반대쪽 경고도 적어 둡니다. 로컬 값이 많으면 값이 어디서 왔는지 가려져 오히려 읽기 어렵다는 것입니다. 여러 번 쓰거나 식이 복잡할 때만 씁니다.
data — 만들지 않고 읽어 오기
AMI ID 를 ami-0f47… 처럼 박아 두면 곤란합니다. AMI ID 는 리전마다 다릅니다. 새 버전 이미지가 나오면 ID 도 새로 생깁니다. 그래서 「Amazon 이 올린, 이름이 이 꼴인 것 가운데 최신」으로 찾게 합니다.
이미 있는 것을 조회만 하는 블록이 데이터 소스(data source)입니다. 공식 문서의 정의는 "Data sources fetch data from the provider, but do not create or modify resources."(프로바이더에서 데이터를 가져오되 자원을 만들거나 바꾸지 않는다)입니다. resource 가 「이것이 있어야 한다」라면 data 는 「저것을 찾아 달라」입니다.
# 최신 Amazon Linux 2023 (arm64) AMI 를 이름으로 찾는다
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
# moto 에 맞춰 커널 6.1 로 좁혔다(아래 본문).
# 실제 AWS 는 "al2023-ami-2023.*-arm64" 도 된다
filter {
name = "name"
values = [
"al2023-ami-2023.*-kernel-6.1-arm64",
]
}
filter {
name = "architecture"
values = ["arm64"]
}
}
owners— 누가 올린 이미지에서 찾을지입니다.amazon은 Amazon 공식 계정의 별칭입니다filter— AWS 이미지 조회 API 에 넘기는 필터입니다. 안의name칸에는 어떤 필터를 쓸지 적습니다.values칸에는 맞출 값을 적습니다.*는 아무 글자나 맞습니다name = "name"— 앞의name은 위의 칸 이름입니다. 뒤의"name"은 AWS 가 정한 필터 이름(AMI 이름으로 거르기)입니다. 둘째 필터의"architecture"도 그런 필터 이름입니다. 쓸 수 있는 이름은 AWS EC2 API 문서의 DescribeImages 항목에 나옵니다most_recent— 여러 개가 걸리면 가장 최근 것을 고릅니다- 다른 블록에서는
data.aws_ami.al2023.id처럼data.을 붙여 읽습니다
AMI 를 arm64 로 찾는 까닭은 인스턴스 유형 t4g 에 있습니다. t4g 계열은 AWS 의 ARM 프로세서 Graviton 을 쓰는 서버라 arm64 이미지만 뜹니다. t3.micro 같은 x86 유형으로 바꾸면 두 필터의 arm64 도 x86_64 로 같이 바꿔야 합니다.
데이터 소스는 plan 단계에서 읽힙니다. plan 첫머리에 data.aws_ami.al2023: Read complete after 1s [id=ami-04fc404d256fd34a2] 가 찍혔습니다. 찾아온 이름은 al2023-ami-2023.12.20260727.0-kernel-6.1-arm64 였습니다.
못 찾으면 plan 이 멈춥니다. 빈 값을 주고 넘어가지 않습니다. 실제 AWS 예제에 흔한 Ubuntu 24.04 arm64 를 같은 방식으로 찾게 하자 Planning failed. 와 함께 Error: Your query returned no results. 가 찍혔습니다(moto 에는 그 이미지가 없습니다). 프로바이더 문서는 하나보다 많거나 적게 맞으면 실패한다고 적습니다. 조건을 좁히거나 most_recent 를 쓰라는 것입니다.
그런데 moto 에서는 most_recent 가 뜻대로 재현되지 않습니다. 패턴을 al2023-ami-2023.*-arm64 로 넓히면 커널 6.12·6.18·6.1 버전 셋이 걸립니다. moto 는 모든 AMI 의 생성 시각을 같은 값(2026-10-04T16:16:07.000Z)으로 채우므로, 셋이 동점이라 6.12 버전 ami-0f47531f8c49bd1c6 이 뽑혔을 뿐입니다. 실제 AWS 에서는 생성 시각이 달라 최신이 뽑힙니다. 이 편의 코드는 그래서 패턴을 kernel-6.1 까지 좁혀 하나만 걸리게 했습니다.
resource — 있어야 할 것
마지막으로 자원 둘입니다. 먼저 보안 그룹입니다.
resource "aws_security_group" "web" {
name = "${local.name_prefix}-web-sg"
description = "demo web sg"
ingress {
from_port = var.ssh_port
to_port = var.ssh_port
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
tags = local.common_tags
}
ingress— 들어오는 통신을 허용하는 규칙 하나입니다. 규칙이 여럿이면 블록을 여러 번 적습니다from_port·to_port— 허용할 포트 범위의 처음과 끝입니다. 포트 하나만 열 때는 둘이 같습니다.protocol은tcp·udp같은 통신 규약입니다cidr_blocks— 접속을 허용할 주소 범위를 CIDR(Classless Inter-Domain Routing) 표기로 적습니다.10.0.0.0/16은 10.0.x.x 범위입니다. 예시용 대역이라 이대로는 실제로 접속할 수 없습니다(접속 키key_name도 없습니다)
egress(나가는 통신 규칙) 블록이 없다는 점에 주의합니다. AWS 는 새 보안 그룹에 「나가는 통신 전부 허용」 규칙을 기본으로 달아 줍니다. 그런데 프로바이더 문서에 따르면 테라폼은 이 기본 규칙을 지웁니다("Terraform will remove this default rule"). moto 에서도 apply 뒤 egress = [] 로 찍혔습니다. 서버가 밖으로 나가야 한다면 egress 블록(포트 0, protocol = "-1", 0.0.0.0/0)을 같이 적습니다.
인스턴스 쪽은 위에서 만든 것을 모두 가져다 씁니다.
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = var.instance_type
vpc_security_group_ids = [
aws_security_group.web.id,
]
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web"
})
}
aws_security_group.web.id— 보안 그룹이 만들어진 뒤 생기는 ID 입니다. 리소스는 접두어 없이<종류>.<이름>으로 읽습니다vpc_security_group_ids— 인스턴스에 달 보안 그룹 ID 목록입니다. 한 인스턴스에 여럿을 달 수 있어 목록으로 받습니다.vpc_는 VPC(Virtual Private Cloud, AWS 안의 사설 네트워크)에 속한 보안 그룹이라는 뜻입니다
두 자원 모두 vpc_id·subnet_id 를 적지 않았습니다. 그러면 그 리전의 기본 VPC 와 그 기본 서브넷에 만들어집니다. 공인 IP 가 붙는지도 그 서브넷 설정을 따릅니다. 기본 VPC 를 지운 계정에서는 생성이 실패합니다.
main.tf 안의 값은 네 가지 꼴로 서로를 가리킵니다.
| 쓰는 꼴 | 가리키는 것 |
|---|---|
var.env |
variable — 바깥에서 들어온 값 |
local.name_prefix |
locals — 코드 안에서 계산한 값 |
data.aws_ami.al2023.id |
data — 조회해 온 값 |
aws_security_group.web.id |
resource — 만든 자원의 속성 |
이 참조가 곧 만드는 순서가 됩니다. apply 는 데이터 소스를 먼저 읽었습니다. 그다음 보안 그룹을 만들고, 그 뒤에 인스턴스를 만들었습니다. 순서는 어디에도 적지 않았습니다. 실습 끝에 terraform destroy 로 지울 때는 거꾸로 인스턴스를 먼저, 보안 그룹을 나중에 지웠습니다.
6. outputs.tf — 밖으로 내놓을 값
apply 가 끝나면 만든 서버의 ID 가 궁금합니다. 그 ID 는 상태 파일 안에 있습니다. 테라폼 바깥(사람·셸 스크립트·다른 테라폼 코드)에 넘길 값을 고르는 블록이 출력 값(output)입니다.
output "instance_id" {
description = "웹 서버 인스턴스 ID"
value = aws_instance.web.id
}
output "ami_id" {
value = data.aws_ami.al2023.id
}
output "ami_name" {
value = data.aws_ami.al2023.name
}
output "sg_id" {
value = aws_security_group.web.id
}
value 에는 어떤 참조든 씁니다. .id·.name 말고 어떤 속성을 꺼낼 수 있는지는 프로바이더 문서의 자원 페이지마다 있는 Attribute Reference 절에 나옵니다. 예를 들어 서버의 공인 IP 를 내놓으려면 aws_instance.web.public_ip 를 씁니다.
비밀번호처럼 화면에 찍히면 안 되는 값에는 sensitive = true 를 더합니다. 그러면 화면에 <sensitive> 로 가려져 나옵니다.
실측 — apply 와 출력 값 (2026-10-05, 라즈베리파이 5, Terraform 1.16.4 + moto)
terraform apply -auto-approve -no-color 출력의 끝부분입니다.
…
Apply complete! Resources: 2 added, …
Outputs:
ami_id = "ami-04fc404d256fd34a2"
ami_name = "al2023-ami-2023.12.20260727.0-…
instance_id = "i-fc44c89490620e5ce"
sg_id = "sg-f808a51a4d20189e6"
나중에 다시 보려면 terraform output 을 씁니다. terraform output instance_id 는 "i-fc44c89490620e5ce" 처럼 따옴표째 찍습니다. -raw 를 붙이면 따옴표가 빠져 셸 명령에 그대로 넘길 수 있습니다(9절에서 그렇게 씁니다). 스크립트가 읽을 때는 -json 이 값마다 sensitive·type·value 를 붙여 줍니다.
7. terraform.tfvars — 값을 넣는 길과 우선순위
변수에 값을 넣는 가장 흔한 길은 terraform.tfvars 파일입니다. 변수 이름 = 값 을 한 줄씩 적으면 따로 지정하지 않아도 테라폼이 찾아 읽습니다. 이 실습의 파일입니다.
env = "dev"
instance_type = "t4g.small"
이 실습에서 잰 것은 값을 넣는 길 셋과 default 입니다. 길 셋은 tfvars 파일, 명령줄의 -var instance_type=t4g.large, 환경 변수 TF_VAR_instance_type=t4g.medium 입니다. 환경 변수는 TF_VAR_ 뒤에 변수 이름을 붙인 이름으로 셸에 설정합니다.
셋 다 넣으면 누가 이기나
instance_type 하나에 세 길로 서로 다른 값을 넣었습니다. default 까지 넷이 다 달라야 어느 쪽이 이겼는지 보입니다. 길을 하나씩 더해 가며 plan 을 돌리고, plan 출력의 instance_type 값만 뽑았습니다. 넷째·다섯째 줄은 terraform.tfvars 를 잠시 치우고 돌렸습니다.
실측 — 값 넣는 길의 우선순위 (2026-10-05, 라즈베리파이 5, Terraform 1.16.4 + moto)
INPUTS 는 그 줄에서 넣은 길, WINNER 는 plan 에 찍힌 값입니다.
INPUTS WINNER
tfvars "t4g.small"
tfvars+TF_VAR "t4g.small"
tfvars+TF_VAR+-var "t4g.large"
TF_VAR only "t4g.medium"
nothing (default) "t4g.micro"
둘째 줄이 1절에서 말한 두 번째 놀람입니다. 환경 변수로 t4g.medium 을 넣었습니다. 그래도 tfvars 의 t4g.small 이 이겼습니다. 환경 변수가 이긴 것은 tfvars 파일을 치운 넷째 줄뿐입니다.
정리하면 -var > terraform.tfvars > TF_VAR_ 환경 변수 > default 입니다. 공식 문서의 우선순위 표도 같습니다. 그 표에는 tfvars 보다 앞서는 *.auto.tfvars 파일과, -var 와 우선순위가 같은 -var-file 옵션도 있습니다. 이 편에서는 둘 다 재지 않았습니다.
이 순서는 CI 에서 문제가 됩니다. TF_VAR_instance_type 으로 큰 서버를 띄우려 해도 레포에 커밋된 terraform.tfvars 에 같은 변수가 있으면 plan 은 tfvars 값으로 계획을 세웁니다. 환경 변수로 바꾸고 싶은 값은 tfvars 에 적지 않거나 -var 로 넘깁니다.
8. validate 는 값을 보지 않는다
파일을 다 채웠으면 terraform validate 로 검사합니다. 기준 코드를 복사해 한 군데씩 망가뜨리고 폴더마다 terraform validate -no-color 를 돌렸습니다.
실측 — validate 가 잡은 것 (2026-10-05, 라즈베리파이 5, Terraform 1.16.4 + moto)
넷 다 잡혔습니다. 오타에는 Did you mean "instance_type"? 으로 바른 이름까지 제안했습니다.
| 망가뜨린 것 | validate 결과 |
|---|---|
인자 이름 오타 instnace_type |
Unsupported argument |
ssh_port default 를 "twenty-two" 로 |
Invalid default value for variable |
없는 자원 aws_security_group.webb |
Reference to undeclared resource |
없는 변수 var.enviroment |
Reference to undeclared input variable |
같은 틀린 값을 tfvars 로 넣으면
이번에는 코드는 손대지 않고 terraform.tfvars 에 ssh_port = "twenty-two" 한 줄을 더했습니다. 위 표 둘째 줄과 같은 값입니다. 그리고 validate 와 plan 을 차례로 돌렸습니다. validate 는 Success! The configuration is valid. 였고, plan 은 이렇게 멈췄습니다.
…
Error: Invalid value for input variable
on terraform.tfvars line 3:
3: ssh_port = "twenty-two"
The given value is not suitable for …
variables.tf:17,1-20: a number is required.
variables.tf:17은 실험 파일의 줄 번호입니다. 4절 코드는condition을 세 줄로 나눠 적어서ssh_port가 19행에 옵니다
이것이 1절에서 말한 첫 번째 놀람입니다. 같은 "twenty-two" 가 default 에 있으면 validate 가 잡습니다. tfvars 에 있으면 통과시킵니다.
둘이 갈리는 까닭은 validate 가 무엇을 읽느냐입니다. 공식 문서는 validate 를 이렇게 설명합니다. "runs checks that verify whether a configuration is syntactically valid and internally consistent, regardless of any provided variables or existing state."(넣은 변수 값이나 상태와 상관없이, 설정이 문법에 맞고 안에서 서로 어긋나지 않는지 검사한다.) default 는 .tf 코드의 일부라 검사 대상입니다. tfvars 는 코드가 아니라 이번 실행에 넣는 값이라 validate 가 보지 않습니다.
validation 블록도 마찬가지입니다. env 에 목록에 없는 qa 를 -var env=qa 로 넣었더니 validate 는 Success! 였습니다. plan 은 Error: Invalid value for variable 아래에 4절에서 적은 error_message(「env 는 dev, staging, prod 가운데 하나여야 합니다.」)를 찍고 멈췄습니다.
그래서 쓰임새가 갈립니다. validate 는 값 없이 코드만 보는 빠른 검사라 편집기 저장 때나 CI 첫 단계에 둡니다. 값까지 맞는지는 plan 을 돌려야 압니다. 공식 문서도 실행 맥락까지 검사하려면 plan 을 쓰라고 합니다.
9. 태그를 한 곳에서 — default_tags
「모든 자원에 프로젝트·관리 주체 태그를 단다」 같은 규칙은 흔합니다. 자원마다 tags 에 같은 두 줄을 적으면 하나를 빼먹기 쉽습니다. AWS 프로바이더는 이를 위해 provider 블록에 default_tags 를 둡니다. 3절 providers.tf 끝의 블록이 그것으로, Project = "demo" 와 ManagedBy = "terraform" 을 모든 자원에 붙입니다.
그러면 plan 에 태그가 두 벌 나옵니다. 보안 그룹 부분만 보입니다.
실측 — tags 와 tags_all (2026-10-05, 라즈베리파이 5, Terraform 1.16.4 + moto)
+ tags = {
+ "Env" = "dev"
}
+ tags_all = {
+ "Env" = "dev"
+ "ManagedBy" = "terraform"
+ "Project" = "demo"
}
tags 는 자원 블록에 적은 것입니다. tags_all 은 거기에 default_tags 를 합친 최종본입니다. AWS 에 붙는 쪽은 tags_all 입니다. apply 뒤 aws CLI(Command Line Interface, 명령줄 도구)의 describe-instances 에 --instance-ids $(terraform output -raw instance_id) 로 물어보니, 인스턴스에 Env·ManagedBy·Name·Project 넷이 다 붙어 있었습니다.
default_tags 를 고치면, 키가 겹치면
default_tags 에 두 줄을 더하고 plan 을 돌렸습니다. Owner = "platform" 은 새 키입니다. Env = "shared" 는 자원 쪽 Env = "dev" 와 키가 겹칩니다.
~ resource "aws_security_group" "web" {
…
~ tags_all = {
+ "Owner" = "platform"
# (3 unchanged elements hidden)
}
…
Plan: 0 to add, 2 to change, 0 to destroy.
두 가지가 보입니다.
첫째, provider 블록 한 곳만 고쳤습니다. 그런데 자원 둘이 모두 제자리 변경(~, 지우지 않고 속성만 고치는 것)으로 잡혔습니다. 인스턴스 쪽 tags_all 에도 Owner 가 더해졌습니다. 그 프로바이더로 만든, 태그를 받는 자원이 백 개라면 백 개가 plan 에 뜹니다. 제자리 변경과 교체가 어떻게 갈리는지는 IAC 02 plan 이 말해 주는 것 — 제자리 변경·교체·상태 파일에서 다룹니다.
둘째, 겹친 Env 는 바뀌지 않았습니다. 감춰진 3개 안에 Env = "dev" 가 그대로 있습니다. 키가 겹치면 자원 쪽 tags 가 이깁니다. AWS 프로바이더 문서도 provider 태그를 덮어쓰려면 자원의 tags 에 같은 키를 새 값으로 적으라고 합니다.
10. 짜는 동안 자주 치는 명령 — fmt · console
fmt — 모양 맞추기
terraform fmt 는 공식 스타일(들여쓰기 두 칸, 같은 블록 안 = 줄 맞춤)로 파일을 고쳐 씁니다. 일부러 들쭉날쭉하게 쓴 ugly.tf 에 돌렸습니다.
terraform fmt -check -no-color; echo "exit=$?"
ugly.tf
exit=3
-check 는 고치지 않고 검사만 합니다. 고칠 파일이 있으면 이름을 찍고 0이 아닌 종료 코드(이번에는 3)로 끝나니 CI 에 넣기 좋습니다. -diff 를 붙여 돌리면 바뀌는 줄을 보여 주면서 파일도 고칩니다. ugly.tf 의 name = "demo-dev-web-sg" 줄은 두 칸 들여쓰기와 name = 정렬로 바뀌었습니다. 그 뒤 다시 -check 하니 종료 코드가 0이었습니다.
console — 식을 미리 계산해 보기
terraform console 은 식을 넣으면 값을 계산해 주는 대화형 창입니다. 상태 파일도 읽으므로 apply 뒤에는 만든 자원의 속성까지 꺼내 볼 수 있습니다. 식을 한 줄씩 넣은 결과 가운데 둘입니다.
> merge({ a = 1, b = 2 }, { b = 20, c = 30 })
{
"a" = 1
"b" = 20
"c" = 30
}
…
> aws_instance.web.id
"i-fc44c89490620e5ce"
merge 에서 겹친 키 b 가 뒤쪽 값 20 이 됐습니다. 5절에서 말한 규칙입니다. 속성 이름을 문서에서 찾았다면 여기서 aws_instance.web.public_ip 처럼 먼저 넣어 보고 output 에 옮기면 됩니다.
11. 완성된 한 벌
1절 트리의 여섯 파일이 2~7절에서 한 번씩 다 나왔습니다. 실제 AWS 에 쓸 때는 3절의 moto 전용 줄만 지웁니다.
이 폴더에서 terraform init → terraform plan → terraform apply 순서로 돌립니다. 실제 AWS 라면 us-east-1 에 기본 VPC 가 있는 계정이어야 합니다. 출력은 6절과 같은 꼴로 나옵니다(ID 와 AMI 이름은 다릅니다). 서버가 실제로 뜨므로 요금이 붙습니다. 다 봤으면 terraform destroy 로 지웁니다.
스테이징 한 벌은 -var env=staging 처럼 값만 바꿔 같은 코드로 띄웁니다. 환경이 늘 때 반복과 재사용을 어떻게 짜는지는 IAC 01-2 반복과 재사용 — count·for_each·dynamic·module에서 이어집니다.
한 장 요약
테라폼은 한 폴더의
.tf파일을 전부 읽어 문서 하나처럼 다룹니다. 파일 이름은 대개 사람을 위한 것이라 관례로 versions·providers·variables·main·outputs 로 나눕니다.terraform.tfvars·*.auto.tfvars·override.tf같은 몇 이름만 동작을 바꿉니다(1절).
required_providers의~> 6.0은 6.x 안의 새 버전을 받아들이는 범위라 닷새 사이 6.66.0 이 6.67.0 으로 바뀌었습니다. init 이 고른 버전은.terraform.lock.hcl에 적힙니다. 이 파일은 git 에 올립니다(2절).
variable은 바깥 입력,locals는 코드 안에서 이름 붙인 식,data는 조회만 하는 블록,output은 바깥에 내놓는 값입니다. 서로를var.·local.·data.·<종류>.<이름>으로 가리킵니다. 그 참조가 만들고 지우는 순서가 됩니다. 꺼낼 수 있는 속성 이름은 프로바이더 문서의 Attribute Reference 에 있습니다(4~6절). data 로 찾는 AMI 가 하나도 안 맞으면 plan 이 멈춥니다.t4g는 ARM 서버라 AMI 도 arm64 여야 합니다. 보안 그룹에egress를 안 적으면 테라폼이 AWS 의 기본 「나가는 통신 허용」 규칙을 지웁니다(5절).값 넣는 길의 우선순위는
-var>terraform.tfvars>TF_VAR_환경 변수 >default입니다. 환경 변수가 tfvars 파일보다 약합니다(7절).
validate는 넣은 값을 보지 않습니다. tfvars 의 틀린 타입과 validation 규칙 위반은 validate 를 통과하고 plan 에서야 걸립니다(8절).
default_tags는 모든 자원의tags_all에 합쳐집니다. 고치면 그 프로바이더의 자원 전부가 제자리 변경됩니다. 키가 겹치면 자원 쪽tags가 이깁니다(9절).
관련 항목
테라폼 코드를 이루는 블록
테라폼 변수 · 로컬 값 · 데이터 소스 · 출력 값 · 리소스 · 프로바이더 · HCL
테라폼이 값을 받고 검사하는 장치
tfvars · 환경 변수 · 변수 우선순위 · 입력 검증 · 타입 제약 · terraform validate · terraform plan
테라폼이 버전을 고정하는 장치
버전 제약 · 의존성 잠금 파일 · 유의적 버전 · terraform init
이 편에서 손에 익힌 명령
terraform fmt · terraform console · terraform output · terraform destroy · terraform apply
이 편의 코드가 다룬 AWS 자원
Amazon EC2 · 보안 그룹 · 머신 이미지 · 태그 · CIDR · VPC · AWS Graviton
테라폼이 기대는 바탕
테라폼 · 상태 파일 · 코드형 인프라 · 선언적 설정 · AWS
실습에 쓴 모의 환경
moto · LocalStack · 라즈베리파이
IaC 시리즈의 다른 편
IAC 01 손으로 만든 서버는 무엇을 잃나 — 명령형 스크립트와 선언형 코드 · IAC 01-2 반복과 재사용 — count·for_each·dynamic·module · IAC 02 plan 이 말해 주는 것 — 제자리 변경·교체·상태 파일 · IAC 03 이미 있는 것을 코드로 가져오기 — import · IAC 04 누가 콘솔에서 손대면 — 드리프트와 테라폼이 못 보는 것 · IAC 05 둘이 같이 apply 하면 — 원격 상태와 잠금