
Terraform 1.12 がGAになりました
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) が使われています。最初の条件は variableknown
を参照し、後ろの条件はterraform_data.unknown.id
を参照しています。 - variable
known
のデフォルト値としてfalse
が設定されているので、-var
オプションなどを使わなければvariableknown
の値はfalse
になります。 terraform_data.unknown.id
の値はplanフェーズでは不明です。terraform_data
のid
の値は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変数 computed
が terraform_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.
とありましたので、identity
は id
のように単一の属性値でリソースを特定するのではなく、複数の属性値の掛け合わせでリソースを特定できるようなものになるのではと予想します。
# identity属性 使用例予想
import {
to = aws_instance.web
identity = {
ami = "ami-0dcc1e21636832c5d"
vpc_id = aws_vpc.example.id
}
}
identity
属性でインポートできるリソースタイプを発見次第、試してみたいと思います。
参考情報
- Release v1.12.0 · hashicorp/terraform
- Test block - Tests - Configuration Language | Terraform | HashiCorp Developer
- Parallel execution - Tests - Configuration Language | Terraform | HashiCorp Developer
- terraform test command reference | Terraform | HashiCorp Developer
- Backend Type: oci | Terraform | HashiCorp Developer
- Update HCL by jbardin · Pull Request #36224 · hashicorp/terraform
- Import - Configuration Language | Terraform | HashiCorp Developer