Automation/Terraform

[Terraform 5장] Terraform Tips andTricks: Loops, If-Statements,Deployment

[앙금빵] 2025. 1. 12.

 

본 글은 Terraform Up& Running 책에 대하여 5장에 대한 내용 정리 글이다.


개요

선언형 언어의 특징을 가지는 테라폼은 절차적 언어보다 현재 인프라 상태를 더 명확하게 파악하는데 있어 장점을 제공하나, 반복문(for), 조건문(if)과 같이 절차적 로직이 없거나 제한적이라서 복잡한 작업을 수행하기 어렵다는 단점을 가진다.

 

그렇다면 아래와 같은 문제상황에 봉착하게 되는데,

  • 여러 개의 유사 리소스를 생성할 때(예: 동일한 구성의 서버 3대를 만드는 상황)에 전통적인 for 루프가 없으니, 어떻게 반복 처리를 해야 할까?
  • 선언형 언어에서 if 문이 제한적이므로, 사용자에 따라 특정 리소스만 선택적으로 생성하려면 어떻게 해야 할까?
  • 무중단 배포처럼 절차적 개념을 선언형 언어로 표현하려면 어떻게 접근해야 할까?

Terraform은 count, for_each, for 표현식 등의 메타 파라미터와 삼항 연산자(ternary operator), create_before_destroy라는 라이프사이클 블록, 그리고 다양한 내장 함수를 통해 반복문과 조건문, 무중단 배포 등의 과정을 어느 정도 구현할 수 있도록 지원한다.


Loop

5-1. ‘count’ 구문

count는 “몇 개의 리소스를 만들 것인가?”라는 수를 지정할 때 사용한다. 예를 들어 count = 3이면 한 개의 리소스 블록이 세 번 복제되어 총 세 개가 만들어진다. 단, count는 단순히 리소스(또는 모듈) 전체를 반복 생성해 줄 뿐, 리소스 내부의 일부 블록을 반복 처리하는 데는 사용할 수 없다. 아래 IAM 여러 사용자 만드는 예시를 살펴보자.

 

한 명만 만드는 코드

resource "aws_iam_user" "example" {
  name = "neo"
}

 

세 명 만들기

만약 일반적인 프로그래밍 언어였다면 for (i=0; i<3; i++) { ... }로 만들 수 있겠지만, Terraform은 절차적 언어가 아니므로 이런 문법이 없다. 대신 count = 3를 이용해 다음과 같이 작성한다.

resource "aws_iam_user" "example" {
  count = 3
  name  = "neo"
}

 

그러나, 이렇게 생성하면 동일한 이름이 세번 생성되기에 충돌이 발생한다. (각 사용자 이름은 고유해야 한다.) 이를 해결하기 위해 사용자별 고유한 이름을 가지게 해야하는데, count.index를 활용하여 현재 반복되는 인덱스 번호를 가져올 수 있다.

resource "aws_iam_user" "example" {
  count = 3
  name  = "neo.${count.index}"
}

이렇게 하면 “neo.0”, “neo.1”, “neo.2”처럼 고유한 이름들이 만들어진다.

 

가독성 높은 이름 사용하기

“neo.0” 대신 더 의미 있는 이름을 쓰고 싶다면, 변수(배열 형태)로 사용자 이름을 정의하고 length 함수를 통해 배열 길이를 계산하면 된다.

variable "user_names" {
  type    = list(string)
  default = ["neo", "trinity", "morpheus"]
}

resource "aws_iam_user" "example" {
  count = length(var.user_names)
  name  = var.user_names[count.index]
}

이렇게 구성하면 ["neo", "trinity", "morpheus"] 각각을 순회하면서 세 명을 생성할 수 있다. count 를 쓰면 해당 리소스는 단일 객체가 아니라 “배열 형태”가 된다. (예: aws_iam_user.example[0], aws_iam_user.example[1])

 

첫 번째 사용자의 ARN만 출력하려면

output "first_arn" {
  value       = aws_iam_user.example[0].arn
  description = "첫 번째 사용자 ARN"
}

 

모든 사용자의 ARN 목록을 출력하고 싶다면

output "all_arns" {
  value       = aws_iam_user.example[*].arn
  description = "모든 사용자 ARN 목록"
}

 

단순 리소스뿐 아니라 모듈에도 count를 적용 가능하다. 아래 예시는 count 구문을 활용하여 여러 IAM 사용자 생성하는 부분이다.

module "users" {
  source   = "../../../modules/landing-zone/iam-user"
  count    = length(var.user_names)
  user_name = var.user_names[count.index]
}

output "user_arns" {
  value       = module.users[*].user_arn
  description = "생성된 모든 IAM 사용자의 ARN"
}

 

Count 구문의 한계점

첫째. 인라인 블록 반복 불가

  • aws_autoscaling_group에서 tag { ... } 블록 같은 “리소스 내부의 블록”은 count로 반복 생성할 수 없다.

 

둘째.인덱스 기반 리소스 식별 문제

  • count는 리소스 위치(인덱스 번호)로 리소스를 식별한다.
  • 예를 들어, IAM 사용자 목록 ["neo", "trinity", "morpheus"] 중간에서 “trinity”를 빼면, 인덱스가 달라져서 terraform plan 시 기존 리소스들이 예상치 못하게 재생성·삭제될 수 있다.

 

이 때문에 실제 운영 환경에서 리스트 중간의 항목을 삭제하거나 순서를 바꾸는 등의 변경은 위험할 수 있다. 이러한 문제를 해결하기 위해 for_each (Terraform 0.12+)를 사용하는 것을 권장한다. for_each는 “인덱스 번호” 대신 “키”를 기준으로 리소스를 추적하기 때문에 중간 항목 삭제 시 리소스를 재생성하는 문제를 예방할 수 있다.


5-2. ‘for_each’ 구문

Terraform에서 count는 단순하고 빠르게 여러 리소스를 생성할 수 있는 방법이지만, 리스트 중간 요소를 삭제할 때 예상치 못한 리소스 재생성·삭제가 발생하는 단점이 존재한다. 이러한 문제를 해결하고 더 다양한 상황(예: 리소스 내부 블록 반복)에 대처하기 위해 나온 것이 바로 for_each이다.

 

5-2-1. for_each 사용법

리소스나 모듈에 for_each = <COLLECTION> 구문을 추가하여, 컬렉션(주로 map이나 set) 안의 요소를 순회하며 여러 개를 생성한다.

  • each.key와 each.value로 현재 반복되는 컬렉션 아이템의 키와 값을 사용할 수 있다.
  • count처럼 인덱스 번호(count.index)를 쓰지 않으므로, 리소스가 “인덱스”가 아니라 “키”로 관리된다.
resource "<PROVIDER>_<TYPE>" "<NAME>" {
  for_each = <COLLECTION>
  
  # 설정
  # each.key, each.value 사용 가능
}

 

예시: IAM 여러 사용자 만들기

variable "user_names" {
  type    = list(string)
  default = ["neo", "trinity", "morpheus"]
}

resource "aws_iam_user" "example" {
  for_each = toset(var.user_names)
  name     = each.value
}

이렇게 하면 ["neo", "trinity", "morpheus"] 각각을 순회하면서 IAM 사용자를 세 개를 생성한다.

 

리소스 접근

  • aws_iam_user.example는 “map” 형태가 된다.
  • aws_iam_user.example["neo"], aws_iam_user.example["trinity"] 식으로 각 사용자에 접근할 수 있다.
  • output "all_users" { value = aws_iam_user.example } 로 출력하면, 키로 “neo”, “trinity”, “morpheus”를 갖는 맵 전체가 출력된다.

모듈에서도 동일하게 for_each를 적용할 수 있다. 아래와 같이 적용함으로써 IAM 유저를 만드는 모듈이 반복 실행되어 각 사용자가 생성된다.

module "users" {
  source   = "../../../modules/landing-zone/iam-user"
  for_each = toset(var.user_names)
  user_name = each.value
}

output "user_arns" {
  value       = values(module.users)[*].user_arn
  description = "생성된 모든 IAM 사용자의 ARN"
}

 

5-2-2. 리소스 내부 블록 반복

for_each의 또 다른 유용한 기능은 리소스 내부의 인라인 블록을 반복적으로 생성하는 것이다.

아래 예시는 aws_autoscaling_group에서 여러 tag {} 블록을 동적으로 생성하고자 할 때이다.

dynamic "<VAR_NAME>" {
  for_each = <COLLECTION>
  content {
    # 반복해서 생성될 블록 내용
    key   = <VAR_NAME>.key
    value = <VAR_NAME>.value
    ...
  }
}

 

내용을 정리하면,

  • count가 단순한 “인덱스 기반” 반복이라면, for_each는 키(key) 기반 반복이라는 점이 가장 큰 차이다.
  • 중간에 항목을 제거할 때도 다른 리소스에 영향을 주지 않고 안전하게 삭제가 가능하므로, 일반적으로 여러 리소스를 만들 때는 for_each가 더 권장되어진다.
  • Dynamic blocks(dynamic "...")를 이용하면, 리소스 내부에 반복적으로 필요한 블록(예: tag)도 탄력적으로 생성할 수 있다.

5-3. ‘for’ 구문

for 구문은 변수나 아웃풋 레벨에서 목록(List)이나 맵(Map)을 “변환”하는 데 초점을 맞춘다. 파이썬의 리스트/딕셔너리 컴프리헨션과 유사한 기능이다.

 

5-3-1. 리스트 전체 변환

<예시: upper(name)로 대문자 변환>

  • for name in var.names를 통해 "neo", "trinity", "morpheus" 각각을 순회
variable "names" {
  type    = list(string)
  default = ["neo", "trinity", "morpheus"]
}

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

# >> ["NEO", "TRINITY", "MORPHEUS"] 형태의 리스트 생성

 

5-3-2. 필터링 적용

특정 조건을 만족하는 아이템만 결과 리스트에 포함하고자 할 때 이용된다.

<예시: length(name) < 5 조건을 만족하는 문자열만 대문자로 변환해 결과에 담음>

# 문법: [for<ITEM>in<LIST>:<OUTPUT>if<CONDITION>]
output "short_upper_names" {
  value = [for name in var.names : upper(name) if length(name) < 5]
}

# ["neo"]만 필터링되어 ["NEO"] 결과 출력

 

5-3-3 맵 변환

<예시: 맵 → 리스트 변환>

  • hero_thousand_faces 맵의 key는 neo, trinity, morpheus 등이고, value는 "hero", "love interest", "mentor" 등
  • ["neo is the hero", "trinity is the love interest", "morpheus is the mentor"] 형태의 리스트가 생성
#[for<KEY>,<VALUE>in<MAP>:<OUTPUT>]
variable "hero_thousand_faces" {
  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}"]
}

 

<예시: 맵 → 맵 변환>

#for<KEY>,<VALUE>in<MAP>:<OUTPUTKEY>=><OUTPUTVALUE>
output "upper_roles" {
  value = {
    for name, role in var.hero_thousand_faces :
    upper(name) => upper(role)
  }
}

 

결과

output "upper_roles" {
  value = {
    for name, role in var.hero_thousand_faces :
    upper(name) => upper(role)
  }
}

 

5-3-4. for expressions vs. for_each의 차이점

  • for_each: 리소스/모듈 단위 반복. 실제로 “여러 리소스”를 만들어낼 때 사용.
  • for expressions: 하나의 변수(list/map)를 다른 형태(list/map)로 가공할 때 사용. (리소스 자체를 생성하는 것이 아니라, “데이터 변환”이 주 목적)

5-4. 문자열 내부에서의 반복

Terraform에서는 "Hello, ${var.name}" 같은 코드를 작성할 수 있다. 그런데 여기에 단순 변수 참조뿐만 아니라 반복 구조도 사용할 수 있도록 가능하게 해주는 것이 “String Directives”이다.

 

String Directive는 “문자열에 반복문이나 조건을 녹여 넣어” 복잡한 문자열을 쉽게 구성하도록 도와주는 도구이다.

 

문자열 안에 %{ … } 구문을 사용하여, for 루프나 if 문 같은 간단한 제어문을 추가할 수 있다.

<예시>

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

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

  • var.names 리스트에서 ["neo", "trinity", "morpheus"]를 순회
  • 각 name에 대해 "<name>, "을 출력
  • 결과: "neo, trinity, morpheus, " (맨 뒤에 “, ”가 붙음)

 

<예시: 인덱스가 필요한 경우>

output "for_directive_index" {
  value = "%{ for i, name in var.names }(${i}) ${name}, %{ endfor }"
}
  • 결과: "(0) neo, (1) trinity, (2) morpheus, "
  • 인덱스와 이름을 함께 출력

Zero-Downtime Deployment

운영 중인 웹 서버 클러스터에 새 코드를 배포하면서도, 사용자들에게 다운타임을 주지 않는 방법에 대해 알아보자. 일반적으로 “서버를 교체”해야 하면 기존 서버를 내리고 새 서버를 띄우는 과정에서 잠시라도 서비스가 중단되는 경우가 있다. Terraform에서는 ASG(오토 스케일링 그룹)를 교체할 때 기존 ASG를 완전히 지우기 전에 새 ASG를 먼저 띄우는 방식을 통해, 다운타임 없이 배포(Zero-Downtime)를 구현할 수 있다.

 

아래와 같은 상황을 가정하자.

  • 코드 버전 v1 ASG가 이미 서비스 중.
  • 새 이미지/코드로 만든 v2 ASG를 배포하려고 terraform apply 실행.

 

핵심 아이디어는 Terraform의 lifecycle 블록 내에 있는 create_before_destroy = true 옵션을 통해, 새로운 ASG를 먼저 띄운 뒤, 기존 ASG를 제거하는 부분이다.

 

Step 1. AMI와 User Data를 변수로 노출

  • 배포 시점마다 “이번엔 어떤 AMI를 쓸까?” “서버에 어떤 텍스트(또는 환경 변수)를 전달할까?” 등을 쉽게 조정 가능
variable "ami" {
  description = "The AMI to run in the cluster"
  type        = string
  default     = "ami-0fb653ca2d3203ac1"
}

variable "server_text" {
  description = "The text the web server should return"
  type        = string
  default     = "Hello, World"
}

 

Step 2. Launch Configuration 설정

  • create_before_destroy = true: Launch Configuration(LC)을 교체할 때 새로운 LC를 먼저 만든 뒤 기존 것을 제거
resource "aws_launch_configuration" "example" {
  image_id        = var.ami
  instance_type   = var.instance_type
  security_groups = [aws_security_group.instance.id]

  user_data = templatefile("${path.module}/user-data.sh", {
    server_port  = var.server_port
    db_address   = data.terraform_remote_state.db.outputs.address
    db_port      = data.terraform_remote_state.db.outputs.port
    server_text  = var.server_text
  })

  lifecycle {
    create_before_destroy = true
  }
}

 

Step 3. Auto Scaling Group 설정

(1) name 항목에 Lauch configuration 포함

  • LC의 이름을 name 항목에 넣어줌으로써 Terraform이 ASG를 교체해야 함을 “감지”하게 한다.
  • LC는 변경될 때마다 이름이 새로 생성되어지기에 ASG.name도 따라 달라지므로 ASG 자체를 “새로운 리소스”로 인식하여 “교체(replace)” 과정을 실행하게 된다.
resource "aws_autoscaling_group" "example" {
  name = "${var.cluster_name}-${aws_launch_configuration.example.name}"
  launch_configuration = aws_launch_configuration.example.name
  ...
}

 

(2) create_before_destroy = true 설정

  • ASG 교체가 필요하면(=이름이 바뀌어야 함) Terraform은 새로운 ASG를 먼저 만들고, 새 인스턴스가 정상 동작(헬스 체크 통과)하는지 확인한다.
  • 만약 새 ASG가 건강 상태를 확보하면, 그제서야 기존 ASG를 제거한다.
  • 새 인스턴스가 ALB에 연결되어 트래픽을 처리하기 시작할 때까지, 기존 인스턴스는 계속 살아 있기 다운타임이 없게된다.
resource "aws_autoscaling_group" "example" {
  ...
  lifecycle {
    create_before_destroy = true
  }
}

 

(3) min_elb_capacity = var.min_size 설정

  • 새 ASG가 충분한 수의 인스턴스를 띄워서 ALB(로드 밸런서)에 등록 & 헬스 체크를 통과하기 전까지 기존 ASG가 제거되지 않도록 지연시키는 역할을 수행한다.
  • 예를 들어, min_size = 2라면, 새 ASG에서 최소 2대 인스턴스가 정상 작동 상태(Health OK)가 되어야 기존 ASG를 줄이기 시작한다.
resource "aws_autoscaling_group" "example" {
  ...
  min_elb_capacity = var.min_size
  ...
}

 

추가적으로,

  • wait_for_capacity_timeout 설정을 통해 새 인스턴스가 헬스 체크를 통과하지 못하면, Terraform은 배포 실패로 보고 새 ASG를 삭제(롤백)한다. 기존 ASG는 살려두어 서비스가 계속 운영되어질 수 있다.
  • 이런 식의 “헬스 체크 + 롤백” 메커니즘으로, 배포 중 문제가 생기면 서비스가 영향을 덜 받도록 설계할 수 있다.

 

 

 

댓글