Terraform 1.12 がGAになりました

Terraform 1.12 がGAになりました

Clock Icon2025.05.27

2025年5月14日に TerraformのVersion 1.12がGAになりました。 (5月21日には 1.12.1もリリース済です。)
アップデート内容をご紹介します。

テストの並列実行ができるようになった

これまではテストは順次(=直列)実行しかできませんでした。これは1.12でもデフォルトの挙動です。が、1.12より設定を変更すれば並列実行が可能になりました。これによりテストプロセスの高速化が期待できます。嬉しいですね!

併せて、 terraform testコマンドにテスト並列実行数最大値を設定するオプションparallelismが追加されました。デフォルト値は10です。

テスト実行を並列化するには、test ブロック、または個別の run ブロック内で parallel 属性を true に設定します。

以下のように testブロックに書けば、同ファイル上のrunブロックをすべて並列実行しようと試みます。後述しますが他の設定値次第で並列実行できないrunブロックも発生します。

test {
  parallel = true
}

以下のように個別のrunブロックに書けばそのrunだけ並列実行可能です。(こちらも他の設定値次第では並列実行できないケースもあります)

run "parallel_test_example" {
  parallel = true

  # 他属性割愛
}

また、以下のようにtestブロックでデフォルト並列実行にしつつ、個々のrunブロックで parallel = falseを入れることでこのrunブロックだけ並列実行を抑止することもできます。

test {
  parallel = true
}

run "not_parallel_test_example" {
  parallel = false

  # 他属性割愛
}

並列実行できないケース

順次(=直列)実行の場合、基本的にファイルに書かれた順に上からテストが実行されていきます。

他runブロックのアウトプットを参照している

例えば以下のような構成になっていた場合、setupの実行が終わった後に test1の実行が開始されます。

run "setup" {
  state_key = "primary"
  module {
    source = "./setup"
  }

  variables {
    input = "foo"
  }

  assert {
    condition = output.value == var.foo
    error_message = "bad"
  }
}

run "test1" {
  state_key = "unique_2"
  variables {
    input = run.primary_db.value # ここで setup runブロックのアウトプットを参照している
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

同じStateファイルを参照している

以下の例では、2つのrun blockは同じstateファイルを参照しています。このような場合にも並列実行は行われず、上に書かれたもの(same_state1)の実行完了をまってから次(same_state2)が実行されます。

run "same_state1" {
  state_key = "same"
  variables {
    input = "hoge"
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

run "same_state2" {
  state_key = "same"
  variables {
    input = "another_input"
  }

  assert {
    condition = output.value == var.foo
    error_message = "double bad"
  }
}

また、state_keyを未設定の場合は、module.sourceの値が同じ場合は並列実行されません。

parallel = trueになっていない

先に説明したとおり、 parallel = false が設定されているrunブロックは並列実行されません。

そのrunブロックより前に定義されている、並列実行可能なすべての run ブロックが完了するまで待ち、その parallel=false の run ブロックが完了した後に、後続の run ブロックの実行が開始されます。

どのぐらいテスト高速化が見込めるのか実際試してみた

まあテストの内容と規模に拠ると思うのですが、実際に試してみました。

使用したコードは Write Terraform Tests | Terraform | HashiCorp Developer という、Terraform Testのチュートリアルで使われるコードです。

もともとのテストコードは 2つのrun blockしかないシンプルなものです。

# Call the setup module to create a random bucket prefix
run "setup_tests" {
  module {
    source = "./tests/setup"
  }
}

# Apply run block to create the bucket
run "create_bucket" {
  variables {
    bucket_name = "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
  }

  # Check that the bucket name is correct
  assert {
    condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
    error_message = "Invalid bucket name"
  }

  # Check index.html hash matches
  assert {
    condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
    error_message = "Invalid eTag for index.html"
  }

  # Check error.html hash matches
  assert {
    condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
    error_message = "Invalid eTag for error.html"
  }
}

※ 参照元: learn-terraform-test/tests/website.tftest.hcl at main · hashicorp-education/learn-terraform-test

かつ、1つ目のrun blockはテストの事前準備(セットアップ)のためのブロックなので、これら2つのrun blockを並列実行させるのは難しそうです。

というわけで、以下のようにこの2つのrun blockグループをコピペして計5つ(10run blocks)に増やしてみました。

# Call the setup module to create a random bucket prefix
run "setup_tests" {
  module {
    source = "./tests/setup"
  }
}

# Apply run block to create the bucket
run "create_bucket" {
  variables {
    bucket_name = "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
  }

  # Check that the bucket name is correct
  assert {
    condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
    error_message = "Invalid bucket name"
  }

  # Check index.html hash matches
  assert {
    condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
    error_message = "Invalid eTag for index.html"
  }

  # Check error.html hash matches
  assert {
    condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
    error_message = "Invalid eTag for error.html"
  }
}

+ # Call the setup module to create a random bucket prefix
+ run "setup_tests2" {
+   module {
+     source = "./tests/setup"
+   }
+ }
+ 
+ # Apply run block to create the bucket
+ run "create_bucket2" {
+   variables {
+     bucket_name = "${run.setup_tests2.bucket_prefix}-aws-s3-website-test"
+   }
+ 
+   # Check that the bucket name is correct
+   assert {
+     condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests2.bucket_prefix}-aws-s3-website-test"
+     error_message = "Invalid bucket name"
+   }
+ 
+   # Check index.html hash matches
+   assert {
+     condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
+     error_message = "Invalid eTag for index.html"
+   }
+ 
+   # Check error.html hash matches
+   assert {
+     condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
+     error_message = "Invalid eTag for error.html"
+   }
+ }
+ 
+ # Call the setup module to create a random bucket prefix
+ run "setup_tests3" {
+   module {
+     source = "./tests/setup"
+   }
+ }
+ 
+ # Apply run block to create the bucket
+ run "create_bucket3" {
+   variables {
+     bucket_name = "${run.setup_tests3.bucket_prefix}-aws-s3-website-test"
+   }
+ 
+   # Check that the bucket name is correct
+   assert {
+     condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests3.bucket_prefix}-aws-s3-website-test"
+     error_message = "Invalid bucket name"
+   }
+ 
+   # Check index.html hash matches
+   assert {
+     condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
+     error_message = "Invalid eTag for index.html"
+   }
+ 
+   # Check error.html hash matches
+   assert {
+     condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
+     error_message = "Invalid eTag for error.html"
+   }
+ }
+ 
+ # Call the setup module to create a random bucket prefix
+ run "setup_tests4" {
+   module {
+     source = "./tests/setup"
+   }
+ }
+ 
+ # Apply run block to create the bucket
+ run "create_bucket4" {
+   variables {
+     bucket_name = "${run.setup_tests4.bucket_prefix}-aws-s3-website-test"
+   }
+ 
+   # Check that the bucket name is correct
+   assert {
+     condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests4.bucket_prefix}-aws-s3-website-test"
+     error_message = "Invalid bucket name"
+   }
+ 
+   # Check index.html hash matches
+   assert {
+     condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
+     error_message = "Invalid eTag for index.html"
+   }
+ 
+   # Check error.html hash matches
+   assert {
+     condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
+     error_message = "Invalid eTag for error.html"
+   }
+ }
+ 
+ # Call the setup module to create a random bucket prefix
+ run "setup_tests5" {
+   module {
+     source = "./tests/setup"
+   }
+ }
+ 
+ # Apply run block to create the bucket
+ run "create_bucket5" {
+   variables {
+     bucket_name = "${run.setup_tests5.bucket_prefix}-aws-s3-website-test"
+   }
+ 
+   # Check that the bucket name is correct
+   assert {
+     condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests5.bucket_prefix}-aws-s3-website-test"
+     error_message = "Invalid bucket name"
+   }
+ 
+   # Check index.html hash matches
+   assert {
+     condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
+     error_message = "Invalid eTag for index.html"
+   }
+ 
+   # Check error.html hash matches
+   assert {
+     condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
+     error_message = "Invalid eTag for error.html"
+   }
+ }

この状態でまず、逐次(=直列)実行させてみます。

% time terraform test
tests/website.tftest.hcl... in progress
  run "setup_tests"... pass
  run "create_bucket"... pass
  run "setup_tests2"... pass
  run "create_bucket2"... pass
  run "setup_tests3"... pass
  run "create_bucket3"... pass
  run "setup_tests4"... pass
  run "create_bucket4"... pass
  run "setup_tests5"... pass
  run "create_bucket5"... pass
tests/website.tftest.hcl... tearing down
tests/website.tftest.hcl... pass

Success! 10 passed, 0 failed.
terraform test  11.90s user 2.14s system 21% cpu 1:06.64 total

1分 6秒かかっていますね。

これを並列実行できるように変更しましょう。

冒頭に testブロックで並列実行を有効化しつつ、各run blockのグループごとに別のstate keyをセットします。

+ test {
+   // This would set the parallel flag to true in all runs
+   parallel = true
+ }

  # Call the setup module to create a random bucket prefix
  run "setup_tests" {
+   state_key = "group1"
    module {
      source = "./tests/setup"
    }
  }

  # Apply run block to create the bucket
  run "create_bucket" {
+   state_key = "group1"
    variables {
      bucket_name = "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
    }

    # Check that the bucket name is correct
    assert {
      condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests.bucket_prefix}-aws-s3-website-test"
      error_message = "Invalid bucket name"
    }

    # Check index.html hash matches
    assert {
      condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
      error_message = "Invalid eTag for index.html"
    }

    # Check error.html hash matches
    assert {
      condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
      error_message = "Invalid eTag for error.html"
    }
  }

  # Call the setup module to create a random bucket prefix
  run "setup_tests2" {
+   state_key = "group2"
    module {
      source = "./tests/setup"
    }
  }

  # Apply run block to create the bucket
  run "create_bucket2" {
+   state_key = "group2"
    variables {
      bucket_name = "${run.setup_tests2.bucket_prefix}-aws-s3-website-test"
    }

    # Check that the bucket name is correct
    assert {
      condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests2.bucket_prefix}-aws-s3-website-test"
      error_message = "Invalid bucket name"
    }

    # Check index.html hash matches
    assert {
      condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
      error_message = "Invalid eTag for index.html"
    }

    # Check error.html hash matches
    assert {
      condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
      error_message = "Invalid eTag for error.html"
    }
  }

  # Call the setup module to create a random bucket prefix
  run "setup_tests3" {
+   state_key = "group3"
    module {
      source = "./tests/setup"
    }
  }

  # Apply run block to create the bucket
  run "create_bucket3" {
+   state_key = "group3"
    variables {
      bucket_name = "${run.setup_tests3.bucket_prefix}-aws-s3-website-test"
    }

    # Check that the bucket name is correct
    assert {
      condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests3.bucket_prefix}-aws-s3-website-test"
      error_message = "Invalid bucket name"
    }

    # Check index.html hash matches
    assert {
      condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
      error_message = "Invalid eTag for index.html"
    }

    # Check error.html hash matches
    assert {
      condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
      error_message = "Invalid eTag for error.html"
    }
  }

  # Call the setup module to create a random bucket prefix
  run "setup_tests4" {
+   state_key = "group4"
    module {
      source = "./tests/setup"
    }
  }

  # Apply run block to create the bucket
  run "create_bucket4" {
+   state_key = "group4"
    variables {
      bucket_name = "${run.setup_tests4.bucket_prefix}-aws-s3-website-test"
    }

    # Check that the bucket name is correct
    assert {
      condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests4.bucket_prefix}-aws-s3-website-test"
      error_message = "Invalid bucket name"
    }

    # Check index.html hash matches
    assert {
      condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
      error_message = "Invalid eTag for index.html"
    }

    # Check error.html hash matches
    assert {
      condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
      error_message = "Invalid eTag for error.html"
    }
  }

  # Call the setup module to create a random bucket prefix
  run "setup_tests5" {
+   state_key = "group5"
    module {
      source = "./tests/setup"
    }
  }

  # Apply run block to create the bucket
  run "create_bucket5" {
+   state_key = "group5"
    variables {
      bucket_name = "${run.setup_tests5.bucket_prefix}-aws-s3-website-test"
    }

    # Check that the bucket name is correct
    assert {
      condition     = aws_s3_bucket.s3_bucket.bucket == "${run.setup_tests5.bucket_prefix}-aws-s3-website-test"
      error_message = "Invalid bucket name"
    }

    # Check index.html hash matches
    assert {
      condition     = aws_s3_object.index.etag == filemd5("./www/index.html")
      error_message = "Invalid eTag for index.html"
    }

    # Check error.html hash matches
    assert {
      condition     = aws_s3_object.error.etag == filemd5("./www/error.html")
      error_message = "Invalid eTag for error.html"
    }
  }

実行してみます。

% time terraform test
tests/website.tftest.hcl... in progress
  run "setup_tests"... pass
  run "setup_tests4"... pass
  run "setup_tests5"... pass
  run "setup_tests2"... pass
  run "setup_tests3"... pass
  run "create_bucket3"... pass
  run "create_bucket"... pass
  run "create_bucket5"... pass
  run "create_bucket4"... pass
  run "create_bucket2"... pass
tests/website.tftest.hcl... tearing down
tests/website.tftest.hcl... pass

Success! 10 passed, 0 failed.
terraform test  23.42s user 5.01s system 33% cpu 1:24.18 total

なんと!実行時間延びてしまいました… tearing downのところがなかなか先に進まなかったですね。バケットの削除に時間がかかったのでしょうか?このあとも数回実行してみましたがどれもおなじくらいの処理時間でした。

あとテストの処理順が(並列実行されているため)ファイルに書かれた順にはなっていないのがわかりますね。

OCI(Oracle Cloud Infrastructure) の Terraform backend typeが追加された

Terraform backendは、Terraform state fileを格納するための機能です。AWSだとS3 Bucket に格納する構成を採れます。

これが今回OCIのサービスであるObject Storageを用いる形式のものが追加されたというわけです。OCIを使われている方にはとても朗報なのではないでしょうか。

私はOCIについて素人なのですが、Object StorageはS3に似たようなサービスのようですね。backendの設定もS3の場合と似通っていると思いました。

terraform {
  backend "oci" {
    # Required
    bucket            = "mybucket"
    namespace         = "my-namespace"
     # Optional
    tenancy_ocid      = "ocid1.tenancy.oc1..xxxxxxx"
    user_ocid         = "ocid1.user.oc1..xxxxxxxx"
    fingerprint       = "xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx"
    private_key_path  = "~/.oci/oci_api_key.pem"
    region            = "us-ashburn-1"
    key               = "path/to/my/key"
    workspace_key_prefix = "envs/"
    kms_key_id        = "ocid1.key.oc1.iad.xxxxxxxxxxxxxx"
    auth              = "APIKey"
    config_file_profile = "DEFAULT"
  }
}

Backend Type: oci | Terraform | HashiCorp Developer より引用

論理二項演算子で短絡評価されるようになった

論理二項演算子である && (AND) と || (OR) の評価ロジックが変更されました。論理式を評価する際に、式の途中で結果が確定した場合、それ以降の評価を行わない挙動になりました。

たとえば、条件A && 条件B という式では、条件A が false であれば、式の全体の結果は必ず false になります。このとき、新しいロジックでは 条件B は評価されずにスキップされます。

同様に、条件A || 条件B という式では、条件A が true であれば、式の全体の結果は必ず true になります。このときも、新しいロジックでは 条件B は評価されずにスキップされます。

試してみた

前述の「条件A && 条件B という式では、条件A が false であれば、式の全体の結果は必ず false になります。このとき、新しいロジックでは 条件B は評価されずにスキップされます。」を確認します。

以下コードを用意しました。

variable "known" {
  type = bool
  default = false
}

resource "terraform_data" "unknown" {
}

locals {
  computed = var.known && tobool(terraform_data.unknown.id)
}

resource "terraform_data" "count" {
  count = local.computed ? 2 : 3
}

このコードはBoolean operators should be capable of converting unknown values to known · Issue #31078 · hashicorp/terraformのコードを元に一部改変しています。

  • 最下部の terraform_data.countリソースの count数は local変数 computed を参照しています。
  • そのlocal変数 computed内で 論理二項演算子 && (AND) が使われています。最初の条件は variable knownを参照し、後ろの条件は terraform_data.unknown.idを参照しています。
  • variable knownのデフォルト値としてfalseが設定されているので、 -varオプションなどを使わなければvariable knownの値はfalseになります。
  • terraform_data.unknown.idの値はplanフェーズでは不明です。 terraform_dataidの値はapplyでリソースが作成された際に付与されるためです。

このコードを Terraform v1.11.4 で terraform planしたところ、以下のエラーになりました。

% terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform planned the following actions, but then encountered a problem:

  # terraform_data.unknown will be created
  + resource "terraform_data" "unknown" {
      + id = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
╷
│ Error: Invalid count argument
│ 
│   on main.tf line 25, in resource "terraform_data" "count":
│   25:   count = local.computed ? 2 : 3
│ 
│ The "count" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the -target argument
│ to first apply only the resources that the count depends on.

terraform_data.unknown.idの値がplanフェーズでは判明せず、その値を参照している local変数 computedterraform_data.countリソースの countで使われているため、planフェーズでterraform_data.countのcount数がわからない、と言っていますね。

これをv1.12.0で実行するとどうなるでしょうか。

% terraform plan 

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # terraform_data.count[0] will be created
  + resource "terraform_data" "count" {
      + id = (known after apply)
    }

  # terraform_data.count[1] will be created
  + resource "terraform_data" "count" {
      + id = (known after apply)
    }

  # terraform_data.count[2] will be created
  + resource "terraform_data" "count" {
      + id = (known after apply)
    }

  # terraform_data.unknown will be created
  + resource "terraform_data" "unknown" {
      + id = (known after apply)
    }

Plan: 4 to add, 0 to change, 0 to destroy.

───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.

v1.11とは対照的にterraform plan実行成功しました。

terraform_data.unknown.idの値がplanフェーズでは判明しない、というのはv1.12でも同じです。なのですが、local変数 computedつまりvar.known && tobool(terraform_data.unknown.id)の値は、var.known がfalseのため、tobool(terraform_data.unknown.id)がtrue/falseどちらでもfalseになります。というわけでtobool(terraform_data.unknown.id)の部分の評価はショートカットされました。そのためterraform_data.countのcount数も導くことが可能になっています。

余談: 1.12より前でのワークアラウンド

論理二項演算子を条件式に変更するとエラー回避できます。

locals {
-  computed = var.known && tobool(terraform_data.unknown.id)
+  computed = (var.known) ? tobool(terraform_data.unknown.id) : false
}

既存リソースのインポート時に id属性の代わりにidentity 属性が使えるように?

既存リソースをTerraform管理下に置くために、import blockを書いてapplyすることができます。

この時既存リソースを特定するために id属性を書きます。以下はEC2インスタンスをインポートする例です。インスタンスIDを id属性に書きます。

import {
  to = aws_instance.web
  id = "i-12345678"
}

aws_instance | Resources | hashicorp/aws | Terraform | Terraform Registry より引用

v1.12より、 id属性の代わりにidentity属性を指定して既存リソースをインポートできるようになりました。id属性とidentity属性はどちらか片方だけが指定できます。両方指定するとエラーになりますし、両方とも指定しないのもエラーになります。

ただ、何がidentity属性として指定できるかは、各Terraform providerが定義する各リソースタイプごとに異なります。そして現時点で私は identity属性でインポートできるリソースタイプを見つけることができませんでした… 例えば前述のEC2インスタンス(aws_instance)についても idでインポートする方法は書かれていますが、 identityでの方法はありませんでした。

ドキュメント

The identity argument is an object of key-value pairs that uniquely identify a resource.

とありましたので、identityidのように単一の属性値でリソースを特定するのではなく、複数の属性値の掛け合わせでリソースを特定できるようなものになるのではと予想します。

# identity属性 使用例予想
import {
  to = aws_instance.web
  identity = {
    ami    = "ami-0dcc1e21636832c5d"
    vpc_id = aws_vpc.example.id    
  }
}

identity属性でインポートできるリソースタイプを発見次第、試してみたいと思います。

参考情報

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.

OSZAR »