2024. 6. 30. 08:44ㆍ카테고리 없음

- CloudNet에서 주관하는 Terraform 스터디 내용입니다
- 내용은 위 책 테라폼으로 시작하는 IaC 를 기준으로 정리하였습니다.
- 실습은 M1 macbook air 에서 진행했습니다.
- 매번 좋은 스터디를 진행해 주시는 CloudNet 팀 감사드립니다
- 잘못된 점, 업데이트된 지식, 다른 코멘트 언제나 환영입니다!
목차
반복문(1): for_each
반복문(2): for_expression
반복문(3): dynamic expression
조건문
프로비저너
null_resource & terraform_data
반복문(1): for_each
반복시 접근하는 각각 값에 대해 object로 접근한다. 각 object는 key, value의 속성으로 구성된다.
key-value 에서 떠오르듯 map이나 set type 만으로 접근할 수 있다.
만약 다른 타입인데 for_each 문법을 사용하고 싶다면? tomap() 이나 toset() 함수를 이용해서 변환 가능하다.
잠깐 map, set, list 를 짚고 넘어가자.
list: 인덱스 기반 집합
set: 값 기반 집합
map: 값=속성 기반 집합
# list
variable "list_sample" {
type = list(string)
default = ['Anxiety', 'Joy', 'Sadness', 'Anger']
}
# set
variable "set_sample" {
type = set(string)
default = ['Anger', 'Anxiety', 'Joy', 'Sadness']
}
# map
variable "map_sample" {
type = map(string)
default = {
'Anger': 'Red',
'Anxiety': 'Orange',
'Joy': 'Yellow',
'Sadness': 'Blue'
}
}
- list & set: array 형태임이 유사하지만 list는 index 를 참조, Map은 Key로 참조
- map & set: 선언한 값이 정렬된다
Map과 set은 Key 로 순회하므로 중간에 리소스 추가/삭제로 인덱스에 변화가 생겨도 안전하게 실행 가능함이 count와 비교했을 때 장점이다. 가능한 for_each 를 쓰는 게 좋을 것 같다.
아래 예시는 username 이 들어 있는 list 를 Set으로 변환한 뒤 for_each 로 순회하는 코드이다. 그러나 list 를 set으로 변환하였기 때문에 key값만 있고 value값은 없다는 점에 주의해야 한다.
resource "aws_iam_user" "the-accounts" {
for_each = toset(["Todd", "James", "Alice", "Dottie"])
name = each.key
}
for_each를 사용해서 리소스를 생성하는 예제를 실행해보자.
아래 main.tf 를 저장하고
resource "local_file" "abc" {
for_each = {
a = "content a"
b = "content b"
}
content = each.value
filename = "${path.module}/${each.key}.txt"
}
terraform을 실행하면
terraform init && terraform plan && terraform apply -auto-approve

이렇게 map의 Key를 제목으로, value를 내용으로 하는 .txt 파일이 모두 생성되었다.
아래 다양한 예시들이 있으니 필요할 때 참고하면 좋을 것 같다
variable "string_a" {
default = "myString"
}
variable "string_b" {
type = string
default = "myString"
}
# variable "string_c" {
# type = string
# default = myString
# }
variable "number_a" {
default = 123
}
variable "number_b" {
type = number
default = 123
}
variable "number_c" {
default = "123"
}
variable "boolean" {
default = true
}
# (Array) list , set , tuple - value , [ ] 사용
variable "list_set_tuple_a" {
default = ["aaa", "bbb", "ccc"]
}
variable "list_set_tuple_b" {
type = list(string)
default = ["bbb", "ccc", "aaa"]
}
variable "list_set_tuple_c" {
type = set(string)
default = ["bbb", "ccc", "aaa"]
}
variable "list_set_tuple_d" {
default = ["aaa", 1, false]
}
variable "list_set_tuple_e" {
type = tuple([string, number, bool])
default = ["aaa", 1, false]
}
# (Object) map , object - key : value , { } 사용
variable "map_object_a" {
default = {"a" : "aaa", "b" : "bbb" , "c" : "ccc"}
}
variable "map_object_b" {
type = map(string)
default = {"b" : "bbb" , "c" : "ccc", "a" : "aaa"}
}
variable "map_object_c" {
default = {"name" : "gasida", "age" : 27 }
}
variable "map_object_d" {
type = object({ name = string, age = number })
default = {"name" : "gasida", "age" : 27 }
}
반복문(2): for_expressions
for_expression 는 복합 형식을 변환하는 데 사용한다 (for_each: 반복 조회). 기존에 코드에서 쓰던 for문과 유사하다.
파이썬 유저의 관점에서는 list comprehension과 비슷하게 느껴졌다.
위의 for_each 에서와 유사하게 a,b,c를 이용해서 for_expression 을 실습해 보자.
우선 jsonencode 라는 함수를 사용해서 반복 결과를 json 으로 변환해 저장할 것이다.
variable "names" {
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
content = jsonencode(var.names) # 결과 : ["a", "b", "c"]
filename = "${path.module}/abc.txt"
}
output "file_content" {
value = local_file.abc.content
}
terraform apply 로 반영하면 ["a", "b", "c"] 가 담긴 리스트가 json으로 변환되어 아래와 같이 저장된다.

그러면 이번에는 같은 list 의 값을 for expression으로 일괄적으로 대문자로 바꿔보자.
upper() 함수를 사용한다.
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"
}
output "file_content" {
value = local_file.abc.content
}
아래와 같이 모두 대문자로 바뀐 것을 볼 수 있다.

이번에는 key-value 값이 같이 묶이는 map type 을 for expression 으로 변환해보자. 예시로 아래 variable 을 사용할 것이다.
variable "members" {
type = map(object({
role = string
}))
default = {
ab = { role = "member", group = "dev" }
cd = { role = "admin", group = "dev" }
ef = { role = "member", group = "ops" }
}
}
우선 가장 기본적으로 map 을 순회한 결과를 tuple(list) 형태로 반환하는 예시를 보자.
value 를 [ ] 로 감쌌기 때문에 tuple 형태로 반환된다.
output "A_to_tupple" {
value = [for k, v in var.members : "${k} is ${v.role}"]
}
for loop으로 항목을 순회하며 key와 role 을 동시에 사용한 tuple 을 구성했다.

이번에는 map -> map 형태로 변환해보자.
map 형태로 묶을 때는 => 을 사용한다.
아래 코드는 user의 role이 admin 일 때 name과 role을 묶어 새로운 map 을 반환한다.
if 를 맨 위가 아닌 맨 아래에 쓰는 점이 특이하다.
output "B_get_only_role" {
value = {
for name, user in var.members : name => user.role
if user.role == "admin"
}
}
위에서 'cd' 만 admin 이므로 cd 의 role 이 출력됨을 확인한다.

set, map, list 의 비교를 악분 님의 실습으로 다시 한번 연습해보자.
사용할 variable 들이다. main.tf 에 저장한다.
variable "fruits_set" {
type = set(string)
default = ["apple", "banana"]
description = "fruit example"
}
variable "fruits_list" {
type = list(string)
default = ["apple", "banana"]
description = "fruit example"
}
variable "fruits_map" {
type = map(string)
default = {"first": "apple", "second": "banana"}
description = "fruit example"
}
Terraform console 을 실행하고 main.tf 에 저장한 variable 들을 찍어보면 사용 가능하다.
var.fruits_set
var.fruits_list
var.fruits_map
type(var.fruits_set)
type(var.fruits_list)
type(var.fruits_map)

set 을 아무것도 감싸지 않고 for loop 으로 순회하면 에러가 발생한다.
[ ] 로 감싸서 for 로 순회하면 tuple(list) 형태로 잘 반환된다.
for item in var.fruits_set: item
[for item in var.fruits_set: item]
type([for item in var.fruits_set: item])

set 을 map 형태로 바꿔보자. 이때 key 없이는 for loop 이 돌지 않으니 주의한다.
{for item in var.fruits_set: item}
{for key,value in var.fruits_set: key => value}
type({for key,value in var.fruits_set: key => value})

이번에는 list 를 순회해보자
마찬가지로 [ ] 로 감싸지지 않은 tuple & key 없이 도는 map 에서는 오류가 발생한다.
[for item in var.fruits_list: item]
type([for item in var.fruits_list: item])
{for key,value in var.fruits_list: key => value}
{for i, v in var.fruits_list: i => v}
type({for i, v in var.fruits_list: i => v})

마지막으로 map 도 순회해본다.
주의점은 위 set, list 와 같다
[for item in var.fruits_map: item]
type([for item in var.fruits_map: item])
{for key,value in var.fruits_map: key => value}
{for k, v in var.fruits_map: k => v}
type({for k, v in var.fruits_map: k => v})

반복문(3): dynamic
dynamic 은 리소스 내부 속성 블록을 동적으로 생성한다. 종속성 있는 다중 for문이랑 비슷한 개념 같다.
aws 에서의 대표적인 사용사례는 security group 리소스에서 Ingress/Egress 를 선언해놓고 security group 별로 ingress/egress 를 계속 가져다 사용할 때이다.
일반적으로 반복한다면 아래와 같이 써야겠지만
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"
}
}
dynamic block 을 이용하면 훨씬 가독성 좋게 사용 가능하다.
main.tf 파일 수정 : 리소스 내에 반복 선언 구성을 dynamic 블록으로 재구성
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"
}
}
}
조건문
조건문은 {조건 정의} ? {옳은 경우} : {틀린 경우} 의 삼항 연산자 형태로 사용된다.
삼항연산자를 처음 보는 분을 위해 예시를 들면 아래와 같다.
{알람울림}?{일어나야됨}:{더자도됨}
개인적으로 옳은경우-틀린경우 순서가 헷갈렸는데, 위 예시로 외우고는 안 헷갈린다...
테라폼에서 조건식의 응용 예시를 들어보았다.
- 특정 속성 정의
- 로컬 변수 재정의
- 출력 값 조건 정의
- 리소스 생성 여부
테라폼에서 조건식 사용시 조건끼리는 형태를 맞춰줄 것을 권장하고 있다.
var.example ? 12 : "hello" # 비권장
var.example ? tostring(12) : "hello" # 권장
실습을 위해 조건식을 포함한 테라폼 파일을 작성한다. enable_file 이 False이면 "", True 이면 "foo" 를 출력한다.
variable "enable_file" {
default = true
}
resource "local_file" "foo" {
count = var.enable_file ? 1 : 0
content = "foo!"
filename = "${path.module}/foo.bar"
}
output "content" {
value = var.enable_file ? local_file.foo[0].content : ""
}
terraform console에서 변수를 내보내고 (코드보다 우선순위가 더 높다) 실행하면 false 이므로 아무것도 메시지가 나오지 않는다.
export TF_VAR_enable_file=false
terraform apply -auto-approve

내보낸 변수를 삭제하면 default 대로 True 를 반환하므로 "foo!" 가 출력된다.
unset TF_VAR_enable_file
terraform apply -auto-approve

프로비저너
프로바이더는 command와 파일 복사 등을 실행할 수 없기 때문에, 이런 나머지 역할들을 프로비저너가 수행한다.
The Last Resort: 꼭 필요한 상황에서만 사용하세요
사용 자제를 요청하는 이유는, 프로비저너의 실행결과는 .tfstate 에 저장되지 않기 때문에 선언적 보장이 불가하기 때문이다.
프로비저너를 사용하는 상황의 예시로는 EC2 생성 이후 패키지 설치, 파일생성 등이 있다.
프로비저너 사용을 실습하기 위해 local에서 command를 실행하고 파일을 저장/삭제 하는 실습을 해보자.
variable "sensitive_content" {
default = "secret"
#sensitive = true
}
resource "local_file" "foo" {
content = upper(var.sensitive_content)
filename = "${path.module}/foo.bar"
#### 프로비저너 사용 ####
provisioner "local-exec" {
command = "echo The content is ${self.content}"
}
provisioner "local-exec" {
command = "abc"
on_failure = continue
}
provisioner "local-exec" {
when = destroy
command = "echo The deleting filename is ${self.filename}"
}
#### 프로비저너 사용 ####
}
위 코드를 apply 하면 foo.bar 라는 파일이 생성된다. 그리고 "abc" 라는 command가 없기 때문에 실행이 안 되고 넘어간다

만약 command="abc" 가 있는 프로비저너에 on_failure = continue 설정이 없다면 거기서 종료되고 뒤로 넘어가지 않는다
sensitive=True 의 주석을 풀어서 활성화시키면 해당 내용은 콘솔에 출력되지 않는다.

local-exec 프로비저너를 간단히 설명하면, 테라폼이 실행되는 환경에서 수행할 커맨드를 정의하는 것이다.
command 의 사용은 필수이고 working_dir (경로설정), interpreter, environment(환경변수) 는 필요에 따라 선택적으로 사용한다.
간단하게 명령어로 /tmp 경로의 file.txt 에 Hello!! World!! 를 쓰는 예제를 실행해보았다.
resource "null_resource" "example1" {
provisioner "local-exec" {
command = <<EOF
echo Hello!! > file.txt
echo $ENV >> file.txt
EOF
interpreter = [ "bash" , "-c" ]
working_dir = "/tmp"
environment = {
ENV = "world!!"
}
}
}

provisioner 을 이용해서 원격지에 연결도 가능하다. 리소스 내 connection block 선언시 해당 리소스 안에 모두 적용되지만 별도로 프로비저너 안에 connection 이 선언되어 있으면 그게 더 우선순위가 높게 적용된다.
resource "null_resource" "example1" {
connection {
type = "ssh"
user = "root"
password = var.root_password
host = var.host
}
provisioner "file" { #기본 connection 사용
source = "conf/myapp.conf"
destination = "/etc/myapp.conf"
}
provisioner "file" { #별도 connection 사용
source = "conf/myapp.conf"
destination = "C:/App/myapp.conf"
connection {
type = "winrm"
user = "Administrator"
password = var.admin_password
host = var.host
}
}
}
Remote-exec provisioner 는 원격지에서 커맨드를 실행하는 것이다.
아래 예시는 web의 aws_instance 에 접속해서 script.sh를 실행하는 예시이다.
resource "aws_instance" "web" {
# ...
# Establishes connection to be used by all
# generic remote provisioners (i.e. file/remote-exec)
connection {
type = "ssh"
user = "root"
password = var.root_password
host = self.public_ip
}
provisioner "file" {
source = "script.sh"
destination = "/tmp/script.sh"
}
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/script.sh",
"/tmp/script.sh args",
]
}
}
null_resource와 terraform_data
null_resource 란 말 그대로 아무 작업도 수행햐지 않는 리소스라고 한다. 처음에는 무슨말인지 이해가 잘 안되었는데... 테라폼 구성정의에서 상호 참조가 발생하는 상황에서 유용하게 쓰인다고 한다.
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_security_group" "instance" {
name = "t101sg"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "example" {
ami = "ami-0c9c942bd7bf113a2"
instance_type = "t2.micro"
subnet_id = "subnet-dbc571b0"
private_ip = "172.31.0.100"
key_name = "my-keypair" # 각자 자신의 EC2 SSH Keypair 이름 지정
vpc_security_group_ids = [aws_security_group.instance.id]
user_data = <<-EOF
#!/bin/bash
echo "Hello, T101 Study" > index.html
nohup busybox httpd -f -p 80 &
EOF
tags = {
Name = "Single-WebSrv"
}
}
resource "aws_eip" "myeip" {
#vpc = true
instance = aws_instance.example.id
associate_with_private_ip = "172.31.0.100"
}
#### NULL RESOURCE ####
resource "null_resource" "echomyeip" {
provisioner "remote-exec" {
connection {
host = aws_eip.myeip.public_ip
type = "ssh"
user = "ubuntu"
private_key = file("/lets/go_to/my.pem") # 각자 자신의 EC2 SSH Keypair 파일 위치 지정
#password = "qwe123"
}
inline = [
"echo ${aws_eip.myeip.public_ip}"
]
}
}
########################
output "public_ip" {
value = aws_instance.example.public_ip
description = "The public IP of the Instance"
}
output "eip" {
value = aws_eip.myeip.public_ip
description = "The EIP of the Instance"
}
이 null resource 부분을 주석처리하고 실행하면 아래와 같이 subnet 이 존재하지 않는 시점에 참조되기 때문에 에러가 발생한다. 따라서 null resource 를 사용하여 Temporary data 를 홀드했다 사용할 수 있다.

null resource 를 보다 간단하게 사용하기 위해 수명주기 관리자와 프로바이더 구성을 통합한 terraform_data 를 1.4 버전부터 지원한다.
아래와 같은 예시로 사용할 수 있다.
resource "terraform_data" "foo" {
triggers_replace = [
aws_instance.foo.id,
aws_instance.bar.id
]
input = "world"
}
output "terraform_data_output" {
value = terraform_data.foo.output # 출력 결과는 "world"
}
잊지 말아요!! 자원 삭제!!
테라폼을 이용하면 휴먼에러를 많이 줄일 수 있을 것 같아서 기대된다.
항상 좋은 스터디 이끌어주셔서 감사합니다!