본문 바로가기
DevOps/Terraform

[Terraform] 테라폼의 팁과 요령: 반복문, if문, 배포 및 주의사항

by BenKangKang 2023. 4. 2.

개요

테라폼은 선언적 언어로써 추론하기 쉬운 장점이 있다.

다만 선언적 언어는 보통 반복문, 조건문이 없어 논리적인 수행이 어렵다. 테라폼은 이를 보완하기 위한 다양한 기능들을 제공한다.

1. 반복문

테라폼은 조금씩 다른 상황에 사용하도록 고안된 몇 가지 반복문 구성을 제공한다

  • count 매개 변수: 리소스를 반복
  • for_each 표현식: 리소스 내에서 리소스 및 인라인 블록을 반복
  • for 표현식: 리스트와 맵을 반복
  • for 문자열 지시어: 문자열 내에서 리스트와 맵을 반복

1.1 count 매개 변수를 이용한 반복

count?

  • 같은 리소스를 n개 생성할 때 사용하는 메타데이터.
  • 가장 오래되고 단순한 반복 구조.

상황

  • 단순한 자원의 사본을 생성하는 경우 사용.

예시

  • count.index 사용해서 interation 인덱스를 얻을 수 있음.

특징

1. 리소스 배열이 생성되기 때문에 Array lookup 구문을 사용해야 한다.

선언

Array lookup 예시

스플랫 연산자로 전체 ARN을 출력하는 예시

2. count 를 사용하여 전체 리소스를 반복할 수는 있지만 인라인 블록을 반복할 수 없다

resource "aws_autoscaling_group" "example" {
  // 태그 같은 인라인 블록에 count 를 지정할 수 없다.
  tag {
    key                 = "Name"
    value               = "terraform-asg-example"
    propagate_at_launch = true
  }
}

3. count 매개변수 사용하는 경우 해당 배열의 index로 리소스를 식별하기 때문에 변경에 유의해야 한다.

아래와 같이 3개의 유저를 생성했다고 가정

이때 index 1에 위치한 trinity 을 삭제해서 apply한다면? index 를 식별자로 보기 때문에 trinity 는 morpheus 로 변경되고, 기존의 morpheus 가 삭제된다.

실제 예시

  • count를 사용하는 경우는 거의 없었음.
  • 중간에 삭제되어도 상관 없는 리소스에만 사용하는 것으로 보임.
resource "aws_autoscaling_attachment" "asg_attachment_0" {
  count = length(var.autoscaling_group_names)

  alb_target_group_arn   = aws_lb_target_group.tg.arn
  autoscaling_group_name = var.autoscaling_group_names[count.index]
}
resource "kubernetes_config_map" "redis-failover-haproxy" {
  count = var.delete ? 0 : 1

1.2 for_each 표현식 사용한 반복문 처리

for_each?

  • 리스트, 집합, 맵을 사용해서 전체 리소스의 복사본을 만들 수 있는 구문.
    • 정확히 for_each 구문은 집합과 맵만 지원.
  • 리소스 내 인라인 블록의 복사본도 생성할 수 있음.

상황

  • count 를 사용하면 안되는 상황에 사용 → 자원 각각이 고유한 경우 (trinity, morpheus …)

예시

resource "aws_iam_user" "example" {
  for_each = toset(var.user_names)
  name     = each.value
}
  • 리스트를 집합으로 변환하여 사용하는 예시
  • each.key, each.value 를 사용하여 항목키와 값에 접근할 수 있음.

특징

1. 모든 ARN을 추출해야 할 경우 약간의 추가 작업이 필요하다.

output "all_arns" {
  value = values(aws_iam_user.example)[*].arn
}

2. 컬렉션 중간 항목도 안전하게 제거할 수 있다.

index를 식별자로 사용하는 count 와 달리 컬렉션 중간 항목을 삭제하더라도 정확히 목표한 리소스만 삭제할 수있다.

3. 동적 인라인 블록을 만들 수 있다.

resource "aws_autoscaling_group" "example" {
  ...

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

  dynamic "tag" {
    // 1. 이곳에서 그냥 배열을 넘기면 key는 index, value는 항목이 된다.
    // 2. 맵을 넘길 경우 key, value는 맵의 키-값 쌍 중 하나가 된다.
    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
    }
  }

}

1.3 for 표현식을 이용한 반복문

for?

  • 단일 값을 생성하기 위해 반복이 필요한 경우 사용

상황

  • 리스트 값을 변경할 때
  • 리스트 값을 필터링할 때

예시

리스트

variable "names" {
  description = "A list of names"
  type        = list(string)
  default     = ["neo", "trinity", "morpheus"]
}

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     = {
    neo      = "hero"
    trinity  = "love interest"
    morpheus = "mentor"
  }
}

# 배열로 출력
output "bios" {
  value = [for name, role in var.hero_thousand_faces : "${name} is the ${role}"]
}
# 출력 bios = ['neo is the hero', ...]

# 맵으로 출력
output "upper_roles" {
  value = {for name, role in var.hero_thousand_faces : upper(name) => upper(role)}
}
# 출력 { "NEO" = "HERO" ... }

실제 예시

리스트 출력 예시

resource "aws_lb" "alb" {
  tags = {
    Domain = join(" ", [for domain in var.domains: replace(domain, "*.", "")])
  }
}
	domains = [
    "*.naver.com",
    "naver.com"
  ]

맵 출력 예시

# Assume that you have the issued certificate
data "aws_acm_certificate" "cert" {
  domain   = var.cert_domain
  statuses = ["ISSUED"]
}
hosts = merge(
    {for domain in data.aws_acm_certificate.cert.*.domain : domain => domain}

1.4 문자열 지시자를 사용하는 반복문

  • 문자열 보간과 같이 for, if 를 제어할 수 있다.
  • 백분율 부호(%{…})를 사용한다.
output "for_directive" {
  value = <<EOF
%{ for name in var.names }
  ${name}
%{ endfor }
EOF
}
  • 물결표를 사용해 줄 바꿈 같은 공백을 없앨 수 있다.
output "for_directive_strip_marker" {
  value = <<EOF
%{~ for name in var.names }
  ${name}
%{~ endfor }
EOF
}

2. 조건문

  • 테라폼에서 조건을 설정하는 여러 가지 방법이 있으며 상황에 따라 사용함.
    • count 매개변수
    • for_each, for 표현식
    • IF 문자열 지시자

2.1 count 매개 변수를 사용한 조건문

  • 조건부로 모듈을 생성하는 분기 처리를 할 수 있음

예시

variable "enable_autoscaling" {
  description = "If set to true, enable auto scaling"
  type        = bool
}

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

복잡 조건

  • boolean 으로 해결할 수 없는 경우
resource "aws_cloudwatch_metric_alarm" "low_cpu_credit_balance" {
  count = format("%.1s", var.instance_type) == "t" ? 1 : 0> 

2.2 for_each와 for 표현식을 사용한 조건문

for, for_each 결합

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

dynamic "tag" {
  // 태그에서 이미 Name 을 지정했기 때문에 Name 은 제외한다.
  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
  }
}

202 ~267

2.3 if 문자열 지시자가 있는 조건문

  • %{ if <CONDITION> } <TRUEVAL> %{ else } <FALSEVAL>%{ endif }
output "if_else_directive" {
  value = "Hello, %{ if var.name != "" }${var.name}%{ else }(unnamed)%{ endif }"
}
  • Hello, World
  • Hello, (unnamed)

3. 무중단 배포

  • 사용자에게 시스템 중단 없이 배포하는 방법
    • 블루 - 그린
    • 롤링 업데이트
    • 블가변적 배포 방법

create_before_destroy 수명 주기를 이용한 무중단 배포

  • false
    • 기존 ASG
  • true
    • ASG 먼저 만든 다음, 원래 ASG를 파기하도록 동작이 변경됨
    • 버전2 min_elb_capacity 서버가 ALB 에 등록되고, 해당 ASG의 서버를 등록 취소한 다음 서버를 종료하여 기존 ASG를 멈추게된다.

 

4. 테라폼의 주의 사항

4.1. count와 for_each의 제한 사항

  1. 리소스 출력을 count 또는 for_each에서 참조할 수 없음.
  2. module 구성 내에서는 count 또는 for_each를 사용할 수 없음.

리소스 출력을 count 또는 for_each에서 참조할 수 없음

resource "random_integer" "num_instances" {
  min = 1
  max = 3
}

resource "aws_instance" "example_3" {
	count = ramdom_integer.number_instances.result
	ami = "ami-..."
  instance_type = "t2.micro"
}

  • 리소스 생성하거나 수정하기 전에 plan 단계에서 for, for_each를 계산할 수 있어야한다.
  • 즉 하드 코딩된 값, 변수, 데이터 소스 및 리소스의 리스트(plan 단계에서 리스트의 길이를 결정할 수 있는 한) 등은 참조할 수 있지만 계산된 리소스 출력은 참조할 수 없다.

모듈 구성 내에서 count 또는 for_each를 사용할 수 없음.

module "count_example" {
	source = "../../../modules/services/webserver-cluster"
	count = 3

	cluster_name = "terraform-up-and"
  server_port = 8080
	instance_type = "t2.micro"
}
  • terramform 0.12.6에서는 module에 count와 for_each 사용을 지원하지 않는다.

4.2 무중단 배포의 제한 사항

현실

create_before_destroy 사용하는 것은 무중단 배포를 위한 훌륭한 기술이지만 한 가지 제약 사항이 있다.

오전 9시에 클러스터 수를 2에서 10으로 늘리는 늘리는 aws_autoscaling_schedule 리소스가 포함되어 있을 때, 11시에 배포를 실행하면 10대가 아닌 2대가 구동된다.

해결

  1. 특정 시간이 아닌 범위를 지정한다
    • 오전 9시(0 9 * * *) → 오전 9시 ~ 오후 5시(0-59 9-15 * * *)
  2. AWS API 이용한 스크립트 방식
    1. AWS API 삿용해 실행 중인 서버 파악하는 스크립트 작성하고 external 데이터 소스 사용해 이 스크립트 호출 후 ASG의 desired_cappacity 매개 변수를 이 스크립트가 반환하는 값으로 설정하는 방법
    2. 이식성이 떨어진다.

4.3 유효한 plan의 실패

plan 명령에는 문제가 없다가 apply시 오류가 발생할 수 있습니다.

예시

  • 중복되는 이름을 가진 IAM 사용자를 생성하려고할 때
    • 정확히는 콘솔에서 수동으로 생성한 리소스와 중복될 때

해결

  • 테라폼을 사용하기 시작했다면 테라폼만 사용해야 한다.
  • 기존 인프라가 있는 경우 terraform import을 사용한다
    • $ terraform import aws_iam_user.existing_user <name>

4.4 리팩터링의 까다로움

  • 외부 동작을 변경하지 않고 기존 코드 내부의 세부 정보를 재구성하는 작업
  • 코드형 인프라에서 이름을 바꾸면 시스템 중단이 발생할 수 있음.

해결

  1. 항상 plan 명령 사용하기
  2. 파기하기 전에 생성하기(create_before_destroy)
  3. 식별자 변경할 때 terraform state mv 사용하기

4.5 최종 일관성

  • 변경사항이 전체 시스템에 전파되는 데 시간이 걸리므로 일관성 없는 응답을 받을 수 있음.
  • 예시
    • AWS EC2 인스턴스를 배포한 경우, 실제 구동되기까지 시간이 걸림 그 전까지는 404 와 같은 오류가 발생할 수 있음.

5. 결론

  • count, for_each, for, create_before_destroy 등 내장 함수 같은 많은 도구가 있다.
  • 잘 활용하여 코드를 잘 유지관리하자.

댓글