Terraform 1.5~1.7の新機能キャッチアップ

はじめに

最近になってようやく業務で Terraform を真面目に触り始めたので、Terraform 1.5 から 1.7 の間に実装された新機能をいくつか使ってみました。
もし間違っている点ありましたらご指摘いただけると幸いです。

import

import は既存のリソースを Terraform 上で管理するための機能です。
Terraform 1.5 以前でもterraform importコマンドを使うことで import できましたが、Terraform 1.5 から import の方法が増えました。

terraform import

まず従来の方法であるterraform importについて紹介します。この方法には以下のような問題点がありました。

  • 1 回の実行で 1 つのリソースしか扱うことができない。
  • import するには、あらかじめ resource を定義する必要があるため、自動でソースコードが書き換えられるわけではない。
  • コマンド実行したタイミングで State ファイルが書き換えられるため、PipeCD などの CD ツールでデプロイしている場合、意図せずリソースの削除が行われてしまう可能性がある。
コマンド実行してみた

GCP 上にあらかじめ tf-import-command-test バケットを作成しておき、以下のような tf ファイルを作成します。

main.tf
resource "google_storage_bucket" "cmd" {
  name     = "tf-import-command-test"
  location = "US"
}

実際に実行してみましょう。

terraform import google_storage_bucket.cmd tf-import-command-test

terraform state を確認してみると、確かに更新されていますね。
これを複数のリソースに対して実行するのは中々手間がかかりそうです。

{
  "mode": "managed",
  "type": "google_storage_bucket",
  "name": "cmd",
  "provider": "provider[\"registry.terraform.io/hashicorp/google\"]",
  "instances": [
    {
      "schema_version": 1,
      "attributes": {
        "location": "US",
        "name": "tf-import-command-test"
      }
    }
  ]
}

import ブロック

Terraform 1.5 から import ブロックが追加され、既存リソースの管理が容易になりました。

import ブロックではidtoのフィールドを必須で埋める必要があります。

  • id: import するリソースの id
  • to: terraform 上の resource id

以下に例を示します。

import {
  id = "tf-import-test"
  to = google_storage_bucket.default
}

resource "google_storage_bucket" "default" {
  name     = "tf-import-test"
  location = "ASIA-NORTHEAST1"
}

terraform import コマンドではリソースの数だけコマンドを実行する必要がありましたが、このように import ブロックを使うことで、 apply 時にまとめて import することができます。

ただ上記の実装方法だと、terraform importの一部分しか改善されていません。resource 定義を行う必要があるからです。

import したリソースの コード生成

上記のやり方では import ブロックと resource ブロックをセットで実装する必要がありました。面倒ですね。
でも安心してください。import ブロックを使う場合は、コードの自動生成が選択できるようになりました。

import {
  id = "tf-import-test"
  to = google_storage_bucket.default
}
-resource "google_storage_bucket" "default" {
-  name     = "tf-import-test"
-  location = "ASIA-NORTHEAST1"
-}

上記の例のように resource 定義を削除して、plan 時に-generate-config-outオプションを付与することで、コードが自動生成されます。

terraform plan -generate-config-out=generated.tf

自動生成されたコードがこちらです。

# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.

# __generated__ by Terraform from "tf-import-test"
resource "google_storage_bucket" "default" {
  default_event_based_hold    = false
  enable_object_retention     = false
  force_destroy               = false
  labels                      = {}
  location                    = "US"
  name                        = "tf-import-test"
  project                     = "******"
  public_access_prevention    = "enforced"
  requester_pays              = false
  rpo                         = "DEFAULT"
  storage_class               = "STANDARD"
  uniform_bucket_level_access = true
  timeouts {
    create = null
    read   = null
    update = null
  }
}

これでterraform importを使うよりもかなり使いやすくなりました。

ただ、import したいリソースの数だけ import ブロックを用意する必要があります。

for_eachで複数のリソースをまとめて import する

Terraform 1.7 で、import ブロックに for_each が使えるようになりました!
for_eachを使うことで、import したいリソースの数だけ import ブロックを用意する必要がなくなります。

使い方

以下のように、同じ種類のリソースをまとめて import したい場合に利用できそうです。

locals {
  buckets = {
    "dev" = "tf-import-dev"
    "stg" = "tf-import-stg"
    "prd" = "tf-import-prd"
  }
}

import {
  for_each = local.buckets
  id       = each.value
  to       = google_storage_bucket.example[each.key]
}

resource "google_storage_bucket" "example" {
  for_each = local.buckets
  name     = each.value
  location = "US"
}

注意点

for_eachを使った import は現時点ではコードの自動生成はできません。resource 定義を事前に行う必要があるので、注意しましょう。

$ tfp -generate-config-out=generated.tf
╷
│ Error: Cannot generate configuration
│
│   on main.tf line 19, in import:
│   19:   to       = google_storage_bucket.example[each.key]
│
│ The given import block is not compatible with config generation. The -generate-config-out option cannot be used with import blocks which use
│ for_each, or resources which use for_each or count.

remove

remove は import と逆で、Terraform の管理から外すことができます。
今まではterraform state rmコマンドを使うことで remove できましたが、Terraform 1.7 から removed ブロックで削除することが可能になりました。

terraform state rm

terraform state rmの動作は今回割愛しますが、terraform import と同じ問題点を抱えていたそうです。

コマンド実行してみた

terraform import でインポートした GCS を state から削除してみます。

main.tf
resource "google_storage_bucket" "cmd" {
  name     = "tf-import-command-test"
  location = "US"
}

実際に実行してみましょう。

$ terraform state rm google_storage_bucket.cmd
Removed google_storage_bucket.cmd
Successfully removed 1 resource instance(s).

State ファイルを確認してみると、削除されていました。

removed ブロック

removed ブロックではfromで管理から外すリソースの id を指定します。
lifecycle ブロックで、destroy = false にすることにより、削除を防ぐことも可能です。デフォルトのままだと削除されてしまうので気をつけましょう。

-resource "google_storage_bucket" "cmd" {
-  name     = "tf-import-command-test"
-  location = "US"
-}
removed {
  from = google_storage_bucket.cmd
  lifecycle {
    destroy = false
  }
}

それでは apply してみましょう。

$ terraform apply
# 中略
 # google_storage_bucket.cmd will no longer be managed by Terraform, but will not be destroyed
 # (destroy = false is set in the configuration)
 . resource "google_storage_bucket" "cmd" {
        id                          = "tf-import-command-test"
        name                        = "tf-import-command-test"
        # (15 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }

Plan: 0 to add, 0 to change, 0 to destroy.
╷
│ Warning: Some objects will no longer be managed by Terraform
│
│ If you apply this plan, Terraform will discard its tracking information for the following objects, but it will not delete them:
│  - google_storage_bucket.cmd
│
│ After applying this plan, Terraform will no longer manage these objects. You will need to import them into Terraform to manage them again.
╵
# 中略
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

これで対象のリソースを管理しないようにできました。

test

Terraform 1.6 からテストができるようになりました。
テストを実行するためには.tftest.hclという拡張子のファイルにテストコードを実装します。

実装例

以下の tf ファイルのテストを行うことを想定します。

main.tf
resource "google_storage_bucket" "default" {
  name = "tf-test-dev"
  location = "ASIA-NORTHEAST1"
}

ではテストコードを実装していきます。テストは run ブロック内に実装する必要があります。
command が plan の場合は plan 結果を元にテストを実行します。
デフォルトは apply なので、実際にリソースが作成され、テスト終了後にリソースが削除されます。

assert ブロックにはテスト条件のconditionとエラーメッセージのerror_messageが必須です。

main.tftest.hcl
run "test" {
  command = plan

  assert {
    condition = google_storage_bucket.default.name == "tf-test-dev"
    error_message = "Wrong bucket name"
  }
}

テスト実行

terraform testコマンドを実行することでテストできます。

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

Success! 1 passed, 0 failed.

成功しました。

問題点

command = apply の場合は実際にリソースを作成してテストを行うのでより正確なテストを行うことができます。
しかし、データベースインスタンスのようなリソース作成に時間がかかるものだと、テスト実行に時間がかかってしまうという問題点があります。

モックを使って実行時間短縮

Terraform 1.7 からモック機能が追加されました。mock_providerブロックを定義することで、モックを利用したテストが実行できます。
mock_resource ブロックを定義したリソースは defaults に指定した値がモック時にセットされます。

main.tftest.hcl
+mock_provider "google" {
+  mock_resource "google_storage_bucket" {
+    defaults = {
+      id = "mock-bucket"
+    }
+  }
+}

run "test" {
-  command = plan
+  command = apply

  assert {
    condition = google_storage_bucket.default.name == "tf-test-dev"
    error_message = "Wrong bucket name"
  }
+  assert {
+    condition = google_storage_bucket.default.id == "mock-bucket"
+    error_message = "Wrong id"
+  }
}

このようにモックを利用することで、テストの実行時間を大幅に短縮することができます。

まとめ

今回は import, remove, test の 3 つの新機能を GCP リソースを用いて試してみました。
率直な感想として、まず import はプロダクトの初期段階から インフラリソースを全て Terraform で実装している場合は使わないなと思いました。ただ、Terraform で書くのが大変なリソースの場合、最初はコンソールで作って後で generate する方法は使えるかもしれないですね。
remove はユースケースがあまり思いつかないですね。基本 Terraform で管理外にすることはなさそうですし、不要になったリソースは削除すると思いますし…
test はかなり使えそうですね。モック機能も実装されたので、Terraform 1.6 と比べるとかなり扱いやすくなったのではないかと思います。自身のプロダクトで使っていきたいです。

https://www.hashicorp.com/blog/terraform-1-7-adds-test-mocking-and-config-driven-removehttps://www.hashicorp.com/blog/terraform-1-7-adds-test-mocking-and-config-driven-remove