Programming/IaC

[IaC] Terraform tips & tricks — Loops & Conditions

Hayley Shim 2023. 10. 28. 17:50

안녕하세요. CloudNet@ Terraform Study를 진행하며 해당 내용을 이해하고 공유하기 위해 작성한 글입니다. 도서 ‘Terraform: Up & Running(By Yevgeniy Brikman)’ 의 내용 및 스터디 시간 동안 언급된 주요 내용 위주로 간단히 정리했습니다.

[Docs] Built-in Fuctions : 참고 & [Docs] Expression : 참고

반복문(Loops)

  • count 매개변수 parameter : 리소스와 모듈의 반복
  • for_each 표현식 expressions : 리소스 내에서 리소스 및 인라인 블록, 모듈을 반복
  • for 표현식 expressions : 리스트 lists 와 맵 maps 을 반복
  • for 문자열 지시어 string directive : 문자열 내에서 리스트 lists 와 맵 maps 을 반복

실습

1. count 매개변수

  • count.index 를 사용하여 반복문 안에 있는 각각의 반복 iteration 을 가리키는 인덱스를 얻을 수 있습니다.
NICKNAME=<각자 닉네임>
NICKNAME=hayley

cat <<EOT > iam.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_iam_user" "myiam" {
  count = 3
  name  = "$NICKNAME.\${count.index}"
}
EOT
$ terraform init && terraform plan 
$ terraform apply -auto-approve

$ terraform state list
aws_iam_user.myiam[0]
aws_iam_user.myiam[1]
aws_iam_user.myiam[2]

$ terraform destroy -auto-approve
  • 입력 변수를 통해 IAM 사용자를 생성합니다.
#variables.tf

variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["user1", "user2", "user3"]
}
cat <<EOT > iam.tf
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_iam_user" "myiam" {
  count = length(var.user_names)
  name  = var.user_names[count.index]
}
EOT
$ terraform init && terraform plan 
cat <<EOT > outputs.tf
output "first_arn" {
  value       = aws_iam_user.myiam[0].arn
  description = "The ARN for the first user"
}

output "all_arns" {
  value       = aws_iam_user.myiam[*].arn
  description = "The ARNs for all users"
}
EOT
$ terraform apply -auto-approve

$ terraform destroy -auto-approve

$ terraform state list

$ terraform output
  • count 사용 시 제약 사항

— 아래 ASG 리소스에 태그 설정 방법에서 생각해보면,

resource "aws_autoscaling_group" "example" {
  launch_configuration = aws_launch_configuration.example.name
  vpc_zone_identifier  = data.aws_subnets.default.ids
  target_group_arns    = [aws_lb_target_group.asg.arn]
  health_check_type    = "ELB"

  min_size = var.min_size
  max_size = var.max_size

  tag {
    key                 = "Name"
    value               = var.cluster_name
    propagate_at_launch = true
  }
}

— 각각의 tag 를 사용하려면 key, value, propagate_at_launch 에 대한 값으로 새 인라인 블록을 만들어야 합니다.

— count 매개변수를 사용해서 이러한 태그를 반복하고 동적인 인라인 tag 블록을 생성하려고 시도할 수도 있지만, 인라인 블록 내에서는 count 사용은 지원하지 않습니다.

  • 중간 user2를 제거하고 적용합니다.
variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["user1", "user3"]
}
$ terraform plan

Terraform will perform the following actions:

  # aws_iam_user.myiam[1] will be updated in-place
  ~ resource "aws_iam_user" "myiam" {
        id            = "user2"
      ~ name          = "user2" -> "user3"
        tags          = {}
        # (5 unchanged attributes hidden)
    }

  # aws_iam_user.myiam[2] will be destroyed
  # (because index [2] is out of range for count)
  - resource "aws_iam_user" "myiam" {
      - arn           = "arn:aws:iam::9XXXXXXXXX:user/user3" -> null
      - force_destroy = false -> null
      - id            = "user3" -> null
      - name          = "user3" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - tags_all      = {} -> null
      - unique_id     = "AIDA5EYKJMNUMN2RKLEHZ" -> null
    }

Plan: 0 to add, 1 to change, 1 to destroy.

Changes to Outputs:
  ~ all_arns = [
        # (1 unchanged element hidden)
        "arn:aws:iam::9XXXXXXXXX:user/user2",
      - "arn:aws:iam::9XXXXXXXXX:user/user3",
    ]
  • 배열의 중간에 항목을 제거하면 모든 항목이 1칸씩 앞으로 당겨집니다.
  • 테라폼이 인덱스 번호를 리소스 식별자로 보기 때문에 ‘인덱스 1에서는 버킷을 바꾸고, 인덱스2에서는 버킷을 삭제한다’라고 해석합니다.
  • 즉 count 사용 시 목록 중간 항목을 제거하면 테라폼은 해당 항목 뒤에 있는 모드 리소스를 삭제한 다음 해당 리소스를 처음부터 다시 만듭니다.
$ terraform apply -auto-approve

Error: Error updating IAM User user2: EntityAlreadyExists: User with name user3 already exists.
│       status code: 409, request id: feb4234f-19b5-442d-9a30-8944cf092e2d
│
│   with aws_iam_user.myiam[1],
│   on iam.tf line 5, in resource "aws_iam_user" "myiam":
│    5: resource "aws_iam_user" "myiam" {
│


$ terraform destroy -auto-approve

2. for_each 표현식

  • for_each 표현식을 사용하면 리스트 lists, 집합 sets, 맵 maps 를 사용하여 전체 리소스의 여러 복사본 또는 리소스 내 인라인 블록의 여러 복사본, 모듈의 복사본을 생성 할 수 있습니다.
resource "<PROVIDER>_<TYPE>" "<NAME>" {
  for_each = <COLLECTION>

  [CONFIG ...]
}
  • COLLECTION 은 루프를 처리할 집합 sets 또는 맵 maps
  • 리소스에 for_each 를 사용할 때에는 리스트는 지원하지 않습니다.
  • 그리고 CONFIG 는 해당 리소스와 관련된 하나 이상의 인수로 구성되는데 CONFIG 내에서 each.key 또는 each.value 를 사용하여 COLLECTION 에서 현재 항목의 키와 값에 접근할 수 있습니다.
provider "aws" {
  region = "ap-northeast-2"
}

resource "aws_iam_user" "myiam" {
  for_each = toset(var.user_names)
  name     = each.value
}
variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["user1", "user2", "user3"]
}
cat <<EOT > outputs.tf
output "all_users" {
  value = aws_iam_user.myiam
}
EOT
$ terraform plan && terraform apply -auto-approve



Outputs:

all_users = {
  "user1" = {
    "arn" = "arn:aws:iam::9XXXXXXXXX:user/user1"
    "force_destroy" = false
    "id" = "user1"
    "name" = "user1"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5EYKJMNUENLLKHS43"
  }
  "user2" = {
    "arn" = "arn:aws:iam::9XXXXXXXXX:user/user2"
    "force_destroy" = false
    "id" = "user2"
    "name" = "user2"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5EYKJMNUBZHQXRNF3"
  }
  "user3" = {
    "arn" = "arn:aws:iam::9XXXXXXXXX:user/user3"
    "force_destroy" = false
    "id" = "user3"
    "name" = "user3"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5EYKJMNUANA3MAL6Z"
  }
}



# 확인
$ terraform state list
aws_iam_user.myiam["user1"]
aws_iam_user.myiam["user2"]
aws_iam_user.myiam["user3"]


$ terraform output

all_users = {
  "user1" = {
    "arn" = "arn:aws:iam::9XXXXXXXXX:user:user/user1"
    "force_destroy" = false
    "id" = "user1"
    "name" = "user1"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5EYKJMNUENLLKHS43"
  }
  "user2" = {
    "arn" = "arn:aws:iam::9XXXXXXXXX:user:user/user2"
    "force_destroy" = false
    "id" = "user2"
    "name" = "user2"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5EYKJMNUBZHQXRNF3"
  }
  "user3" = {
    "arn" = "arn:aws:iam::9XXXXXXXXX:user:user/user3"
    "force_destroy" = false
    "id" = "user3"
    "name" = "user3"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5EYKJMNUANA3MAL6Z"
  }
}
  • all_users 출력 변수가 for_each 의 키, 즉 사용자 이름을 키로 가지며 값이 해당 리소스의 전체 출력인 맵을 포함합니다.
  • 만약 arn 만 가져오려면 맵에서 값만 반환하는 내장 함수 values 를 이용해 ARN을 추출하고 splat 표현식을 사용하면 됩니다.
cat <<EOT > outputs.tf
output "all_users" {
  value = values(aws_iam_user.myiam)[*].arn
}
EOT
$ terraform apply -auto-approve

# 확인
$ terraform state list

all_users = [
  "arn:aws:iam::9XXXXXXXXX:user/user1",
  "arn:aws:iam::9XXXXXXXXX:user/user2",
  "arn:aws:iam::9XXXXXXXXX:user/user3",
]
  • for_each 를 사용해 리소스 으로 처리하면 컬렉션 중간의 항목도 안전하게 제거할 수 있어서, count  리소스를 배열 처리보다 이점이 큽니다.
  • 이전 실습에서 진행했듯 var.user_names 리스트 중간에 값을 제거하고 plan 으로 확인해봅니다.
cat <<EOT > variables.tf
variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["user1", "user3"]
}
EOT
$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with 
the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_iam_user.myiam["user2"] will be destroyed
  # (because key ["user2"] is not in for_each map)
  - resource "aws_iam_user" "myiam" {
      - arn           = "arn:aws:iam::903575331688:user/user2" -> null
      - force_destroy = false -> null
      - id            = "user2" -> null
      - name          = "user2" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - tags_all      = {} -> null
      - unique_id     = "AIDA5EYKJMNUBZHQXRNF3" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.
  • 이제 주변 모든 리소스를 옮기지 않고 정확히 목표한 리소스만 삭제가 됩니다.
  • 따라서 리소스의 여러 복사본을 만들 때는 count 대신 for_each 를 사용하는 것이 바람직합니다.
  • 이밖에도 for_each는 인라인 블록, 모듈에서 사용가능합니다.
  • 예를 들어 for_each 를 사용하여 ASG 에 tag 인라인 블록을 동적으로 생성 할 수 있습니다.
  • 먼저 사용자가 사용자 정의 태그를 지정할 수 있게 modules/services/webserver-cluster/variables.tf에 custom_tags 라는 새로운 맵 입력 변수를 추가합니다. 코드 참고: Github repo
  • 다음으로 프로덕션 환경의 prod/services/webserver-cluster/main.tf에서 아래와 같이 일부 사용자 정의 태그를 설정합니다. 코드 참고: Github repo

3. for 표현식

  • 테라폼은 for 표현식으로 유사한 기능을 제공합니다. 기본 구문은 아래와 같습니다.
[for <KEY>, <VALUE> in <MAP> : <OUTPUT>]
  • MAP은 반복되는 맵이고, KEY와 VALUE는 MAP의 각 키-값 쌍에 할당할 로컬 변수의 이름입니다.
  • OUTPUT은 KEY와 VALUE를 어떤 식으로든 변환하는 표현식입니다.
# main.tf

variable "names" {
  description = "A list of names"
  type        = list(string)
  default     = ["user1", "user2", "user3"]
}

output "upper_names" {
  value = [for name in var.names : upper(name)]
}

output "short_upper_names" {
  value = [for name in var.names : upper(name) if length(name) < 5]
}

variable "hero_thousand_faces" {
  description = "map"
  type        = map(string)
  default     = {
    user1    = "hero"
    user2     = "love interest"
    user3  = "mentor"
  }
}

output "bios" {
  value = [for name, role in var.hero_thousand_faces : "${name} is the ${role}"]
}
$ terraform apply -auto-approve

Outputs:

bios = [
  "user1 is the hero",
  "user2 is the love interest",
  "user3 is the mentor",
]
short_upper_names = []
upper_names = [
  "USER1",
  "USER2",
  "USER3",
]
# 리스트를 반복하고 맵을 출력 Loop over a list and output a map
{for <ITEM> in <LIST> : <OUTPUT_KEY> => <OUTPUT_VALUE>}

# 맵을 반복하고 리스트를 출력 Loop over a map and output a map
{for <KEY>, <VALUE> in <MAP> : <OUTPUT_KEY> => <OUTPUT_VALUE>}
# main.tf

variable "names" {
  description = "A list of names"
  type        = list(string)
  default     = ["user1", "user2", "user3"]
}

output "upper_names" {
  value = [for name in var.names : upper(name)]
}

output "short_upper_names" {
  value = [for name in var.names : upper(name) if length(name) < 5]
}

variable "hero_thousand_faces" {
  description = "map"
  type        = map(string)
  default     = {
    user1    = "hero"
    user2     = "love interest"
    user3  = "mentor"
  }
}


output "upper_roles" {
  value = {for name, role in var.hero_thousand_faces : upper(name) => upper(role)}
}
$ terraform apply -auto-approve

upper_names = [
  "USER1",
  "USER2",
  "USER3",
]
upper_roles = {
  "USER1" = "HERO"
  "USER2" = "LOVE INTEREST"
  "USER3" = "MENTOR"
}

4. 문자열 지시자

  • 문자열 내에 테라폼 코드를 참조할 수 있는 문자열 보간법은 아래와 같습니다.
"Hello, ${var.name}"
  • 문자열 지시자를 사용하면 문자열 보간과 유사한 구문으로 문자열 내에서 for 반복문, if문 같은 제어문을 사용할 수 있습니다.
  • for 문자열 지시자는 다음 구문을 사용합니다.
%{ for <ITEM> in <COLLECTION> }<BODY>%{ endfor }
  • COLLECTION 은 반복할 리스트 또는 맵이고 ITEM은 COLLECTION 의 각 항목에 할당할 로컬 변수의 이름이며 BODY는 ITEM을 참조할 수 있는 각각의 반복을 렌더링하는 대상입니다. 예를 들면 아래와 같습니다.
cat <<EOT > main.tf
variable "names" {
  description = "A list of names"
  type        = list(string)
  default     = ["user1", "user2", "user3"]
}

output "for_directive" {
  value = "%{ for name in var.names }\${name}, %{ endfor }"
}
EOT

조건문(Conditionals)

  • count 매개 변수 parameter : 조건부 리소스에서 사용
  • for_each 와 for 표현식 expressions : 리소스 내의 조건부 리소스 및 인라인 블록에 사용
  • If 문자열 지시자 if string directive : 문자열 내의 조건문에 사용

1. count 매개변수

  • 분기 처리 첫 번째 단계는 모듈의 오토스케일링 사용 여부를 지정하는데 사용할 Boolean 입력 변수 를 modules/services/webserver-cluster/variables.tf에 추가하는 것입니다.
  • 코드 참고: Github repo
variable "enable_autoscaling" {
  description = "If set to true, enable auto scaling"
  type        = bool
}
  • 리소스에 count 를 1로 설정하면 해당 리소스의 사본 하나를 얻습니다. count 를 0으로 설정하면 해당 리소스가 만들어지지 않습니다.
<CONDITION> ? <TRUE_VAL> : <FALSE_VAL>
  • CONDITION 에서 boolean logic 를 평가하고 결과가 true 이면 TRUE_VAL을 반환하고, 결과가 false이면 FALSE_VAL을 반환합니다.
  • 아래와 같이 webserver-cluster 모듈을 업데이트 할 수 있습니다. 코드 참고: Github repo
resource "aws_autoscaling_schedule" "scale_out_during_business_hours" {
  count = var.enable_autoscaling ? 1 : 0

  scheduled_action_name  = "${var.cluster_name}-scale-out-during-business-hours"
  min_size               = 2
  max_size               = 10
  desired_capacity       = 10
  recurrence             = "0 9 * * *"
  autoscaling_group_name = aws_autoscaling_group.example.name
}

resource "aws_autoscaling_schedule" "scale_in_at_night" {
  count = var.enable_autoscaling ? 1 : 0

  scheduled_action_name  = "${var.cluster_name}-scale-in-at-night"
  min_size               = 2
  max_size               = 10
  desired_capacity       = 2
  recurrence             = "0 17 * * *"
  autoscaling_group_name = aws_autoscaling_group.example.name
}
  • enable_autoscaling 가 false 로 설정하여 오토스케일링을 비활성화하기 위해, stage/services/webserver-cluster/main.tf에 있는 스테이징에서 다음과 같이 이 모듈의 사용법을 업데이트 할 수 있습니다. 코드 참고: Github repo
module "webserver_cluster" {
  source = "../../../modules/services/webserver-cluster"

  # (parameters hidden for clarity)

  cluster_name           = var.cluster_name
  db_remote_state_bucket = var.db_remote_state_bucket
  db_remote_state_key    = var.db_remote_state_key

  instance_type = "t3.nano"
  min_size      = 2
  max_size      = 2
  enable_autoscaling   = false //추가
}
  • 마찬가지로 enable_autoscaling 가 true 로 설정하여 오토스케일링을 활성화하기 위해, prod/services/webserver-cluster/main.tf에 있는 프로덕션에서 이 모듈의 사용법을 업데이트 할 수 있습니다. 코드 참고: Github repo
module "webserver_cluster" {
  source = "../../../../modules/services/webserver-cluster"

  cluster_name           = "webservers-prod"
  db_remote_state_bucket = "(YOUR_BUCKET_NAME)"
  db_remote_state_key    = "prod/data-stores/mysql/terraform.tfstate"

  instance_type        = "m4.large"
  min_size             = 2
  max_size             = 10
  enable_autoscaling   = true

  custom_tags = {
    Owner     = "team-foo"
    ManagedBy = "terraform"
  }
}

2. for_each 와 for 표현식

  • modules/services/webserver-cluster/main.tf 의 webserver-cluster 모듈이 태그를 어떻게 설정하는지 다시 확인합니다.코드 참고: Github repo
  • 아래와 같이 for_each 표현식을 for 표현식과 결합할 수 있습니다.
 dynamic "tag" {
    for_each = {
      for key, value in var.custom_tags:
      key => upper(value)
      if key != "Name"
    }

    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
  • for 표현식에서 값을 필터링하여 임의 조건부 논리를 구현할 수 있습니다.
  • 즉, 리소스를 조건부로 생성할 때는 count 를 사용할 수 있지만, 그 외 모든 유형의 반복문 또는 조건문에는 for_each 를 사용합니다.

3. If 문자열 지시자

%{ if <CONDITION> }<TRUEVAL>%{ endif }
  • CONDITION은 boolean 으로 평가되는 표현식이고, TRUEVAL은 CONDITION이 True로 평가되면 렌더링할 표현식입니다.
%{ if <CONDITION> }<TRUEVAL>%{ else }<FALSEVAL>%{ endif }
  • FALSEVAL은 CONDITION이 false로 평가되면 렌더링할 표현식입니다.
#main.tf

variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["user1", "user2", "user3"]
}

output "for_directive" {
  value = "%{ for name in var.names }${name}, %{ endfor }"
}

output "for_directive_index" {
  value = "%{ for i, name in var.names }(${i}) ${name}, %{ endfor }"
}

output "for_directive_index_if" {
  value = <<EOF
%{ for i, name in var.names }
  ${name}%{ if i < length(var.names) - 1 }, %{ endif }
%{ endfor }
EOF
}
$ terraform init && terraform plan && terraform apply -auto-approve

for_directive = "user1, user2, user3, "
for_directive_index = "(0) user1, (1) user2, (2) user3, "
for_directive_index_if = <<EOT

  user1,

  user2,

  user3


EOT


# 확인
$ terraform output
  • 문자열 지시자 시작에 공백이 있으며 지시자 앞에, 문자열 지시자 끝에 공백이 있으면 지시자 뒤에 물결표를 사용합니다.
#main.tf

variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["user1", "user2", "user3"]
}

output "for_directive" {
  value = "%{ for name in var.names }${name}, %{ endfor }"
}

output "for_directive_index" {
  value = "%{ for i, name in var.names }(${i}) ${name}, %{ endfor }"
}


output "for_directive_index_if_strip" {
  value = <<EOF
%{~ for i, name in var.names ~}
  ${name}%{ if i < length(var.names) - 1 }, %{ endif }
%{~ endfor ~}
EOF
}
$ terraform apply -auto-approve
Outputs:

for_directive_index_if = <<EOT

  user1,

  user2,

  user3


EOT
for_directive_index_if_strip = "  user1,   user2,   user3"
  • else 를 활용하여 끝에 마침표(.)를 찍어봅니다.
variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["user1", "user2", "user3"]
}

output "for_directive" {
  value = "%{ for name in var.names }${name}, %{ endfor }"
}

output "for_directive_index" {
  value = "%{ for i, name in var.names }(${i}) ${name}, %{ endfor }"
}


output "for_directive_index_if_else_strip" {
  value = <<EOF
%{~ for i, name in var.names ~}
  ${name}%{ if i < length(var.names) - 1 }, %{ else }.%{ endif }
%{~ endfor ~}
EOF
}
$ terraform apply -auto-approve

for_directive_index_if_else_strip = "  user1,   user2,   user3."
for_directive_index_if_strip = "  user1,   user2,   user3"

 

 

blog migration project

written in 2022.11.13

https://medium.com/techblog-hayleyshim/iac-terraform-tips-tricks-loops-conditions-960449b8740

'Programming > IaC' 카테고리의 다른 글

[IaC] Production-grade Terraform code  (0) 2023.10.28
[IaC] Managing Secrets with Terraform  (0) 2023.10.28
[IaC] Terraform modules  (0) 2023.10.28
[IaC] Terraform state -상태파일격리  (0) 2023.10.28
[IaC] Terraform state -상태파일공유  (0) 2023.10.28