LoginSignup
0
0

Terraform 1.7で利用可能なテスト手法の比較と使用例

Last updated at Posted at 2024-06-14

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 のリージョン指定する変数があるとして、日本のリージョンでしか使えなくするならこんな感じですね。

main.tf
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 インスタンスのみ作成可能にするには、下記のように記載します。

main.tf
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 アドレスが割り当てられていることを確認するには、下記のように記載します。

main.tf
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つまでに制限したい場合は下記のようになります。

main.tf
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 と一致するか確認するテストは、下記のように書けます。

main.tf
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
}
bucket_name.tftest.hcl
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.hclmock_provider "aws" {} を追記して下記のようにすると、aws プロバイダーの振る舞いを Mock にしてくれます。

bucket_name.tftest.hcl
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)には何も値をセットしていないのでランダムな値となり、この値に関するテストは組めません。

下記のように書いてテストしてみます。

bucket_name.tftest.hcl
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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0