ライフサイクルとドリフト — どちらへ合わせるのか
一言でいうと
ドリフト対応の核心の質問は、「どう直すか」ではなく、「コードを実体に合わせるのか、実体をコードに合わせるのか」です。
なぜ必要なのか
午前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で出力するスクリプトを作り、自動化の形を整えます。