for_each 반복문
지난 글에 이어 terraform 반복문에 대해 정리하겠다.
count로 반복했던 지난 글에서 중간 리소스를 삭제할 경우 에러가 발생했고,
그에 대한 대응 방안으로 for_each가 있다고 작성했다.
for_each : 반복문, 선언된 key 값 개수만큼 리소스를 생성한다.
- for_each는 반복할 때 타입 값에 대해 하나하나 each object로 접근한다는 의미이다.
- each object는 key, value 2개 속성을 가지고 있다.
- each.key : 선언된 for_each 항목의 key를 나타낸다.
- each.value : 선언된 for_each 항목의 value를 나타낸다.
- for_each는 모든 타입 object에 접근 할 수 있고, map, set 타입만 허용한다.
- map,set 타입이 아닐 경우 map, set으로 타입변환(toset 등)을 해야한다.
resource "<PROVIDER>_<TYPE>" "<NAME>" {
for_each = <COLLECTION>
...
}
#################################################
resource "local_file" "name" {
for_each = {
a = "test"
b = "abc"
}
filename = "${path.module}/${each.key}.txt"
content = "${each.value}"
}
- for_each를 사용하여 map, set을 이용하여 리소스 생성을 실습해보자
2개의 인스턴스를 생성하되 각 ami가 다른 인스턴스를 생성한다.
덤으로 key pair도 함께 생성해주었다.
뒤에서 작성할 for expression을 통해 인스턴스의 public ip도 출력해보았다.
개인 pem 키를 output으로 뽑아보려 했으나, sensitive true 설정을 안해주면 계속 오류를 뱉어내었다.
(뽑아내는 방법은 찾아봐야겠다.)
provider "aws" {
region = "ap-northeast-2"
}
resource "tls_private_key" "pub_key" {
algorithm = "RSA"
rsa_bits = 2048
}
resource "aws_key_pair" "ssungz-key" {
key_name = "ssungz-key"
public_key = tls_private_key.pub_key.public_key_openssh
}
# map을 이용하여 테스트
resource "aws_instance" "map_test" {
for_each = tomap({
amzn = {
instance_type = "t2.micro"
ami = "ami-0ebb3f23647161078"
}
ubuntu = {
instance_type = "t2.micro"
ami = "ami-0bcdae8006538619a"
}
})
instance_type = each.value.instance_type
ami = each.value.ami
key_name = aws_key_pair.ssungz-key.key_name
tags = {
Name = each.key
}
}
output "instance_ip" {
value = { for instance, ip in aws_instance.map_test : instance => ip.public_ip }
}
output "key_pair_name" {
value = aws_key_pair.ssungz-key.key_name
}
output "private_key" {
value = tls_private_key.pub_key.private_key_pem
sensitive = true
}
output "public_key" {
value = tls_private_key.pub_key.public_key_openssh
}
resource "aws_ami_user" "accounts" {
#set을 이용한 테스트
for_each = toset(["hoon", "ssungz", "won", "hye"])
name = each.key
}
key 값은 count의 index와는 달리 고유하기 때문에 중간 값을 삭제해도, count 처럼 번호가 당겨져 오류가 발생하지 않고,
지정된 리소스만 삭제한다.
중간 계정 ssungz를 삭제하고 다시 배포해보자.
terraform plan 했을 때 지정된 계정만 삭제되는 것이 보인다.
(count에서 동일한 동작을 했을 때 index 번호가 당겨져 다른 계정의 replace가 이뤄지는 것과는 다른 모습)
조금 더 간편하게 테스트해보자.
파일 2개를 만들고 각각의 파일에 내용을 입력하는 예제이다.
resource "local_file" "test" {
for_each = {
a = "content a"
b = "content b"
}
content = each.value
filename = "${path.module}/${each.key}.txt"
}
for_each에 정의된 이름과 content로 잘 생성되었다.
for_each로 생성되는 리소스의 경우 <리소스 타입>.<이름>[<key>], 모듈의 경우 module.<모듈 이름>[<key>]로 해당 리소스의 값을 참조한다.
서로 참조하는 리소스 생성 예제이다.
변수로 abc 리소스 블럭의 자원을 생성하고, 생성된 abc 블럭의 리소스를 참조하여 def 블럭의 리소스를 생성한다.
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"
}
Data 유형
- 기본 유형
- string : 글자 유형
- number : 숫자 유형
- bool : true 또는 false
- any : 명시적으로 모든 유형이 허용됨을 표시
- 집합 유형
- list(<유형>) : 인덱스 기반 집합
- map(<유형>) : 값 = 속성 기반 집합이며, 키값 기준 정렬
- set(<유형>) : 값 기반 집합이며 정렬 키값 기준 정렬
- object({<인수 이름>=<유형>, ...})
- tuple([<유형>, ...])
- list와 set은 선언하는 형태가 비슷하지만 참조 방식이 인덱스와 키로 각각 차이가 있고, map과 set의 경우 선언 값이 정리되는 특징이 있다.
데이터 형식 예제 코드
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 }
}
각 데이터 형식에 따라 terraform console에서 var.{variable name}, type(var.variable} 명령어를 통해 값을 조회할 수 있다.
list_set_tuple_b와 list_set_tuple_c 는 동일한 순서의 변수 값이 선언되어 있으나, 출력 시 순서가 다른 것을 확인할 수 있다.
위에 언급한 것과 같이 List 와 set의 차이 때문으로 set은 선언 값을 정렬해주는 특성이 있기 때문에 출력 시 정렬되어 출력된 것으로 이해할 수 있다.
For Expressions
for expression은 복합 형식의 형태를 변환하는 데 사용 한다.
- 예를 들어 list 값의 포맷을 변경하거나, 특정 접두사 prefix를 추가할 수 있고, output에 원하는 형태로 반복적인 결과를 표현할 수 있다.
- list 타입은 값 또는 인덱스와 값을 반환
- map 타입의 경우 키 또는 키와 값을 반환
- set 타입의 경우 키 값에 대해 반환
for expression 실습
- jsonencode로 타입을 변환하고, 요소를 대문자로 변환하기
variable "names" {
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
content = jsonencode(var.names)
filename = "${path.module}/abc.txt"
}
output "file_content" {
value = local_file.abc.content
}
jsonencode는 JSON 구문을 사용하여 주어진 값을 문자열로 인코딩하는 내장 함수이다. Docs
jsonencode - Functions - Configuration Language | Terraform | HashiCorp Developer
The jsonencode function encodes a given value as a JSON string.
developer.hashicorp.com
코드 수정
- for expression 적용
variable "names" {
default = ["a", "b", "c"]
}
resource "local_file" "abc" {
content = jsonencode([for s in var.names : upper(s)])
filename = "${path.module}/abc.txt"
}
output "file_content" {
value = local_file.abc.content
}
- for 구문의 사용하는 몇 가지 규칙이 있다.
- list 유형의 경우 반환 받는 값이 하나로 되어 있으면 값을, 두 개의 경우 앞의 인수가 인덱스를 반환하고 뒤의 인수가 값을 반환
- 관용적으로 인덱스틑 i(ndex), 값은 v(alue)로 표현
- map 유형의 경우 반환 받는 값이 하나로 되어 있으면 키를, 두 개의 경우 앞의 인수가 키를 반환하고 뒤의 인수가 값을 반환
- 관용적으로 인덱스틑 k(ey), 값은 v(alue)로 표현
- 결과 값은 for 문을 묶는 기호가 []인 경우 tuple로 반환하고, {}로 묶여있는 경우 object 형태로 반환
- object 형태의 경우 키와 값에 대한 쌍은 => 기호로 구분
- {} 형식을 사용해 object 형태로 결과를 반환하는 경우 키 값은 고유해야 하므로 값 뒤에 그룹화 모드 심볼(...)를 붙여서 키의 중복을 방지
- if 구문을 추가해 조건 부여 가능
- list 유형에 대한 for 구문 예제
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"]
}
- 출력 및 확인 결과
3.9 ➤ terraform console
# 기본 변수 확인
> var.names
tolist([
"a",
"b",
])
> type(var.names)
list(string)
# tuple 리턴 결과 및 타입 확인
> [for v in var.names : upper(v)]
[
"A",
"B",
]
> type([for v in var.names : upper(v)])
tuple([
string,
string,
])
# tuple 리턴 결과 시 index / value 결과 및 출력 확인
> [for i, v in var.names : "${i} is ${v}"]
[
"0 is a",
"1 is b",
]
> type([for i,v in var.names : "${i} is ${v}"])
tuple([
string,
string,
])
# object 리턴 결과 및 타입 확인
> { for v in var.names : v => upper(v)}
{
"a" = "A"
"b" = "B"
}
> type({ for v in var.names : v => upper(v)})
object({
a: string,
b: string,
})
# if 조건문을 통해 제외 조건부여
> [for v in var.names : upper(v) if v != "a"]
[
"B",
]
> type([for v in var.names : upper(v) if v != "a"])
tuple([
string,
])
실습 2)
map 유형에 대한 for 구문 처리의 예제
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...
}
}
확인 코드
#
terraform console
-----------------
var.fruits_set
var.fruits_list
var.fruits_map
type(var.fruits_set)
type(var.fruits_list)
type(var.fruits_map)
#
for item in var.fruits_set: item
[for item in var.fruits_set: item]
type([for item in var.fruits_set: item])
{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})
#
for item in var.fruits_list: item
[for item in var.fruits_list: item]
type([for item in var.fruits_list: item])
{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})
#
for item in var.fruits_map: item
[for item in var.fruits_map: item]
type([for item in var.fruits_map: item])
{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})
exit
-----------------
for expression S3 이름 일괄 변경 실습
시나리오
ssungz-bucket-01, ssungz-bucket-02 두개 버킷이 있는데 이 이름의 뒤에 -test를 추가하는 요청이 있다.
요청에 따라 버킷 이름을 일괄로 수정하자.
provider "aws" {
region = "ap-northeast-2"
}
variable "s3_name" {
type = set(string)
default = ["ssungz-bucket-01", "ssungz-bucket-02"]
description = "aws s3 bucket names"
}
variable "postfix" {
type = string
default = "test"
description = "postfix"
}
resource "aws_s3_bucket" "mys3bucket" {
for_each = toset([for bucket in var.s3_name : format("%s-%s", bucket, var.postfix)])
bucket = "${each.key}"
}
변경된 이름으로 잘 배포되었다.
추가로 해볼 것 (update 예정)
terraform plan을 해보면 update가 아니고 destroy and create 이다. s3는 버킷 이름 변경을 제공하지 않고 있으므로
이 코드 수행 시 버킷 내부의 오브젝트가 모두 삭제된다.
그렇다면 어떻게 해야할까, [신규 버킷 생성 > 기존 버킷 오브젝트를 신규 버킷으로 복제 > 기존 버킷 삭제] 의 절차를 수해하면 버킷 URL은 달라지지만 오브젝트는 유지될 것이라고 생각했는데.....
버킷 정책 쪽에서 신규 버킷에 정책을 할당해주지 못해서 object 이전이 안되는 문제가 생겼고,
기존 버킷의 오브젝트도 생각처럼 삭제되지 않아서 이러저러한 오류가 발생하고 있었다.
provider "aws" {
region = "ap-northeast-2"
}
variable "s3_name" {
type = set(string)
default = ["ssungz-bucket-01", "ssungz-bucket-02"]
description = "aws s3 bucket names"
}
variable "postfix" {
type = string
default = "test"
description = "postfix"
}
locals {
new_buckets = [for bucket in var.s3_name : format("%s-%s", bucket, var.postfix)]
}
resource "aws_s3_bucket" "new_bucket" {
for_each = toset(local.new_buckets)
bucket = each.key
}
data "aws_s3_bucket_policy" "old_bucket" {
for_each = toset(var.s3_name)
bucket = each.key
}
resource "aws_s3_bucket_policy" "new_bucket" {
for_each = toset(local.new_buckets)
bucket = each.key
policy = data.aws_s3_bucket_policy.old_bucket[replace(each.key, "-${var.postfix}", "")].policy
}
resource "null_resource" "copy_objects" {
for_each = toset(var.s3_name)
provisioner "local-exec" {
command = <<EOT
aws s3 sync s3://${each.key} s3://${each.key}-${var.postfix}
EOT
}
}
resource "null_resource" "delete_old_buckets" {
for_each = toset(var.s3_name)
depends_on = [ null_resource.copy_objects ]
provisioner "local-exec" {
command = <<EOT
aws s3 rb s3://${each.key} --force
EOT
}
}
![](https://blog.kakaocdn.net/dn/cxjESs/btsIhtZJwxn/nx1RzQdOKcxbTw5o4so4D0/img.png)
Dynamic
리소스 내부 속성 블록을 동적인 블록으로 생성
- count나 for_each 구문을 사용한 리소스 전체를 여러 개 생성하는 것 이외도 리소스 내에 선언되는 구성 블록을 다중으로 작성해야 하는 경우가 있다.
- 예를 들어 ASG 리소스 구성에 ingress, egress 요소가 리소스 선언 내부에서 블록 형태로 여러 번 정의되는 경우이다.
resource "aws_security_group" "test-sg" {
name = "example-sg"
description = "test sg"
vpc_id = aws_vpc.main.id
ingress = {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress = {
from_port = 0
to_port = 0
protocol = -1
cidr_blocks = ["0.0.0.0/0"]
}
}
- 리소스 내의 블록 속성(Attributes as Blocks)은 리소스 자체의 반복 선언이 아닌 내부 속성 요소 중 블록으로 표현되는 부분만 반벅 구문을 사용해야 하므로, 이때 dynamic 블록을 사용해 동적인 블록을 생성 할 수 있다.
- dynamic 블록을 작성하려면, 기존 블록의 속성 이름을 dynamic 블록의 이름으로 선언하고 기존 블록 속성에 정의되는 내용을 content 블록에 작성한다.
- 반복 선언에 사용되는 반복문 구문은 for_each를 사용한다. 기존 for_each 적용 시 each 속성에 key,value 가 적용되었다면 dynamic 에서는 dynamic에 지정한 이름에 대해 속성이 부여된다.
일반적인 블록 속성 반복 적용 시 | dynamic 블록 적용 시 |
resource "provider_resource" "name" { name = "some_resource" some_setting { key = a_value } some_setting { key = b_value } some_setting { key = c_value } some_setting { key = d_value } |
resource "provider_resource" "name" { name = "some_resource" dynamic "some_setting" { for_each = { a_key = a_value b_key = b_value c_key = c_value d_key = d_value } content { key = some_setting.value } } } |
archive 프로바이더의 archive_file에 source 블록 선언을 반복
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"
}
}
txt 파일은 원래는 보이지 않고, zip 파일을 압축 해제했을 때 확인할 수 있다.
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"
}
}
}
배포 시 동일한 결과로 변화는 발생하지 않았다.
'Cloud > Terraform' 카테고리의 다른 글
[T101] Terraform 101 Study 실습(1) - ec2 배포 (0) | 2024.07.01 |
---|---|
[T101] Terraform 101 Study 3주차 (2) (0) | 2024.06.30 |
[T101] Terraform 101 Study 2주차 (3) (0) | 2024.06.22 |
[T101] Terraform 101 Study 2주차 (2) (0) | 2024.06.21 |
[T101] Terraform 101 Study 2주차 (1) (0) | 2024.06.18 |