plan に「中身の見えない変更」が出た
terraform plan を流したとき、変更するつもりのないリソースが ~ update in-place として表示されることがあります。
しかもよく見ると、変わる属性が1つも出ておらず、(29 unchanged attributes hidden) のように「変更なしの属性を N 件隠した」とだけ書かれている。
私が遭遇したのは、CloudWatch アラームの小さな修正を当てようとしたときに、無関係なはずの ROSA(OpenShift)クラスタが in-place で出てきたケースでした。
# module.rosa_hcp...rhcs_cluster_rosa_hcp.this will be updated in-place
~ resource "rhcs_cluster_rosa_hcp" "this" {
id = "5fjf...(クラスタID)"
name = "example-cluster"
# (29 unchanged attributes hidden)
}
Plan: 0 to add, 2 to change, 0 to destroy.
相手が本番系のクラスタだと、「これは適用して大丈夫なのか」で一瞬手が止まります。
apply するのも、根拠なく無視するのも避けたいところです。
plan が本当は何を変えようとしているのか念のため確認してみました。
「update in-place」と「unchanged attributes hidden」の読み方
Terraform の plan 記号は、+ が作成、- が削除、~ が変更(in-place)、-/+ が再作成を表します。~ update in-place は、リソースを壊さず属性だけを更新する意味です。
通常、~ の更新では「どの属性がどう変わるか」が ~ attr = "old" -> "new" の形で表示されます。(N unchanged attributes hidden) は「変わらない属性を N 件省略した」という意味で、変わる属性のほうは省略されません。
しかし変わる属性が1つも表示されていないのに update 扱いになるケースがあります。
これは、プロバイダが「中身は同じでも更新が必要」と判断するときに起こります。
computed な値の再計算や、プロバイダ内部での値の正規化など、表示される属性の比較だけでは見えない理由で update フラグが立つことがあります。
このあたりの詳細は、また後続で書いていきます。
-target で絞っても消えない
対応方法として最初に思いつくのは、-target で目的のリソースだけに絞ることです。
terraform apply -target=aws_cloudwatch_metric_alarm.example -var-file=env.tfvars
ところが、これでもクラスタは plan に残りました。
-target は指定したリソースと、その依存先を一緒に含めます。目的のリソースが間接的にクラスタへ依存していると、依存をたどってクラスタも巻き込まれます。
-target は便利ですが、無関係なものを完全に締め出すことはできません。
巻き込まれて出てきたリソースが安全かどうかは、結局 plan の中身を読んで判断することになります。
plan を JSON に落として before/after を見る
plan の画面表示は属性を省略しますが、plan をファイルに保存して JSON で出力すると、各リソースの before / after が省略なしで取り出せます。
ここでは jq(コマンドラインで JSON を加工するツール)を使います。
# 1) plan をファイルに保存する
terraform plan -var-file=env.tfvars -out=tfplan
# 2) JSON 化して、対象リソースで「実際に値が変わるキー」だけ抽出する
terraform show -json tfplan | jq -r '
.resource_changes[]
| select(.address | test("rhcs_cluster")) # 調べたいリソースに合わせて変える
| .change as $c
| ($c.before // {}) as $b
| ($c.after // {}) as $a
| (($b + $a) | keys_unsorted | unique)[] as $k
| select($b[$k] != $a[$k])
| "\($k): \($b[$k] | tojson) => \($a[$k] | tojson)"
'
この jq は before と after を突き合わせ、値が異なるキーだけを表示します。
何も出力されなければ、画面に表示されていなかった属性も含めて差分がゼロ、ということです。
あわせて、apply 後に確定する computed 値(after_unknown)も見ておきます。
terraform show -json tfplan | jq '
.resource_changes[]
| select(.address | test("rhcs_cluster"))
| .change.after_unknown
'
これが {} なら、「apply 後に判明する値」もありません。
私のケースでは、差分キーは0件、after_unknown は {} でした。before と after が全属性で一致し、計算され直す値もない。表示上は ~ update でも、実体は何も変わらない更新だと確定できました。
no-op と確証してから適用する
差分がゼロだと分かれば、
レビューした内容をそのまま当てればよく、保存しておいた plan をそのまま apply します。
# さっき保存した tfplan をそのまま適用する(再 plan しない)
terraform apply tfplan
保存済みの plan を渡すと、Terraform は再計算せずにその plan の内容だけを適用します。画面で見たものと実際に当たるものがズレる心配がありません。
逆に、手順2で version や replicas、instance_type のような実属性が差分に出てきたら、それは本物の変更です。クラスタのバージョン変更などは後から戻せないことがあるので、その場合は apply せず、なぜその差分が出るのかを先に調べます。
なぜ「差分0なのに update in-place」が出るのか
ここまでで「実体は何も変わらない更新だった」と確認できましたが、
ではそもそも なぜ差分が無いのに update が立つのか。
Terraform は「config と state」を比べているのではなく、「state(before)」と「プロバイダが『こうなる』と返した値(after)」 を比べている。
terraform plan を流すと、core は各リソースについてプロバイダに「この config を当てたら最終的にどういう状態になる?」を問い合わせます(内部的には PlanResourceChange という呼び出し)。返ってきた planned new state(after) を、リフレッシュ後の prior state(before) と突き合わせ、ズレていれば update を立てる。
つまり config を1文字も触っていなくても、プロバイダが before と微妙に違う after を返せば update になる ということです。「自分は何も変えていないのに」という感覚と、Terraform の判定基準がそもそもズレている、というのが正体です。
プロバイダが違う値を返す理由
1. 書き方の正規化ズレ
自分が書いた値と、API が返す正規化済みの値が「意味は同じだが表記が違う」パターンです。
- ポリシー JSON の空白やキー順が詰められて返ってくる
- ARN・リージョン・enum の大文字小文字
-
listの順序、nullと[]の食い違い - config では未指定だが、API 側がデフォルトを補完して state に入れる
これは 本記事の手法(terraform show -json + jq)でちゃんと差分として出ます。前段で「差分キー0件」だったということは、少なくともこの系統ではなかった、と一旦言えます。
2. プロバイダのバージョン更新
プロバイダを上げると、state の持ち方や正規化ロジックそのものが変わることがあります。上げた直後の plan だけ空更新が出る、というのはこれが原因のことが多い。.terraform.lock.hcl でバージョンを確認しておきます。
terraform version
grep -A2 'terraform-redhat/rhcs' .terraform.lock.hcl
3. プロバイダ側の実装クセ/バグ
JSON でも差分0、after_unknown も {} なのに update、という状態は、本来 core なら no-op を出すはずなので、プロバイダが「変更なしのときに prior state をそのまま返していない」 ことを示唆します。
私が踏んだ rhcs_cluster_rosa_hcp(ROSA)は、属性の扱いが一般的な AWS リソースより独特で、ここを踏みやすいリソースでした。恒久対応としては、該当属性に ignore_changes を当てて空更新を抑える手があります。
resource "rhcs_cluster_rosa_hcp" "this" {
# ...
lifecycle {
ignore_changes = [
# 毎回空更新で出てくる属性をここに
]
}
}
どこが原因かを切り分ける
慌てて apply も無視もせず、次の順で原因を絞れます。
# ① state と実体のズレだけが原因か切り分ける
terraform plan -refresh-only
# ② 本当の差分を確認する(0件なら値としては空更新)
terraform show -json tfplan | jq -r '
.resource_changes[]
| select(.address | test("rhcs_cluster"))
| {actions: .change.actions, replace: .change.replace_paths}
'
- ① で差分が消えるなら、state と実体のズレ(理由2の系統) を整えれば終わり。
-refresh-onlyを apply して state を合わせればupdateは消えます。 - ② で
actionsが["update"]だけ・replace_pathsが空・値の差分も0なら、当てても何も動かない空更新。 - どちらでもなければ、バージョン由来(理由2)かプロバイダのクセ(理由3) を疑う。最終的には
TF_LOG=trace terraform planでPlanResourceChangeの中身を見れば、core が何を before・after と見て差分を立てたのかまで追えます。
「差分0なのに update」は、Terraform の判定基準(config ではなく before/after)を思い出せば、ちゃんと理由のある表示だと分かります。
まとめ
「差分が表示されないのに update in-place」は、慌てて適用する必要も、根拠なく無視する必要もありません。次の3手で判定できます。
-
terraform plan -out=tfplanで plan を保存する -
terraform show -json tfplan | jq ...で before/after の差分キーを抽出する(0件なら no-op、after_unknownも確認) - 差分が無ければ
terraform apply tfplanでそのまま適用、本物の差分が出たら止めて原因を調べる