CLOVER🍀

That was when it all began.

TerraformリソースのLifecycleのカスタマイズを行う

これは、なにをしたくて書いたもの?

TerraformリソースのLyfecycleのカスタマイズした時の動きを、1度自分でも確認しておきたいなということで。

TerraformのMeta Arguments

Terraformのリソース定義には、Meta Argumentsと呼ばれる引数を含めることができます。

Resources / Meta-Arguments

Meta Argumentsは、以下があるそうです。

  • depends_on … リソースの依存関係を設定する
  • count … 指定したカウントに従い、複数のリソースを作成する
  • for_each … MapまたはStringに従って、複数のリソースを作成する
  • provider … デフォルト以外のProviderを使用する
  • lifecycle … リソースのライフサイクルをカスタマイズする
  • provisioner/connection … リソース作成後に、追加のアクションを実行する

今回は、このうちの「lifecycle」を見ていきます。

Lifecycleのカスタマイズ

Lyfecycleのカスタマイズについての記述は、以下になります。

Lifecycle Customizations

リソースのライフサイクル自体は、以下に記載があります。

Resource Behavior

ざっくり、以下の流れになります。

  • リソース定義で表現されたインフラオブジェクトの作成
  • Stateへの保存
  • リソース定義を変更した場合、Stateと比較して必要に応じて実際にインフラオブジェクトを更新

これをカスタマイズできるというのが、こちらの話です。

Lifecycle Customizations

lifecycleはリソース定義の中でブロックとして指定し、以下の3つのMeta Argumentsを使用することができます。

  • create_before_destroy
  • prevent_destroy
  • ignore_changes

create_before_destroyは、boolで指定します。対象のインフラオブジェクトをインプレースで更新できない場合(API制限など)、
Terraformはリソースを削除して、新しいリソースを作成して置き換えます。これが、デフォルトの挙動です。

create_before_destroyをtrueにした場合、新しいインフラオブジェクトを最初に作成して、置き換え対象のオブジェクトが
できあがった後に既存のオブジェクトを破棄します。

なお、多くのリソースでは2つのオブジェクトは同時に存在できないことが多いので、一部のリソースに対してはリソースの
衝突を回避するためのオプションが提供されていたりします。たとえば、オブジェクトの名前にランダムなサフィックスを
付与するなど。このあたりついては、使用するリソースタイプの制約を確認しておく必要があります。

prevent_destroyは、boolで指定します。この引数をtrueにすると、リソース定義が構成ファイルに残っている限り、
Terraformはリソース定義に関連付けられたインフラオブジェクトの破棄を拒否します(エラーになります)。

これは、誤って重要なリソースを破棄しないために使われますが、一方でterraform destroyを伴うような操作ができなく
なることに注意する必要があります。

ignore_changesには、属性名のリストを指定します。Terraformは、デフォルトでリソース定義と実際のインフラオブジェクトの
違いを検出し、リソース定義と一致するように更新しようとしますが、この挙動を変更します。

ignore_changesで指定した属性は、リソースの作成時には考慮されますが、リソースの更新時には無視されることになります。
これは、実際のインフラオブジェクトがTerraform以外から変更される可能性がある時に利用することになるでしょう。

ドキュメントには、Mapを使った場合の注意事項が書かれています。Mapのキーを指定することで個別の要素に対する変更を
無視することができますが、キーの追加や削除はMap自体の更新と捉えられるため、外部システムがMapの特定の要素を
無視したい場合はあらかじめ初期値を入れておくべきだとしています。

この例だと、MapであるtagsにあるNameを無視させたいわけですが、ダミーでも初期値として指定しておくことでTerraformに
以後は無視対象として認識してもらうということです。

resource "aws_instance" "example" {
  tags = {
    Name = "placeholder"
  }

  lifecycle {
    ignore_changes = [
      tags["Name"],
    ]
  }
}

また、特別なキーワードとして「all」というものがあり、こちらを使用することで全属性に対する変更を無視させることも
できます。

なお、create_before_destroy、prevent_destroy、ignore_changesいずれにも言えますが、使用できるのはリテラル値のみです。

と、前置きがだいぶ長くなりましたが、使っていってみましょう。

環境

今回の環境は、こちら。

$ terraform version
Terraform v0.12.24

MySQL Providerを使って、lifecycleの設定をしていってみましょう。MySQLは8.0.20を使用します。

ベースとなる構成

簡単に、こんな感じで2つのデータベース定義を用意。
main.tf

terraform {
  required_version = ">= 0.12.24"
}

provider "mysql" {
  endpoint = "172.17.0.2:3306"
  username = "root"
  password = "password"

  version = "1.9.0"
}

resource "mysql_database" "database1" {
  name = "database1"
}

resource "mysql_database" "database2" {
  name = "database2"
}

init等々。

$ terraform init
$ terraform version
Terraform v0.12.24
+ provider.mysql v1.9.0
$ terraform fmt -check -recursive -diff
$ terraform validate

apply。

$ terraform apply -auto-approve

データベースができていることを確認。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| database1          |
| database2          |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
6 rows in set (0.00 sec)

いったん、破棄。

$ terraform destroy -force

これで、下準備はOKです。

prevent_destroy

では、最初にprevent_destroyから確認してみましょう。

以下のように、database1にだけprevent_destroyをtrueに設定してみます。

resource "mysql_database" "database1" {
  name = "database1"

  lifecycle {
    prevent_destroy = true
  }
}

resource "mysql_database" "database2" {
  name = "database2"
}

apply。

$ terraform apply -auto-approve

この後、destroyしようとすると

$ terraform destroy -force

database1を破棄できないと言われ、失敗します。

mysql_database.database1: Refreshing state... [id=database1]
mysql_database.database2: Refreshing state... [id=database2]

Error: Instance cannot be destroyed

  on main.tf line 13:
  13: resource "mysql_database" "database1" {

Resource mysql_database.database1 has lifecycle.prevent_destroy set, but the
plan calls for this resource to be destroyed. To avoid this error and continue
with the plan, either disable lifecycle.prevent_destroy or reduce the scope of
the plan using the -target flag.

2つのデータベースが、両方とも残っています。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| database1          |
| database2          |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
6 rows in set (0.00 sec)

この状態で、どうしてもdatabase2の方だけ削除したいという場合は「-target」オプションで対象のリソースを指定するようです。

$ terraform destroy -force -target=mysql_database.database2

Command: destroy - Terraform by HashiCorp

Command: plan / Resource Targeting

ところで、先ほどのこのログを見るとdatabase1、database2の順に消そうとしているように見えなくもないです。

mysql_database.database1: Refreshing state... [id=database1]
mysql_database.database2: Refreshing state... [id=database2]

Error: Instance cannot be destroyed

  on main.tf line 13:
  13: resource "mysql_database" "database1" {

database2の方だけをprevent_destroyをtrueにしたら、database1は削除されたりするのでしょうか?

そこで、いったんデータベースを全部削除して以下のように変更。

resource "mysql_database" "database1" {
  name = "database1"
}

resource "mysql_database" "database2" {
  name = "database2"

  lifecycle {
    prevent_destroy = true
  }
}

applyして

$ terraform apply -auto-approve

destroy。

$ terraform destroy -force

すると、なんと順番が入れ替わります。

mysql_database.database2: Refreshing state... [id=database2]
mysql_database.database1: Refreshing state... [id=database1]

Error: Instance cannot be destroyed

  on main.tf line 20:
  20: resource "mysql_database" "database2" {

lifecycleの定義有無を見てるということでしょうかねぇ…。どちらにせよ、片方だけが消えるということにはなりませんでした。

ちなみに、prevent_destroyをfalseにするとあっさりと削除することができます。設定しない場合と同じですね。

resource "mysql_database" "database2" {
  name = "database2"

  lifecycle {
    prevent_destroy = false
  }
}

あと、prevent_destroyは「リソース定義が残っている場合に有効」だということだったので、こちらも確認してみましょう。

まずは、以下の定義にして

resource "mysql_database" "database1" {
  name = "database1"

  lifecycle {
    prevent_destroy = true
  }
}

resource "mysql_database" "database2" {
  name = "database2"
}

apply。

$ terraform apply -auto-approve

そして、database1のリソース定義をコメントアウトします。

/*
resource "mysql_database" "database1" {
  name = "database1"

  lifecycle {
    prevent_destroy = true
  }
}
*/

resource "mysql_database" "database2" {
  name = "database2"
}

この状態でplanを見ると、database1が削除予告されます。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

mysql_database.database1: Refreshing state... [id=database1]
mysql_database.database2: Refreshing state... [id=database2]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # mysql_database.database1 will be destroyed
  - resource "mysql_database" "database1" {
      - default_character_set = "utf8" -> null
      - default_collation     = "utf8_general_ci" -> null
      - id                    = "database1" -> null
      - name                  = "database1" -> null
    }

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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

実際、削除できます。

$ terraform destroy -force

これで、挙動は確認できましたね。

create_before_destroy

次に、create_before_destroyを見ていってみましょう。

1度、データベースはすべて削除しておきます。

以下のようなリソース定義にします。database1のみ、create_before_destroyをtrueにしました。

resource "mysql_database" "database1" {
  name = "database1"

  lifecycle {
    create_before_destroy = true
  }
}

resource "mysql_database" "database2" {
  name = "database2"
}

apply。

$ terraform apply -auto-approve

で、リソースにちょっと大きな変更を入れてみます。データベースの名前を変えることにしました。それぞれ、1 → 3、2 → 4と
なります。

resource "mysql_database" "database1" {
  name = "database3"

  lifecycle {
    create_before_destroy = true
  }
}

resource "mysql_database" "database2" {
  name = "database4"
}

planを見てみると

$ terraform plan

両方とも、作り直しになっています。

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement
+/- create replacement and then destroy

Terraform will perform the following actions:

  # mysql_database.database1 must be replaced
+/- resource "mysql_database" "database1" {
        default_character_set = "utf8"
        default_collation     = "utf8_general_ci"
      ~ id                    = "database1" -> (known after apply)
      ~ name                  = "database1" -> "database3" # forces replacement
    }

  # mysql_database.database2 must be replaced
-/+ resource "mysql_database" "database2" {
        default_character_set = "utf8"
        default_collation     = "utf8_general_ci"
      ~ id                    = "database2" -> (known after apply)
      ~ name                  = "database2" -> "database4" # forces replacement
    }

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

------------------------------------------------------------------------

applyします。

$ terraform apply -auto-approve

こんな感じになりました。

mysql_database.database2: Refreshing state... [id=database2]
mysql_database.database1: Refreshing state... [id=database1]
mysql_database.database2: Destroying... [id=database2]
mysql_database.database1: Creating...
mysql_database.database2: Destruction complete after 0s
mysql_database.database1: Creation complete after 0s [id=database3]
mysql_database.database1: Destroying... [id=database1]
mysql_database.database2: Creating...
mysql_database.database1: Destruction complete after 0s
mysql_database.database2: Creation complete after 0s [id=database4]

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

よく見ると、database2 → database4はdestroy → createになっていますが

mysql_database.database2: Destroying... [id=database2]
mysql_database.database1: Creating...
mysql_database.database2: Destruction complete after 0s

mysql_database.database2: Creation complete after 0s [id=database4]

database1 → database3はcreate → destroyになっていますね。

mysql_database.database1: Creation complete after 0s [id=database3]
mysql_database.database1: Destroying... [id=database1]
mysql_database.database2: Creating...
mysql_database.database1: Destruction complete after 0s

これで、create_before_destroyの効果を確認できました。

1度destroy。

$ terraform destroy -force

一応、create_before_destroyをfalseにしても確認してみます。

resource "mysql_database" "database1" {
  name = "database1"

  lifecycle {
    create_before_destroy = false
  }
}

resource "mysql_database" "database2" {
  name = "database2"
}

applyして

$ terraform apply -auto-approve

先ほどと同じようにデータベース名を変更。

resource "mysql_database" "database1" {
  name = "database3"

  lifecycle {
    create_before_destroy = false
  }
}

resource "mysql_database" "database2" {
  name = "database4"
}

applyすると、両方ともdestroy → createになりました。これは、デフォルトの挙動です。

$ terraform apply -auto-approve
mysql_database.database2: Refreshing state... [id=database2]
mysql_database.database1: Refreshing state... [id=database1]
mysql_database.database2: Destroying... [id=database2]
mysql_database.database1: Destroying... [id=database1]
mysql_database.database2: Destruction complete after 0s
mysql_database.database1: Destruction complete after 0s
mysql_database.database2: Creating...
mysql_database.database1: Creating...
mysql_database.database2: Creation complete after 0s [id=database4]
mysql_database.database1: Creation complete after 0s [id=database3]

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

ignore_changes

最後に、ignore_changesを確認してみましょう。

再度、データベースを全部削除してやり直します。

今度は、以下のようにCharacter SetとCollationを設定した定義にしました。

resource "mysql_database" "database1" {
  name = "database1"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"

  lifecycle {
    ignore_changes = [
      default_collation
    ]
  }
}

resource "mysql_database" "database2" {
  name = "database2"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

database1のみ、default_collationをignore_changesの対象に含めています。

  lifecycle {
    ignore_changes = [
      default_collation
    ]
  }

まずはapply。

$ terraform apply -auto-approve

次に、Collationの設定を変更してみます。

resource "mysql_database" "database1" {
  name = "database1"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs"

  lifecycle {
    ignore_changes = [
      default_collation
    ]
  }
}

resource "mysql_database" "database2" {
  name = "database2"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs"
}

planを見てみます。

$ terraform plan

database1はignore_changesにdefault_collationを指定しているので、更新対象はdatabase2のみとなりました。

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # mysql_database.database2 will be updated in-place
  ~ resource "mysql_database" "database2" {
        default_character_set = "utf8mb4"
      ~ default_collation     = "utf8mb4_ja_0900_as_cs_ks" -> "utf8mb4_ja_0900_as_cs"
        id                    = "database2"
        name                  = "database2"
    }

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

------------------------------------------------------------------------

applyしても、変更されるのはdatabase2だけです。

$ terraform apply -auto-approve
mysql_database.database2: Refreshing state... [id=database2]
mysql_database.database1: Refreshing state... [id=database1]
mysql_database.database2: Modifying... [id=database2]
mysql_database.database2: Modifications complete after 0s [id=database2]

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

ここで、default_collation以外のものを両方のデータベースで変更してみます。

resource "mysql_database" "database1" {
  name = "database1"

  default_character_set = "utf8"
  default_collation     = "utf8mb4_ja_0900_as_cs"

  lifecycle {
    ignore_changes = [
      default_collation
    ]
  }
}

resource "mysql_database" "database2" {
  name = "database2"

  default_character_set = "utf8"
  default_collation     = "utf8mb4_ja_0900_as_cs"
}

planを確認。

$ terraform plan

これは、両方のデータベースで差分として検出されます。

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # mysql_database.database1 will be updated in-place
  ~ resource "mysql_database" "database1" {
      ~ default_character_set = "utf8mb4" -> "utf8"
        default_collation     = "utf8mb4_ja_0900_as_cs_ks"
        id                    = "database1"
        name                  = "database1"
    }

  # mysql_database.database2 will be updated in-place
  ~ resource "mysql_database" "database2" {
      ~ default_character_set = "utf8mb4" -> "utf8"
        default_collation     = "utf8mb4_ja_0900_as_cs"
        id                    = "database2"
        name                  = "database2"
    }

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

------------------------------------------------------------------------

もっとも、MySQLの定義としては矛盾しているので、これのapplyはエラーになりますが。

$ terraform apply -auto-approve
mysql_database.database2: Refreshing state... [id=database2]
mysql_database.database1: Refreshing state... [id=database1]
mysql_database.database1: Modifying... [id=database1]
mysql_database.database2: Modifying... [id=database2]

Error: Error 1253: COLLATION 'utf8mb4_ja_0900_as_cs_ks' is not valid for CHARACTER SET 'utf8'

  on main.tf line 13, in resource "mysql_database" "database1":
  13: resource "mysql_database" "database1" {



Error: Error 1253: COLLATION 'utf8mb4_ja_0900_as_cs' is not valid for CHARACTER SET 'utf8'

  on main.tf line 26, in resource "mysql_database" "database2":
  26: resource "mysql_database" "database2" {

いったん、リソースを破棄。

$ terraform destroy -force

最後に、ignore_changesをallにしてみましょう。allにすると、すべての属性の変更を無視するはずです。

resource "mysql_database" "database1" {
  name = "database1"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"

  lifecycle {
    ignore_changes = all
  }
}

resource "mysql_database" "database2" {
  name = "database2"

  default_character_set = "utf8mb4"
  default_collation     = "utf8mb4_ja_0900_as_cs_ks"
}

この定義でリソースを作成します。

$ terraform apply -auto-approve

そして、両方のデータベースの属性をまるっと変更します。

resource "mysql_database" "database1" {
  name = "database3"

  default_character_set = "utf8"
  default_collation     = "utf8_unicode_ci"

  lifecycle {
    ignore_changes = all
  }
}

resource "mysql_database" "database2" {
  name = "database4"

  default_character_set = "utf8"
  default_collation     = "utf8_unicode_ci"
}

planを確認してみます。

$ terraform plan

差分が検出されたのは、database2のみになります。

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # mysql_database.database2 must be replaced
-/+ resource "mysql_database" "database2" {
      ~ default_character_set = "utf8mb4" -> "utf8"
      ~ default_collation     = "utf8mb4_ja_0900_as_cs_ks" -> "utf8_unicode_ci"
      ~ id                    = "database2" -> (known after apply)
      ~ name                  = "database2" -> "database4" # forces replacement
    }

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

------------------------------------------------------------------------

ここまでやると、変更が大きくて作り直しですが。

applyしても、変更があるのはdatabase2のみです。

$ terraform apply -auto-approve
mysql_database.database1: Refreshing state... [id=database1]
mysql_database.database2: Refreshing state... [id=database2]
mysql_database.database2: Destroying... [id=database2]
mysql_database.database2: Destruction complete after 0s
mysql_database.database2: Creating...
mysql_database.database2: Creation complete after 0s [id=database4]

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

これで、およその挙動は確認できましたね。