ライフサイクルで統制しドリフトを戻す
目標
lifecycleの4つの引数で、置き換え・削除・無視を制御し、コードの外で生じた変更を検知して、2つの方向で対応したあと、その判定をスクリプトで自動化します。
なぜ重要なのか
インフラコードがずれる仕組みは、いつも同じです。急な事態が起きて、誰かがコンソールで直接直し、その変更がコードに戻ってこないことです。その状態で数日が過ぎると、まったく別の理由で実行されたapplyが、明け方の応急措置を、黙って元に戻します。そのため、ドリフト対応で本当に難しいのは、コマンドではなく、方向を決める判断です。応急措置が誤りだったなら、コードを基準に元に戻し、正しい判断だったなら、コードを実体に合わせて直す必要があります。この判断なしに自動復旧を回すのが、最も危険です。lifecycleブロックは、この流れに、あらかじめ介入する仕組みです。特に、ignore_changesは、オートスケーラーのように意図的にコードの外で変わる値を、検知の対象から外して、誤報を減らし、prevent_destroyは、削除されてはいけないリソースにブレーキをかけます。最後に、判定は、人の目ではなく-detailed-exitcodeで行ってこそ、cronとCIに載せられます。
ステップ
/root/tf/lifecycle/main.tfにlocal_fileリソースendpointを宣言してください。ファイルパスは/root/tf/lifecycle/out/endpoint.txt、内容は改行のない1行の文字列です。このリソースのlifecycleブロックにcreate_before_destroy = trueを入れて、tofu initのあとに適用してください。local_fileリソースstatefile_guard(ファイル/root/tf/lifecycle/out/guard.txt)を追加し、lifecycleにprevent_destroy = trueを入れて適用してください。そのあと、そのリソースだけを対象に削除を試み(tofu destroy -target=local_file.statefile_guard)、出力を標準エラー出力まで含めて、/root/tf/lifecycle/out/prevent.txtに保存してください。拒否メッセージが残っている必要があり、状態のstatefile_guardとguard.txtは、そのまま生きている必要があります。local_fileリソース1つを、ファイル/root/tf/lifecycle/out/managed-note.txtとして作成し、内容をignored-valueにして適用してください。そのあと、lifecycleにignore_changes = [content]を入れ、コードの内容の値だけを別の文字列に変更したうえで、tofu planの出力を/root/tf/lifecycle/out/ignore-plan.txtに保存してください。プランはNo changesである必要があり、実際のファイルの内容は、依然としてignored-valueである必要があります。null_resourceリソース1つに、triggersでバージョンの値を持たせ、local_fileリソースchecksum(ファイル/root/tf/lifecycle/out/checksum.txt)に、replace_triggered_by = [null_resource.<이름>](プレースホルダーはリソース名です)をかけてください。checksum自身の引数はそのままにして、バージョンの値だけを変更し、tofu plan -out=<계획파일>(プレースホルダーはプランファイル名です)で保存したあと、tofu show -json <계획파일>の結果を/root/tf/lifecycle/out/trigger-plan.jsonに保存してください。JSONのresource_changesで、local_file.checksumのアクションが2つ(削除と作成)である必要があります。確認後に適用してください。- コードを経由せずに、
/root/tf/lifecycle/out/endpoint.txtを直接直して、内容にhand-editedが入るようにしてください。そのあと、tofu plan -refresh-only -detailed-exitcode -out=<계획파일>を実行して、終了コードだけを/root/tf/lifecycle/out/drift-exit.txtに書き(2である必要があります)、tofu show -json <계획파일>の結果を/root/tf/lifecycle/out/drift.jsonに保存してください。JSONのresource_driftにendpointが入っている必要があります。このプランは適用しないでください。 tofu applyで実体をコードを基準に元に戻し、出力を/root/tf/lifecycle/out/remediate.txtに保存してください。Apply completeという文言が残っている必要があり、endpoint.txtからhand-editedが消え、ファイルの内容が、状態に記録されたcontentの値と、正確に同じである必要があります。local_fileリソースquota(ファイル/root/tf/lifecycle/out/quota.txt)を、内容256で適用しておいてください。今回は、ファイルを手で512に直したあと、元に戻さずに、main.tfの値を512に変更してコードに吸収し、適用してください。そのあと、tofu planの出力を/root/tf/lifecycle/out/accept-plan.txtに保存すると、No changesである必要があり、quota.txtとmain.tfの両方が、512を含んでいる必要があります。/root/tf/lifecycle/drift-report.shを作成して、実行権限を付けてください。このスクリプトは、-detailed-exitcodeを使うプランコマンドでドリフトを判定し、結果を/root/tf/lifecycle/out/drift-report.jsonに、exit_code、drift、checked_at、addressesの4つのフィールドで残す必要があります。ラボを締めくくる時点では、ドリフトがないはずなので、exit_codeは0、driftはfalseである必要があります。
参考
- このラボの状態ファイルは、
/root/tf/lifecycle/terraform.tfstateです。ドリフトの判断は、この状態と実体を比較して出ます。 -detailed-exitcodeは、変更なしなら0、エラーなら1、変更ありなら2です。driftフィールドは、この値が2のときだけtrueである必要があります。tofu show -json <계획파일>は、プランを機械が読める形で出力します(プレースホルダーはプランファイル名です)。resource_changesはこれからやること、resource_driftは、コードの外ですでに起きたことです。local_fileの内容は、改行なしの1行にしてください。ステップ6は、ファイルの内容と状態に記録された値を、文字単位で比較します。- よくある間違い1: ステップ8のスクリプトに
set -eを入れることです。終了コード2でスクリプトがそのまま落ち、レポートが作成されません。 - よくある間違い2: ステップ3で、
ignore_changesを先に入れて適用することです。無視する値が状態に入ったあとで、コードを変更して初めて、効果が見えます。 - よくある間違い3: ステップ5で、コードを直しておいて、それをドリフトと呼ぶことです。ドリフトは、コードを経由しない変更だけを指します。
create_before_destroyをかける
/root/tf/lifecycle/main.tfにlocal_fileリソースendpointを宣言してください。ファイルパスは/root/tf/lifecycle/out/endpoint.txt、内容は改行のない1行の文字列です。このリソースのlifecycleブロックにcreate_before_destroy = trueを入れて、tofu initのあとに適用してください。
lifecycleブロックは、リソースの中に入ります。置き換えが必要なときに、新しいものを先に作る引数を探してください。
prevent_destroyで削除を止める
local_fileリソースstatefile_guard(ファイル/root/tf/lifecycle/out/guard.txt)を追加し、lifecycleにprevent_destroy = trueを入れて適用してください。そのあと、そのリソースだけを対象に削除を試み(tofu destroy -target=local_file.statefile_guard)、出力を標準エラー出力まで含めて、/root/tf/lifecycle/out/prevent.txtに保存してください。拒否メッセージが残っている必要があり、状態のstatefile_guardとguard.txtは、そのまま生きている必要があります。
この引数は、プランの段階でエラーを出します。対象を1つだけ指定して削除を試み、その出力を丸ごと保存してください。
ignore_changesで特定の属性を無視する
local_fileリソース1つを、ファイル/root/tf/lifecycle/out/managed-note.txtとして作成し、内容をignored-valueにして適用してください。そのあと、lifecycleにignore_changes = [content]を入れ、コードの内容の値だけを別の文字列に変更したうえで、tofu planの出力を/root/tf/lifecycle/out/ignore-plan.txtに保存してください。プランはNo changesである必要があり、実際のファイルの内容は、依然としてignored-valueである必要があります。
先に適用して、値を状態に入れてから、コードの値を変更して初めて、効果が見えます。実体のファイルの内容は、最初に適用した値のままである必要があります。
別のリソースの変更で再作成を引き起こす
null_resourceリソース1つに、triggersでバージョンの値を持たせ、local_fileリソースchecksum(ファイル/root/tf/lifecycle/out/checksum.txt)に、replace_triggered_by = [null_resource.<이름>](プレースホルダーはリソース名です)をかけてください。checksum自身の引数はそのままにして、バージョンの値だけを変更し、tofu plan -out=<계획파일>(プレースホルダーはプランファイル名です)で保存したあと、tofu show -json <계획파일>の結果を/root/tf/lifecycle/out/trigger-plan.jsonに保存してください。JSONのresource_changesで、local_file.checksumのアクションが2つ(削除と作成)である必要があります。確認後に適用してください。
自分の引数はそのままなのに、他者の変更のせいで置き換えられることが核心です。プランをファイルに保存してから、JSONに変換してください。
コードの外での変更を、ドリフトとして検知する
コードを経由せずに、/root/tf/lifecycle/out/endpoint.txtを直接直して、内容にhand-editedが入るようにしてください。そのあと、tofu plan -refresh-only -detailed-exitcode -out=<계획파일>を実行して、終了コードだけを/root/tf/lifecycle/out/drift-exit.txtに書き(2である必要があります)、tofu show -json <계획파일>の結果を/root/tf/lifecycle/out/drift.jsonに保存してください。JSONのresource_driftにendpointが入っている必要があります。このプランは適用しないでください。
コードを経由せずに、実体を直接直してこそ、ドリフトです。リフレッシュ専用のプランと、詳細終了コードを一緒に使ってください。
コードを基準に実体を元に戻す
tofu applyで実体をコードを基準に元に戻し、出力を/root/tf/lifecycle/out/remediate.txtに保存してください。Apply completeという文言が残っている必要があり、endpoint.txtからhand-editedが消え、ファイルの内容が、状態に記録されたcontentの値と、正確に同じである必要があります。
対応の方向のうち、1つ目です。元に戻したあとは、状態に記録された値と、実体の内容が同じである必要があります。
ドリフトをコードに吸収する
local_fileリソースquota(ファイル/root/tf/lifecycle/out/quota.txt)を、内容256で適用しておいてください。今回は、ファイルを手で512に直したあと、元に戻さずに、main.tfの値を512に変更してコードに吸収し、適用してください。そのあと、tofu planの出力を/root/tf/lifecycle/out/accept-plan.txtに保存すると、No changesである必要があり、quota.txtとmain.tfの両方が、512を含んでいる必要があります。
今回は、反対の方向です。手で変更した値が正しいと判断したなら、コードをその値に直します。
ドリフト検知のスクリプトとレポートを作る
/root/tf/lifecycle/drift-report.shを作成して、実行権限を付けてください。このスクリプトは、-detailed-exitcodeを使うプランコマンドでドリフトを判定し、結果を/root/tf/lifecycle/out/drift-report.jsonに、exit_code、drift、checked_at、addressesの4つのフィールドで残す必要があります。ラボを締めくくる時点では、ドリフトがないはずなので、exit_codeは0、driftはfalseである必要があります。
判定は、人の目ではなく、終了コードで行います。set -eが、終了コード2でスクリプトを止めてしまわないように、注意してください。