TL;DR
Terraform の templatefile()
と Terragunt の generate()
を組み合わせることで下記のメリットを得られる
-
removed
対象が多くても転記ミスに強い -
removed
の結果を反映した plan 差分を確認できる
想定する状況
-
やりたいこと:
for_each
で作成済みのリソースの一部を一括で(伝われ)terraform state rm
したい -
課題:
- 数が多いので手で
terraform rm
したくない - remove 対象をレビューしたいのでコードにしたい
- スクリプトにすると手元での実行になりがちなので
removed
ブロックを使いたい - 投稿時点において
removed
ブロックはインデックス形式のリソース指定をサポートしていないのでmoved
ブロックを併用したワークアラウンドが必要 -
面倒なので転記ミスのリスクがあるのでよい方法を探したい
- 数が多いので手で
例えば下記のようにリソースを定義したとする:
locals {
repeats = ["foo", "bar", "baz"]
}
resource "something" "this" {
for_each = toset(local.repeats)
name = each.value
}
これを apply すると、下記のリソース管理することになる:
something.this["foo"]
something.this["bar"]
something.this["baz"]
これらのうち、foo
と bar
のリソースだけを rm
しようとする場合、何度も moved {}
と removed {}
を書かないといけないので面倒という話。
解決策
Terraform の templatefile()
と Terragunt1 の generate()
を組み合わせることでうまくいく:
locals {
something_removed = [
"foo",
"bar",
]
}
generate "removed" {
path = "removed.tf"
if_exists = "overwrite"
contents = templatefile("templates/removed.tmpl", {
remove_targets = local.something_removed
})
}
%{ for target in remove_targets }
moved {
from = something.this["${target}"]
to = something.this_${target}
}
removed {
from = something.this_${target}
lifecycle {
destroy = false
}
}
%{ endfor }
.tmpl
中の変数( ${}
で囲ったもの) は Terraform の変数ではなくテンプレートの変数なので注意。
バリエーション: Terraform の組み込み関数をはさむ
- 課題:
- Terraform ではTerraform リソース名にはハイフンではなくアンダースコアを使うという慣習があるが、実際のリモートリソース名にはハイフンを使いたいので変数はハイフンを利用した名前で持っておきたい
-
moved
&removed
ブロック中のリソース名はアンダースコアを徹底する必要がある
解決策
Terraform の replace() を使って下記のように書く:
locals {
something_removed = [
"something-1",
"something-2",
]
something_removed_sanitized = {
for name in local.something_removed : name => replace(name, "-", "_")
}
}
generate "removed" {
path = "removed.tf"
if_exists = "overwrite"
contents = templatefile("templates/removed.tmpl", {
remove_target_names_map = local.something_removed_sanitized
})
}
%{ for name_hyphen, name_underscore in remove_target_names_map }
moved {
from = something.this["${name_hyphen}"]
to = something.this_${name_underscore}
}
removed {
from = something.this_${name_underscore}
lifecycle {
destroy = false
}
}
%{ endfor }
大事なのでもう一度言うが、.tmpl
中の変数 ${}
は Terraform の変数ではなくテンプレート中の変数なので注意。
下記のようにしても、local.target_sanitized
というテンプレート変数がない旨のエラーが出て動かない:
# does not work
%{ for target in remove_targets }
locals {
target_sanitized = replace($target, "-", "_")
}
}
moved {
from = something.this["${target}"]
to = something.this_${local.target_sanitized}
}
removed {
from = something.this_${local.target_sanitized}
lifecycle {
destroy = false
}
}
%{ endfor }
Terragrunt が便利だよという補足
テンプレートを使ったループ展開は Terraform の機能なので、本記事で扱った問題は Terragrunt なしでも解決できそうにも見える。
下記のように書いてみると、一応は期待通りの removed.tf
が生成されそうな動きをするのだが、実際に apply するまで removed.tf
の実体はないため、plan 時点では removed
の結果を反映した plan 差分は確認不可能。
locals {
teams_removed = [
"team-1",
]
teams_removed_sanitized = {
for name in local.teams_removed : name => replace(name, "-", "_")
}
}
resource "local_file" "removed" {
filename = "removed.tf"
content = templatefile("templates/removed.tmpl", {
team_names_map = {
for name in local.teams_removed : name => replace(name, "-", "_")
}
})
}
一方、Terragrunt の場合には plan 時点で removed.tf
が生成されるため removed {}
の結果も反映した plan 差分を検討できる(removed.tf
の ignore を忘れずに)。
参考にした情報
- https://github.com/hashicorp/terraform/issues/34439#issuecomment-2178128452
- https://github.com/gruntwork-io/terragrunt/issues/1443#issuecomment-735771468
-
しれっと別ツールを持ち出してしまったが、実務の IaC を助けてくれる便利機能(要件次第ではほぼ必須なこともある)がいろいろ揃っているのでおすすめ。本記事で扱った課題を解決するためだけに導入しようという意味ではない ↩