目的が明確でないTestは負債

はじめに

近年IaC(infrastructure as code)ツールとして非常に注目されているのがterraformです.terraformはHCL(HashiCorp Configuration Language)と呼ばれる独自の言語により構成を記述してデプロイなどを自動で行います.独自言語の習得という手間はあるものの,HCLは構成記述に最適化されて組まれているため,一度習得してしまえば複雑な構成であっても簡単に記述することができるようになります.
terraformの便利な機能の一つにmoduleがあります.moduleは関数のような機能で,複数のresouceをまとめて1つのmoduleという単位で呼び出せるようにします.適切にmodule化をするとコードが疎になるため,保守性が非常に高まります.しかし,既にdeployされたresourceをmodule化しようとするととある問題が発生します.本記事ではこの問題の概要と解決法を提案します.
なお,本記事内ではterraformのバージョンとしてv1.7.3を使用します.

module

moduleは複数のresouceをまとめて1つのresoueceとして扱えるようにする機能です.詳細は公式documentを参照ください.

以下に簡単なmodule化の例を示します.

# 本質外のファイルは除外している
├ module/
| └ module_main.tf
└ main.tf
  • module_main.tf
variable "name" {
  type = string
}
variable "resource_group_name" {
  type = string
}

resource "azurerm_virtual_network" "vnet" {
  name                = var.name
  address_space       = ["10.0.0.0/16"]
  location            = "japanwest"
  resource_group_name = var.resource_group_name
}
  • main.tf
module "network" {
  source = "../module"

  name                = "name"
  resource_group_name = "resource_group_name"
}

この例の場合はrootディレクトリでterrform applyしてできるresourceとmodule/内でterraform applyしてできるresouceは全く一緒になります.main.tf内ではmoduleの呼び出ししかしていないことからこれは明らかで,正しくmodule化できていることの証明でもあります.

applyされたresourceのmodule化

terraformは既にapplyされている構成に変更があった場合,その差分を検知することができ,差分だけを更新することが可能です.そのため,既にapplyされているresourceをmodule化する場合は,この差分が存在しないことを確認すれば良さそうです.
先述した例で実際に差分を確認してみます.出力は一部塗りつぶしてます.

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

Terraform will perform the following actions:

  # azurerm_virtual_network.vnet will be destroyed
  # (because azurerm_virtual_network.vnet is not in configuration)
  - resource "azurerm_virtual_network" "vnet" {
      - address_space           = [
          - "10.0.0.0/16",
        ] -> null
      - dns_servers             = [] -> null
      - flow_timeout_in_minutes = 0 -> null
      - guid                    = "***" 
-> null
      - id                      = "***" -> null
      - location                = "japanwest" -> null
      - name                    = "***" -> null
      - resource_group_name     = "***" -> null
      - subnet                  = [] -> null
      - tags                    = {} -> null
    }

  # module.network.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                = "***"
      + resource_group_name = "***"
      + subnet              = (known after apply)
    }

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

全く同じresourceのはずですが,全て差分として検出されました.azurerm_virtual_network.vnet will be destroyedmodule.network.azurerm_virtual_network.vnet will be createdなどから推測するに,展開したresourceが同じであったとしても異なるmoduleであれば別のresourceとして判定されるようです.
本例では量が少ないため目視で差分を確認すればresourceに差分がないことは確認できますが,実際は目視での確認は困難です.しかし,この確認ができなければ既に稼働しているresourceを変更してしまう恐れがありmodule化も踏みとどまってしまいます.
そこで,本問題の解決としてTestを書くことを提案します.

Terraform Test

terraform testはv1.6より導入された機能で,公式がサポートするTestです.詳細は公式documentを参照してください.先人の解説記事も豊富にあります.
例) Terraform testコマンドを使ってみた|KotaTakahashi
以下に簡単なtestの例を示します.

  • main.tftest.hcl
mock_provider "azurerm" {}


variables {
  name                = "name"
  resource_group_name = "resource_group_name"
}

run "test" {
  command = apply

  assert {
    condition     = azurerm_virtual_network.vnet.name == "name"
    error_message = "error_message"
  }
}

同様に追記していくことで,moduleが想定しているresourceを正しく生成できるかを確認することができます.

module化とTest

先述したようにTestを記載することで,moduleが想定しているresourceを正しく生成できるかを確認することができます.つまり,module化する前と全く同一のresourceが生成できるか,といったことも自動で判定が可能ということです.
具体的には次の手順を踏みます.
Step 1. 既存のresourceに対してTestを追加する
Step 2. module化したいresourceを選定し,そのTestを抽出する
Step 3. module内外で連携する部分があればそのTestを追加する
Step 4. module化してTestが通るか確認する
この場合,Step 1.で追加するテストがmodule化前の状態を表現しており,Step 4.で同一のTestが通ることを確認することで,module化前後で差分がないことを確認します.

おわりに

本記事では既にapplyされたresourceを安全にmodule化するためのTestの重要性について記載しました.正直本記事を書くまではterraformのTestの使い道が分かっておらず,ないよりは良いだろくらいのノリでTestを書こうとしていました.しかし,具体的なissueが出てきたことで目的が明確化され,Testの重要性を見出すことができました.
私自身の反省点でもありますが,本件に限らずTestは重要性だけが宙に浮き,目的が明確でないことがよくあります."とりあえず重要だから書く"ではなく,"こうしたいから書く"といった目的ベースで考えることによって,本記事のように書くべきTestが明確化され,不要なTestが溢れることを防ぐことができます.今後はこういった意識をもっと持っていこうと思います.
でも意味ないTestも書きたい(目的)ので書きます.