TFAS (TerraForm Addicted School)의 내용을 정리한 글입니다.
심각한 테라폼 중독입니다 - 책으로 스터디를 진행합니다.

데이터 타입
모든 프로그래밍 언어를 배우면 자료형부터 배우듯이 테라폼의 기본적인 문법을 배울 때에도 데이터 타입부터 배운다. 테라폼에는 문자열, 숫자, 불리언, 리스트, 맵, 집합 데이터 타입이 존재한다. 값을 누락시키기 위해서는 null이라는 특수한 값을 사용한다.
| 데이터 타입 | 설명 | 예시 |
| string | 유니코드 문자열 | "hello", "123", "한글" |
| number | 숫자 (정수/실수/음수) | 0, 15, -2, 3.14 |
| bool | 불리언 | true/false |
| list (tuple) | 연속적인 값, 인덱스 0부터 시작하는 순서 | ["a", "b", "c"], [142, 4544, 3.14, -11] |
| set | 일련의 값, 순서 없음 | ["a", "b", "c"], [142, 4544, 3.14, -11] |
| map (object) | key=value 값 그룹 | {name="sehyeong", age=11} |
반복문
테라폼에서 사용하는 반복문은 count 와 for_each 두 메타인수를 통해 만들 수 있다. 메타인수란 모든 타입의 리소스에서 사용할 수 있는 선언 가능한 특수한 인수를 의미한다. 각각은 다른 장점을 가지고 있지만 반복적으로 리소스를 생성할 때 사용하는 방법은 for_each를 강력 권장한다.
count
count 는 정수를 매개변수로 받아, 해당 정수의 값만큼 리소스를 반복 생성한다. 동일한 설정을 가진 리소스를 여러 개 생성할 때 사용 가능하나, 사용하지 말자.
resource "aws_instance" "this" {
count = 3
ami = "ami-0123456789"
instance_type = "t3.micro"
private_ip = "10.0.0.${count.index + 1}"
tags = {
Name = "EC2-${count.index + 1}"
}
}
동일한 ami 와 instance type을 가지는 EC2 인스턴스를 3개 생성한다. 각 인스턴스의 리소스 주소는 `aws_instance.this [0]`, `aws_instance.this [1]`, `aws_instance.this [2]` 이 된다. count의 index 값을 사용할 수도 있다.
일반적으로는 다음과 같이 리스트의 길이를 count 메타인수에 입력하는 방법이 조금 더 일반적으로 사용하는 방법이다.
locals {
ami_list = ["ami-123456", "ami-456789", "ami-987654"]
}
resource "aws_instance" "this" {
count = length(local.ami_list) # length 로 list 길이 입력
ami = local.ami_list[count.index] # list 의 값을 index 로 가지고 온다.
instance_type = "t3.micro"
private_ip = "10.0.0.${count.index + 1}"
tags = {
Name = "EC2-${count.index + 1}"
}
}
for_each
for_each 는 map이나 set 데이터 유형을 매개변수로 받아 각 데이터를 순회하여 리소스를 생성한다.
먼저 아래 예시를 확인해보자. 윈도 서버, 리눅스 서버를 생성하며, 두 서버는 서로 다른 리소스 요구사항이 존재한다. 이 경우 list로는 옵션을 각각 명시할 수 없어 map 타입으로 두대의 서버를 각각 생성한다.
resource "aws_instance" "this" {
for_each = {
windows = {
ami = "ami-0123456789"
type = "t3.micro"
}
linux = {
ami = "ami-9876543210"
type = "r5.2xlarge"
}
}
ami = each.value.ami
instanct_type = each.value.type
subnet_id = "subnet-123456"
vpc_security_group_ids = ["sg-123456"]
tags = {
Name = each.key
}
}
이렇게 생성된 리소스는 테라폼 상태에서 각각 `aws_instance.this["windows"]`, `aws_instance.this ["linux"]` 의 값을 가진다. 인덱스로 리소스의 번호를 명시하는 것보다 키값을 사용해 표현하면서 의미가 명확한 리소스 주소를 가지게 된다.
for_each를 사용하면 map의 key를 사용한 리소스 주소를 사용할 수 있기 때문에 리소스 주소의 가독성이 올라간다. 리소스를 생성하고 사용하는 의미가 인덱스를 사용하는 것보다 명확해진다. 또한 인덱스 방식인 count 는 순서가 반드시 테라폼 상태와 동일해야 한다. 결국 리스트의 순서가 실제 리소스에 영향을 끼치기 때문에 잠재적인 장애 유발 지점이 될 수 있다.
그러므로 반복된 리소스를 생성하려는 경우 for_each 를 사용하자. count 의 쓸모 있는 사용법은 뒤에서 알아보도록 하자.
삼항 조건문
테라폼에서 조건부 로직을 처리하는 경우 삼항 연산자를 사용한다. 삼항 연산자는 다음과 같은 문법 구조를 가진다.
조건 ? 참일 때의 값 : 거짓일 때의 값
이는 특정 조건에 따른 변수 혹은 리소스 설정을 결정할 때 유용하다.
앞서 count 의 사용에 대해 이야기했는데, 리소스를 만드는지 만들지 않을 것인지 결정할 때 유용하게 쓰일 수 있다.
variable "env" {
type = string
default = "develop"
}
resource "aws_instance" "nginx" {
count = var.env == "production" ? 1 : 0
ami = "ami-123456"
instance_type = "t3.micro"
}
특정 조건에 맞는 경우 리소스를 생성하기 때문에 리소스 생성 여부를 확인하는 0과 1로 리소스를 사용하는 경우 삼항 연산자와 count를 사용하여 구성할 수 있다.
for 표현식
리스트, 맵, 집합인 컬렉션 데이터 타입의 요소를 순회하며 변환할 수 있다.
for 을 사용해 리스트 또는 집합 생성하기
리스트 (list) 와 집합(set) 은 대괄호[]로 묶여 있는 값이고 미리 정의된 타입에 따라 리스트 또는 집합이 정의된다. 두 컬렉션 사이에서 타입을 변경하는 경우 tolist, toset 함수를 사용할 수 있다.
for 구문을 사용하여 리스트 또는 집합을 생성하는 사용 구조는 다음과 같다.
[for <element> in <collection> : <expression>]
3개의 vpc 를 생성하고, 생성한 vpc의 id를 추출해서 list로 만들어야 한다면 다음과 같이 사용할 수 있다.
resource "aws_vpc" "this" {
for_each = toset(["a", "b", "c"])
cidr_block = "10.0.0.0/16"
}
> aws_vpc.this["a"]
{
"arn" = "arn:aws:ec2:ap-northeast-2:xxxx"
"cidr_block" = "10.0.0.0/16"
"id" = "vpc-123123123123"
}
> [for k, v in aws_vpc.this : v.id]
[
"vpc-123123123123",
"vpc-456456456456",
"vpc-789789789789"
]
맵 타입의 컬렉션이라면 for 구문의 element 는 key와 value를 나타내는 k, v이다. 만약 리스트나 집합이라면 하나의 element로 해당 요소를 받을 수 있다.
for을 사용해 맵 생성하기
맵 타입의 경우 중괄호 안에 키와 값을 가지는 쌍을 가지는 데이터 구조이다.
맵을 만들기 위한 for 구문은 다음과 같다.
{for <element> in <collection> : <key_expression> => <value_expression>}
이전 예제에서 생성한 vpc의 결과를 맵으로 만든다면 다음과 같이 사용 가능하다.
> {for k, v in aws_vpc.this : k => v.id}
{
"a" = "vpc-123123123123",
"b" = "vpc-456456456456",
"c" = "vpc-789789789789"
}
key_expression 이 생성될 맵의 키가 되고, value_expression 이 생성될 맵의 키에 연결되는 value 가 된다.
중요한 점은 키와 값 쌍에서 값은 한 번 더 맵으로 만들 수 있다.
> {for k, v in aws_vpc.this : k => {id = v.id, cidr = v.cidr_block}}
{
"a" = {
id = "vpc-123123123123"
cidr = "10.0.0.0/16"
},
"b" = {
id = "vpc-456456456456",
cidr = "10.0.0.0/16"
}
"c" = {
id = "vpc-789789789789"
cidr = "10.0.0.0/16"
}
}
for 표현식에서의 if
for 표현식에서는 if 조건자를 사용할 수 있다. if를 사용하면 특정 조건을 만족하는 요소에 한해서 최종 결괏값에 포함할 수 있으며, 컬렉션 필터링에 유용하게 사용할 수 있다.
locals {
server_map = {
ad_server = {
active = true
is_window = true
}
squid_proxy = {
active = false
is_window = false
}
web_server = {
active = true
is_window = false
}
}
}
active_server = [
for k, v in local.server_map : k
if v.active
]
# ["ad_server", "web_server"]
linux_server = {
for k, v in local.server_map k => v
if !v.is_window
}
# {
# squid_proxy = {
# active = false
# is_window = false
# }
# web_server = {
# active = true
# is_window = false
# }
# }
테라폼에서 사용하는 블록
HCL 은 미리 정의된 다양한 종류의 블록을 사용해 인프라 관점에서 명확한 의미를 가지는 코드를 작성할 수 있다. HCL 코드의 특정 부분은 리소스 관리를 위한 부분으로 지정하거나 데이터 참조, 출력 변수로 지정 등 다양한 블록을 통해 논리적으로 인프라 구성 코드를 작성할 수 있다.
<BLOCK_TYPE> "<BLOCK_LABEL_1>" "<BLOCK_LABEL_2>" {
<ARGUMENT_1> = <VALUE_1>
<ARGUMENT_2> = <VALUE_2>
# ...
}
테라폼 블록
테라폼 블록은 테라폼 실행 환경 자체의 작동을 설정하는데 사용되며 블록 타입은 terraform이다.
테라폼 버전, 상태 백엔드 설정, 테라폼 프로바이더의 요구사항 등을 지정할 수 있다. 테라폼 블록의 기본 구조는 다음과 같다.
terraform {
required_version = ">= 1.11.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
baucket = "terraform-state-backend"
key = "state/terraform.tfstate"
region = "ap-northeast-2"
}
}
required_version 인수는 테라폼 버전을 지정하기 위해 사용하고 버전의 제약 조건을 담은 문자열을 값으로 받는다.
required_providers 블록은 각 실행 환경에서 사용할 프로바이더의 종류와 버전을 지정할 수 있다. 버전지정은 앞서 확인한 required_version과 동일한 연산자를 사용해서 지정한다.
backend 블록은 테라폼의 상태 파일을 관리할 상태 백엔드의 종류와 설정을 지정한다.
프로바이더 블록
클라우드 서비스나 기술 스택과의 상호작용을 정의하는 테라폼 프로바이더를 설정하는 데 사용하는 블록으로 타입은 provider이다.
provider "aws" {
region = "ap-northeast-2"
access_key = "xxxxx"
secret_key = "yyyyy"
}
aws 프로바이더를 사용하는 프로바이더 블록의 예시이다. 프로바이더 설정에 access_key와 secret_key 가 설정된 코드이다. 프로바이더 블록 설정 방법은 프로바이더별로 다르기 때문에 레지스트리에서 확인해 볼 수 있다.
aws 프로바이더를 예시로 든다면 자격 증명을 사용하는 방법은 프로파일을 지정하거나 assume_role을 지정하는 등 access_key와 secret_key를 하드코딩하지 않고 보안적으로 안전한 방법을 사용할 수 있다.
# profile
provider "aws" {
region = "ap-northeast-2"
profile = "terraform"
}
# assume role
provider "aws" {
region = "ap-northeast-2"
profile = "terraform"
assume_role = {
role_arn = "arn:aws:iam::${account_id}:role/AssumeRole"
}
}
만약 한 종류의 프로바이더를 여러 번 선언할 수도 있다. 다만 한 종류의 프로바이더를 두 개 이상 선언하는 경우 구분을 위해 별칭(alias) 설정을 필수로 해주어야 한다.
# AWS 기본 프로바이더
provider "aws" {
region = "ap-northeast-2"
profile = "terraform-a"
}
# AWS 추가 프로바이더
provider "aws" {
region = "ap-northeast-1"
profile = "terraform-b"
alias = "terraform-b"
}
별칭이 선언되지 않은 프로바이더는 기본 프로바이더이고, 다른 블록에서 특정 프로바이더를 사용한다고 명시하지 않는 경우 기본 프로바이더가 사용된다. 따라서 기본 프로바이더는 프로바이더 종류별로 한 개만 존재해야 한다.
기본 프로바이더와 추가 프로바이더를 사용하는 리소스 블록과 모듈 블록의 예시를 확인해 보자.
# 기본 프로바이더 사용
resource "aws_instance" "this" {
provider = "aws"
instance_type = "t3.medium"
}
# 추가 프로바이더 사용
resource "aws_instance" "this" {
provider = aws.terraform-b
instance_type = "t3.medium"
}
# 기본 프로바이더 사용
module "my_module" {
source = "./modules/my_module"
providers = {
aws = aws
}
}
# 추가 프로바이더 사용
module "my_module" {
source = "./modules/my_module"
providers = {
aws = aws.terraform-b
}
}
기본 프로바이더의 경우 이름이 리소스 주소이며 기본 프로바이더가 아닌 경우 alias까지 명시해야 하고 생략할 수 없다.
리소스 블록
클라우드 서비스나 인프라의 개별 리소스를 정의하고 관리하는 데 사용되는 리소스 블록의 타입은 resource이다.
구체적인 인프라의 구성 요소를 직접적으로 나타내며 다음과 같은 구조를 가진다.
resource "type" "name" {
# ...
}
type 은 리소스 유형을 나타낸다. 리소스 유형은 프로바이더가 제공하는 리소스의 종류를 의미한다. 예를 들어 AWS의 EC2 인스턴스라는 리소스를 사용하려고 하면 aws_instance라는 리소스 유형을 사용해야 하며, GCP의 구글 컴퓨트 엔진을 사용하려 한다면 google_compute_instance라는 리소스 유형을 사용해야 한다.
name 은 현재 테라폼 실행 환경에서 인식하게 될 리소스의 이름이다. 구성한 리소스 블록은 {type}. {name}의 리소스 주소로 참조할 수 있다.
데이터 블록
기존에 수동으로 생성한 리소스나 외부 데이터소스로부터 데이터를 조회할 때 데이터 블록을 사용하며 블록 타입은 data이다.
리소스 블록이 실제 리소스를 생성/수정한다면, 데이터 블록은 읽기 전용으로 데이터를 가져오는 것만 가능하다.
데이터 블록의 사용에 대한 가장 큰 이유는 테라폼으로 만들지 못하는 정보가 필요한 경우 사용할 수 있다. 예를 들어 AWS에서 제공하는 공식 AMI의 아이디를 가지고 오는 경우 반드시 데이터 블록을 사용해야 한다. 최신 우분투 AMI 정보를 조회하는 예시는 다음과 같다. "owners" 필터를 사용해 우분투의 공식 AMI 대상으로 하고 "filters"를 통해 특정 이미지 패턴에 일치하는 AMI 만 검색한다.
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-22.04-amd64-server-*"]
}
}
AWS의 현재 계정 정보와 리전 정보를 가지고 오려는 경우 사용할 수 있다.
# 현재 AWS Account ID 조회
data "aws_caller_identity" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
# 현재 AWS Account Alias 조회
data "aws_iam_account_alias" "current" {}
output "account_alias" {
value = data.aws_iam_account_alias.current.account_alias
}
# 현재 AWS 리전 정보 조회
data "aws_region" "current" {}
output "region_name" {
value = data.aws_region.current.name
}
# 현재 리전에서 사용 가능한 AZ 조회
data "aws_availability_zones" "available" {
state = "available"
}
output "available_az" {
value = data.aws_availability_zones.available.names
}
모듈 블록
모듈을 호출하기 위해 사용하는 모듈 블록은 블록 타입은 module이다.
모듈이란 특정 인프라 구성을 재사용할 수 있는 형태로 포장한 코드 패키지이며, 여러 테라폼 프로젝트에서 쉽게 선언해 사용할 수 있다.
module "module_name" {
source = "path/to/module/source"
# 모듈에 전달할 입력 변수
}
외부 모듈을 바로 사용할 수도, 프로젝트 내에서 다른 경로에 디렉터리로 구성해 둔 모듈을 불러와 사용할 수도 있다.
변수 블록
인프라 구성에 사용할 입력값을 정의하고, 테라폼 코드 안에서 재사용할 수 있는 매개변수로 활용하는 변수 블록은 variable 이란 블록 타입을 가진다.
variable "variable_name" {
type = <type>
default = <default_value>
description = <description>
}
variable_name 은 변수의 이름이며 변수 이름만 지정하면 모든 매개변수들은 생략 가능하다. type 은 데이터 타입이 명시되며, default는 변수의 기본값을 지정할 수 있는 매개변수이고, description 에는 변수가 무엇을 위한 것인지 설명을 작성할 수 있다. 변수 블록은 외부에서 입력값이 필요한 경우 사용할 수 있다.
민감정보를 다룰 때에도 변수 블록을 사용할 수 있다. 테라폼 구성 파일에 값을 입력한다면 git에 이력이 남기 때문에 보안상 문제가 있다. 이럴 때 외부에서 입력되는 값으로 참조하여 사용할 수 있다. 예를 들어 데이터베이스 비밀번호를 형상에 남기지 않으면서 값을 지정하는 경우는 다음과 같다.
resource "aws_db_instance" "rds" {
identifier = "example-cluster"
db_name = "example"
allocated_storage = 20
storage_type = "gp3"
engine = "postgres"
engine_version = "16.3"
instance_class = "db.t3.micro"
username = "example"
password = var.db_password
}
variable "db_password" {
type = string
description = "db password"
}
변수로 지정해서 참조하도록 하며 plan 혹은 apply 명령을 수행할 때 -var-file=secrets.tfvars 와 같이 tfvars 확장자를 가지는 파일을 명시해 줌으로써 해당 변수의 파일을 읽어 들인다. tfvars 확장자를 가지는 파일은 형상에 들어가지 않도록 지정할 수 있다.
로컬 블록
테라폼 프로젝트 안에서만 사용할 변수로 정의하는 것으로 블록 타입은 locals이다. 로컬 변수는 프로젝트 내 여러 곳에서 참조할 수 있는 값을 저장하거나 복잡한 표현식의 결과를 저장해서 단순화하기 위한 용도로 사용한다.
locals {
service_name = "web-app"
common_tags = {
Service = local.service_name
ManagedBy = "Terraform"
}
}
출력 블록
테라폼 코드의 실행 결과 중 외부에 출력하는 값을 설정하기 위한 블록이다. output 블록 타입이다.
output "output_name" {
value = <value>
description = <description>
sensitive = <true_or_false>
}
모듈 혹은 실행 환경에서 출력값을 선언해야 다른 실행환경에서 해당 출력값을 참조할 수 있게 된다. 다른 환경에서 현재 환경의 값을 참조할 수 있도록 하는 용도로 output을 사용한다.
테라폼 함수
테라폼은 내장 함수도 지원한다. 이를 통해 더욱 효과적으로 인프라를 관리할 수 있다.
수치 함수
# abs: 절대값 반환
abs(-12.3) = 12.3
abs(0) = 0
# ceil: 올림
ceil(5.1) = 5
# floor: 내림
floor(4.9) = 4
# max: 최댓값
max(14, 23, 11) = 23
max([14, 23, 11]...) = 23
# min: 최솟값
min(12, 44, 3) = 3
min([12, 44, 3]...) = 3
문자열 함수
# format: 문자열 포멧팅
format("Hello, %s!", "Terraform") = Hello, Terraform!
# replace: 문자열 내 특정 패턴 교체
replace("1 + 2 + 3", "+", "-") = 1 - 2 - 3
# split: 지정한 구분자 기준 문자열 분할
split(",", "te,rra,form") = ["te", "rra", "form"]
# strcontains: 문자열 내 특정 문자열 존재 여부 반환
strcontains("Hello Terraform", "o T") = true
strcontains("Hello Terraform", "TTT") = false
# startswith: 문자열이 특정 접두사로 시작하는지 여부 반환
startswith("hello terraform", "hello") = true
startswith("hello terraform", "terraform") = false
# endswith: 문자열이 특정 접미사로 끝나는지 여부 반환
endswith("hello terraform", "hello") = false
endswith("hello terraform", "terraform") = true
컬렉션 함수
# length: 컬렉션의 길이 혹은 갯수 반환
length("terraform") = 9
length(["te", "rra", "form"]) = 3
# lookup: 맵에서 키에 해당하는 값을 찾고 없는 경우 기본값 설정
lookup({a = 1, b = 2}, "a", 0) = 1
lookup({a = 1, b = 2}, "c", 0) = 0
# concat: 두 개 이상의 리스트 병합
concat(["a", ""], ["b", "c"]) = ["a", "", "b", "c"]
# flatten: 중첩된 리스트를 단일 리스트로 평탄화
flatten([["a", "b"], [], ["c"]]) = ["a", "b", "c"]
# merge: 두 개 이상의 맵을 하나로 병합, 동일한 키의 경우 뒤의 맵의 키로 병합
merge({a = "b", c = "d"}, {e = "f", c = "w"}) = {a = "b", c = "w", e = "f"}
# tolist, toset, tomap
# allture, index, compact, slice, sort, contains 등..
인코딩 함수
# base64encode/base64decode: Base64 인코딩 디코딩
# jsonencode/jsondecode: JSON 인코딩 디코딩
# yamlencode/yamldecode: YAML 인코딩 디코딩
# csvdecode: CSV 디코딩
# urlencode: URL 인코딩
파일 시스템 함수
# abspath: 상대 경로 -> 절대 경로
abspath(path.root) = /home/user/terraform/hello
# dirname: 경로에서 상위 디렉토리 경로 반환
dirname("te/rra/form.tf") = te/rra
# basename: 경로에서 파일 이름만 반환
basename("te/rra/form.tf") = form.tf
# fileexists: 로컬에 해당 파일 존재 여부 반환
# fileset: 지정 디렉토리 내 특정 패턴에 일치하는 파일들의 이름을 집합으로 반환
# file: 지정된 파일의 전체 내용을 문자열 반환
날짜와 시간 함수
# timestamp: 현재 날짜와 시간을 UTC 로 반환
timestamp() = "2025-11-24T15:00:00Z"
# formatdata: 지정된 포멧에 따라 날짜와 시간 포멧
formatdata("YYYY-MM-DD", timestamp()) = "2025-11-24"
해시 함수와 암호화 함수
# sha1("xxx")
# sha256("xxx")
# base64sha256("xxx")
# uuid()
IP Network 함수
# cidrsubnet: 주어진 cidr 블록에서 새로운 서브넷 생성
# 4 비트 더한 서브넷 두 번째
cidrsubnet("192.168.0.0/24", 4, 2) = "192.168.0.32/28"
# 8 비트 더한 서브넷의 세번째
cidrsubnet("192.168.0.0/24", 8, 3) = "192.168.0.3/32"
# cidrnetmask: 주어진 cidr 블록의 넷마스크
cidrnetmask("192.168.0.0/24") = "255.255.255.0"
오류 핸들링 및 예외처리 함수
# can: 주어진 표현식을 실행하고 결과 유효 여부를 불린 값으로 반환
can(tonumber("string")) = false
can(tonumber("9")) = true
# try: 하나이상의 표현식을 순차적으로 시도하고 처음 성공한 표현식의 결과를 반환
try(tonumber("string"), 0) = 0
try(tonumber("9"), 0) = 9'School > TFAS' 카테고리의 다른 글
| [TFAS] 테라폼 기능별 실무 사례 (챕터 6, 7, 8, 9) (1) | 2025.12.01 |
|---|---|
| [TFAS] 챕터 5 테라폼 모듈 & 챕터 10 모듈을 직접 만드는 이유와 만드는 방법 (1) | 2025.11.29 |
| [TFAS] 챕터 3 테라폼 작동 방식 (0) | 2025.11.24 |
| [TFAS] 챕터 2 우리는 왜 테라폼을 쓰는가? (0) | 2025.11.15 |
| [TFAS] 챕터 1 클라우드와 코드형 인프라 스트럭처 (0) | 2025.11.08 |