Terraform Testに触れてみる
Terraform Testとは
Terraform v1.6から実装されたTerraform組み込みのテスト機能。
従来、TerraformのコードをテストするにはTerratestなどのツールを使うのがスタンダードだった。
Terratest[1]はGoライブラリであり、Terraformに限らずk8sマニフェストやDockerFileもテストする事ができた。
しかしGoライブラリなので、Goのテストコードが書ける必要があった。
本テスト機能は、もちろんk8sマニフェストやDockerFileのテストはできないが、Terraformで使われるHCLでテストコードを書くことができる。[2]
これにより、Goユーザでなくても平易にTerraformのテストができるようになる。
Terraform Testの基本構文
runブロック
JUnitにおけるテストメソッドのようなもの。テストコードの本体。
runブロックの中で後述のassertなどを定義して、テストが実行できる。
1つのテストファイルの中で複数ブロック記述できる。
variableブロック
文字通りテスト用変数を定義するブロック。テスト対象のコードで定義したvariableを上書きできる。
runブロックの実行よりも先にセットアップのような形で処理される。
テストファイルのルートに定義すると全てのrunブロックに変数を適用できる。
runブロックの中に定義すると、そのrunブロックに限定して変数を適用できる。
テストファイルのルートとrunブロックの両方に定義された場合は、runブロックの定義で上書きされる。
定義できるのはテストファイルのルートに最大1つ、1つのrunブロックの中に最大1つまで。
tfversファイルとしてtftestファイルの外部に定義して利用することも可能。
providerブロック
文字通りテスト用プロバイダーを定義するブロック。テスト対象のコードで定義したproviderを上書きできる。
variableブロックと役割は異なるが、上書きなど大体同じ仕様。
command=plan/apply
runブロックの中でplanかapplyのどちらでテストを実行するのか選択できる。
run {
command = plan
}
とすれば、terraform plan
の結果とassertしてくれる。
run {
command = apply
}
とすれば、terraform apply
の結果とassertしてくれる
デフォルトではcommand=apply
が設定されているため、既存リソースを破壊しないように十分注意する必要がある。
GCPでの使い方
GCSバケットの作成をテストする
# GCSバケットを作成するmain.tf
provider "google" {
project = "testproject-f7740"
region = "asia-northeast"
zone = "asia-northeast-1"
}
locals {
multi_bucket_name_list = [
"sample11-maashiro-202406",
"sample22-maashiro-202406",
"sample33-maashiro-202406"
]
}
resource "google_storage_bucket" "single_bucket" {
name = "sample-maashiro-202406-bucket"
location = "ASIA-NORTHEAST1"
lifecycle_rule {
condition {
age = 3
}
action {
type = "Delete"
}
}
}
resource "google_storage_bucket" "multi_bucket" {
for_each = toset(local.multi_bucket_name_list)
name = "${each.value}-bucket"
location = "ASIA-NORTHEAST1"
lifecycle_rule {
condition {
age = 3
}
action {
type = "Delete"
}
}
}
run "single_bucket_test" {
command = apply
assert {
condition = google_storage_bucket.single_bucket.name == "sample-maashiro-202406-bucket"
error_message = "single test bucket is not test-bucket"
}
}
run "multi_bucket_test" {
command = apply
assert {
condition = length(google_storage_bucket.multi_bucket) == 3
error_message = "bucket count error"
}
assert {
condition = google_storage_bucket.multi_bucket["sample11-maashiro-202406"].name == "sample11-maashiro-202406-bucket"
error_message = "bucket name error"
}
assert {
condition = google_storage_bucket.multi_bucket["sample22-maashiro-202406"].name == "sample22-maashiro-202406-bucket"
error_message = "bucket name error"
}
assert {
condition = google_storage_bucket.multi_bucket["sample33-maashiro-202406"].name == "sample33-maashiro-202406-bucket"
error_message = "bucket name error"
}
}
このコードでterrafom test
を実行した結果
maashiro % terraform test
gcs_bucket.tftest.hcl... in progress
run "multi_bucket_test"... pass
run "single_bucket_test"... fail
╷
│ Error: googleapi: Error 409: Your previous request to create the named bucket succeeded and you already own it., conflict
│
│ with google_storage_bucket.single_bucket,
│ on main.tf line 16, in resource "google_storage_bucket" "single_bucket":
│ 16: resource "google_storage_bucket" "single_bucket" {
│
╵
╷
│ Error: googleapi: Error 409: Your previous request to create the named bucket succeeded and you already own it., conflict
│
│ with google_storage_bucket.multi_bucket["sample11-maashiro-202406"],
│ on main.tf line 29, in resource "google_storage_bucket" "multi_bucket":
│ 29: resource "google_storage_bucket" "multi_bucket" {
│
╵
╷
│ Error: googleapi: Error 409: Your previous request to create the named bucket succeeded and you already own it., conflict
│
│ with google_storage_bucket.multi_bucket["sample33-maashiro-202406"],
│ on main.tf line 29, in resource "google_storage_bucket" "multi_bucket":
│ 29: resource "google_storage_bucket" "multi_bucket" {
│
╵
╷
│ Error: googleapi: Error 409: Your previous request to create the named bucket succeeded and you already own it., conflict
│
│ with google_storage_bucket.multi_bucket["sample22-maashiro-202406"],
│ on main.tf line 29, in resource "google_storage_bucket" "multi_bucket":
│ 29: resource "google_storage_bucket" "multi_bucket" {
│
╵
gcs_bucket.tftest.hcl... tearing down
gcs_bucket.tftest.hcl... fail
Failure! 1 passed, 1 failed.
command=plan
とした"multi_bucket_test"は成功しているが、
command=apply
とした"single_bucket_test"は環境上に既に同名のバケットが存在するため、エラーになった。
SA作成時のIAMロールをテストする
# SAを作成するmain.tf
provider "google" {
project = "testproject-f7740"
region = "asia-northeast"
zone = "asia-northeast-1"
}
variable "sa-name" {
type = string
default = "sa-maashiro-sample"
}
resource "google_service_account" "sa" {
account_id = var.sa-name
display_name = "sample sa"
}
resource "google_service_account_iam_member" "sample_sa" {
service_account_id = google_service_account.sa.name
role = "roles/iam.serviceAccountUser"
member = "user:jane@example.com"
}
variables {
sa-name = "test-sa"
}
run "sa_id_test"{
command = plan
assert {
condition = google_service_account.sa.account_id == "test-sa"
error_message = "account id error"
}
}
このコードでterrafom test
を実行した結果
maashiro % terraform test
sa.tftest.hcl... in progress
run "sa_id_test"... pass
sa.tftest.hcl... tearing down
sa.tftest.hcl... pass
Success! 1 passed, 0 failed.
variablesでテスト用に書き換えたaccount_idでassertが行われている。
v1.7.0でbetaとして実装されたMock機能
現在beta版ではあるが、terraform test
でMockが使えるようになった。
これにより、providerの認証情報が不要になったり、依存関係のあるリソースが不要でテストが実行できる。
Mockを使ったproviderの省略
公式ドキュメントでサンプルコードとして提示されている[3]次のコードはAWSの認証情報がなければテストの成功を試す事はできない。
# valid_string_concat.tftest.hcl
variables {
bucket_prefix = "test"
}
run "valid_string_concat" {
command = plan
assert {
condition = aws_s3_bucket.bucket.bucket == "test-bucket"
error_message = "S3 bucket name did not match expected"
}
}
# valid_string_concat.tftest.hcl
variables {
bucket_prefix = "test"
}
run "valid_string_concat" {
command = plan
assert {
condition = aws_s3_bucket.bucket.bucket == "test-bucket"
error_message = "S3 bucket name did not match expected"
}
}
実行結果
maashiro % terraform test
sa.tftest.hcl... in progress
run "valid_string_concat"... fail
╷
│ Error: No valid credential sources found
│
│ with provider["registry.terraform.io/hashicorp/aws"],
│ on main.tf line 3, in provider "aws":
│ 3: provider "aws" {
│
│ Please see https://registry.terraform.io/providers/hashicorp/aws
│ for more information about providing credentials.
│
│ Error: failed to refresh cached credentials, no EC2 IMDS role found, operation error ec2imds: GetMetadata, request canceled, context deadline exceeded
│
╵
sa.tftest.hcl... tearing down
sa.tftest.hcl... fail
Failure! 0 passed, 1 failed.
ここで、Mock機能のmock_provider "aws" {}
をテストコードの最上部に追加すると、
maashiro % terraform test
sa.tftest.hcl... in progress
run "valid_string_concat"... pass
sa.tftest.hcl... tearing down
sa.tftest.hcl... pass
Success! 1 passed, 0 failed.
このように、認証情報をmockで省略してテストを実行,成功させることができる。
まとめ
TerraformのTest機能を使えば、様々なテストを実行できる。
特にMockをうまく使うことで、疎結合なUTを実施したり、command=apply
でも実環境への影響を排除したUTを実施することができそう。
今回は記載できなかったが、異常系テストを実現するための構文も用意されているため、使いこなすことができればIaCの信頼性を高められる。
-
HCL以外にJSONによるテストコードも対応している ↩︎
-
Tests - Configuration Language | Terraform | HashiCorp Developer |#Example ↩︎
Discussion