"테라폼으로 시작하는 IaC"책을 기준으로 2주차 정리 내용입니다.
데이터 소스는 테라폼으로 정의되지 않은 외부 리소스 또는 저장된 정보를 테라폼 내에서 참조할 때 사용한다.
데이터 소스 블록은 data로 시작되고 이후 "데이터 소스 유형"을 정의한다. Resource 블록 정의와 유사하다.
data "local_file" "abc" {
filename = "${path.module}/abc.txt"
}
데이터 소스로 읽은 대상을 참조하는 방식은 리소스와 구별되게 data가 앞에 붙는다.
아래는 데이터 소스 인수 코드와 참조 코드이다.
# Terraform Code
data "<리소스 유형>" "<이름>" {
<인수> = <값>
}
# 데이터 소스 참조
data.<리소스 유형>.<이름>.<속성>
다음 코드는 리소스와 데이터 소스의 참조 방식을 확인한다.
resource "local_file" "abc" {
content = "123!"
filename = "${path.module}/abc.txt"
}
data "local_file" "abc" {
filename = local_file.abc.filename
}
resource "local_file" "def" {
content = data.local_file.abc.content
filename = "${path.module}/def.txt"
}

def 리소스 생성은 abc를 참조(data.local_file.abc.content)하여 만들었다. 결과로 abc.txt와 def.txt가 생성되고 내용이 같다.

입력 변수는 인프라를 구성하는 데 필요한 속성 값을 정의해 코드의 변경 없이 여러 인프라를 생성하는 데 목적이 있으며, 테라폼에서는 입력 변수(Input Variables)로 정의한다.
변수는 variable로 시작되는 블록으로 구성되고 블록 뒤 이름은 동일 모듈선언에서 고유해야 한다.
variable "<이름>" {
<인수> = <값>
}
variable "image_id" {
type = string
}
아래는 예약 변수 목록이다.
변수 정의 시 사용 가능한 메타인수이다.
variable "string" {
type = string
description = "var String"
default = "myString"
}
variable "number" {
type = number
default = 123
}
variable "boolean" {
default = true
}
variable "list" {
default = [
"google",
"vmware"
]
}
variable "map" { # Sorting
default = {
aws = "amazon",
azure = "microsoft"
}
}
variable "set" { # Sorting
type = set(string)
default = [
"google",
"vmware"
]
}
variable "object" {
type = object({name=string, age=number})
default = {
name = "abc"
age = 12
}
}
variable "tuple" {
type = tuple([string, number, bool]}
default = ["abc", 123, true]
}
validation 블록에서 조건인 condition에 지정되는 규칙이 true/false를 반환하며, error_message는 false인 경우 출력되는 메시지이다. regex 함수는 대상의 문자열에 정규식을 적용하고 일치하는 문자열을 반환한다. validation은 중복으로 선언할 수도 있다.
variable "image_id" {
type = string
description = "The id of the machine image (AMI) to use for the server."
validation {
condition = length(var.image_id) > 4
error_message = "The image_id value must exceed 4."
}
validation {
# regex(...) fails if it cannot find a match
condition = can(regex("^ami-", var.image_id))
error_message = "The image_id value must starting with \"ami-\"."
}
}
variable은 코드 내에서 var.<이름>으로 참조된다.
variable "my_password" {}
resource "local_file" "abc" {
content = var.my_password
filename = "${path.module}/abc.txt"
}
var.my_password로 변수를 참조하고 있다. 값이 없어서 입력하라 나온다.
sensitive를 추가하여 변수 값을 감출 수 있습니다.
variable "my_password" {
default = "password"
sensitive = true
}
resource "local_file" "abc" {
content = var.my_password
filename = "${path.module}/abc.txt"
}

content에서 var 부분을 참조하는데, sensitive가 true로 돼 있기에 값을 볼 수 없다.
하지만 tfstate에는 평문으로 기록되므로 파일 보안이 필요하다.
variable의 목적은 코드 내용을 수정하지 않고 테라폼의 모듈적 특성을 통해 입력되는 변수로 재사용성을 높이는 데 있다. 원하는 값으로 변수에 정의할 수 있다. 기본 코드로 확인해 보자.
variable "my_var" {}
resource "local_file" "abc" {
content = var.my_var
filename = "${path.module}/abc.txt"
}

var1을 입력하니 출력도 var1이 나왔다.
variable "my_var" {
default = "var2"
}
resource "local_file" "abc" {
content = var.my_var
filename = "${path.module}/abc.txt"
}

default의 var2로 content가 replacement로 되었고 출력도 동일하다.
시스템 환경 변수의 접두사에 TFVAR 가 포함되면 그 뒤의 문자열을 변수 이름으로 인식한다.
export TF_VAR_my_var=var3
terraform apply -auto-approve

export로 TF_VAR를 붙인 var를 선언하여 다른 값을 입력하니 값이 변경되었다.
루트 모듈의 main.tf 파일과 같은 위치에 terraform.tfvars 파일을 생성해 변수에 대한 값을 추가하고 앞서 선언한 변수 선언과 비교해 우선순위를 확인하자.
echo 'my_var="var4"' > terraform.tfvars

후에 추가한 tfvars에 있는 "var4"가 출력된다.
echo 'my_var="var5_a"' > a.auto.tfvars

4 수준보다 높은 auto의 var5_a가 출력된다.
# a.auto.tfvars.json 파일 생성
cat <<EOF > a.auto.tfvars.json
{
"my_var" : "var6_a"
}
EOF

더 높은 json인 var6_a가 출력된다.
변수 값을 선언해 우선순위를 확인하며, 여러 인수가 선언되는 경우 나중에 선언된 변수의 순위가 높다.
terraform apply -auto-approve -var=my_var=var7

terraform apply -auto-approve -var=my_var=var7 -var=my_var=var8

두 번째로 선언한 변수 "var8"이 출력된다.
# var9.txt 파일 생성
echo 'my_var="var9"' > var9.txt
terraform apply -auto-approve -var=my_var=var7 -var-file="var9.txt"

마지막 변수 "var9"이 적용되어 출력되었다. 이로써 각 방식마다 우선순위가 있음을 예제를 통해서 확인하였다. 다양한 이유는 테라폼 실행 환경, 방식에 따라 입력 변수 값을 선언하도록 해 동일한 코드로 다수의 프로비저닝을 수행하도록 디자인하기 위함이다.
.tfvars 확장자로 생성된 파일에 변수를 미리 기입하면 실행 시 입력해야 하는 변수 값을 하나의 파일에서 관리할 수 있는 장점이 있다.
코드 내에서 사용자가 지정한 값 또는 속상 값을 가공해 참조 가능한 local은 외부에서 입력되지 않고, 코드 내에서만 가공되어 동작하는 값을 선언한다. 선언된 모듈 내에서만 접근 가능하고, 변수처럼 실행 시에 입력받을 수 없다.
선언 블록은 locals로 시작한다. 상수만이 아닌 리소스의 속성, 변수의 값들도 조합해 정의할 수 있다.
variable "prefix" {
default = "hello"
}
locals {
name = "terraform"
content = "${var.prefix} ${local.name}"
my_info = {
age = 20
region = "KR"
}
my_nums = [1, 2, 3, 4, 5]
}
locals {
content = "content2" # 중복 선언되었으므로 오류가 발생한다.
}
선언된 local 값은 local.<이름>으로 참조할 수 있다.
# main.tf
variable "prefix" {
default = "hello"
}
locals {
name = "terraform"
}
resource "local_file" "abc" {
content = local.content # sub.tf에서 정의된 local값
filename = "${path.module}/abc.txt"
}
# sub.tf
locals {
content = "${var.prefix} ${local.name}"
}

main의 content 내용 값으로 local.content를 참조하여 다른 구성 파일에 있지만 실행에서는 마치 하나의 구성 파일처럼 표기, 출력됨을 확인할 수 있다.
출력 값은 주로 테라폼 코드의 프로비저닝 수행 후의 결과 속성 값을 확인하는 용도로 사용된다.
모듈 내에서 생성되는 속성 값들은 output 블록에 정의된다.
output 결과에서 리소스 생성 후 결정되는 속성 값은 프로비저닝이 완료되어야 최종적으로 결과를 확인할 수 있고 terraform plan 단계에서는 적용될 값이 출력하지 않는다는 것이다.
output "instance_ip_addr" {
value = "http://${aws_instance.server.private_ip}"
}
resource "local_file" "abc" {
content = "abc123"
filename = "${path.module}/abc.txt"
}
output "file_id" {
value = local_file.abc.id
}
output "file_abspath" {
value = abspath(local_file.abc.filename)
}

이미 정해진 속성에 대해서는 출력을 예측하지만 아직 생성되지 않은 file_id 같은 값의 경우는 예측이 되지 않아 "known after apply"로 표기된다.

apply 이후에 file_id가 output에서 출력되었다.
list 형태의 값 목록이나 Key-Value 형태의 문자열 집합인 데이터가 있는 경우 동일한 내용에 대해 테라폼 구성 정의를 반복적으로 하지 않고 관리할 수 있다.
count 인수가 포함된 경우 선언된 정수 값만큼 리소스나 모듈을 생성하게 된다. 참조값은 count.index로 반복하는 경우 0부터 1씩 증가해 인덱스가 부여된다.
resource "local_file" "abc" {
count = 5
content = "abc"
filename = "${path.module}/abc.txt"
}
의도대로라면 다섯 개의 파일이 생성되어야 하지만 파일명이 다르지 않기에 결론적으로 하나만 존재하게 됩니다.
resource "local_file" "abc" {
count = 5
content = "abc${count.index}"
filename = "${path.module}/abc${count.index}.txt"
}

이전과 다르게 다수의 파일이 생성되었다. 때때로 여러 리소스나 모듈의 count로 지정되는 수량이 동일해야 하는 상황이 있다. 이 경우 count에 부여되는 정수 값을 외부 변수에 식별되도록 구성할 수 있다.
variable "names" {
type = list(string)
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
count = length(var.names)
content = "abc"
# 변수 인덱스에 직접 접근
filename = "${path.module}/abc-${var.names[count.index]}.txt"
}
resource "local_file" "def" {
count = length(var.names)
content = local_file.abc[count.index].content
# element function 활용
filename = "${path.module}/def-${element(var.names, count.index)}.txt"
}

list의 variable 길이 3을 참조하였기에 각각 abc, def에서 a/b/c가 붙은 총 6개 파일이 생성되었다.
리소스 또는 모듈 블록에서 for_each에 입력된 데이터 형태가 map 또는 set이면, 선언된 key 값 개수만큼 리소스를 생성하게 된다.
resource "local_file" "abc" {
for_each = {
a = "content a"
b = "content b"
}
content = each.value
filename = "${path.module}/${each.key}.txt"
}

key 값이 2개이므로 a, b 총 2개의 파일이 생성되었다.
생성되는 리소스의 경우 <리소스 타입>.<이름>[<"key">], 모듈의 경우 module.<모듈 이름>[<"key">]로 해당 리소스의 값을 참조한다.
variable "names" {
default = {
a = "content a"
b = "content b"
c = "content c"
}
}
resource "local_file" "abc" {
for_each = var.names
content = each.value
filename = "${path.module}/abc-${each.key}.txt"
}
resource "local_file" "def" {
for_each = local_file.abc
content = each.value.content
filename = "${path.module}/def-${each.key}.txt"
}
key 값은 count의 index와는 달리 고유하므로 중간에 값을 삭제한 후 다시 적용해도 삭제한 값에 대해서만 리소스를 삭제한다.
for문은 복합 형식 값의 형태를 변환하는 데 사용된다.
variable "names" {
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
content = jsonencode(var.names) # 결과 : ["a", "b", "c"]
filename = "${path.module}/abc.txt"

variable "names" {
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
content = jsonencode([for s in var.names : upper(s)]) # 결과 : ["A", "B", "C"]
filename = "${path.module}/abc.txt"
}

소문자가 대문자로 변경된 결과를 볼 수 있습니다.
for 구문을 사용하는 몇 가지 규칙.
- list 유형의 경우 반환 받는 값이 하나로 되어 있으면 값을, 두 개의 경우 앞의 인수가 인덱스를 반환하고 뒤의 인수가 값을 반환
- 관용적으로 인덱스는 i, 값은 v로 표현
- map 유형의 경우 반환 받는 값이 하나로 되어 있으면 키를, 두 개의 경우 앞의 인수가 키를 반환하고 뒤의 인수가 값을 반환
- 관용적으로 키는 k, 값은 v로 표현
- 결과 값은 for 문을 묶는 기호가 [ ]인 경우 tuple로 반환되고 { }인 경우 object 형태로 반환
- object 형태의 경우 키와 값에 대한 쌍은 ⇒ 기호로 구분
- { } 형식을 사용해 object 형태로 결과를 반환하는 경우 키 값은 고유해야 하므로 값 뒤에 그룹화 모드 심볼(…)를 붙여서 키의 중복을 방지(SQL의 group by 문 또는 Java의 MultiValueMap과 같은 개념)
- if 구문을 추가해 조건 부여 가능
variable "names" {
type = list(string)
default = ["a", "b"]
}
output "A_upper_value" {
value = [for v in var.names : upper(v)]
}
output "B_index_and_value" {
value = [for i, v in var.names : "${i} is ${v}"]
}
output "C_make_object" {
value = { for v in var.names : v => upper(v) }
}
output "D_with_filter" {
value = [for v in var.names : upper(v) if v != "a"]
}

variable "members" {
type = map(object({
role = string
}))
default = {
ab = { role = "member", group = "dev" }
cd = { role = "admin", group = "dev" }
ef = { role = "member", group = "ops" }
}
}
output "A_to_tupple" {
value = [for k, v in var.members : "${k} is ${v.role}"]
}
output "B_get_only_role" {
value = {
for name, user in var.members : name => user.role
if user.role == "admin"
}
}
output "C_group" {
value = {
for name, user in var.members : user.role => name...
}
}

count 나 for_each 구문을 사용한 리소스 전체를 여러 개 생성하는 것 이외도 리소스 내에 선언되는 구성 블록을 다중으로 작성해야 하는 경우가 있다.
resource "aws_security_group" "example" {
name = "example-security-group"
description = "Example security group"
vpc_id. = aws_vpc.main.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
ipv6_cidr_blocks = ["::/0"]
}
}
리소스 내의 블록 속성(Attributes as Blocks)은 리소스 자체의 반복 선언이 아닌 내부 속성 요소 중 블록으로 표현되는 부분에 대해서만 반복 구문을 사용해야 하므로, 이때 dynamic 블록을 사용해 동적인 블록을 생성 할 수 있다.
data "archive_file" "dotfiles" {
type = "zip"
output_path = "${path.module}/dotfiles.zip"
source {
content = "hello a"
filename = "${path.module}/a.txt"
}
source {
content = "hello b"
filename = "${path.module}/b.txt"
}
source {
content = "hello c"
filename = "${path.module}/c.txt"
}
}

variable "names" {
default = {
a = "hello a"
b = "hello b"
c = "hello c"
}
}
data "archive_file" "dotfiles" {
type = "zip"
output_path = "${path.module}/dotfiles.zip"
dynamic "source" {
for_each = var.names
content {
content = source.value
filename = "${path.module}/${source.key}.txt"
}
}
}

동일한 파일들을 볼 수 있습니다.