二度回しても同じでなければならない
一言でいうと
冪等性は、同じ作業を何度実行しても結果が同じになる性質で、これがなければ、自動化は、回すたびに結果が変わる賭けになります。
なぜ必要なのか
自動化は、1回だけ実行されるわけではありません。ランナーがタイムアウトで死んで再実行され、他の人が同じプレイブックをまた回し、スケジューラーが毎時間同じものを適用します。このとき、「すでにできていれば何もしない」が保証されていないと、再実行そのものが危険になります。結局、人は自動化を怖がるようになり、怖いから手でやるようになり、手でやるからまたスノーフレークサーバーになります。
証明方法は、意外と単純です。applyを2回続けて回し、2回目のplanが変更なし(終了コード0)かどうかを見ればよいのです。この1行の検査に通らない自動化は、まだ信頼できません。
どう動くのか
冪等性は、「実行する前に現在の状態を確認する」から生まれます。ディレクトリを作る前に、あるかどうかを見て、ファイルを書く前に、内容が同じかどうかを見て、権限を変える前に、現在の権限を読みます。そのため、よくできたモジュールは、いつも2つに分かれます。変更したならchanged、すでに合っているならunchangedです。
冪等性が崩れる最もよくある場所は、専用のモジュールの代わりに、shellやcommandでコマンドを直接実行するときです。ツールは、そのコマンドが何をするのか知らないので、毎回実行します。そのため、creates=やremoves=のような条件を付けて、すでに終わった作業をスキップさせます。代表的なバグは、設定ファイルに1行を追加するといって、>>で追記することです。実行するたびに同じ行が1つずつたまり、10回回せば10行になります。正しい実装は、その行がすでにあるかどうかを先に確認し、同じキーの別の値があれば、その場所を置き換えることです。
適用する前に、何が変わるかを事前に見るには、チェックモードとdiffを一緒に使います(--check --diff)。実際には変更せず、変わる内容だけを見せてくれるので、本番サーバーに初めて適用するときは必須です。そして、結果は必ず数えて見せる必要があります。changedとunchangedを集計したレポートがあってはじめて、2回目の実行でchangedが0であるという事実を、人が確認できます。
現場での姿
「プレイブックは成功したのに、なぜ設定がおかしいのですか」という質問の半分は、冪等でないタスクから生まれます。ログにはokとしか出力されていなくても、実際には同じ行が3回入っていたり、再起動が毎回起きて、サービスが定期的に途切れていたりします。そのため、成熟したチームは、CIでプレイブックを2回回します。2回目の実行のchangedが0でなければ、ビルドを失敗させます。冪等性を、ドキュメントではなくテストで強制するのです。
もう1つ、状態を記録しておくと、検知する能力が生まれます。適用の時点で、各リソースのパスとハッシュを残しておけば、あとでそのファイルが外で変更されたかどうかを、比較だけで知ることができます。状態ファイルを持つツールがやっていることが、まさにこれです。
冪等性と収束は違う
2つの単語はよく混ぜて使われますが、保証する範囲が違います。冪等性は、同じ作業を何度行っても結果が同じになるという性質で、収束は、今の状態が何であっても、望ましい状態へ持っていくという性質です。ほとんどの自動化は、前者だけを満たして、後者を見落とします。
違いは「削除すること」で明らかになります。設定ファイルを3つ置くプレイブックを2回回すと、changedは0です。ここまでが冪等性です。ところが、誰かが手で4つ目のファイルを作っておいたとしたら、このプレイブックは、それを永遠に発見できません。望ましい状態を「この3つがある」としか書いておらず、「この3つだけがある」と書いていないからです。収束させるには、ディレクトリの内容全体を管理対象として宣言して、リストにないものは削除する必要があります。
そのため、自動化を設計するときは、2つを明示的に決めます。どこまでが自分の所有する領域か、そしてその領域の中で見慣れないものを見つけたら、削除するのか、知らせるのかです。静かに削除すると他人の作業を吹き飛ばし、何もしなければ、サーバーは徐々に互いに違っていきます。
副作用のある作業でも、同じ区別が必要です。サービスの再起動が代表的です。設定ファイルが変わったときだけ再起動しなければならないのに、これを無条件に毎回実行すると、プレイブック自体は冪等に見えても(ファイルの内容はいつも同じなので)、実際には回すたびにサービスが途切れます。通知(handler)方式が存在する理由がこれで、このとき、再起動が実際に何回起きたかを結果のレポートに残しておけば、あとで「なぜ毎時間、少しずつ途切れるのですか」という質問を、数秒で片付けられます。
最後に、リトライとの関係を指摘しておきます。冪等な作業は、失敗したときにそのままもう一度実行すればよいのですが、冪等でない作業は、リトライがそのまま重複実行です。ネットワークエラーで応答を受け取れなかったとき、その作業が実際に実行されたかどうかわからないという点が核心です。リトライするつもりなら、その作業は必ず冪等でなければならず、冪等にできないなら、作業ごとに一意のマーカーを付けて、すでに処理したものかどうかを確認するしかありません。
次のラボですること
ディレクトリ・ファイル・行・権限を保証する小さなスクリプトを4つ作り、それぞれがchangedとunchangedを正確に区別するようにします。そのあと、この4つを束ねたランナーを2回続けて実行して、2回目にchangedが0になるかどうかで冪等性を証明します。最後に、状態ファイルとドリフトの検知、そして同時実行を防ぐロックまで付けます。