Terraform 運用 #2 drift と state の手術 — 定期検知、import ブロック、removed ブロック
#1のワークフローで、コードを通る変更はすべて関門を通過するようになりました。残る問題は、関門を通らない変更です。障害対応中のコンソール修正、別のチームが付けたタグ、セキュリティチームのツールが変えた設定。基礎 #5でこれをドリフト(drift)と呼び、「Terraform のリソースは Terraform だけで触る」が原則だと言いました。運用の現実は、その原則が崩れる日が必ず来るということであり、今回は崩れたときの対応体系です。
検知 — 定期 plan と exit code #
ドリフトの怖さは発生ではなく、気づかないまま積み上がることです。次のデプロイの日に plan へ正体不明の変更が大量に現れると、自分の変更と他人のドリフトが混ざってレビューが不可能になります。答えは、何の変更がなくても定期的に plan を回してみることです。このために用意されているフラグが -detailed-exitcode です。
terraform plan -detailed-exitcode
# exit 0: 変更なし
# exit 1: エラー
# exit 2: 変更あり = ドリフトexit code 2 を失敗として扱う夜間ワークフローを 1 つ置けば、ドリフト検知器になります。
# .github/workflows/drift-check.yml (中心部)
on:
schedule:
- cron: "0 21 * * *" # 毎日 06:00 KST
jobs:
drift:
steps:
# ... init までは #1 と同じ ...
- run: terraform -chdir=envs/prod plan -detailed-exitcode
# exit 2 ならジョブが失敗し、失敗の通知がドリフトの通知になりますドリフトが 1 日以内に見つかれば「昨日、何があったのか」に絞り込めるので、原因の追跡も楽になります。
対応 — 3 択の判断基準 #
ドリフトを見つけたときの選択肢は 3 つだけです。
- 現実をコードに吸収する: コンソールでの変更が正しかった場合です(障害対応で伸ばしたタイムアウトなど)。その値をコードに反映して PR に上げれば、plan が「変更なし」になって収束します。緊急対応そのものを禁止することはできないので、対応後にコードへ反映するところまでが対応手順だとチームの規約に入れるのが現実的です。
- apply で現実を戻す: 変更がミスだったか、無断だった場合です。コードが真実なので、apply 一度で原状回復できます。戻す前に、その変更が本当に不要なのかを確認することだけ忘れないようにします。
- ignore_changes で管轄を移す: その属性を別のシステムが変え続けるのが正常なら、基礎 #7で見たとおり、Terraform の管轄から外すのが正解です。
import ブロック — コンソールで生まれたリソースを編入する #
ドリフトの拡張版として、リソースそのものがコードの外で生まれた場合があります。Terraform 導入前からあったリソースや、急ぎでコンソールから作ったリソースをコード管理に取り込む道具が import です。以前は terraform import コマンドで 1 個ずつ処理していましたが、今は import ブロック(Terraform 1.5 以上)が標準です。コードで宣言するので PR レビューに残り、plan で結果を事前に確認できます。
import {
to = aws_s3_bucket.legacy_logs
id = "myapp-legacy-logs" # リソースタイプごとの import ID
}
resource "aws_s3_bucket" "legacy_logs" {
bucket = "myapp-legacy-logs"
# 実際の設定と一致している必要がある
}編入の手順で大変なのは import 自体ではなく、既存の設定と正確に一致する resource ブロックを書く作業です。ここに補助の道具があります。
terraform plan -generate-config-out=generated.tfimport ブロックだけがあって resource ブロックがない状態でこのフラグを付けると、実際のリソースを読み取ってコードのドラフトを生成してくれます。生成されたコードは冗長でそのまま使う品ではありませんが、属性値を目で書き写す労働をなくしてくれます。整えて resource ブロックとして確定し、plan が「変更なし」(import だけ 1 件)になれば編入完了です。apply 後に import ブロックは削除します。
removed ブロック — 消さずに送り出す #
逆方向もあります。リソースを実際には残したまま、Terraform の管理からだけ外す場合です(別のチーム・別のスタックへの移管、手動管理への切り替え)。基礎 #5で terraform state rm コマンドを紹介しましたが、これにも宣言型の対になる存在として、removed ブロック(Terraform 1.7 以上)が生まれました。
removed {
from = aws_s3_bucket.legacy_logs
lifecycle {
destroy = false # リソースは残して state からだけ削除
}
}resource ブロックを消しながらこのブロックを一緒に置くと、plan が「forget」(破壊なしの削除)を見せてくれます。destroy = false を書き忘れると実際の削除になるので、この 1 行がこのブロックのすべてだと言っても過言ではありません。moved(実践 #9)、import、removed が揃ったことで、state の手術 3 種がすべて CLI コマンドではなくレビュー可能なコードになったわけです。命令型の道具(state mv、state rm、import コマンド)は今も動きますが、チーム運用ではコード側を基本にすることをおすすめします。
まとめ #
今回扱った内容です。
- ドリフトは発生より蓄積が問題です。
-detailed-exitcodeを使った夜間 plan ワークフローが検知器になります - 対応は 3 つだけです。正しい変更はコードに吸収、間違った変更は apply で原状回復、正常な外部の変更は ignore_changes で管轄を移します
- 緊急のコンソール対応は禁止するのではなく、「コードへの反映までが手順」として規約化します
- 既存リソースの編入は import ブロック +
-generate-config-outのドラフト生成が現行の標準です。完了の基準は「変更なし」の plan です - 管理からの除外は removed ブロックに
destroy = falseです。moved、import、removed で state の手術がすべてコード化されました
次回(#3 コード品質とテスト)では、人のレビューの手前に自動チェックを積み上げます。tflint と pre-commit、そして Terraform 内蔵のテストフレームワークである terraform test です。