はじめに
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にした場合は実際にリソースが作成されるという点については意識する必要があるので、テスト用の環境を作っておくなどするとより安全にテストが実施できると感じます。ありがとうございました!
参考文献