見出し画像

Terraform testコマンドを使ってみた

初めまして、株式会社Acompanyの高橋です。
本記事は私がTerraform v1.6で追加されたtestコマンドを使った際の備考録に
なります。

実行環境

  • Terraform v1.7.0


Terraform testとは

Terraform testとは実際にTerraform moduleをplan,applyをしテストを行う機能です。(もちろんmodule以外で使うことも可能です。)
これまではTerratestなど他言語で書かれたツールを使う必要がありましたが、1.6.0以降では公式にHCL(Hashicorp Configuration Language)でtestをかけるようになりました。

使い方

テストを記述する際は.tftest.hcl拡張子のファイルを作成し、ファイル内にrunブロックで定義を記述することでテストを実行できます。
テストファイルには以下のブロックが使用可能です。

  • variables

    • 変数を記載する。また。terafform.tfvarsファイルで記載することも可能です。

  • provider

    • provider定義を記載する。ソースコード側にproviderの定義がある場合、上書きすることができる。

  • run

    • テストコードを記述する

runブロックには以下のブロックが使用可能です。

  • command

    • planかapplyを設定可能。applyの場合、実際に一時的なリソースが作成される。デフォルトではapply

  • variables

    • 変数を記載する。runブロック外で記述されたvariablesを上書きすることが可能。

  • module

    • 使用するmoduleを指定する。カレントディレクトリ以外を使う場合に記述する。

  • assert

    • アサーションを記載する。

provider "azurerm" {
  skip_provider_registration = "true"
  features {}
}


variable "resource_group_name" {
  type     = string
  nullable = false
}

variable "location" {
  type     = string
  nullable = false
}

variable "vnet_name" {
  type     = string
  nullable = false
}

variable "subnet_name" {
  type     = string
  nullable = false
}

resource "azurerm_virtual_network" "vnet" {
  name                = var.vnet_name
  address_space       = ["10.0.0.0/16"]
  location            = var.location
  resource_group_name = var.resource_group_name
}

resource "azurerm_subnet" "subnet" {
  name                                          = var.subnet_name
  resource_group_name                           = var.resource_group_name
  virtual_network_name                          = azurerm_virtual_network.vnet.name
  address_prefixes                              = ["10.0.2.0/24"]
  private_link_service_network_policies_enabled = false
}

例えば、上記のようなAzureリソースを管理するコードがあるとします。azurerm_virtual_network と azurerm_subnet リソースの定義を想定します。 これらのリソースが実際にデプロイされた後、virtual_networkとsubnetリソースの命名が適切かどうかを検証するテストコードを作成してみます。

variables {
  resource_group_name = "Sandbox-RD"
  location            = "japanwest"

  vnet_name           = "vnet-test"
  subnet_name         = "subnet-test"
}

run "test" {
  command = plan

  assert {
    condition     = azurerm_virtual_network.vnet.name == var.vnet_name
    error_message = "virtual network name does not match var.vnet_name"
  }

  assert {
    condition     = azurerm_subnet.subnet.name == var.subnet_name
    error_message = "virtual network name does not match var.subnet_name"
  }
}

実際にtestを実行してみるとテストが通ったことがわかります。

$ terraform init
$ terraform test
test.tftest.hcl... in progress
  run "test"... pass
test.tftest.hcl... tearing down
test.tftest.hcl... pass

Success! 1 passed, 0 failed.

今度は意図的にエラーが出るtestコードを書いてみます。
assertの部分を以下に変更します。

assert {
  condition     = azurerm_subnet.subnet.name == "Failed"
  error_message = "virtual network name does not match var.subnet_name"
}

testを実行してみると以下のようにエラーが表示されます。

$ terraform test 
test.tftest.hcl... in progress
  run "test"... fail
╷
│ Error: Test assertion failed
│ 
│   on test.tftest.hcl line 25, in run "test":
│   25:     condition     = azurerm_subnet.subnet.name == "Failed"
│     ├────────────────
│     │ azurerm_subnet.subnet.name is "subnet-test"
│ 
│ virtual network name does not match var.subnet_name
╵
test.tftest.hcl... tearing down
test.tftest.hcl... fail

Failure! 0 passed, 1 failed.

test実行時に-verboseオプションをつけることでplanやapplyの実行結果を出力することも可能です。

$ terraform test -verbose
test.tftest.hcl... in progress
  run "test"... pass

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # azurerm_subnet.subnet will be created
  + resource "azurerm_subnet" "subnet" {
      + address_prefixes                               = [
          + "10.0.2.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "subnet-test"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = false
      + resource_group_name                            = "Sandbox-RD"
      + virtual_network_name                           = "vnet-test"
    }

  # azurerm_virtual_network.vnet will be created
  + resource "azurerm_virtual_network" "vnet" {
      + address_space       = [
          + "10.0.0.0/16",
        ]
      + dns_servers         = (known after apply)
      + guid                = (known after apply)
      + id                  = (known after apply)
      + location            = "japanwest"
      + name                = "vnet-test"
      + resource_group_name = "Sandbox-RD"
      + subnet              = (known after apply)
    }

Plan: 2 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + subnet = {
      + address_prefixes                               = [
          + "10.0.2.0/24",
        ]
      + delegation                                     = []
      + enforce_private_link_endpoint_network_policies = (known after apply)
      + enforce_private_link_service_network_policies  = (known after apply)
      + id                                             = (known after apply)
      + name                                           = "subnet-test"
      + private_endpoint_network_policies_enabled      = (known after apply)
      + private_link_service_network_policies_enabled  = false
      + resource_group_name                            = "Sandbox-RD"
      + service_endpoint_policy_ids                    = null
      + service_endpoints                              = null
      + timeouts                                       = null
      + virtual_network_name                           = "vnet-test"
    }

test.tftest.hcl... tearing down
test.tftest.hcl... pass

Success! 1 passed, 0 failed.

使ってみた

作成されたリソースの命名が適切かどうかを検証する手法の他にも、特定のURLにGETリクエストを送信しレスポンスボディを検証するテストなども考えられます。URLのレスポンス検証のために簡単なWebサーバーを建てて検証してみます。
Webサーバは80番ポートで公開し、/helloに対してHello, World!を返すようにします。

ディレクトリ構成は以下の通りになっています。

terraform
├── main.tf
├── modules
│   ├── network
│   │   ├── main.tf
│   │   ├── output.tf
│   │   ├── test.tftest.hcl
│   │   └── variables.tf
│   └── vm
│       ├── main.tf
│       ├── outputs.tf
│       ├── test.tftest.hcl
│       └── variables.tf
├── output.tf
├── terraform.tfvars
├── test
│   ├── main.tftest.hcl
│   └── request
│       ├── main.tf
│       └── variables.tf
├── variables.tf
└── version.tf

テストの内容はrun "deploy"ブロックでサーバを立て、run "request"ブロックで立てたサーバに対してGETリクエストを送り、レスポンスボディが"Hello, World!"と一致しているか確認します。

run "deploy" {}

run "request" {
  module {
    source = "./test/request"
  }

  variables {
    server_url = run.deploy.server_url
  }

  assert {
    condition     = chomp(data.http.request.response_body) == "Hello, World!"
    error_message = "Response body is not \"Hello, World!\""
  }
}

request moduleは以下の内容になっています。

variable "server_url" {
    type = string
}

data "http" "request" {
    url = var.server_url
}

実際に実行してみます。
testディレクトリ内でテスト内容を定義しているため、terraform testを実行する際に-test-directoryオプションでtestディレクトリを指定します。

$ terraform init -test-directory=./test
$ terraform test -test-directory=./test
test/main.tftest.hcl... in progress
  run "deploy"... pass
  run "request"... pass
test/main.tftest.hcl... tearing down
test/main.tftest.hcl... pass

Success! 2 passed, 0 failed.

無事にtestに通りました。
Terraform testはrunブロック一つにつき1テストのようです。

おまけ

runブロック内のcommandをapplyにしていた場合、テスト実行後にリソースを削除する処理が入ります。
その際、稀に以下のエラーで処理が停止することがあります。

Terraform encountered an error destroying resources created while executing test/main.tftest.hcl/deploy.
╷
│ Error: waiting for update of Network Interface: (Name "nic-test" / Resource Group "Sandbox-RD"): Code="InternalServerError" Message="An error occurred." Details=[]
│
│
╵

Terraform left the following resources in state after executing test/main.tftest.hcl/deploy, and they need to be cleaned up manually:
  - module.network.azurerm_subnet.subnet
  - module.network.azurerm_virtual_network.vnet
  - module.vm.azurerm_network_interface.network_interface
  - module.vm.azurerm_network_interface_security_group_association.nic_nsg
  - module.vm.azurerm_network_security_group.nsg
  - module.vm.azurerm_public_ip.ip
Terraform encountered an error destroying resources created while executing test/main.tftest.hcl/request.

Terraform testではterraform.stateをメモリ内で管理しており、一度エラーが発生するとTerraformで削除することができなくなります。
その際はAzureのPortal上などから手動で削除する必要があります。

最後に宣伝

Acompanyはプライバシー保護とデータ活用の両立を追求するデータクリーンルームをベースに、次なるデータ市場を拓くプラットフォームを展開しています!

Acompanyでは一緒に困難を乗り越えてくれる仲間を募集しています!


この記事が気に入ったらサポートをしてみませんか?