🔰

Terraform Testに触れてみる

2024/07/20に公開

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バケットの作成をテストする

main.tf
# 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"
    }
  }
}
gcs_bucket.tftest.hcl
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ロールをテストする

main.tf
# 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"
}
sa.tftest.hcl
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の認証情報がなければテストの成功を試す事はできない。

main.tf
# 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
# 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の信頼性を高められる。

脚注
  1. Terratest公式 ↩︎

  2. HCL以外にJSONによるテストコードも対応している ↩︎

  3. Tests - Configuration Language | Terraform | HashiCorp Developer |#Example ↩︎

Discussion