TT Lab
はじめる
学ぶ 学習パス コース

Terraform実戦

ライフサイクルとドリフト — どちらへ合わせるのか

TT Labで続きを見る

一言でいうと

ドリフト対応の核心の質問は、「どう直すか」ではなく、「コードを実体に合わせるのか、実体をコードに合わせるのか」です。

なぜ必要なのか

午前3時、トラフィックが集中して、サービスが落ちます。担当者がコンソールに入り、インスタンスタイプを大きくし、オートスケーリングの上限を上げ、デバッグ用のポートを一時的に開けます。サービスは復活します。ここまでは、正しい判断です。

問題は、その次です。この3つの変更は、どこにも記録されていません。コードは依然として以前の値を語っており、状態ファイルも同様です。数日後、誰かがまったく別の理由でapplyを回すと、ツールは律儀に、明け方の変更をすべて元に戻します。障害が再現し、誰も原因がわかりません。

これがドリフトです。コード(あるべき状態)、状態ファイル(最後に把握した状態)、実体(いまある状態)の、3者の間の不一致です。ドリフト自体は、罪ではありません。検知されないドリフトが罪です。

どう動くのか

検知の基本ツールは、planです。プランは、実行前に実体をリフレッシュして状態と比較し、次に状態とコードを比較します。人が読むには十分ですが、自動化には不足です。出力文字列をパースする方式は、バージョンが変わると壊れます。

そこで、-detailed-exitcodeがあります。

終了コード 意味
0 変更なし。コード・状態・実体が一致しています
1 エラー
2 変更あり。ドリフト、または未適用の変更です

この3つの値があれば、ドリフト検知を、cronやCIにそのまま載せられます。ここでよく引っかかる落とし穴が、set -eです。終了コード2を失敗とみなして、スクリプトが落ちてしまうため、判定コマンドは、必ず例外処理をして実行する必要があります。

さらに精密に見るには、plan -refresh-onlyでプランを保存したあと、show -jsonで取り出します。JSONには、resource_drift配列が別にあるため、コードの変更で生じた差と、コードの外で生じた差を区別できます。アラートに、「誰かがコンソールで触った」と「まだデプロイされていないコードがある」を混ぜて送ると、人々はすぐにアラートを無視するようになります。

lifecycleブロックは、この流れに介入する4つのノブです。

引数 役割 使う場面
create_before_destroy 新しいものを先に作成してから、古いものを削除します 置き換えの間の中断を避けたいとき
prevent_destroy 破棄のプラン自体をエラーにして止めます 状態ストア、本番DB
ignore_changes 特定の属性の差を、プランから除外します 外部システムが管理する属性
replace_triggered_by 別のリソースが変わったら、このリソースを再作成します 更新手段のないリソースの強制的な置き換え

ignore_changesは、特に重要です。オートスケーラーが管理するレプリカ数や、コンソールが自動で付けるタグのように、意図的にコードの外で変わる値までドリフトとして検知すると、誤報があふれ、あふれるアラートは、やがて無視されるアラートになります。ただし、無視リストには、正確にその属性だけを入れる必要があります。まるごと無視すると、本当の事故も一緒に静かになります。

prevent_destroyは、プランの段階でエラーを出して止まります。誤ってリソースブロックを削除したり、名前を変えたりしたときに、破棄が承認される前にブレーキがかかるという意味です。その代わり、意図的に削除するときは、コードからこの行を先に削除する必要があるため、削除が必ずレビューを経るようにする仕組みでもあります。

現場での姿

1つ目に、対応の方向は3つあります。①applyで、実体をコードに合わせて元に戻す、②変更が意図されたものであれば、コードを実体に合わせて直す、③実体が正解で、コードもすでに合っているなら、apply -refresh-onlyで状態だけを更新する。明け方の応急措置が正しい判断だったなら、元に戻すことのほうが、むしろ事故です。判断なしに自動復旧を回すのが、最も危険です。

2つ目に、重大度で分けて自動化します。タグ1つのずれは、自動で元に戻してもよいですが、セキュリティグループのルールやインスタンスタイプは、人が見る必要があります。実務の標準パターンは、重大度の低いものだけを自動で復旧し、重大度の高いものは、アラートを出してから止まる、選択的復旧です。

3つ目に、検知自体がコストです。プランは、管理中のすべてのリソースに対して、プロバイダーAPIを呼び出すため、大規模なインフラで頻繁に回すと、APIの制限に引っかかります。検知中には、状態の読み取りロックもかかり、同時に動くデプロイと衝突することがあります。そして、プランの出力には、パスワードのような機密性の高い値が混ざることがあるため、ログとして残すときは、必ずフィルタリングする必要があります。

4つ目に、プロバイダーのアップグレード後の大量のドリフトです。新しいバージョンが属性のデフォルト値を変えると、誰も何もしていないのに、数百件がドリフトとして表示されます。このときは、実体ではなくプロバイダーが変わったので、バージョンの固定と、チェンジログの確認が先です。

次のラボですること

create_before_destroyとprevent_destroyを実際にかけて、削除が止まることを確認し、ignore_changesで、コードの値を変えてもプランが空になることを見ます。replace_triggered_byで、別のリソースの変更が再作成を引き起こすようにしたあと、コードを経由せずに実体を手で直して、ドリフトを作ります。それを-refresh-onlyのプランと終了コード2で検知し、1回はコードを基準に元に戻し、もう1回は、実体をコードに吸収するという反対の方向で対応します。最後に、検知結果をJSONで出力するスクリプトを作り、自動化の形を整えます。