Terraform 1.7 で利用可能なテストについて、比較のためポイントになりそうな部分を記載しました。
利用可能な選択肢(2024/6/14 現在)
手法 | 概要 | 記述場所 | 評価タイミング |
---|---|---|---|
validation ブロック | plan / apply 時に variable の値を検査する | variable ブロック | terraform plan / apply |
precondition / postcondition ブロック | plan / apply 時にリソース等のプロパティを検査する | lifecycle ブロック | terraform plan / apply |
check ブロック | plan / apply 後の結果の内容を検査する | tf ファイル | terraform plan / apply |
terraform test(mock 利用なし) | 一時的なリソースを作成し、そのリソースの内容を検査する | .tftest.hcl ファイル | terraform test |
terraform test(mock 利用あり) | 擬似的なリソースを作成し、そのリソースの内容を検査する | .tftest.hcl ファイル | terraform test |
validation ブロック
variable は type を指定すればざっくりと文字列や数字等に入力を限定できますが、もう少し細かい検査を実施できます。
例えば AWS のリージョン指定する変数があるとして、日本のリージョンでしか使えなくするならこんな感じですね。
variable "region" {
type = string
description = "AWS region"
validation {
condition = var.region == "ap-northeast-1" || var.region == "ap-northeast-3"
error_message = "Only Japanese regions are allowed."
}
}
terraform plan / apply の時に評価してくれます。
下記のようにエラーを吐いてくれました。
│ Error: Invalid value for variable
│
│ on main.tf line N:
│ N: variable "region" {
│ ├────────────────
│ │ var.region is "value"
│
│ Only Japanese regions are allowed.
│
│ This was checked by the validation rule at main.tf:N,N-N.
precondition / postcondition ブロック
こちらも terraform plan / apply の時に評価してくれます。
precondition
precondition は、リソースの作成や変更前に特定の条件を満たしているかを確認します。
例えば、特定のインスタンスタイプの EC2 インスタンスのみ作成可能にするには、下記のように記載します。
locals {
allowed_instance_types = ["t2.micro", "t2.small", "t3.micro"]
}
variable "instance_type" {
default = "t2.medium"
}
resource "aws_instance" "example" {
ami = "ami-12345678"
instance_type = var.instance_type
lifecycle {
precondition {
condition = contains(local.allowed_instance_types, var.instance_type)
error_message = "Instance type must be one of the allowed types."
}
}
}
変数 instance_type
の値が、ローカル変数 allowed_instance_types
の中に含まれないので、下記のエラーが出ます。
│ Error: Resource precondition failed
│
│ on main.tf line NN, in resource "aws_instance" "example":
│ NN: condition = contains(local.allowed_instance_types, var.instance_type)
│ ├────────────────
│ │ local.allowed_instance_types is tuple with 3 elements
│ │ var.instance_type is "t2.medium"
│
│ Instance type must be one of the allowed types.
似たような制限を varidation ブロックでもできますが、precondition で指定した方が、より特定のリソースに関する条件として検査できますね。
postcondition
postcondition は、リソースの作成や変更後に特定の条件を満たしているかを確認します。
例えば、作成されたインスタンスにパブリック IP アドレスが割り当てられていることを確認するには、下記のように記載します。
resource "aws_instance" "example" {
ami = "ami-hogehoge"
instance_type = "t2.micro"
lifecycle {
postcondition {
condition = self.public_ip != ""
error_message = "The instance must have a public IP address."
}
}
}
もし「パブリック IPv4 アドレスを自動割り当て」が「いいえ」なサブネットに EC2 インスタンスが作成され、パブリック IP アドレスが割り当てられないと下記のようにエラーになります。
│ Error: Resource postcondition failed
│
│ on main.tf line NN, in resource "aws_instance" "example":
│ NN: condition = self.public_ip != ""
│ ├────────────────
│ │ self.public_ip is ""
│
│ The instance must have a public IP address.
リソースは作成されたままになるので注意が必要です。
check ブロック
check は事後処理として検証を行い、警告表示のみを行います。
例えば何らかの設計上の規定により、VPC 内のサブネット数の上限を3つまでに制限したい場合は下記のようになります。
locals {
max_subnet_count = 3
}
resource "aws_vpc" "example" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "example1" {
vpc_id = aws_vpc.example.id
cidr_block = "10.0.0.0/24"
}
resource "aws_subnet" "example2" {
vpc_id = aws_vpc.example.id
cidr_block = "10.0.1.0/24"
}
resource "aws_subnet" "example3" {
vpc_id = aws_vpc.example.id
cidr_block = "10.0.2.0/24"
}
data "aws_subnets" "example_data" {
filter {
name = "vpc-id"
values = [aws_vpc.example.id]
}
}
check "validate_subnet_count" {
assert {
condition = length(data.aws_subnets.example_data.ids) <= local.max_subnet_count
error_message = "By design rule, you should not create more than 4 subnets in VPC."
}
}
こんな感じで書いておけば、誤ってこのVPCにサブネットを追加してしまった場合に警告を表示してくれます。
警告表示だけなので、異常終了はしない点に注意してください。
下記を追記してみると・・・
resource "aws_subnet" "example4" {
vpc_id = aws_vpc.example.id
cidr_block = "10.0.3.0/24"
}
data
でとってきたときの情報を見ているので、とりあえず terraform apply は成功します。
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_subnet.example4: Creating...
aws_subnet.example4: Creation complete after 2s [id=subnet-hogefuga]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
その後 terraform plan すると再度 data
が評価されるので、警告が表示されます。
│ Warning: Check block assertion failed
│
│ on main.tf line NN, in check "validate_subnet_count":
│ NN: condition = length(data.aws_subnets.example_data.ids) <= local.max_subnet_count
│ ├────────────────
│ │ data.aws_subnets.example_data.ids is list of string with 4 elements
│ │ local.max_subnet_count is 3
│
│ By design rule, you should not create more than 4 subnets in VPC.
terraform test
terraform test を使うと、実際に tf ファイルのコードを動かして、その結果に対して評価し、最後に作成したリソースを削除する、ということをしてくれます。
実際にリソースを作るのでより実際の動作に近いテストができますが、グローバルにユニークな ID が必要なリソースは外部から与えられる変数で ID を変えられるようにしておかないと、すでにリソースが作成された tf ファイルのテストを実施できませんので注意が必要です。
例えば S3 バケットの命名が変数 bucket_name
と一致するか確認するテストは、下記のように書けます。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
variable "bucket_name" {
type = string
}
resource "aws_s3_bucket" "my_bucket" {
bucket = var.bucket_name
}
run "sets_correct_name" {
variables {
bucket_name = "my-bucket-name"
}
assert {
condition = aws_s3_bucket.my_bucket.bucket == "my-bucket-name"
error_message = "incorrect bucket name"
}
}
tftest.hcl に記載した内容がテストコードです。
variables {}
で変数値を渡し、assert
で作成されたリソースの評価を行っています。
terraform test
コマンドでテストを実施します。
※ これはあくまで例です。 my-bucket-name
という名前の S3 バケットがグローバルで存在していればエラーになります
run "sets_correct_name"... fail
╷
│ Error: creating Amazon S3 (Simple Storage) Bucket (my-bucket-name): BucketAlreadyExists: The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again
もし main.tf で下記のように S3 バケット名がベタ書きになっていると、すでに terraform apply 後には my-bucket-name
という名前の S3 バケットが存在するので、テストが実施できません。
resource "aws_s3_bucket" "my_bucket" {
bucket = "my-bucket-name"
}
また、実際にリソースを作成するので、テスト完了まで時間がかかります。
terraform test での Mock の利用
mock_provider を使用すると、元のプロバイダーと同じスキーマを返すがリソースの作成はしない、ということをしてくれます。
例えば先ほどの例で、bucket_name.tftest.hcl
に mock_provider "aws" {}
を追記して下記のようにすると、aws プロバイダーの振る舞いを Mock にしてくれます。
mock_provider "aws" {}
run "sets_correct_name" {
variables {
bucket_name = "my-bucket-name"
}
assert {
condition = aws_s3_bucket.my_bucket.bucket == "my-bucket-name"
error_message = "incorrect bucket name"
}
}
このため、実際は S3 バケットが作成されずにテストが実施できますので、リソースの作成の待ち時間がなくスピーディーです。
注意点として、設定していないプロパティはダミーの値が入ります。
Mocked providers only generate data for computed attributes. All required resource attributes must be set when you use a mocked provider. If you do not provide a value for an optional computed attribute, Terraform will automatically generate one. The values that Terraform generates depends on the data type:
- Numbers will be 0.
- Booleans will be false.
- Strings will be a random 8 character alphanumeric string.
- Collections, including sets, lists, and maps, will be empty collections.
- Objects will contain all required sub-attributes generated using this same set of rules recursively.
この特徴を十分に理解した上で利用する必要がありそうです。
例えば先ほどの例だと、S3 バケットのドメイン名(bucket_domain_name)には何も値をセットしていないのでランダムな値となり、この値に関するテストは組めません。
下記のように書いてテストしてみます。
mock_provider "aws" {}
run "sets_correct_name" {
variables {
bucket_name = "my-bucket-name"
}
assert {
condition = aws_s3_bucket.my_bucket.bucket_domain_name == "my-domain-name"
error_message = "incorrect domain name"
}
}
そうすると、下記のような出力となり、適切なテストにはなりません。
(43r4r8wn
の部分は毎回変わります)
│ Error: Test assertion failed
│
│ on sample.tftest.hcl line N, in run "sets_correct_name":
│ N: condition = aws_s3_bucket.my_bucket.bucket_domain_name == "my-domain-name"
│ ├────────────────
│ │ aws_s3_bucket.my_bucket.bucket_domain_name is "43r4r8wn"
│
│ incorrect domain name