테라폼 0.12 베타 출시 및 새로운 HCL 문법
들어가며: 테라폼(Terraform) 0.12 베타 1 출시
테라폼Terraform은 클라우드 상의 리소스들을 코드로 작성하고 관리할 수 있게 도와주는 도구로서 코드로서의 인프라스트럭처Infrastructure as Code를 실현합니다. 테라폼의 기본적인 사용법에 대해서는 44bits에서도 다룬 적이 있습니다.
작년 6월 하시코프Hashicorp 블로그에는 테라폼 0.12 버전의 새 기능을 소개하는 글이 연재되었습니다. 그 이후로 업데이트 없이 어느덧 일년이 다 되어 가는데요. 2019년 2월 28일, 드디어 베타 1 버전이 출시되었습니다. 이 글에서는 테라폼 0.12 베타 1 버전을 설치하는 방법과 개선된 HCL 문법에 대해서 소개합니다.
테라폼 0.12 베타 1 설치
테라폼 0.12 베타 1 버전을 사용하려면 먼저 설치를 해야합니다. 하시코프에서는 각 운영체제 별로 바이너리 파일을 준비해두었습니다. 자신의 운영체제에 맞는 파일을 다운로드 받아 압축을 풀면 새로운 테라폼을 체험해볼 수 있습니다. 여기서는 맥OSmacOS을 가정하고 설치를 진행해보겠습니다.
$ mkdir terraform-012-demo
$ cd terraform-012-demo
$ wget https://releases.hashicorp.com/terraform/0.12.0-beta1/terraform_0.12.0-beta1_darwin_amd64.zip
$ unzip terraform_0.12.0-beta1_darwin_amd64.zip
terraform
이름을 가진 바이너리 파일이 생성됩니다. 이 파일을 실행해 버전을 확인해봅니다.
$ ./terraform -v
Terraform v0.12.0-beta1
이제 새로운 버전을 사용할 수 있습니다. direnv를 사용하면 특정 프로젝트(디렉터리)에서만 새로운 테라폼 버전을 사용하는 것도 가능합니다. 이에 대한 자세한 정보는 부록 1을 참고해주세요.
AWS 프로바이더 설치
테라폼 0.12 베타 1과 호환되는 프로바이더는 아직 공식 릴리스되지 않았기 때문에, 여기서는 개발 버전을 다운로드하여 사용하겠습니다. 테라폼 0.12 베타 1과 호환되는 개발 버전 프로바이더 모음에 가면 다양한 프로바이더를 다운받을 수 있습니다. 여기서는 aws 프로바이더를 기준으로 설명하겠습니다.
$ wget http://terraform-0.12.0-dev-snapshots.s3-website-us-west-2.amazonaws.com/terraform-provider-aws/1.60.0-dev20190216H00-dev/terraform-provider-aws_1.60.0-dev20190216H00-dev_darwin_amd64.zip
$ mkdir -p terraform.d/plugins/darwin_amd64
$ unzip terraform-provider-aws_1.60.0-dev20190216H00-dev_darwin_amd64.zip -d terraform.d/plugins/darwin_amd64/
실습용 테라폼 파일
이제 실습을 위해 다음 내용으로 terraform.tf
파일을 생성합니다.
# terraform.tf
provider "aws" {
version = "~> 1.60.0-dev"
region = "ap-northeast-2"
}
그리고 명령창에서 terraform init
을 실행하여 다음과 같은 메시지가 나타난다면 설정은 끝났습니다.

테라폼 코드를 0.12 베타 1 버전에 맞춰 수정하기
테라폼 0.12부터는 새 HCL 문법을 사용합니다. 그런데 기존에 사용하던 HCL 코드를 일일이 새 문법으로 바꾸기란 쉽지 않은 일이기 때문에 테라폼에서는 0.12upgrade
라는 명령어를 제공합니다. 우선, 이전 버전에 맞춰 작성된 간단한 테라폼 파일(main.tf
)을 하나 만들어 봅시다.
# main.tf
variable "vpc_cidr" {
type = "string"
default = "172.17.0.0/16"
}
variable "public_cidr" {
type = "string"
default = "172.17.1.0/24"
}
resource "aws_subnet" "public" {
vpc_id = "${aws_vpc.this.id}"
cidr_block = "${var.public_cidr}"
}
resource "aws_vpc" "this" {
cidr_block = "${var.vpc_cidr}"
}
이제 terraform 0.12upgrade
명령을 실행합니다.
This command will rewrite the configuration files in the given directory so
that they use the new syntax features from Terraform v0.12, and will identify
any constructs that may need to be adjusted for correct operation with
Terraform v0.12.
We recommend using this command in a clean version control work tree, so that
you can easily see the proposed changes as a diff against the latest commit.
If you have uncommited changes already present, we recommend aborting this
command and dealing with them before running this command again.
Would you like to upgrade the module in the current directory?
Only 'yes' will be accepted to confirm.
Enter a value:
정말로 실행할지를 묻을 때 yes
를 입력하면, 현재 디렉터리의 모든 파일을 테라폼 0.12에 맞춰 수정해줍니다. 이제 main.tf
파일을 다시 열어봅시다.
# main.tf (자동 변경된 내용)
variable "vpc_cidr" {
type = string
default = "172.17.0.0/16"
}
variable "public_cidr" {
type = string
default = "172.17.1.0/24"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.this.id
cidr_block = var.public_cidr
}
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
}
전체적으로 따옴표가 많이 사라졌음을 알 수 있습니다. 그리고 다음과 같은 내용으로 versions.tf
파일이 생성되었습니다.
# versions.tf
terraform {
required_version = ">= 0.12"
}
아마도 terraform 0.12upgrade
명령을 또다시 실행하는 행위를 막기 위함이 아닌가 싶습니다. (실제로 terraform 0.12upgrade
명령을 다시 실행해보면, 이미 업그레이드되었다는 메시지가 나타납니다.)
새 HCL 문법과 테라폼 0.12의 새 기능 둘러보기
이제 본격적으로 테라폼 0.12의 새 기능을 둘러보겠습니다.
변수처럼 리소스/모듈 참조
이전에는 리소스나 모듈을 참조할 때도 문자열 안에서 ${}
표현식을 사용해야 했습니다.
# 테라폼 이전 버전
resource "aws_subnet" "public" {
vpc_id = "${aws_vpc.this.id}"
...
}
테라폼 0.12부터는 번거로운 ${}
표현식 없이도 리소스나 모듈을 참조할 수 있습니다.
# 테라폼 0.12
resource "aws_subnet" "public" {
vpc_id = aws_vpc.this.id
...
}
다양한 변수 타입
이전 버전에서는 변수의 타입을 지정할 때 type = "string"
처럼 문자열을 사용하였습니다.
# 테라폼 이전 버전
variable "public_cidrs" {
type = "string"
}
이제는 타입 자체를 그대로 인식합니다. 따라서 type = string
처럼 입력하면 됩니다.
# 테라폼 0.12
variable "public_cidrs" {
type = string
}
복잡한 변수 선언
아울러, 좀더 복잡한 방식으로 변수를 선언할 수 있습니다.
# 테라폼 0.12
module "vpc" {
source = "./modules/vpc"
public_subnets = {
zone_a = {
cidr_block = "172.17.1.0/24"
availability_zone = "ap-northeast-2a"
}
zone_b = {
cidr_block = "172.17.2.0/24"
availability_zone = "ap-northeast-2b"
}
zone_c = {
cidr_block = "172.17.3.0/24"
availability_zone = "ap-northeast-2c"
}
}
}
퍼스트 클래스 표현식
이전 버전에서 변수를 사용할 때는 문자열 속에서 ${}
표현식과 함께 사용해야 했었는데요.
# 테라폼 이전 버전
resource "aws_vpc" "this" {
cidr_block = "${var.vpc_cidr}"
}
이제부터는 그럴 필요가 없습니다.
# 테라폼 0.12
resource "aws_vpc" "this" {
cidr_block = var.vpc_cidr
}
for 구문
값을 조금씩만 바꾸면서 반복적으로 리소스를 참조하거나 생성할 때, for
구문을 사용할 수 있습니다.
# 테라폼 0.12
variable "subnet_numbers" {
type = list
default = [1, 2, 3]
}
resource "aws_vpc" "this" {
cidr_block = "172.17.0.0/16"
}
resource "aws_subnet" "public" {
count = 3
vpc_id = aws_vpc.this.id
cidr_block = [
for num in var.subnet_numbers:
cidrsubnet(aws_vpc.this.cidr_block, 8, num)
][count.index]
}
output "subnet_cidrs" {
value = [
for num in var.subnet_numbers:
cidrsubnet(aws_vpc.this.cidr_block, 8, num)
]
}
이 예시를 적용한 후 출력되는 값을 보면 for
구문이 어떻게 적용되었는지 확인할 수 있습니다.
$ terraform apply
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
subnet_cidrs = [
"172.17.1.0/24",
"172.17.2.0/24",
"172.17.3.0/24",
]
for
구문은 리스트 뿐 아니라 맵 형태의 데이터를 만들 때도 사용할 수 있습니다. 이때는 []
기호 대신 {}
기호를 사용합니다.
for_each 구문
단순한 값 대신 키-밸류 형태의 값을 순회할 때는 for_each
구문을 사용하면 됩니다.
예를 들어 이전 버전에서는 이렇게 작성하던 테라폼 코드를,
# 테라폼 이전 버전
variable "subnet_numbers" {
type = "list"
default = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
}
resource "aws_vpc" "this" {
cidr_block = "172.17.0.0/16"
}
resource "aws_subnet" "this" {
count = 3
vpc_id = "{aws_vpc.this.id}"
availability_zone = "${var.subnet_numbers[count.index]}"
cidr_block = "${cidrsubnet(aws_vpc.this.cidr_block, 8, count.index)}"
}
이렇게 작성할 수 있습니다.
# 테라폼 0.12
variable "subnet_numbers" {
default = {
"ap-northeast-2a" = 1
"ap-northeast-2b" = 2
"ap-northeast-2c" = 3
}
}
resource "aws_vpc" "this" {
cidr_block = "172.17.0.0/16"
}
resource "aws_subnet" "this" {
for_each = var.subnet_numbers
vpc_id = aws_vpc.this.id
availability_zone = each.key
cidr_block = cidrsubnet(aws_vpc.this.cidr_block, 8, each.value)
}
좀더 자세한 내용은 테라폼 0.12 프리뷰: For와 For-Each 문서를 참고하세요.
문자열 표현식에서 리스트와 맵 참조법 개선
var.security_group_id
가 존재한다면 해당 보안 그룹과 기본 보안 그룹을 리스트 안에 넣어 지정하고, 존재하지 않으면 기본 보안 그룹만 지정하려는 상황이라고 가정해봅시다.
이전 버전이라면 이렇게 작성했을 겁니다.
# 테라폼 이전 버전
resource "aws_instance" "example" {
...
vpc_security_group_ids = "${var.security_group_id != "" ? [var.security_group_id] : [aws_default_security_group.this.id, var.security_group_id]}" # 문법 오류 발생
vpc_security_group_ids = "${var.security_group_id != "" ? list(var.security_group_id) : list(aws_default_security_group.this.id, var.security_group_id)}" # 바른 사용법
}
2행처럼 사용하면 그나마 괜찮지만, 실제로는 문자열 안에서 배열 값을 참조할 수 없어서 오류가 발생합니다. 이런 문법 오류를 피하고자 3행처럼 list()
함수를 사용했었죠.
0.12부터는 문자열 안에 표현식을 넣지 않아도 되어서 다음과 같이 간결하게 작성할 수 있습니다.
# 테라폼 0.12
resource "aws_instance" "example" {
...
vpc_security_group_ids = var.security_group_id != "" ? [aws_default_security_group.this.id, var.security_group_id] : [var.security_group_id]
}
Splat 연산자
이전 버전에서는 리스트 형태 리소스(count
로 개수를 조정하는)에 대해서만 *
연산자로 접근할 수 있었습니다.
# 테라폼 이전 버전
resource "aws_subnet" "public" {
count = 2
...
}
resource "aws_default_network_acl" "this" {
...
subnet_ids = [
"${aws_subnet.public.*.id}"
]
}
0.12 이후부터는 모든 리스트 형태의 값에 *
연산자로 접근할 수 있습니다.
# 테라폼 0.12
resource "aws_default_security_group" "this" {
...
ingress {
to_port = 80
...
}
ingress {
to_port = 443
...
}
}
output "security_gorup_ingress_to_port_list" {
value = aws_default_security_group.this.ingress.*.to_port
}
output
결과를 보면 다음과 같을 겁니다.
$ tf apply
...
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
security_gorup_ingress_list = [
443,
80,
]
또한, 이전 버전에서는 리스트 형태의 값 중 특정 인덱스에 접근할 때, 무조건 가장 마지막에 인덱스를 지정하곤 했는데요.
# 테라폼 이전 버전
resource "aws_default_network_acl" "this" {
...
subnet_ids = [
"${aws_subnet.public.*.id[count.index]}"
]
}
0.12부터는 문법이 좀더 명확해졌습니다.
# 테라폼 0.12
resource "aws_default_network_acl" "this" {
...
subnet_ids = [
aws_subnet.public[count.index].id
]
}
조건 연산자 사용법 개선
조건에 따라 변수를 선언하거나 리소스를 생성하기 위해 조건 연산자(condition ? A : B
)를 많이 사용하곤 했는데요. 다음과 같은 제한이 있었습니다.
- 원시 타입을 선언할 때만 사용할 수 있음(리스트나 맵에서 사용 불가)
- A, B 중 하나만 필요하더라도 둘 다 실제로 평가됨
0.12에서는 이러한 제한이 풀려, 다음과 같이 사용할 수 있습니다.
# 테라폼 0.12
locals {
first_id = length(aws_subnet.public) > 0 ? aws_subnet.public[0].id : ""
buckets = (var.env == "dev" ? [var.build_bucket, var.qa_bucket] : [var.prod_bucket])
}
null 값
0.12부터 변수에 null
을 대입할 수 있습니다. null
이 대입되면 해당 리소스를 생성하지 않는다는 의미입니다.
# 테라폼 0.12
variable "override_private_ip" {
type = string
default = null
}
resource "aws_instance" "this" {
...
private_ip = var.override_private_ip
}
템플릿 문법
이전 버전에서도 템플릿 안에서 ${}
표현식을 사용하여 변수를 집어넣을 수 있었지만, 0.12부터는 for
구문이나 조건식 같은 좀더 다양한 표현을 사용할 수 있습니다.
# 테라폼 0.12
locals {
cidr_list = <<EOT
%{ for subnet in aws_subnet.this ~}
SUBNET ${subnet.availability_zone} ${subnet.cidr_block}
%{ endfor }
EOT
}
local.cidr_list
의 실제 값은 다음과 같습니다.
$ terraform console
> local.cidr_list
SUBNET ap-northeast-2c 172.17.1.0/24
SUBNET ap-northeast-2a 172.17.2.0/24
SUBNET ap-northeast-2a 172.17.3.0/24
테라폼 0.12 업그레이드 가이드
얼핏만 보더라도 변경 사항이 꽤 많다는 점을 알 수 있습니다. 따라서 테라폼 공식 문서에는 0.12로 업그레이드하기 가이드 문서가 공개되어 있습니다. 가이드 문서를 아주 간략하게 정리해보면 다음과 같은 순서를 제안하고 있네요.
- 테라폼 실행 파일 업그레이드
- 프로바이더 업그레이드
- 테라폼 설정 파일 업그레이드
- count를 사용하는 리소스 수정
- 퍼스트 클래스 표현식 참조 방식을 적용
- 공용 모듈 수정
마치며
이렇게 해서 테라폼 0.12의 새로운 모습을 미리 살펴보았습니다. 꽤나 편리한 기능을 담고 있어 어서 적용해보고 싶어지는데요. 이미 일년 여를 기다려 온 만큼 어서 베타 딱지를 떼고 정식 릴리스되길 바랍니다.
부록 1: direnv로 특정 프로젝트에서만 테라폼 새 버전 사용하기
direnv를 사용하면 .envrc
파일에 현재 프로젝트에 적용할 테라폼 버전을 지정할 수 있습니다.
$ mkdir -p .direnv/terraform/bin/
$ unzip terraform_0.12.0-beta1_darwin_amd64.zip -d .direnv/terraform/bin/
$ echo "load_prefix $(direnv_layout_dir)/terraform" >> .envrc
$ terraform -v
Terraform v0.12.0-beta1
이제 이 디렉터리에서 terraform
을 실행하면 테라폼 0.12 베타 1 버전이 실행됩니다.
direnv에 대한 자세한 내용은 direnv를 사용한 개발환경 구축을 참고해주세요.