Terraform v0.12で変わるHCLの記述について

この記事は第6回名古屋若手Webエンジニア交流会で話した内容を元に加筆修正したものになります。

Terraform v0.11とv0.12でリソースの記述方法がどう異なるかを示しました。



Terraformでできること


  • インフラのコード化


    • 例えばAWSコンソールを使わずにインスタンスを生成できる



  • JSON互換のHCLという記法でコードを記述


    • HCL(HashiCorp Configuration Language)

    • プログラミング言語ではない。設定とかを記述するためのもの



  • AWS,Azure,GCPといった様々なクラウドインフラに対応

  • インフラの状態を管理できる(S3とかに保存できる)



なぜTerraformを使っているのか


  • AWSコンソールでの作業に限界を感じたから


    • 例えばVPC、サブネット、セキュリティグループの設定


      • インスタンスを使うために必要な設定なので、何度やっても一回で出来る気がしない





  • ステージング環境と本番環境の差異をゼロにしたいから


    • v0.10からworkspaceという機能があり、単一のtfファイルから異なる環境のインフラを構築出来る



  • アラート等の設定漏れをなくしたいから


    • 1台目のサーバーにあるcloudwatchの設定が2台目にはない





Terraformを使って満足したところ


  • インフラを構成をコード管理出来るのと、現在のインフラ状態をS3等に保存できるので、安心出来る。

  • 複数のサービスを構築したいときは一回Terraformで作ると展開しやすい



Terraformの痛み


  • 既存のリソースを管理できない


    • いつの間にかコードにないリソースが作られてる

    • 緊急避難的にコンソールで変更した内容がTerraformの実行でもとに戻る



  • 複雑な記法


    • 複雑なことをやろうとすると大変


      • repl環境が使いにくい



    • output周りや、既存のリソースを他のリソースで使うときの適切な式表現を見つけるのに難しいときがある




Terraform 0.12でのHCLの改良点


  • First-ClassExpressions

  • Expressions with Lists and Maps

  • For expressions

  • Dynamic blocks

  • Generalized Splat Operator

  • Conditional Operator Improvements

  • Rich Value Types

  • Reliable JSON Syntax



インストール

まだ正式リリースではないので、下記からダウンロードしてインストールしてください。

https://releases.hashicorp.com/terraform/

記事執筆時点ではv0.12.0-alpha4が最新でしたので、v0.12.0-alpha4

を試しました。

Provider関連でエラーが出る場合は同梱されているproviderファイルを

.terraform/plugins/darwin_amd64/

にコピーしてください。



リストにない変更点

0.12からterraform consoleでlocal変数を取得出来るようになりました。

今までは

locals {

region = "ap-northeast-1"
}

上記のような変数を定義していて、terraform consoleを実行すると

$ terraform console

> local.region
unknown values referenced, can't compute value

このようにエラーになってしまいましたが、0.12からは

$ terraform console

> local.region
ap-northeast-1

上記のように正しい結果が返ってきます。

その一方で、作成済みのリソースに対して、terraform consoleで取得を試みると

$ terraform console

> aws_security_group.this.id

>
Error: Result depends on values that cannot be determined until after "terraform apply".

エラーが発生するようになってしまいました。このような変更の表記は見つけられなかったので、

環境依存の問題か、あるいは今回試したTerraform v0.12.0-alpha4に起因するエラーなのかも

しれません。



First-ClassExpressions

・式を表現するときにダブルクォーテーションで括る必要がなくなりました。

"${var.foo}"

var.foo

v0.12をインストールした状態でterraform 0.12upgradeというコマンドを実行すると

terraform 0.11のhclファイルの内容をすべて新しい記述に変更してくれます。



Expressions with Lists and Maps

・リストとマップをダイレクトに式で使えるようになった。

ids = ["${local.sg_1}", "${local.sg_2}"]

ids = ids



Before Terraform 0.11(First-ClassExpressions and Expressions with Lists and Maps)

locals {

sg_a = "sg-xxxxxxxx"
sg_b = "sg-zzzzzzzz"
sg_c = "sg-yyyyyyyy"
ami = "ami-016ad6443b4a3d960"
instance_type = "t3.micro"
}

resource "aws_instance" "example" {
ami = "${local.ami}"
instance_type = "${local.instance_type}"

vpc_security_group_ids = ["${local.sg_a}", "${local.sg_b}", "${local.sg_c}"]
}



After Terraform 0.12(First-ClassExpressions and Expressions with Lists and Maps)

式の表現にダブルクォートが必要なくなったのと、Listの定義もそのまま式で使えるよう

になったため、かなりすっきりとした見た目になったと思います。

locals {

vpc_security_group_ids = ["sg-xxxxxxxx","sg-zzzzzzzz","sg-yyyyyyyy"]
ami = "ami-016ad6443b4a3d960"
instance_type = "t3.micro"
}

resource "aws_instance" "example" {
ami = local.ami
instance_type = local.instance_type

vpc_security_group_ids = local.vpc_security_group_ids
}



For expressions


  • For文を使えるようになり、ListやMapの内容を参照するのが簡単になりました。

  • ourputの整形にも使えます。

  • 待望の機能だけど、resourceの複数定義にはまだ使えなさそう



Before Terraform 0.11(For expressions)

resource "aws_instance" "this" {

count = 3
ami = "${local.ami}"
instance_type = "${local.instance_type}"
availability_zone = "${format("%s%s", "${local.region}", "${local.azs[count.index]}")}"
associate_public_ip_address = true
vpc_security_group_ids = ["${local.sg_a}", "${local.sg_b}", "${local.sg_c}"]
subnet_id = "${local.subnets[count.index]}"

tags = {
Name = "${format("terraform-0.11-for-demo-%d", "${count.index}")}"
}
credit_specification {
cpu_credits = "unlimited"
}
}

output "instance_private_ip_addresses" {
value = "${aws_instance.this.*.private_ip}"
}
>
Outputs:

instance_private_ip_addresses = [
10.2.0.22,
10.2.4.162,
10.2.9.38
]



After Terraform 0.12(For expressions)

map型にまとめる記述も出来ると書いてあるのですが、現在のバージョンでは

エラーになってしまったため、List型の記述のみ載せて置きます。

resource "aws_instance" "this" {

count = 3
ami = local.ami
instance_type = local.instance_type
availability_zone = format("%s%s", local.region, local.azs[count.index])
associate_public_ip_address = true
vpc_security_group_ids = local.vpc_security_group_ids
subnet_id = local.subnets[count.index]

tags = {
Name = format("terraform-0.12-for-demo-%d", count.index)
}
credit_specification {
cpu_credits = "unlimited"
}
}

output "instance_private_ip_addresses" {
value = [
for instance in aws_instance.this:
instance.private_ip
]
}

>
Outputs:

instance_private_ip_addresses = [
"10.2.1.79",
"10.2.7.42",
"10.2.11.106",
]



Dynamic blocks

・タグ等の繰り返し記述されるブロックを動的に表現出来るようになりました。

・繰り返し記述するAutoScalingのタグや、SecurityGroupのingressに使えます。



Before Terraform 0.11(For expressions)

resource "aws_autoscaling_group" "example" {

# ...

tag {
key = "Name"
value = "example-asg-name"
propagate_at_launch = false
}

tag {
key = "Component"
value = "user-service"
propagate_at_launch = true
}

tag {
key = "Environment"
value = "production"
propagate_at_launch = true
}
}



After Terraform 0.12(For expressions)

locals {

standard_tags = {
Component = "serviceA"
Environment = "production"
}
}

resource "aws_autoscaling_group" "example" {
max_size = 1
min_size = 1
tag {
key = "Name"
value = "example-asg-name"
}
dynamic "tag" {
for_each = local.standard_tags

content {
key = tag.key
value = tag.value
}
}
}



Generalized Splat Operator


  • スプラット演算子が複数存在するリソースだけでなく、一般的なListにも使えるようになった


    • 今まではcountで作成したものしか使えなかった



  • スプラット演算子の記法が直感的になった


    • aws_instance.this.*.public_dns


    • aws_instance.this[*]public_dns


    • ただし0.12−alpha4ではエラーになる





Conditional Operator Improvements


  • 条件演算子の改善、今まではプリミティブな型にしか使えなかった。

  • 今までは両方の式の値を評価していたが、参考演算子的に使えるようになった。



Before Terraform 0.11(Conditional Operator Improvements)

output "first_id" {

value = "${"${length("${aws_instance.this.*.private_ip}")}" > 0 ? "${element("${aws_instance.this.*.id}", 0)}" : "2"}"
}
>
Error: Error refreshing state: 1 error(s) occurred:

* output.first_id: element: element() may not be used with an empty list in:

${"${length("${aws_instance.this.*.private_ip}")}" > 0 ? "${element("${aws_instance.this.*.id}", 0)}" : "2"}



After Terraform 0.12(Conditional Operator Improvements)

今まで配列のindexでアクセス出来なかった部分も出来るようになったため

処理の見通しがかなり改善されました。

output "first_id" {

value = length(aws_instance.this) > 0 ? aws_instance.this[0].id : ""
}
>
Outputs:

first_id =



Rich Value Types


  • Terraform 0.11以前では変数の型として単純なlistとmapしか使えなかった。

  • 今後はもっと複雑な型を表現出来るようになる

  • localはvariablesの記述をきれいにできる


- outputでのリソース情報の出力が容易になる


After Terraform 0.12(Rich Value Types)

outputにもresourceをそのまま記述しても、エラーにならずに出力されます。

作成したリソースの内容を次のリソースで使う際などの確認に便利です。

resource "aws_security_group" "this" {

name = "ingress"
description = "Allow some inbound traffic"
vpc_id = local.vpc

ingress {
from_port = 8200
to_port = 8200
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

ingress {
from_port = 8500
to_port = 8500
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
output "sg" {
value = aws_security_group.this
}
>
sg = {
"arn" = "arn:aws:ec2:ap-northeast-1:000000000000:security-group/sg-099659d43b1534ffa"
"description" = "Allow some inbound traffic"
"egress" = []
"id" = "sg-099659d43b1534ffa"
"ingress" = [
{
"cidr_blocks" = [
"0.0.0.0/0",
]
"description" = ""
"from_port" = 8500
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 8500
},
{
"cidr_blocks" = [
"0.0.0.0/0",
]
"description" = ""
"from_port" = 8200
"ipv6_cidr_blocks" = []
"prefix_list_ids" = []
"protocol" = "tcp"
"security_groups" = []
"self" = false
"to_port" = 8200
},
]
"name" = "ingress"
"owner_id" = "000000000000"
"revoke_rules_on_delete" = false
"timeouts" = {}
"vpc_id" = "vpc-xxxxxxxx"
}



Template Syntax


  • テンプレート構文の中でもFor文が使えるように

  • 引数展開のための変数の定義を少なくできる



Before Terraform 0.11

    "environment": [

{
"name": "AWS_S3_BUCKET",
"value": "${s3_bucket}"
},
{
"name": "AWS_S3_URL",
"value": "${s3_url}"
},
{
"name": "ENCRYPT_SECURE_KEY",
"value": "${secure_key}"
},
{
"name": "MYSQL_HOST",
"value": "${rds_endpoint}"
},
{
"name": "MYSQL_ROOT_PASSWORD",
"value": "${password}"
}
]



After Terraform 0.12

fargateやECSのコンテナ定義はjson形式で行います。この場合に環境依存の

変数等をtemplateの変数として入れることが多いと思いますが、コンテナ内で

使用する環境変数等をfor文を使って記述できることにより、繰り返しの記述

を少なくすることができます。

locals {

fg_env = [
{
"name": "AWS_S3_BUCKET",
"value": "my.bukect"
},
{
"name": "AWS_S3_UR",
"value": "assets.localhost"
},
{
"name": "ENCRYPT_SECURE_KEY",
"value": "xxxxxxxxxxxxxxxxxxxxxxxxxx"
},
]
fargate_config = <<EOT
%{ for instance in local.fg_env ~}
{
"name": local.env.name,
"value": local.env.value
},
%{ endfor }
EOT
}
> local.fg_env
[
{
"name" = "AWS_S3_BUCKET"
"value" = "my.bukect"
},
{
"name" = "AWS_S3_UR"
"value" = "assets.localhost"
},
{
"name" = "ENCRYPT_SECURE_KEY"
"value" = "xxxxxxxxxxxxxxxxxxxxxxxxxx"
},
]



Reliable JSON Syntax

-HCLとJSONが完全に1:1の互換に

-エラーメッセージの改善

-JSON表記でのコメントサポート



Before Terraform 0.11(Reliable JSON Syntax:エラー文)

{

"variable": {
"example": "foo"
}
}
$ terraform apply

Error: Error loading /home/ec2-user/terraform-0.11-example/variables.tf.json: -: "variable" must be followed by a name



Before Terraform 0.11(Reliable JSON Syntax:エラー文)

無効な構造のjsonを記述したときのエラーメッセージがわかりやすくなりました。

Error: Incorrect JSON value type

on variable.tf.json line 3, in variable:
3: "example": "foo"

Either a JSON object or a JSON array is required, representing the contents of
one or more "variable" blocks.



After Terraform 0.12(Reliable JSON Syntax:コメント記述)

この構文を0.11で試してもエラーになってしまうだけなので、割愛しますが、

コメントを記述する際に"//"という特別なキーでコメントを記述できるようになりました。

この"//"のキーはTerraformを実行する際には無視されます。


example.tf.json

{

"locals": {
"example": {
"//": "This property is comment",
"default": "foo"
}
}
}



まとめ

Terraform v0.12ではHCLに大きなバージョンアップが入り、記述がかなり変更されます。

ですが、今までの記述方式も推奨はされないものの使えるものが多く、既存のプロジェクト

をアップデートするのに、そこまで負担の大きいものではないと思います。

追記

Terraform 0.12がリリースされました。0.12upgradeというコマンドが用意されていて、

このコマンドを実行するとFirst-ClassExpressionsやtagの定義に=が必要になった部分などの

軽微な変更は自動で修正してくれます。