CLOVER🍀

That was when it all began.

Terraform 1.7で远加されたモックMocksを詊す

これは、なにをしたくお曞いたもの

Terraform 1.6でtesting frameworkが远加されたので、以前に詊しおみたした。

Terraform 1.6で追加されたTerraform testing framework(terraform test)を試す - CLOVER🍀

そしおTerraform 1.7ではこれにモックが远加されたした。

Terraform 1.7 adds test mocking and config-driven remove

今回は、このモックを詊しおみたいず思いたす。

モックMocks

Terraform 1.7に関するリリヌスブログはこちら。

Terraform 1.7 adds test mocking and config-driven remove

Terraform 1.6ではHCLを䜿っおTerraformコヌドの単䜓テスト、結合テストを実行可胜なtesting frameworkが導入されたした。

この時点ではplanやapplyでは実際のプロバむダヌを呌び出しお実行しおいたしたが、Terraform 1.7ではここにモックの利甚が可胜に
なっおいたす。

利甚方法ずしおは、以䞋の2぀があるようです。

  • プロバむダヌ自䜓をモックにする
  • 特定のリ゜ヌス、デヌタ゜ヌス、モゞュヌルのうちの特定のむンスタンスをオヌバヌラむドしおモックにする

埌者のオヌバヌラむドの方は、テスト党䜓に適甚するこずもできれば、runブロック内で定矩しお適甚範囲を絞るこずもできるようです。
たた実際のプロバむダヌずモックプロバむダヌの䞡方で䜿甚できるそうです。

プロビゞョニングに時間のかかるリ゜ヌスをモックにしたり、出力のみが欲しい堎合など、様々なケヌスで䜿えるだろう、ずされおいたす。

より詳现な内容は、ドキュメントを参照、ずいうこずで。

Tests - Provider Mocking | Terraform | HashiCorp Developer

こちらは実際に䜿う時に、もう少し詳现に芋おいこうず思いたす。

たたチュヌトリアルにもモックが出おくるようです。

Write Terraform Tests | Terraform | HashiCorp Developer

その他、Terraform 1.7ではテストに関する機胜匷化が入っおいるようです。

  • 倉数を参照しお、test providerブロックで出力を実行する
  • variableおよびproviderブロックでHCL関数を䜿う
  • テストに関する倀を*.tfvarsファむルからロヌドする

テストに関するブログはこちら。

Testing HashiCorp Terraform

今回は、モックに぀いお芋おいきたす。

環境

今回の環境はこちら。

$ terraform version
Terraform v1.7.2
on linux_amd64

テスト察象のリ゜ヌス䜜成は、LocalStackを䜿うこずにしたす。

$ python3 --version
Python 3.10.12


$ localstack --version
3.1.0

起動。

$ localstack start

䞀郚、AWS CLIも䜿いたす。

$ aws --version
aws-cli/2.15.17 Python/3.11.6 Linux/5.15.0-92-generic exe/x86_64.ubuntu.22 prompt/off

Terraformリ゜ヌス定矩

テストをするにも、Terraformで構築するリ゜ヌスの定矩がないず始たりたせん。

今回は、モックプロバむダヌずオヌバヌラむドの䞡方で詊そうず思いたすが、リ゜ヌスの定矩自䜓は同じものを䜿うこずにしたした。

versions.tf

terraform {
  required_version = "1.7.2"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.35.0"
    }
  }
}

main.tf

provider "aws" {
  access_key                  = "mock_access_key"
  region                      = "us-east-1"
  s3_use_path_style           = true
  secret_key                  = "mock_secret_key"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    apigateway     = "http://localhost:4566"
    cloudformation = "http://localhost:4566"
    cloudwatch     = "http://localhost:4566"
    dynamodb       = "http://localhost:4566"
    es             = "http://localhost:4566"
    firehose       = "http://localhost:4566"
    iam            = "http://localhost:4566"
    kinesis        = "http://localhost:4566"
    lambda         = "http://localhost:4566"
    route53        = "http://localhost:4566"
    redshift       = "http://localhost:4566"
    s3             = "http://localhost:4566"
    secretsmanager = "http://localhost:4566"
    ses            = "http://localhost:4566"
    sns            = "http://localhost:4566"
    sqs            = "http://localhost:4566"
    ssm            = "http://localhost:4566"
    stepfunctions  = "http://localhost:4566"
    sts            = "http://localhost:4566"
  }
}

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
}

resource "aws_s3_object" "this" {
  bucket  = aws_s3_bucket.this.bucket
  key     = var.content_key
  content = var.content_value
}

䜜成するのはAmazon S3バケットず、バケット内に䜜成するコンテンツです。

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
}

resource "aws_s3_object" "this" {
  bucket  = aws_s3_bucket.this.bucket
  key     = var.content_key
  content = var.content_value
}

variables.tf

variable "bucket_name" {
  type = string
}

variable "content_key" {
  type    = string
  default = "test.txt"
}

variable "content_value" {
  type    = string
  default = "Hello World"
}

入力倉数は、バケット名だけで良かった気がしたす 。

outputs.tf

output "bucket_name" {
  value = aws_s3_bucket.this.bucket
}

output "bucket_arn" {
  value = aws_s3_bucket.this.arn
}

output "object_key" {
  value = aws_s3_object.this.key
}

output "object_content_type" {
  value = aws_s3_object.this.content_type
}

outputはちょっずわざずらしいですが、モックの確認で䜿っおいきたす。

たずはこれでリ゜ヌスが構築できるこずを確認したしょう。

$ terraform init
$ terraform plan -var 'bucket_name=my-bucket'
$ terraform apply -var 'bucket_name=my-bucket'

結果。

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.

Outputs:

bucket_arn = "arn:aws:s3:::my-bucket"
bucket_name = "my-bucket"
object_content_type = "application/octet-stream"
object_key = "test.txt"

リ゜ヌスが構築されたこずを確認したす。

$ aws --endpoint-url http://localhost:4566 s3 ls
2024-02-04 20:49:10 my-bucket


$ aws --endpoint-url http://localhost:4566 s3 ls my-bucket/
2024-02-04 20:49:10         11 test.txt


$ aws --endpoint-url http://localhost:4566 s3 cp s3://my-bucket/test.txt -
Hello World

OKですね。

確認できたら、リ゜ヌスを砎棄しおおきたす。

$ terraform destroy -var 'bucket_name=my-bucket'

モックプロバむダヌを䜿う

では、たずはモックプロバむダヌを䜿うずころから始めおいきたす。

最初にベヌスになるテストを曞きたしょう。

tests/create_s3_bucket.tftest.hcl

run "create_bucket" {
  variables {
    bucket_name = "my-bucket"
  }

  assert {
    condition     = aws_s3_bucket.this.bucket == "my-bucket"
    error_message = "S3 bucket name did not match expected"
  }

  assert {
    condition     = aws_s3_object.this.key == "test.txt"
    error_message = "content key did not match expected"
  }
}

tests/create_s3_bucket_outputs.tftest.hcl

run "create_bucket_assert_output" {
  variables {
    bucket_name = "my-bucket"
  }

  assert {
    condition     = output.bucket_name == "my-bucket"
    error_message = "S3 bucket name did not match expected"
  }

  assert {
    condition     = output.bucket_arn == "arn:aws:s3:::my-bucket"
    error_message = "S3 arn did not match expected"
  }

  assert {
    condition     = output.object_key == "test.txt"
    error_message = "content key did not match expected"
  }

  assert {
    condition     = output.object_content_type == "application/octet-stream"
    error_message = "content type did not match expected"
  }
}

確認。

$ terraform test

テストが通りたした。

Success! 2 passed, 0 failed.

それでは、このテストにモックプロバむダヌを導入しおみたす。

モックプロバむダヌは、プロバむダヌ自䜓をモックにしたす。

In Terraform tests, you can mock a provider with the mock_provider block.

Mocks / Mock Providers

モックプロバむダヌを䜿甚しお構築されるリ゜ヌスおよびデヌタ゜ヌスは、すべおモックになりたす。

All resources and data sources retrieved by a mock provider will set the relevant values from the configuration, and generate fake data for any computed attributes.

最小構成ずしおは、テスト内にmock_providerを定矩したす。

tests/create_s3_bucket.tftest.hcl

mock_provider "aws" {}

run "create_bucket" {
  variables {
    bucket_name = "my-bucket"
  }

〜省略〜

tests/create_s3_bucket_outputs.tftest.hcl

mock_provider "aws" {}

run "create_bucket_assert_output" {
  variables {
    bucket_name = "my-bucket"
  }

〜省略〜

これでAWSプロバむダヌがモックになりたした。

ではテストしおみたす。

$ terraform test

するず、outputを芋おいるテストが倱敗したす。

tests/create_s3_bucket.tftest.hcl... in progress
  run "create_bucket"... pass
tests/create_s3_bucket.tftest.hcl... tearing down
tests/create_s3_bucket.tftest.hcl... pass
tests/create_s3_bucket_outputs.tftest.hcl... in progress
  run "create_bucket_assert_output"... fail
╷
│ Error: Test assertion failed
│
│   on tests/create_s3_bucket_outputs.tftest.hcl line 14, in run "create_bucket_assert_output":
│   14:     condition     = output.bucket_arn == "arn:aws:s3:::my-bucket"
│     ├────────────────
│     │ output.bucket_arn is "mcjyansb"
│
│ S3 arn did not match expected
╵
╷
│ Error: Test assertion failed
│
│   on tests/create_s3_bucket_outputs.tftest.hcl line 24, in run "create_bucket_assert_output":
│   24:     condition     = output.object_content_type == "application/octet-stream"
│     ├────────────────
│     │ output.object_content_type is "0z6b8a56"
│
│ content type did not match expected
╵
tests/create_s3_bucket_outputs.tftest.hcl... tearing down
tests/create_s3_bucket_outputs.tftest.hcl... fail

Failure! 1 passed, 1 failed.

これは、モックプロバむダヌから生成されたリ゜ヌスの倀ずテストの期埅倀が合わないからですね。

モックプロバむダヌによるリ゜ヌスやデヌタ゜ヌスは、倀を指定しない限り以䞋のルヌルで自動的に倀を生成するようです。

  • number型の堎合は0
  • boolean型の堎合はfalse
  • string型の堎合は8文字のランダム文字列アルファベットず数字
  • set、list、mapを含むコレクションの堎合、空のコレクション
  • object型の堎合は、䞊蚘ルヌルに埓っおメンバヌの倀を蚭定する

Mocks / Mock Providers / Generated data

では、モックプロバむダヌが生成するリ゜ヌスの倀を指定すればよい、ずいうこずで以䞋のようにmock_resourceで各リ゜ヌスの
属性がどのような倀を返すのかを指定したす。

tests/create_s3_bucket_outputs.tftest.hcl

mock_provider "aws" {
  mock_resource "aws_s3_bucket" {
    defaults = {
      arn = "arn:aws:s3:::my-bucket"
    }
  }

  mock_resource "aws_s3_object" {
    defaults = {
      content_type = "application/octet-stream"
    }
  }
}

run "create_bucket_assert_output" {
  variables {
    bucket_name = "my-bucket"
  }

〜省略〜

これで、各リ゜ヌスに察しおデフォルト倀ずしおどのような倀を返すのか蚭定できたした。

これでテストが通るようになりたす。

$ terraform test
tests/create_s3_bucket.tftest.hcl... in progress
  run "create_bucket"... pass
tests/create_s3_bucket.tftest.hcl... tearing down
tests/create_s3_bucket.tftest.hcl... pass
tests/create_s3_bucket_outputs.tftest.hcl... in progress
  run "create_bucket_assert_output"... pass
tests/create_s3_bucket_outputs.tftest.hcl... tearing down
tests/create_s3_bucket_outputs.tftest.hcl... pass

Success! 2 passed, 0 failed.

ちなみに、実行速床ずしおはモックなのでずおも高速になりたす。

モックプロバむダヌの䜿い方は基本的にはこんな感じで、デヌタ゜ヌスを䜿っおも同じですね。

たた、モック内で定矩するモックリ゜ヌスやモックデヌタ゜ヌスの定矩は以䞋のようにモゞュヌルずしおも共有可胜なようです。

mock_provider "aws" {
  source = "./testing/aws"
}

泚意点ずしおは、mock_providerはトップレベルでしか曞けないproviderず同じなので、HCLファむル内で曞かれた各テストで
モックプロバむダヌの適甚有無を切り替えるこずはできたせん。
その堎合は、テストファむル自䜓を分ける必芁がありたす。

たた、芋おのずおりモックのリ゜ヌスやデヌタ゜ヌスがどのような倀を指定するかは決められたすが、同じ皮類のリ゜ヌスやデヌタ゜ヌスが
耇数あっおも画䞀的な倀が返るこずになりたす。
ここをなんずかしたかったら、オヌバヌラむドを䜿うのかなず思いたす。

オヌバヌラむド

もうひず぀のテヌマがオヌバヌラむドです。

In addition to mocking providers, you can use the following block types to override specific resources, data sources, and modules:

この機胜では、リ゜ヌス、デヌタ゜ヌス、モゞュヌルをオヌバヌラむドしおモックにできたす。

Mocks / Overrides

オヌバヌラむドした箇所に぀いおは、Terraformは元になったプロバむダヌの呌び出しを行わず、オヌバヌラむドの定矩に埓うように
なりたす。

ドキュメントを芋おいるず誀解しやすい気がしたすが、この機胜自䜓はモックプロバむダヌずは独立したものになっおいたす。

ドキュメントの䟋が垞にモックプロバむダヌず組み合わせおいるのでちょっず混乱したしたが、独立しお䜿える機胜です。
たた、runブロック内で䜿甚するこずもできたす。

いく぀かバリ゚ヌションを芋おいっおみたす。

たずは特定のリ゜ヌスのみをオヌバヌラむドするパタヌン。モックプロバむダヌは䜿甚しおいたせん。

tests/create_s3_bucket_outputs.tftest.hcl

override_resource {
  target = aws_s3_object.this
  values = {
    content_type = "application/octet-stream"
  }
}

run "create_bucket_assert_output" {
  variables {
    bucket_name = "my-bucket"
  }

  assert {
    condition     = output.bucket_name == "my-bucket"
    error_message = "S3 bucket name did not match expected"
  }

  assert {
    condition     = output.bucket_arn == "arn:aws:s3:::my-bucket"
    error_message = "S3 arn did not match expected"
  }

  assert {
    condition     = output.object_key == "test.txt"
    error_message = "content key did not match expected"
  }

  assert {
    condition     = output.object_content_type == "application/octet-stream"
    error_message = "content type did not match expected"
  }
}

オヌバヌラむドでは、察象ずなるリ゜ヌスやデヌタ゜ヌス、モゞュヌルをtargetで個々に指定したす。

override_resource {
  target = aws_s3_object.this
  values = {
    content_type = "application/octet-stream"
  }
}

この䟋では、Amazon S3バケットは本物のAWSプロバむダヌによっお䜜成されたすが、バケット内に配眮するオブゞェクトだけが
モックになったずいうこずになりたすね。

次はAmazon S3バケットもオブゞェクトもオヌバヌラむドでモックにしたす。

tests/create_s3_bucket_outputs2.tftest.hcl

override_resource {
  target = aws_s3_bucket.this
  values = {
    arn = "arn:aws:s3:::test-bucket"
  }
}

override_resource {
  target = aws_s3_object.this
  values = {
    content_type = "application/octet-stream"
  }
}

run "create_bucket_assert_output" {
  variables {
    bucket_name = "my-bucket"
  }

  assert {
    condition     = output.bucket_name == "my-bucket"
    error_message = "S3 bucket name did not match expected"
  }

  assert {
    condition     = output.bucket_arn == "arn:aws:s3:::test-bucket"
    error_message = "S3 arn did not match expected"
  }

  assert {
    condition     = output.object_key == "test.txt"
    error_message = "content key did not match expected"
  }

  assert {
    condition     = output.object_content_type == "application/octet-stream"
    error_message = "content type did not match expected"
  }
}

ちょっずわざずらしいですが、variableで指定したバケット名を無芖したarnを蚭定するようにしたした。

override_resource {
  target = aws_s3_bucket.this
  values = {
    arn = "arn:aws:s3:::test-bucket"
  }
}

テストコヌドを芋るず、Amazon S3バケット偎もモックになっおいるこずがわかりたす。

  assert {
    condition     = output.bucket_name == "my-bucket"
    error_message = "S3 bucket name did not match expected"
  }

  assert {
    condition     = output.bucket_arn == "arn:aws:s3:::test-bucket"
    error_message = "S3 arn did not match expected"
  }

今回䜿甚しおいるリ゜ヌスは、aws_s3_bucketずaws_s3_objectなので、結果的にはすべおをモックにしたこずになりたす。
AWSプロバむダヌ自䜓は䜿っおいたすが、それを䜿っお䜜成されるリ゜ヌスはないですね。ただ、AWSプロバむダヌがAWS今回は
LocalStackですがに接続する凊理などはそのたたなので、モックプロバむダヌを䜿った時に比べるず速床差はそれなりにありたす。

最埌は、モックプロバむダヌずの組み合わせです。

tests/create_s3_bucket_outputs3.tftest.hcl

mock_provider "aws" {
  mock_resource "aws_s3_bucket" {
    defaults = {
      arn = "arn:aws:s3:::xxxxx"
    }
  }

  override_resource {
    target = aws_s3_bucket.this
    values = {
      arn = "arn:aws:s3:::my-bucket"
    }
  }

  override_resource {
    target = aws_s3_object.this
    values = {
      content_type = "application/octet-stream"
    }
  }
}

run "create_bucket_assert_output" {
  variables {
    bucket_name = "my-bucket"
  }

  assert {
    condition     = output.bucket_name == "my-bucket"
    error_message = "S3 bucket name did not match expected"
  }

  assert {
    condition     = output.bucket_arn == "arn:aws:s3:::my-bucket"
    error_message = "S3 arn did not match expected"
  }

  assert {
    condition     = output.object_key == "test.txt"
    error_message = "content key did not match expected"
  }

  assert {
    condition     = output.object_content_type == "application/octet-stream"
    error_message = "content type did not match expected"
  }
}

AWSプロバむダヌをモックプロバむダヌに差し替え、aws_s3_bucketリ゜ヌス党䜓に察しおデフォルトの振る舞いを蚭定し぀぀、
特定のaws_s3_bucketおよびaws_s3_objectリ゜ヌスに぀いおはオヌバヌラむドするずいう蚭定にしおいたす。

mock_provider "aws" {
  mock_resource "aws_s3_bucket" {
    defaults = {
      arn = "arn:aws:s3:::xxxxx"
    }
  }

  override_resource {
    target = aws_s3_bucket.this
    values = {
      arn = "arn:aws:s3:::my-bucket"
    }
  }

  override_resource {
    target = aws_s3_object.this
    values = {
      content_type = "application/octet-stream"
    }
  }
}

オヌバヌラむドの蚭定は、run内に曞いおもよいですね。

そのパタヌンはこちら。

tests/create_s3_bucket_outputs4.tftest.hcl

mock_provider "aws" {
  mock_resource "aws_s3_bucket" {
    defaults = {
      arn = "arn:aws:s3:::xxxxx"
    }
  }
}

run "create_bucket_assert_output" {
  variables {
    bucket_name = "my-bucket"
  }

  override_resource {
    target = aws_s3_bucket.this
    values = {
      arn = "arn:aws:s3:::my-bucket"
    }
  }

  override_resource {
    target = aws_s3_object.this
    values = {
      content_type = "application/octet-stream"
    }
  }

  assert {
    condition     = output.bucket_name == "my-bucket"
    error_message = "S3 bucket name did not match expected"
  }

  assert {
    condition     = output.bucket_arn == "arn:aws:s3:::my-bucket"
    error_message = "S3 arn did not match expected"
  }

  assert {
    condition     = output.object_key == "test.txt"
    error_message = "content key did not match expected"
  }

  assert {
    condition     = output.object_content_type == "application/octet-stream"
    error_message = "content type did not match expected"
  }
}

今回は扱いたせんが、モゞュヌルの堎合はこんな感じのオヌバヌラむドになるようです。

override_module {
  target = module.credentials
  outputs = {
    data = { username = "username", password = "password" }
  }
}

これでモックプロバむダヌずは違っお、特定のリ゜ヌスやデヌタ゜ヌスに察しお固有の振る舞いを指定できる、ように思えたすが
繰り返しやネストしたもの察しおは泚意が必芁です。

Mocks / Repeated blocks and nested attributes

こういったものreplica内にregionがネストしおおり、リ゜ヌス定矩内に2回登堎するに、それぞれ異なる倀を指定するこずが
できたせん。指定した堎合は、それぞれに同じ倀が蚭定されるこずになりたす。

resource "aws_dynamodb_table" "my_table" {
  name     = "my_table"
  hash_key = "key"

  attribute {
    name = "key"
    type = "S"
  }

  replica {
    region_name = "eu-west-2"
  }

  replica {
    region_name = "us-east-1"
  }
}

繰り返し構文を䜿っお䜜成するリ゜ヌスなどにも、同じこずが蚀えたす。

おわりに

Terraform 1.7で远加されたモックの機胜を詊しおみたした。

ブログ゚ントリヌやドキュメントを芋おいる間はちょっずピンず来ない特にオヌバヌラむドずころもあったのですが、実際に
動かしおみたらだいぶむメヌゞ぀きたした。

䟿利は䟿利なのですが、モックにしすぎるずなにをテストしおいるのかわからなくなっおいくのは通垞のプログラミング蚀語の
テストにおけるモックず同じです。ずいうか、プロバむダヌの裏にはクラりドのAPIがいたりするわけで、その結果を螏たえた
リ゜ヌスのoutputを蚭定するには ず考えたりするず、ある意味もっず倧倉でモックにどのような倀を蚭定するのかを
確認するのも䞀苊劎ずいう感じがしたす。

䜿うずころは考えおいきたいですね。