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


この記事は[第6回名古屋若手Webエンジニア交流会](https://wakateweb-nagoya.connpass.com/event/113111/)で話した内容を元に加筆修正したものになります。
Terraform v0.11とv0.12でリソースの記述方法がどう異なるかを示しました。

---
# Terraformでできること

- インフラのコード化
    - 例えばAWSコンソールを使わずにインスタンスを生成できる
- JSON互換のHCLという記法でコードを記述
    - HCL（HashiCorp Configuration Language）
    - プログラミング言語ではない。設定とかを記述するためのもの
- AWS,Azure,GCPといった様々なクラウドインフラに対応
- インフラの状態を管理できる(S3とかに保存できる)

---
# なぜTerraformを使っているのか
- AWSコンソールでの作業に限界を感じたから
    - 例えばVPC、サブネット、セキュリティグループの設定
        - インスタンスを使うために必要な設定なので、何度やっても一回で出来る気がしない
- ステージング環境と本番環境の差異をゼロにしたいから
    - v0.10からworkspaceという機能があり、単一のｔｆファイルから異なる環境のインフラを構築出来る
- アラート等の設定漏れをなくしたいから
    - 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
```

ｖ０．１２をインストールした状態で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     
    - ただし０．１２−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を実行する際には無視されます。

```json:example.tf.json
{
  "locals": {
    "example": {
      "//": "This property is comment",
      "default": "foo"
    }
  }
}
```

---
# まとめ
Terraform v0.12ではHCLに大きなバージョンアップが入り、記述がかなり変更されます。
ですが、今までの記述方式も推奨はされないものの使えるものが多く、既存のプロジェクト
をアップデートするのに、そこまで負担の大きいものではないと思います。

追記
Terraform 0.12がリリースされました。0.12upgradeというコマンドが用意されていて、
このコマンドを実行するとFirst-ClassExpressionsやtagの定義に=が必要になった部分などの
軽微な変更は自動で修正してくれます。