Terraform 0.11→0.12で追加された新機能

こんにちは佐伯です。

先日Terraform 0.12がリリースされ、Terraform 0.12へのアップグレードについて以下エントリを投稿しましたが、今回はTerraform 0.11からTerraform 0.12で追加された新機能について確認してみました。

Terraform 0.12がリリースされたのでアップグレードしてみた

Terraform 0.12の新機能

First-class expressions

Terraform 0.12以前ではリソースの設定に変数や他のリソースの値を指定する場合、ami = "${var.ami_id}"のように"${}"で囲う必要がありました。Terraform 0.12ではami = var.ami_idのように定義できます。

Terraform 0.11

resource "aws_instance" "web" {
  instance_type = "t3.small"
  ami           = "${var.ami_id}"
}

Terraform 0.12

resource "aws_instance" "web" {
  instance_type = "t3.small"
  ami           = var.ami_id
}

For expressions

forが追加され、listやmapの要素をループして参照ができるようになりました。Builf-in Functionと組み合わせて変換したり、ifを含めてフィルタリングもできます。var.listを定義して、terraform consoleで確認してみました。

variable list {
  type = list(string)

  default = [
    "a",
    "b",
    "c",
  ]
}

forを囲む括弧の種類によって生成させるコレクションのタイプが決まります。以下の例では[]で囲っているのでlistが生成されます。

> [for s in var.list : upper(s)]
[
  "A",
  "B",
  "C",
]

同じvar.listからmapを生成する場合は{}で囲い、=>記号で区切ります。

> {for s in var.list : s => upper(s)}
{
  "a" = "A"
  "b" = "B"
  "c" = "C"
}

その他にforifを含めてフィルタリングも可能です。

> [for s in var.list : upper(s) if s !="a"]
[
  "B",
  "C",
]

ユースケースとしては以下の様にoutputで作成したリソースのインスタンスIDとプライベートIPのmapを出力したりする場合などでしょうか。

output "instance_private_ip_addresses" {
  value = {
    for instance in aws_instance.example:
    instance.id => instance.private_ip
  }
}

詳しくはドキュメントを確認ください。

ただし、やりすぎると可読性が低下するので個人的にはあんまり使わない方がいい気がしてます。

Dynamic configuration blocks

リソースによっては設定をブロックで定義するものがあります。例えばaws_security_groupのingress, egreessなどはブロックで定義します。

Terraform 0.11

resource "aws_security_group" "web_service" {
  name        = "web-service"
  description = "Allow HTTP/HTTPS inbound traffic"
  vpc_id      = "${aws_vpc.main.id}"

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

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

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Terraform 0.12

Terraform 0.12ではfor_eachを使ってブロック内の引数を動的に生成できます。上記の例をDynamic configuration blocksで書くと以下のようになります。

variable web_service_ports {
  type        = list(number)
  description = "list of web service ports"
  default     = [80, 443]
}

resource "aws_security_group" "web_service" {
  name        = "web-service"
  description = "Allow HTTP/HTTPS inbound traffic"
  vpc_id      = aws_vpc.main.id

  dynamic "ingress" {
    for_each = var.web_service_ports

    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

mapを動的ブロックで使用もでき、その場合は<動的ブロック名>.key<動的ブロック名>.valueで定義します。ただし、動的ブロックも使いすぎると可読性が低下するので使い所は注意が必要だと思います。

Generalised "splat" operator

Terraform 0.11以前ではcountを使ってリソースを複数作成した際などに${aws_instance.foo.*.id}で参照ができました。Terraform 0.12からはforでループするか、aws_instance.foo.[*].idで参照します。

例えば以下は同じ式となります。

# For expression
[for o in var.list : o.id]

# Splat expression
var.list[*].id

複合型のリストから属性やインデックスの参照も可能です。以下は同じ式となります。

# For expression
[for o in var.list : o.interfaces[0].name]

# Splat expression
var.list[*].interfaces[0].name

Nullable argument values

Terraform 0.12では引数にnullを割り当てて未設定にできます。プロバイダーのデフォルト動作を保持しながら、モジュールの呼び出し側で値の上書きが可能なモジュールを作成できます。

以下はモジュールのコード例です。モジュール呼び出し側がoverride_private_ipを指定しなければ、プライベートIPは動的に割り当てられ、指定すれば明示的にプライベートIPを設定できます。

variable "override_private_ip" {
  type    = string
  default = null
}

resource "aws_instance" "example" {
  # ... (other aws_instance arguments) ...

  private_ip = var.override_private_ip
}

Rich types in module inputs variables and output values

Terraform 0.11以前でも単純なlistやmapには対応していましたが、listとmapがネストされた変数は使える場合もあるけど制限が多かったです。Local Valuesが追加されてからは困ることはなかったのですが、モジュールの場合はvariableを使わざるを得えないので引数が多くなりがちでした。

Terraform 0.12では様々な型制約を変数に指定できます。以下はコレクション型の例です。

variable list-string {
  type    = list(string)
  default = ["a", "b", "c"]
}

variable list-number {
  type    = list(number)
  default = [1, 2, 3]
}

variable map-string {
  type    = map(string)
  default = { key1 = "a", key2 = "b", key3 = "b"}
}

variable map-number {
  type    = map(number)
  default = { key1 = 1, key2 = 2, key3 = 3}
}

以下は構造型の例です。

variable object {
  type    = object({
    number = number
    string = string
  })

  default = {
    number = 1
    string = "aaa"
  }
}

variable tuple {
  type    = tuple([
    string, number, string, number
  ])

  default = ["a", 1, "b", 2]
}

コレクション型と構造型を合わせた指定も可能です。

variable "ipset" {
  type = list(object({
    value = string
    type  = string
  }))
 
  default = [
    { value = "1.1.1.1/32", type="IPV4" },
    { value = "2.2.2.2/32", type="IPV4" },
  ]
}

Resource and module object values

属性なしで作成したリソースを参照してリソースやモジュール全体の情報を参照できるようになりました。以下の様にモジュールでVPCとサブネットを作成し、モジュール呼び出し側へ作成したリソースの全ての情報を返すことができます。

output "vpc" {
  value = aws_vpc.my_vpc
}

output "subnet" {
  value = aws_subnet.my_subnet
}
output "vpc" {
  value = module.network.vpc
}

output "subnet" {
  value = module.network.subnet
}

Extended template syntax

テンプレートシンタックスが追加されました。${...}で単純に文字列に変換したり、ディレクティブ(%{...})では%{ if <BOOL>} / %{ else } / %{ endif }でテンプレートを2つのパターンに変換したり、%{ for <NAME> in <COLLECTION> }/ %{ endfor }で要素をループしてテンプレートを変換できます。

# Template syntax
"Hello, ${var.name}!"

# Template syntax with if expression
"Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"

# Template syntax with for expression
<<EOT
%{ for ip in aws_instance.example.*.private_ip }
server ${ip}
%{ endfor }
EOT

jsondecode and csvdecode interpolation functions

jsondecodecsvdecode関数が追加されました。ユースケースがあまり思いつかないのですが、ファイルから値取りたい場合とかですかね...?

# jsondecode
> jsondecode("{\"hello\": \"world\"}")
{
  "hello" = "world"
}
> jsondecode("true")
true

# csvdecode
> csvdecode("a,b,c\n1,2,3\n4,5,6")
[
  {
    "a" = "1"
    "b" = "2"
    "c" = "3"
  },
  {
    "a" = "4"
    "b" = "5"
    "c" = "6"
  }
]

Revamped error messages

エラーメッセージが改善されました。例えばタイプがlistのvariableにリスト以外の値を定義した場合、Terraform 0.11では以下の出力でした。

Terraform 0.11

$ terraform validate

Error: Error parsing /path/to/variable.tf: At 2:13: Unknown token: 2:13 IDENT list

Terraform 0.12

Terraform 0.12ではどの部分の何がエラーなのかまで出力してくれるようになりました。

$ terraform validate

Error: Invalid default value for variable

  on variable.tf line 3, in variable "string":
   3:   default = 1

This default value is not compatible with the variable's type constraint: list
of any single type required.

Structual plan output

IAM PolicyなどJSONで定義するリソースにおいて、terraform planの出力結果が見やすくなりました。テキストでは少しわかりにくいので画像で貼ります。

参考リンク

最後に

個人的にはforfor_each、テンプレートシンタックスは使いすぎると自分で書いたのに読み直すと「なにやってるんだこれ...」ってなりかねないのであんまり使わない方針ではありますが、抽象化を目的としたモジュールで使うのはありかなーって思ってます。CHANGELOGの新機能部分のみ確認しましたが、その他にも色々細かい改善がされているようです。まだ全部移行できてないので頑張って0.12にアップグレードしていくぞ!