これは、なにをしたくて書いたもの?
TerraformリソースのLyfecycleのカスタマイズした時の動きを、1度自分でも確認しておきたいなということで。
TerraformのMeta Arguments
Terraformのリソース定義には、Meta Argumentsと呼ばれる引数を含めることができます。
Meta Argumentsは、以下があるそうです。
- depends_on … リソースの依存関係を設定する
- count … 指定したカウントに従い、複数のリソースを作成する
- for_each … MapまたはStringに従って、複数のリソースを作成する
- provider … デフォルト以外のProviderを使用する
- lifecycle … リソースのライフサイクルをカスタマイズする
- provisioner/connection … リソース作成後に、追加のアクションを実行する
今回は、このうちの「lifecycle」を見ていきます。
Lifecycleのカスタマイズ
Lyfecycleのカスタマイズについての記述は、以下になります。
リソースのライフサイクル自体は、以下に記載があります。
ざっくり、以下の流れになります。
- リソース定義で表現されたインフラオブジェクトの作成
- Stateへの保存
- リソース定義を変更した場合、Stateと比較して必要に応じて実際にインフラオブジェクトを更新
これをカスタマイズできるというのが、こちらの話です。
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.
これで、およその挙動は確認できましたね。