はじめに
Google CloudのインフラをTerraformで記述しており、コードの堅牢性を高めるために単純でも単体テストのようなものを作った方がよいとなりました。そこで、初めてでも最低限単体テストのようなもの作ってみるというのが今回の記事になります。
.tftest.hcl
Terraformのテストは通常*.tftest.hcl
という名前のファイルに記載します。「*
」は任意の文字列を意味しますので、「test.tftest.hcl
」や「validate_variables.tftest.hcl
」などのようなファイル名になります。
以下はTerraformのスタイルガイドの構成にテストファイルを追加したものです。modules
の各フォルダ内にテストファイルを追加しています。
.
├── modules
│ ├── function
│ │ ├── main.tf # contains aws_iam_role, aws_lambda_function
│ │ ├── outputs.tf
│ │ └── variables.tf
| | └── test.tftest.hcl # 追加したテストファイル
│ ├── queue
│ │ ├── main.tf # contains aws_sqs_queue
│ │ ├── outputs.tf
│ │ └── variables.tf
| | └── test.tftest.hcl # 追加したテストファイル
│ └── vpc
│ ├── main.tf # contains aws_vpc, aws_subnet
│ ├── outputs.tf
│ └── variables.tf
| └── test.tftest.hcl # 追加したテストファイル
├── main.tf
├── outputs.tf
└── variables.tf
.tftest.hclの書き方
次のようなコードを仮定しますGoogle CloudでCloud Storageを作成するものです。
# main.tf
resource "google_storage_bucket" "data" {
name = "cs-${var.env}"
location = var.location
force_destroy = true
storage_class = "STANDARD"
}
# variable.tf
variable "env" {
description = "Environment name"
type = string
# 1文字以上3文字以内の半角英小文字とするバリデーション
validation {
condition = can(regex("^[a-z]{1,3}$", var.env))
error_message = "The environment name must be 1 to 3 length a-z characters."
}
}
variable "location" {
description = "Location for the Cloud Storage"
type = string
# GCPで定義されているリージョンのバリデーション(簡易版:[文字列]-[文字列][数字列])
validation {
condition = can(regex("^([a-z]+-[a-z]+[0-9]+)$", var.location))
error_message = "region must be GCP region(ex. asia-northeast1)."
}
}
このコードに対してテストを作成してみます。
# test.tftest.hcl
variables {
env = "dev"
location = "asia-northeast1"
}
provider "google" {
project = "project-${var.env}"
}
run "vaild_variables" {
command = plan
variables {
env = "dev"
location = "asia-northeast1"
}
assert {
condition = can(regex("^[a-z]{1,3}$", var.env))
error_message = "The environment name must be 1 to 3 length a-z characters."
}
assert {
condition = can(regex("^([a-z]+-[a-z]+[0-9]+)$", var.location))
error_message = "region must be GCP region(ex. asia-northeast1)."
}
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# フォーマットが不正なロケーション(ハイフンなし)
run "invalid_location2" {
command = plan
variables {
env = "dev"
location = "asianortheast2"
}
expect_failures = [var.location]
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
provider
このテストファイルを実行される際に用いるproviderの設定を行います。
注意点として、後述するcommand
ブロックで値をapply
にした場合、テストの実行時に実際にリソースがproviderで指定したプロジェクトに作成されるということです。実運用環境を指定して稼働中のサービスを破壊してしまわないように注意する必要があります。
また、基本的にはテスト終了時にここで作成されたリソースは削除されますが、(削除保護がされていたり、リソース間の依存関係の問題で)削除に失敗してしまった場合には、手動でリソースを削除する必要があります。
mock
providerで述べたように、テスト時にcommand
の値をapply
にした場合は実際に環境にリソースが作成されます。これを避ける方法としてmock
の機能が提供されています。
基本的な使い方は、provider
ブロックを置き換えるだけです。
mock_provider "google" {}
run
run
ブロックがテストを実行する単位です。この中にテストしたい処理を書いていきます。
command
command
はそのrun
がterraformのどの部分の実行を行うかを指定するものです。
command
にはplan
またはapply
の引数を受け取り、デフォルト値はapply
です。
つまり、command
でこのテストがterraform plan
とterraform apply
のどちらを実行するのかを指定するものです。
assert
assert
はある条件を検証してそれが真であることを期待する動作をするブロックです。
assert
にはcondition
とerror_message
を指定でき、condition
には検証する条件を記載します。
assert
はこのcondition
に書かれた条件が真になることを期待します。
variables {
env = "dev"
location = "asia-northeast1"
}
assert {
condition = can(regex("^[a-z]{1,3}$", var.env))
error_message = "The environment name must be 1 to 3 length a-z characters."
}
例で、上記のテストを考えてみます。変数env
のバリデーション条件として、値が「1~3文字の小文字アルファベット」であることを期待しています。
このときvariables
で指定した値はdev
であるため、この条件は成功します
では、emvの値をprod
に変更してみます。これは変数のバリデーションに違反します。
variables {
env = "prod"
location = "asia-northeast1"
}
assert {
condition = can(regex("^[a-z]{1,3}$", var.env))
error_message = "The environment name must be 1 to 3 length a-z characters."
}
このとき、assert
は失敗してしまいます。以下のようなメッセージが確認できます
test.tftest.hcl... in progress
run "vaild_variables"... fail
╷
│ Error: Invalid value for variable
│
│ on test.tftest.hcl line 15, in run "vaild_variables":
│ 15: env = "prod"
│ ├────────────────
│ │ var.env is "prod"
│
│ The environment name must be 1 to 3 length a-z characters.
│
│ This was checked by the validation rule at variables.tf:5,3-13.
expect_failures
expect_failures
はterraform test
において、テストが失敗することを期待する際に用いるプロパティです。
expect_failures
の引数にはresourceやvariables、outputやdata sourceなどのオブジェクトをリストで与えることができ、これらをチェック対象として、問題が発生することを期待するものです。
例えば、ここでlocation
変数は「[文字列]-[文字列][数字列]」といった形式であるように条件が設定されています。
そしてテストでは「asianortheast2」という条件に反する値が設定されています。
expect_failures = [var.location]
で指定したようにこのテストではvar.location
が問題を返すことが期待されているためこのテストは成功します。(逆説的に、location
のバリデーションがこのパターンでは正しく機能することがテストできています。)
variable "location" {
description = "Location for the Artifact Registry repository"
type = string
# GCPで定義されているリージョンのバリデーション(簡易版:[文字列]-[文字列][数字列])
validation {
condition = can(regex("^([a-z]+-[a-z]+[0-9]+)$", var.location))
error_message = "region must be GCP region(ex. asia-northeast1)."
}
}
# フォーマットが不正なロケーション(ハイフンなし)
run "invalid_location2" {
command = plan
variables {
env = "dev"
location = "asianortheast2"
}
expect_failures = [var.location]
}
逆に、location
にバリデーションチェックを通過する正しい値を設定してテストした場合は、このテストケースは失敗します。
run "invalid_location2" {
command = plan
variables {
env = "dev"
location = "asia-northeast2"
}
expect_failures = [var.location]
}
╷
│ Error: Missing expected failure
│
│ on test.tftest.hcl line 80, in run "invalid_location2":
│ 80: expect_failures = [var.location]
│
│ The checkable object, var.location, was expected to report an error but did not.
╵
run "invalid_location3"... skip
run "invalid_location4"... skip
test.tftest.hcl... tearing down
test.tftest.hcl... fail
Failure! 5 passed, 1 failed, 2 skipped.
テストの実行について
テスト実行はterraform test
というコマンドで実行することができますが、いくつか注意点があります。
terraform initが必要
terraform test
コマンドを実行するためには、そのテストファイルがあるディレクリでterraform init
が必要です。
つまり、例のフォルダ構成の場合にfunction
モジュールのテストを実施したいならば./modules/function
ディレクトリでterraform init
が必要です。
そのため、以下のようなコマンドを実行して再帰的にinitを行っておくことをお勧めします。
コマンドでは拡張子が.tf
のファイルを含むディレクトリを探して重複を排除し、それぞれのディレクトリに対してterraform init
を行っています。
find . -name "*.tf" -exec dirname {} \; | sort -u | xargs -I {} sh -c 'cd "{}" && terraform init'
.
├── modules
│ ├── function
│ │ ├── main.tf # contains aws_iam_role, aws_lambda_function
│ │ ├── outputs.tf
│ │ └── variables.tf
| | └── test.tftest.hcl # 追加したテストファイル
│ ├── queue
│ │ ├── main.tf # contains aws_sqs_queue
│ │ ├── outputs.tf
│ │ └── variables.tf
| | └── test.tftest.hcl # 追加したテストファイル
│ └── vpc
│ ├── main.tf # contains aws_vpc, aws_subnet
│ ├── outputs.tf
│ └── variables.tf
| └── test.tftest.hcl # 追加したテストファイル
├── main.tf
├── outputs.tf
└── variables.tf
terraform testは再帰的に実行するオプションがない
terraform test
は再帰的に実行するオプションがないので、例のディレクトリ構造のような場合で./
がカレントディレクトリの状態で実行すると、何のテストケースも実行されず結果が以下のようになります。
Success! 0 passed, 0 failed.
そのため、このコマンドも以下のように拡張子が.tftest.hcl
のファイルを探し出してきて再帰的に実行するようにすることをお勧めします
find . -name "*.tftest.hcl" -exec dirname {} \; | sort -u | xargs -I {} sh -c 'cd "{}" && terraform test'
まとめ
今回は変数のバリデーションにのみ着目したようなテストでしたが、このように最低限でもテストケースを作成することでterraformの安全性を高めることができるので、触れてみてよかったと感じています。
command = apply
にした場合は実際にリソースが作成されるという点については意識する必要があるので、テスト用の環境を作っておくなどするとより安全にテストが実施できると感じます。ありがとうございました!
参考文献