05. 테라폼 팁과 요령: 반복문, 조건문, 배포 및 주의사항
이 문서는 책 "테라폼 설치에서 운영까지"을 읽고 작성되었습니다. 최대한 요약해서 책 내용을 최소로 하는데 목표를 두고 있어서, 더 친절하고 정확한 내용을 원하신다면 책을 구매하시는 것을 추천드립니다. => 책 링크
Terraform
은 선언형 언어이다. 이는 인프라의 최종 형태를 추론하는데 적절하지만 반대로 절차적인 작업, 반복하거나, 조건에 따라 분기하거나, 무중단 배포 같은 작업들을 표현하기는 어려운 언어이다. 5장에서는 Terraform
을 통해서 반복문, 조건문, 무중단 배포 작업 등에 대해 공부할 수 있다.
반복문
Terraform
에서 반복문을 표현하는 것 중 가장 간단한 것은 바로 count
이다. 예를 들어서 AWS IAM user
를 3개를 반복적으로 생성해야 한다고 해보자. 그럼 이렇게 표현할 수 있다.
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_user" "example" {
count = 3
name = "gurumee"
}
이 때, AWS IAM user
는 안 만들어진다. 왜냐하면, 한 계정당 전역적으로 이름은 유일해야하기 때문이다. 이를 해결하기 위해서 다음과 같이 변경하면 된다.
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_user" "example" {
count = 3
name = "gurumee.${count.index}"
}
이렇게 하면 gurumee.0
, gurumee.1
, gurumee.2
이름의 AWS IAM user
가 3개 만들어진다. 그리고 최근 언어들은 모두 for-each
형태의 반복문을 지원하는데 가령 리스트, 맵 등의 컬렉션 데이터 타입을 순회하는 반복문을 뜻한다. Golang
으로 따지면 다음의 코드이다.
user_names := []string{"gurumee", "brenden", "ingoo"}
for idx, user_name := range user_names {
fmt.Println("idx: ", idx, " item: ", user_name)
}
위와 같은 코드 역시 Terraform
에서 지원한다. 먼저 variables.tf
를 다음과 같이 만든다.
src/ch05/global/iam/variables.tf
variable "user_names" {
type = list(any)
description = "create iam users with three names"
default = ["gurumee", "brenden", "ingoo"]
}
# ...
위는 리스트 데이터 타입을 선언한 것이다. 기본 값으로 세개의 이름이 들어있다. 그 후 main.tf
를 다음과 같이 만들면 된다.
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_user" "example" {
count = length(var.user_names)
name = element(var.user_names, count.index)
}
# ...
여기서 length
함수는 리스트 user_names
의 길이를 구할 수 있다. 기본 값이 들어가면 count=3
이 된다. 또한, element
함수는 첫 번째 인자로 리스트, 두 번째 인자로 접근해야 할 인덱스를 받는다. 따라서, 차례대로 user_names[0]
, user_names[1]
, user_names[2]
의 이름을 할당하게 된다. 따라서 AWS IAM user
는 총 3개, 각각의 이름은 gurumee
, brenden
, ingoo
가 할당된다. Terraform
역시 이러한 for-each
형태의 반복은 리스트 뿐 아니라 맵 데이터 타입도 가능하다.
조건문
책에서는 예제가 복잡한데 간소하게 변경해보았다. 먼저 Terraform
은 "조건식"을 제공한다.
condition ? true_val : false_val
이런식으로 condition
이 참이면 true_val
을 거짓이면 false_val
을 할당하는게 가능하다. 예를 들면 다음 코드를 살펴보자.
# ...
resource "aws_iam_user_policy_attachment" "gurumee_cloudwatch_full_access" {
count = var.give_gurumee_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" "gurumee_cloudwatch_read_only" {
count = var.give_gurumee_cloudwatch_full_access ? 0 : 1
user = aws_iam_user.example.0.name
policy_arn = aws_iam_policy.cloudwatch_read_only.arn
}
위 코드는 give_gurumee_cloudwatch_full_access
변수가 참이면 AWS CloudWatch
에 대한 모든 액세스 권한을, 거짓이면 Read Only 권한을 주는 코드이다. 먼저 give_gurumee_cloudwatch_full_access
이라는 변수가 src/ch05/global/iam/variables.tf에 작성되어 있다. 이는 bool
변수이다.
gurumee_cloudwatch_full_access
는 give_gurumee_cloudwatch_full_access
가 참이면 count
값이 1이 되어 1개 생성된다. 거짓이면 0이라서 생성되지 않는다. 반대로, gurumee_cloudwatch_read_only
는 give_gurumee_cloudwatch_full_access
가 참이면 count
값이 0이 되어 생성되지 않고 거짓이면 1이라서 1개 생성하게 된다. 이렇게 조건문 if-else
를 표현할 수 있다.
사실 개인적인 생각으로는 한 조건에 따른 유사한 리소스 묶음을 2개나 만들어야 하는게 비효율적으로 보이긴 한데, 이렇게 쓰라니 넘어가야지..
무중단 배포하기
먼저 AWS ELB
를 통해서, 무중단 배포 과정을 살펴보자.
- Launch Configuration이 새로 만들어진다.
- AutoScaling Group이 (1)을 통해서 새로 만들어진다.
- (2)가 만들어지면 ELB는 (2)를 참조하게 된다.
- 이전에 만들어진 Launch Configuration과 AutoScaling Group은 삭제된다.
Terraform
을 통해서 어떻게 표현할 수 있을까? 이를 위해서 Terraform
은 메타 아규먼트로 lifecycle
이란 것이 있다. lifecycle
에는 다음의 형태로 리소스를 관리할 수 있게 도와준다.
- create_before_destroy : 대체하는 리소스 생성 후 파괴한다.
- prevent_destroy : 리소스를 파괴할 수 없게 한다.
- ignore_changes : 리소스가 변경되도 무시한다.
여기서 create_before_destroy
를 이용하면 무중단 배포를 구현할 수 있다. 다음 코드를 살펴보자.
src/ch05/modules/services/webserver-cluster/main.tf
# ...
resource "aws_launch_configuration" "example" {
image_id = "ami-0d5eff06f840b45e9"
instance_type = var.instance_type
security_groups = [aws_security_group.instance.id]
user_data = templatefile("${path.module}/user-data.tpl", {
server_port = var.server_port,
server_text = var.server_text,
db_address = data.terraform_remote_state.db.outputs.address,
db_port = data.terraform_remote_state.db.outputs.port,
})
# 여기 주목!
lifecycle {
create_before_destroy = true
}
}
resource "aws_autoscaling_group" "example" {
# 여기 주목!
name = "${var.cluster_name}-${aws_launch_configuration.example.name}"
launch_configuration = aws_launch_configuration.example.id
availability_zones = data.aws_availability_zones.all.names
load_balancers = [aws_elb.example.name]
health_check_type = "ELB"
min_size = var.min_size
max_size = var.max_size
# 여기 주목!
lifecycle {
create_before_destroy = true
}
tag {
key = "Name"
value = var.cluster_name
propagate_at_launch = true
}
}
# ...
주목해야 하는 것은 aws_launch_configuration
, aws_autoscaling_group
리소스들의 메타 아규먼트로 lifecycle
에 create_before_destroy
가 true로 지정된 것이다. 또한 aws_autoscaling_group
은 launch_configuration
에 할당된 값이 변경되도 수동으로 인스턴스 개수를 조절하지 않는 한 자동으로 무중단 배포를 하지 않는다.
name
이 바뀌지 않아서 그러는데 다행히 aws_launch_configuration
는 업데이트 될 때마다 이름이 바뀐다. 그래서 이를 참조하도록 aws_autoscaling_group
의 name
에서 aws_launch_configuration
의 이름을 참조하게 만든다.
실제 값을 변경하면 무중단 배포가 일어나는 것을 확인할 수 있다. 다음 업데이트 이전 코드
로 인프라스트럭처를 배포한 후 업데이트 이후 코드
로 인프라스트럭처를 수정해보자. 변경되는데 5-10분 정도 시간이 걸리니 천천히 확인하면 된다.
업데이트 이전 코드
src/ch05/prod/services/webserver-cluster/main.tf
# ...
module "webserver_cluster" {
source = "github.com/gurumee92/today-i-learned//terraform_up_and_running/src/ch05/modules/services/webserver-cluster"
cluster_name = "websever-prod"
db_remote_state_bucket = "gurumee-terraform-state"
db_remote_state_key = "prod/data-stores/mysql/terraform.tfstate"
instance_type = "m4.large"
min_size = 2
max_size = 10
server_port = 8080
# 이 부분이 바뀐다.
server_text = "Hello Version1"
enable_autoscaling = true
}
업데이트 이후 코드
src/ch05/prod/services/webserver-cluster/main.tf
# ...
module "webserver_cluster" {
source = "github.com/gurumee92/today-i-learned//terraform_up_and_running/src/ch05/modules/services/webserver-cluster"
cluster_name = "websever-prod"
db_remote_state_bucket = "gurumee-terraform-state"
db_remote_state_key = "prod/data-stores/mysql/terraform.tfstate"
instance_type = "m4.large"
min_size = 2
max_size = 10
server_port = 8080
# 이 부분이 바뀐다.
server_text = "Hello Version2"
enable_autoscaling = true
}
주의 사항
책에서 소개된 몇 개의 문제점은 다음과 같다.
- 카운트의 제한
- 무중단 배포의 제한 사항
- 유효한 계획의 실패
- 까다로운 리팩토링
- 강력한 일관성과 최종 일관성
이중 count
의 제한과 무중단 배포 제한 사항은 2021년 현재 해결되었다. 유효한 계획의 실패는 빈번히 일어난다. A가 AWS Console
에서 "gurumee" 계정을 이미 만든 상태에서 B가 Terraform
으로 "gurumee" 계정을 만들 때 terraform plan
은 성공이지만 terraform apply
는 실패한다. 이미 같은 이름의 사용자가 있기 때문이다. 이 문제에 대해서는 책에서는 이렇게 대처할 것을 권고한다.
- 테라폼을 사용하기 시작했다면 계속 테라폼으로 관리한다.
- 기존 인프라가 있는 경우 terraform import 명령어를 활용한다.
까다로운 리팩토링 문제는 뭐.. 여느 프로그래밍 언어나 솔루션을 이용하는 스크립트나 동일할 것이다. 책에서는 리팩토링 과정에서 다음을 할 것을 추천하고 있다.
- 항상 terraform plan 명령어를 사용한다.
- 삭제 전에 생성한다. (create_before_destroy를 적극 활용하자)
- 모든 식별자는 불변하다.
- 일부 변수는 불변하다.
마지막 문제는 가끔 발생한다. 특히 terraform apply
옵션에 -parallelism=n
이 있다. 이 때 n에 숫자를 넣어주면 n 숫자만큼 병렬적으로 인프라스트럭처를 구성하게 된다. 매우 빠르게 구성할 수 있어서 좋지만 많은 리소스들을 병렬적으로 생성하다보면, A 리소스를 참조하는 B 리소스가 A가 채 생성되기도 전에 생성되서 실패하는 경우가 있다. 이 때는 여러번 실행을 한다든가 병렬 옵션을 제거하는 방법 등으로 일차원적으로 해결할 수 있다. 큰 문제는 아니기 때문에, 이 정도만 알고 넘어가자.
5장 전체 코드
5장 전체 코드는 다음 링크에서 확인할 수 있다.