• Home
  • About
    • lahuman photo

      lahuman

      열심히 사는 아저씨

    • Learn More
    • Facebook
    • LinkedIn
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

#5 반복문 & 조건문

13 Nov 2022

Reading time ~26 minutes

반보문 & 조건문

  • 참고 링크
    • [Docs] Built-in Fuctions - 링크 & Expressions - 링크
    • Functions 활용하기 - 링크

소개

테라폼은 유형을 반복 Loops, if 문 If-Statements을 사용하거나 무중단 배포를 할 수 있도록 count 메타 변수, for_each 표현식, create_before_destroy 생명 주기 블록 등 함수를 제공

  • 반복문 Loops
  • 조건문 Conditionals
  • 무중단 배포 Zero-downtime deployment
  • 테라폼의 주의 사항 Terraform gotchas

아래 구문 함수 표현식등에 대한 연습은 terraform console 에서 간편하고 빠르게 테스트 해 볼 수 있음

반복문 Loops

테라폼이 제공하는 반복문 구성 looping constructs

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

count 매개 변수

실습1 : IAM 사용자 3명 생성

  • IAM 사용자 1명 생성 코드
provider "aws" {
  region = "us-east-2"
}

resource "aws_iam_user" "example" {
  name = "neo"
}
  • 범용 프로그래밍 언어 for 반복문 사용 ⇒ 테라폼은 for 반복문 또는 언어에 내장된 절차 논리가 없어서 아래 구문 동작 불가능
# This is just pseudo code. It won't actually work in Terraform.
for (i = 0; i < 3; i++) {
resource "aws_iam_user" "example" {
  name = "neo"
}
}
  • 테라폼에서 count 를 사용하여 3명 IAM 사용자 생성 → 3명의 IAM 사용자 이름 중복으로 오류 발생 ⇒ for 반복문의 인덱스를 사용하여 각 사용자에게 고유한 이름 지정 가능
resource "aws_iam_user" "example" {
count = 3
name  = "neo"
}
실습 시작 : iam.tf
  • 아래 처럼 count.index 를 사용하여 반복문 안에 있는 각각의 반복 ieration 을 가리키는 인덱스를 얻을 수 있음
NICKNAME=<각자 닉네임>
NICKNAME=gasida

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

resource "aws_iam_user" "myiam" {
**count = 3**
name  = "$NICKNAME.**\${count.index}**"
}
EOT
  • init & plan 실행 및 apply
#
**terraform init && terraform plan**
...
# aws_iam_user.myiam[0] will be created
# aws_iam_user.myiam[1] will be created
# aws_iam_user.myiam[2] will be created

# apply
terraform apply -auto-approve

# 확인
**terraform state list**
aws_iam_user.myiam[0]
aws_iam_user.myiam[1]
aws_iam_user.myiam[2]

**aws iam list-users | jq**
...
  • 다음 실습을 위해 iam user 삭제
terraform destroy -auto-approve

실습2 : 입력 변수를 통해 IAM 사용자 생성

  • 입력 변수 코드 생성 variables.tf
cat <<EOT > variables.tf
variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["**gasida**", "**akbun**", "**fullmoon**"]
}
EOT
  • 테라폼에서 count 와 함께 배열 조회 구문과 length 함수를 사용해서 사용자들 생성 가능
    • 배열 조회 구문 Array lookup syntax
      • ARRAY[]
      • 예를 들어 다음은 var.user_names 의 인덱스 1에서 요소를 찾는 방법
      • var.user_names[1]
    • length (내장) 함수 built-on function
      • length()
      • 주어진 ARRAY 의 항목 수를 반환하는 함수. 문자열 및 맵을 대상으로도 동작
  • 위 적용한 코드 생성 iam.tf
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
  • init & plan : 리소스에 count 사용한 후에는 하나의 리소스가 아니라 리소스의 배열이 됩니다.
#
**terraform plan**
...
  # aws_iam_user.myiam[0] will be created
  # aws_iam_user.myiam[1] will be created
  # aws_iam_user.myiam[2] will be created
#
  • .
    • aws_iam_user.myiam 은 이제 IAM 사용자의 배열이므로 표준 구문을 사용하여 해당 리소스인 _.. 에서 속성을 읽은 대신 배열에서 인덱스를 지정해서 IAM 사용자를 명시해야합니다.
    • _.[INDEX].ATTRIBUTE
  • IAM 사용자 한명의 ARN과 사용자 전체의 ARN 을 출력 변수 outputs 으로 제공 : output.tf
    • IAM 사용자 전체의 ARN을 원하면 인덱스 대신 스플랫 splat 연산자인 * 를 사용
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
  • apply
#
**terraform apply -auto-approve**
Outputs:

all_arns = [
  "arn:aws:iam::911283464785:user/gasida",
  "arn:aws:iam::911283464785:user/akbun",
  "arn:aws:iam::911283464785:user/fullmoon",
]
first_arn = "arn:aws:iam::911283464785:user/gasida"

**terraform state list**
**terraform output**
  • 다음 실습을 위해 iam user 삭제 및 코드 파일 삭제
terraform destroy -auto-approve
rm <코드 파일 삭제>.tf
  • (참고) terrraform 0.13 부터는 모듈에 count 매개변수 사용 가능
#
**As of Terraform 0.13, the count parameter can also be used on modules. 
For example, imagine you had a module at modules/landing-zone/iam-user that can create a single IAM user:** 

resource "aws_iam_user" "example" {
  name = var.user_name
} 

**The username is passed into this module as an input variable:** 

variable "user_name" {
  description = "The user name to use"
  type        = string
} 

**And the module returns the ARN of the created IAM user as an output variable:** 

output "user_arn" {
  value       = aws_iam_user.example.arn
  description = "The ARN of the created IAM user"
} 

**You could use this module with a count parameter to create three IAM users as follows:** 

module "users" {
  source = "../../../modules/landing-zone/iam-user"

  count     = length(var.user_names)
  user_name = var.user_names[count.index]
} 

**The preceding code uses count to loop over this list of usernames:** 

variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["neo", "trinity", "morpheus"]
} 

**And it outputs the ARNs of the created IAM users as follows:**

output "user_arns" {
  value       = module.users[*].user_arn
  description = "The ARNs of the created IAM users"
} 

**Just as adding count to a resource turns it into an array of resources, adding count to a module turns it into an array of modules.
If you run apply on this code, you’ll get the following output:**

$ terraform apply

(...)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

all_arns = [
  "arn:aws:iam::123456789012:user/neo",
  "arn:aws:iam::123456789012:user/trinity",
  "arn:aws:iam::123456789012:user/morpheus",
]

count 두 가지 제약 사항 & 실습3

1. 전체 리소스를 반복할 수는 있지만 리소스 내에서 인라인 블록을 반복할 수는 없습니다.
  • 예를 들어 아래 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 사용은 지원하지 않습니다.
2. 변경하려고 할 때 발생합니다.
  • 실습을 위해 다시 IAM 사용자 생성
**terraform apply -auto-approve
...**
aws_iam_user.myiam[2]: Refreshing state... [id=fullmoon]
aws_iam_user.myiam[1]: Refreshing state... [id=akbun]
aws_iam_user.myiam[0]: Refreshing state... [id=gasida]
...
  • IAM 사용자 생성 목록에서 생각
variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["**gasida**", "**akbun**", "**fullmoon**"]
}
  • 중간에 있는 akbun 을 제거하고 plan & apply
cat <<EOT > variables.tf
variable "user_names" {
description = "Create IAM users with these names"
type        = list(string)
default     = ["**gasida**", "**fullmoon**"]
}
EOT
# plan : 출력 내용 확인!
**terraform plan**
...
**~ update in-place
- destroy**

Terraform will perform the following actions:

# aws_iam_user.myiam[1] will be **updated in-place**
~ resource "aws_iam_user" "myiam" {
    id            = "akbun"
  ~ name          = "**akbun**" -> "**fullmoon**"
    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::911283464785:user/fullmoon" -> null
  - force_destroy = false -> null
  - id            = "fullmoon" -> null
  - name          = "**fullmoon**" -> null
  - path          = "/" -> null
  - tags          = {} -> null
  - tags_all      = {} -> null
  - unique_id     = "AIDA5ILF2FJI2ELMQRCZV" -> null
}

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

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

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

Terraform will perform the following actions:

  # aws_iam_user.myiam[1] will be updated in-place
  ~ resource "aws_iam_user" "myiam" {
        id            = "akbun"
      ~ name          = "akbun" -> "fullmoon"
        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::911283464785:user/fullmoon" -> null
      - force_destroy = false -> null
      - id            = "fullmoon" -> null
      - name          = "fullmoon" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - tags_all      = {} -> null
      - unique_id     = "AIDA5ILF2FJI2ELMQRCZV" -> null
    }

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

Changes to Outputs:
  ~ all_arns = [
        # (1 unchanged element hidden)
        "arn:aws:iam::911283464785:user/akbun",
      - "arn:aws:iam::911283464785:user/fullmoon",
    ]
aws_iam_user.myiam[2]: Destroying... [id=fullmoon]
aws_iam_user.myiam[1]: Modifying... [id=akbun]
aws_iam_user.myiam[2]: Destruction complete after 1s
╷
│ Error: Error updating IAM User akbun: EntityAlreadyExists: User with name fullmoon already exists.
│ 	status code: 409, request id: 899d7c85-3bd0-4cc0-a556-6426f6466ea1
│
│   with aws_iam_user.myiam[1],
│   on iam.tf line 5, in resource "aws_iam_user" "myiam":
│    5: resource "aws_iam_user" "myiam" {
│
  • 다음 실습을 위해 iam user 삭제
terraform destroy -auto-approve
aws iam list-users | jq

for_each 표현식

for_each 표현식을 사용한 반복문 처리 & 실습4

배열 ARRAY 와 리스트 LIST 의 차이점은 무엇인가요?

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

  **[CONFIG ...]**
}
  • .
    • COLLECTION 은 루프를 처리할 집합 sets 또는 맵 maps
    • 리소스에 for_each 를 사용할 때에는 리스트는 지원하지 않습니다.
    • 그리고 CONFIG 는 해당 리소스와 관련된 하나 이상의 인수로 구성되는데 CONFIG 내에서 each.key 또는 each.value 를 사용하여 COLLECTION 에서 현재 항목의 키와 값에 접근할 수 있습니다.
  • 예를 들어 다음은 for_each 를 사용하여 3명의 IAM 사용자를 생성하는 방법
    • var.user_names 리스트를 집합(set)으로 변환하기 위해 toset 사용. for_each 는 리소스에 사용될 때는 집합과 맵만 지원.
    • for_each 가 이 집합을 반복하면 each.value 에서 각 사용자 이름을 사용할 수 있습니다.
    • 일반적으로는 each.key 는 키/값 쌍 맵에서만 사용가능하지만, 사용자 이름은 each.key 에서도 사용할 수 있습니다.

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

resource "aws_iam_user" "myiam" {
**for_each = toset(var.user_names)**
**name     = each.value**
}
EOT
cat <<EOT > variables.tf
variable "user_names" {
description = "Create IAM users with these names"
type        = list(string)
default     = ["**gasida**", "**akbun**", "**fullmoon**"]
}
EOT
  • for_each 를 사용한 후에는 하나의 리소스 또는 count 를 사용한 것과 같은 리소스 배열이 되는 것이 아니리 리소스 맵 list into a set 이 됩니다.
  • 이 의미를 확인하려면 원래의 outputs 을 제거하고 새로운 all_users 출력 변수를 추가하자
cat <<EOT > outputs.tf
output "all_users" {
  value = aws_iam_user.myiam
}
EOT
  • plan & apply
#
**terraform plan && terraform apply -auto-approve**
Outputs:

all_users = {
  "akbun" = {
    "arn" = "arn:aws:iam::911283464785:user/akbun"
    "force_destroy" = false
    "id" = "akbun"
    "name" = "akbun"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5ILF2FJI7S7KCZYGA"
  }
  "fullmoon" = {
    "arn" = "arn:aws:iam::911283464785:user/fullmoon"
    "force_destroy" = false
    "id" = "fullmoon"
    "name" = "fullmoon"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5ILF2FJI4MUNCWZC2"
  }
  "gasida" = {
    "arn" = "arn:aws:iam::911283464785:user/gasida"
    "force_destroy" = false
    "id" = "gasida"
    "name" = "gasida"
    "path" = "/"
    "permissions_boundary" = tostring(null)
    "tags" = tomap(null) /* of string */
    "tags_all" = tomap({})
    "unique_id" = "AIDA5ILF2FJIWDUCOXT2E"
  }
}

# 확인
terraform state list
aws_iam_user.myiam["akbun"]
aws_iam_user.myiam["fullmoon"]
aws_iam_user.myiam["gasida"]

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

# 확인
terraform output
all_users = [
  "arn:aws:iam::911283464785:user/akbun",
  "arn:aws:iam::911283464785:user/fullmoon",
  "arn:aws:iam::911283464785:user/gasida",
]
  • 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     = ["**gasida**", "**fullmoon**"]
}
EOT
**terraform plan**
aws_iam_user.myiam["akbun"]: Refreshing state... [id=akbun]
aws_iam_user.myiam["fullmoon"]: Refreshing state... [id=fullmoon]
aws_iam_user.myiam["gasida"]: Refreshing state... [id=gasida]

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["akbun"] will be destroyed
  # (because key ["akbun"] is not in for_each map)
  - resource "aws_iam_user" "myiam" {
      - arn           = "arn:aws:iam::911283464785:user/akbun" -> null
      - force_destroy = false -> null
      - id            = "akbun" -> null
      - name          = "akbun" -> null
      - path          = "/" -> null
      - tags          = {} -> null
      - tags_all      = {} -> null
      - unique_id     = "AIDA5ILF2FJI7S7KCZYGA" -> null
    }

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

Changes to Outputs:
  ~ all_users = [
      - "arn:aws:iam::911283464785:user/akbun",
        "arn:aws:iam::911283464785:user/fullmoon",
        # (1 unchanged element hidden)
    ]
  • .
    • 이제 주변 모든 리소스를 옮기지 않고 정확히 목표한 리소스만 삭제가 됩니다.
    • 따라서 리소스의 여러 복사본을 만들 때는 count 대신 for_each 를 사용하는 것이 바람직합니다.
  • for_each 인라인 블록에 반복문 사용

    for_each 또 다른 장점은 리소스 내에서 여러 개의 인라인 블록을 만들 수 있다는 점입니다.

    • 예를 들어 for_each 를 사용하여 ASG 에 tag 인라인 블록을 동적으로 생성 할 수 있습니다.
    • 먼저 사용자가 사용자 정의 태그를 지정할 수 있게 modules/services/webserver-cluster/variables.tf에 custom_tags 라는 새로운 맵 입력 변수를 추가합니다.
variable "custom_tags" {
  description = "Custom tags to set on the Instances in the ASG"
  type        = map(string)
  default     = {}
}
- 다음으로 프로덕션 환경의 live/prod/services/webserver-cluster/main.tf에서 아래와 같이 일부 사용자 정의 태그를 설정합니다.
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

  **custom_tags = {
    Owner     = "team-foo"
    ManagedBy = "terraform"
  }**
}
  • 앞 코드에서 몇 가지 유용한 태그가 설정되어 있습니다.
  • Owner 태그는 이 ASG를 소유한 팀을 명시하고, ManagedBy 는 이 인프라가 테라폼을 사용하여 배포되었음을 며잇하빈다.
  • 이 인프라를 수동으로 수정해서는 안 되는 의미입니다. 이 부분은 뒤에 ‘유효한 계획의 실패’에서 다룹니다.
  • 태그를 사용할 때는 일반적으로 팀의 태그 지정 표준을 제시하고 이 표준을 코드로 적용하게 하는 테라폼 모듈을 만드는 것이 바람직합니다.
  • 이제 이것을 ASG 리소스에 설정합니다. 아래 코드는 pseudo code 로 테라폼에서는 동작하지 않습니다.
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
  }

  # This is just pseudo code. It won't actually work in Terraform.
  for (tag in var.custom_tags) {
    tag {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}
  • for_each 를 사용해 인라인 블록을 동적으로 생성하는 구문은 아래와 같습니다.
dynamic "<VAR_NAME>" {
  for_each = <COLLECTION>

  content {
    [CONFIG...]
  }
}
  • .
    • 여기서 VAR_NAME 은 각 ‘반복’의 값을 저장할 변수에 사용할 이름이고, COLLECTION 은 반복되는 리스트 또는 맵이며, content 블록은 각 반복에서 생성되는 항목입니다.
    • content 블록 내에서 .key 및 .value 를 사용해 COLLECTION 에 있는 현재 항목의 키와 값에 각각 액세스할 수 있습니다.
    • for_each 를 리스트와 함께 사용하는 경우 key는 인덱스가 되고 value는 해당 인덱스 목록에 있는 항목이 된다는 점을 기억합니다.
    • 그리고 for_each 를 맵과 함께 사용하는 경우 key와 value는 맵의 키-값 쌍 중 하나가 된다는 점도 유의합니다.
  • 종합하면 ASG 리소스에서 for_each 를 사용하여 tag 블록을 동적으로 생성하는 방법은 아래 코드와 같습니다.
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
}

dynamic "tag" {
  for_each = var.custom_tags

  content {
    key                 = tag.key
    value               = tag.value
    propagate_at_launch = true
  }
}
}
  • .
    • plan
$ terraform plan

Terraform will perform the following actions:

# aws_autoscaling_group.example will be updated in-place
~ resource "aws_autoscaling_group" "example" {
    (...)

    tag {
        key                 = "Name"
        propagate_at_launch = true
        value               = "webservers-prod"
    }
  + tag {
      + key                 = "Owner"
      + propagate_at_launch = true
      + value               = "team-foo"
    }
  + tag {
      + key                 = "ManagedBy"
      + propagate_at_launch = true
      + value               = "terraform"
    }
}

Plan: 0 to add, 1 to change, 0 to destroy.
  • (참고) ENFORCING TAGGING STANDARDS
provider "aws" {
  region = "us-east-2"

  # Tags to apply to all AWS resources by default
  default_tags {
    tags = {
      Owner     = "team-foo"
      ManagedBy = "Terraform"
    }
  }
}
  • (참고) for_each 를 모듈에서 활용
for_each works with modules in a more or less identical fashion. Using the iam-user module from earlier, 
you can create three IAM users with it using for_each as follows:

module "users" {
  source = "../../../modules/landing-zone/iam-user"

  for_each  = toset(var.user_names)
  user_name = each.value
} 

And you can output the ARNs of those users as follows: 

output "user_arns" {
  value       = values(module.users)[*].user_arn
  description = "The ARNs of the created IAM users"
}

When you run apply on this code, you get the expected output: 

$ terraform apply

(...)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

all_arns = [
  "arn:aws:iam::123456789012:user/morpheus",
  "arn:aws:iam::123456789012:user/neo",
  "arn:aws:iam::123456789012:user/trinity",
]

for 표현식

  • for 표현식 Expressions 을 이용한 반복문 & 실습5
    • 리소스와 인라인 블록을 반복하는 법을 알아보았습니다. 하지만 단일 값을 생성하기 위해 반복이 필요한 경우에는 어떻게 해야 할까요?
    • names 의 리스트를 가진 테라폼 코드를 작성했다고 가정합니다 → 모든 이름을 대문자로 변환하려면 어떻게 해야 할까요?
cat <<EOT > main.tf
variable "**names**" {
  description = "A list of names"
  type        = list(string)
  default     = ["**gasida**", "**akbun**", "**fullmoon**"]
}
EOT
  • 테라폼은 for 표현식으로 유사한 기능을 제공합니다. 기본 구문은 아래와 같습니다. (for_each 표현식과 다릅니다!)
[for <ITEM> in <LIST> : <OUTPUT>]
  • .
    • 여기서 LIST 는 반복할 리스트이고 ITEM은 LIST의 각 항목에 할당할 변수의 이름이며 OUTPUT은 ITEM을 어떤 식으로든 변환하는 표현식입니다.
  • 예를 들어 var.names 의 이름 목록을 대문자로 변환하는 테라폼 코드는 다음과 같습니다.

    cat <<EOT > main.tf
    variable "names" {
      description = "A list of names"
      type        = list(string)
      default     = ["gasida", "akbun", "fullmoon"]
    }
      
    output "upper_names" {
      value = [for name in var.names : upper(name)]
    }
    EOT
    
  • init & plan & apply
#
terraform init && terraform plan && terraform apply -auto-approve
Outputs:

upper_names = [
  "GASIDA",
  "AKBUN",
  "FULLMOON",
]

# 확인
**terraform output
terraform state list**
  • 파이썬의 리스트 내포 구문과 마찬가지로 조건을 지정해서 결과 리스트를 필터링할 수 있습니다.
cat <<EOT > main.tf
variable "names" {
  description = "A list of names"
  type        = list(string)
  default     = ["gasida", "akbun", "fullmoon"]
}

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) < 6]
}
EOT
#
**terraform apply -auto-approve**
Outputs:

short_upper_names = [
  "AKBUN",
]
upper_names = [
  "GASIDA",
  "AKBUN",
  "FULLMOON",
]
  • 다음과 같은 구문을 사용하여 맵을 반복할 수 있습니다.
[for <KEY>, <VALUE> in <MAP> : <OUTPUT>]
  • .
    • 여기서 MAP은 반복되는 맵이고, KEY와 VALUE는 MAP의 각 키-값 쌍에 할당할 로컬 변수의 이름입니다.
  • OUTPUT은 KEY와 VALUE를 어떤 식으로든 변환하는 표현식입니다. 예를 들면 아래와 같습니다.
cat <<EOT > main.tf
variable "names" {
  description = "A list of names"
  type        = list(string)
  default     = ["gasida", "akbun", "fullmoon"]
}

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     = {
    gasida    = "hero"
    akbun     = "love interest"
    fullmoon  = "mentor"
  }
}

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

bios = [
  "akbun is the love interest",
  "fullmoon is the mentor",
  "gasida is the hero",
]
...
  • for 표현식을 리스트가 아닌 맵을 출력하는 데 사용할 수도 있습니다.
# 리스트를 반복하고 맵을 출력 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>}
  • 식을 대괄호 대신 중괄호로 묶고 각 반복마다 단일 값을 출력하는 대신 키와 값을 화살표로 구분하여 출력하는 것이 유일한 차이점입니다.
  • 예를 들어 다음은 맵을 변환하여 모든 키와 값을 대문자로 만드는 방법입니다.
cat <<EOT > main.tf
variable "names" {
  description = "A list of names"
  type        = list(string)
  default     = ["gasida", "akbun", "fullmoon"]
}

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     = {
    gasida    = "hero"
    akbun     = "love interest"
    fullmoon  = "mentor"
  }
}

output "bios" {
  value = [for name, role in var.hero_thousand_faces : "\${name} is the \${role}"]
}

output "upper_roles" {
  value = {for name, role in var.hero_thousand_faces : upper(name) => upper(role)}
}
EOT
  • .
    • apply
#
terraform apply -auto-approve
...
upper_names = [
  "GASIDA",
  "AKBUN",
  "FULLMOON",
]
upper_roles = {
  "AKBUN" = "LOVE INTEREST"
  "FULLMOON" = "MENTOR"
  "GASIDA" = "HERO"
}

문자열 지시자

  • 문자열 지시자를 사용하는 반복문 Loops with the for String Derective & 실습6
    • 앞에서 문자열 내에 테라폼 코드를 참조할 수 있는 문자열 보간법을 배웠습니다.
"Hello, ${var.name}"
  • 문자열 지시자를 사용하면 문자열 보간과 유사한 구문으로 문자열 내에서 for 반복문, if문 같은 제어문을 사용할 수 있습니다.
  • 다만 달러 부호와 중괄호 ${..} 대신 백분율 부호 %{}.. 를 사용한다는 차이가 있습니다.
  • 테라폼은 두 가지 유형의 문자열 지시자, for 반복문과 조건문을 지원합니다.
  • 이번 절에서는 for 반복문을 살펴보고 이 장의 뒷부분에서 조건문을 다룹니다.
  • for 문자열 지시자는 다음 구문을 사용합니다.
%{ for <ITEM> in <COLLECTION> }<BODY>%{ endfor }
  • 여기서 COLLECTION 은 반복할 리스트 또는 맵이고 ITEM은 COLLECTION 의 각 항목에 할당할 로컬 변수의 이름이며 BODY는 ITEM을 참조할 수 있는 각각의 반복을 렌더링하는 대상입니다. 예를 들면 아래와 같습니다.
cat <<EOT > main.tf
variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["gasida", "akbun", "fullmoon"]
}

output "for_directive" {
  value = "%{ for name in var.names }\${name}, %{ endfor }"
}
EOT
  • .
    • apply
#
terraform apply -auto-approve
Outputs:

for_directive = "gasida, akbun, fullmoon, "
  • .
    • 인덱스를 추가
%{ for <INDEX>, <ITEM> in <COLLECTION> }<BODY>%{ endfor }
  • .
    • 코드 수정
cat <<EOT > main.tf
variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["gasida", "akbun", "fullmoon"]
}

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 }"
}
EOT
  • .
    • apply
#
terraform apply -auto-approve
Outputs:

for_directive = "gasida, akbun, fullmoon, "
for_directive_index = "(0) gasida, (1) akbun, (2) fullmoon, "
  • .
    • 다음 실습을 위해 삭제
terraform destroy -auto-approve
rm <코드 파일 삭제>.tf
  • 그외 표현식 코드 참고

terraform-up-and-running-code/main.tf at master · brikis98/terraform-up-and-running-code

조건문 Conditionals

테라폼이 제공하는 조건문 구성 conditionals

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

count 매개 변수

count 매개변수를 사용하면 기본 반복문을 수행할 수 있습니다. 이를 응용하여 기본 조건문 작업을 수행할 수 있습니다. 먼저 if문을 살펴보고 이어서 if-else문으로 넘어가겠습니다.

count 매개 변수를 사용한 if문 if-statements with the count parameter

  • 챕터 4 ASG 환경에서 조건부로 일부 사용자에게는 모듈을 생성해주고 나머지 사용자에게는 생성해주지 않을 방법이 있을까요?
  • 이렇듯 조건부로 모듈을 생성하는 방법을 분기 처리라고 합니다.
  • 분기 처리 첫 번째 단계는 모듈의 오토스케일링 사용 여부를 지정하는데 사용할 Boolean 입력 변수 를 modules/services/webserver-cluster/variables.tf에 추가하는 것입니다.
    • 코드 - 링크
variable "enable_autoscaling" {
  description = "If set to true, enable auto scaling"
  type        = bool
}

<CONDITION> ? <TRUE_VAL> : <FALSE_VAL>
  • count 매개변수와 두 가지 특성을 활용하여 동작 수행할 수 있다
    1. 리소스에 count 를 1로 설정하면 해당 리소스의 사본 하나를 얻습니다. count 를 0으로 설정하면 해당 리소스가 만들어지지 않습니다.
    2. 테라폼은 ** ? : ** 형식의 조건 표현식 conditional expression 을 지원합니다.
      1. CONDITION 에서 boolean logic 를 평가하고 결과가 ture dㅣ며 TRUE_VAL을 반환하고, 결과가 false이면 FALSE_VAL을 반환
  • 위 2가지 특성을 종합하여 아래 코드 처럼 webserver-cluster 모듈을 업데이트 할 수 있다
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
}
  • .
    • var.enable_autoscaling 가 true 인 경우 각각의 aws_autoscaling_schedule 리소스에 대한 count 매개 변수가 1로 설정되므로 리소스가 각각 하나씩 생성됩니다.
    • var.enable_autoscaling 가 false 인 경우 각각의 aws_autoscaling_schedule 리소스에 대한 count 매개 변수가 0로 설정되므로 리소스가 생성되지 않습니다.
  • enable_autoscaling 가 false 로 설정하여 오토스케일링을 비활성화하기 위해, live/stage/services/webserver-cluster/main.tf에 있는 스테이징에서 다음과 같이 이 모듈의 사용법을 업데이트 할 수 있습니다.
    • 코드 - 링크
module "webserver_cluster" {
  source = "../../../../modules/services/webserver-cluster"

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

  instance_type        = "t2.micro"
  min_size             = 2
  max_size             = 2
  **enable_autoscaling   = false**
}
  • 마찬가지로 enable_autoscaling 가 true 로 설정하여 오토스케일링을 활성화하기 위해, live/prod/services/webserver-cluster/main.tf에 있는 프로덕션에서 이 모듈의 사용법을 업데이트 할 수 있습니다.
    • 코드 - 링크
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"
  }
}

count 매개 변수를 사용한 if-else문 if-else-statements with the count parameter

  • IAM 사용자 neo 에 클라우드워치에 대한 액세스 권한을 부여하려고 합니다.
  • 그런데 이때 테라폼 구성을 적용하는 사람이 neo에게 읽기 권한만 부여할 것인지 아니면 읽기와 쓰기 권한을 모두 부여할 것인지 결정하게 한다고 가정하겠습니다.
    • 읽기 전용 IAM 정책
resource "aws_iam_policy" "**cloudwatch_read_only**" {
  name   = "**cloudwatch-read-only**"
  policy = data.aws_iam_policy_document.cloudwatch_read_only.json
}

data "aws_iam_policy_document" "cloudwatch_read_only" {
  statement {
    effect    = "Allow"
    actions   = [
      "cloudwatch:Describe*",
      "cloudwatch:Get*",
      "cloudwatch:List*"
    ]
    resources = ["*"]
  }
}
  • .
    • 읽기/쓰기 IAM 정책
resource "aws_iam_policy" "**cloudwatch_full_access**" {
  name   = "**cloudwatch-full-access**"
  policy = data.aws_iam_policy_document.cloudwatch_full_access.json
}

data "aws_iam_policy_document" "cloudwatch_full_access" {
  statement {
    effect    = "Allow"
    actions   = ["cloudwatch:*"]
    resources = ["*"]
  }
}
  • if 나 else 중 하나가 실행되면 나머지 테라폼 코드는 둘 중 어느 것이 실행되었는지도 알 필요가 없습니다.
  • give_neo_cloudwatch_full_access라는 새로운 입력 변수의 값에 기반해 이러한 IAM 정책 중 하나를 neo에 연결하는 것이 목표입니다.
variable "give_neo_cloudwatch_full_access" {
  description = "If true, neo gets full access to CloudWatch"
  type        = bool
}
  • 테라폼에서 이 동작 수행을 위해서 count 매개변수와 각 리소스에 대한 조건 표현식을 사용할 수 있습니다.
resource "aws_iam_user_policy_attachment" "neo_cloudwatch_full_access" {
  count = var.give_neo_cloudwatch_full_access ? 1 : 0

  user       = aws_iam_user.example[0].name
  policy_arn = aws_iam_policy.cloudwatch_full_access.arn
}

resource "aws_iam_user_policy_attachment" "neo_cloudwatch_read_only" {
  count = var.give_neo_cloudwatch_full_access ? 0 : 1

  user       = aws_iam_user.example[0].name
  policy_arn = aws_iam_policy.**cloudwatch_read_only**.arn
}
  • .
    • 위 코드에는 2개의 aws_iam_user_policy_attachment리소스가 포함되어 있습니다.
    • 윗 부분은 전체 액세스 권한을 첨부하는 if절 이고, 아래는 읽기 전용 권한을 첨부하는 else 절입니다.
    • 위 접근 방식은 테라폼 코드가 if 나 else 절을 알필요가 없습니다.
    • 그러나 if 나 else 절에서 나오는 리소스의 출력 속성에 액세스 해야 하는 경우 어떻게 해야 할까요?
  • 예를 들어 webserver-cluster 모듈에서 서로 다른 2개의 사용자 데이터 스크립트를 제공하고 사용자가 어느 스크립트를 실행할지 선택할 수 있게 하려면 어떻게 해야 할까요?

for_each 와 for 표현식

for_each 와 for 표현식을 사용한 조건문 Conditionals with for_each and for Expressions

  • 이제 for_each 표현식으로 유사한 전략을 사용하여 조건 논리를 수행할 수 있습니다.
  • for_each 표현식을 빈 컬렉션으로 전달하면 0개의 리소스 또는 0개의 인라인 블록을 생성합니다.
  • 비어 있지 않은 컬렉션을 전달하면 하나 이상의 리소스 또는 인라인 블록을 만듭니다.
  • 그렇다면 컬렉션이 비어 있는지 여부를 조건부로 어떻게 결정할 수 있을까요? ⇒ for_each 표현식과 for 표현식의 결합
  • 예를 들어 modules/services/webserver-cluster/main.tf 의 webserver-cluster 모듈이 태그를 어떻게 설정하는지 다시 확인합니다.
dynamic "tag" {
    for_each = var.custom_tags

    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
  • var.custom_tags 가 비어 있으면 for_each 표현식에는 반복할 항목이 없으므로 태그가 설정되지 않습니다.
  • 다시 말해 여기에는 이미 몇 가지 조건 논리가 있습니다.
  • 그러나 아래와 같이 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 표현식은 일관성을 위해 var.custom_tags 를 반복하며 각 값을 대문자로 변환하고 모듈이 이미 자체 Name 태그를 설정했으므로,
    • for 표현식의 조건을 사용하여 key 집합을 Name 으로 필터링합니다.
  • for 표현식에서 값을 필터링하여 임의 조건부 논리를 구현할 수 있습니다.
  • 리소스의 복사본을 여러 개 만들 때는 count 보다 for_each 를 사용하는 것이 더 낫지만,
    • 조건 논리의 경우 비어 있지 않은 컬렉션에 for_each 를 설정하는 것보다 count 를 0 또는 1로 설정하는 것이 간단합니다.
  • 즉, 리소스를 조건부로 생성할 때는 count 를 사용할 수 있지만, 그 외 모든 유형의 반복문 또는 조건문에는 for_each 를 사용합니다.

if 문자열 지시자

if 문자열 지시자가 있는 조건문 Conditionals with the if String Directive & 실습7

  • if 문자열 지시자를 살펴보겠습니다
%{ if <CONDITION> }<TRUEVAL>%{ endif }
  • .
    • CONDITION은 boolean 으로 평가되는 표현식이고, TRUEVAL은 CONDITION이 True로 평가되면 렌더링할 표현식입니다.
    • 다음과 같이 else절을 선택적으로 포함할 수 있습니다.
%{ if <CONDITION> }<TRUEVAL>%{ else }<FALSEVAL>%{ endif }
  • .
    • FALSEVAL은 CONDITION이 false로 평가되면 렌더링할 표현식입니다.
  • 예시 코드 입니다.
cat <<EOT > main.tf
variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["gasida", "akbun", "fullmoon"]
}

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
}
EOT
  • init & plan & apply
#
terraform init && terraform plan && terraform apply -auto-approve
Outputs:

for_directive = "gasida, akbun, fullmoon, "
for_directive_index = "(0) gasida, (1) akbun, (2) fullmoon, "
for_directive_index_if = <<EOT

  gasida,

  akbun,

  fullmoon

EOT

# 확인
terraform output
  • .
    • 줄 바꿈 추가됨, 스페이스나 줄 바꿈 같은 공백을 없애기 위해 문자열 지시자의 앞이나 뒤에 물결표(~)를 사용할 수 있다.
  • 문자열 지시자 시작에 공백이 있으며 지시자 앞에, 문자열 지시자 끝에 공객이 있으면 지시자 뒤에 물결표를 사용합니다.
cat <<EOT > main.tf
variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["gasida", "akbun", "fullmoon"]
}

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
}

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

  gasida,

  akbun,

  fullmoon

EOT
for_directive_index_if_strip = "  gasida,   akbun,   fullmoon"
  • else 를 활용하여 끝에 마침표(.)를 찍어보자
cat <<EOT > main.tf
variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["gasida", "akbun", "fullmoon"]
}

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
}

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

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
}
EOT
#
terraform apply -auto-approve
...
for_directive_index_if_else_strip = "  gasida,   akbun,   fullmoon"
for_directive_index_if_strip = "  gasida,   akbun,   fullmoon"
  • 실습 리소스 삭제
terraform destroy -auto-approve
rm <코드 파일 삭제>.tf


terraformstudy Share Tweet +1