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

Ansible基礎

チェックモードは何を真似て、何を真似られないのか

TT Labで続きを見る

一言でいうと

--checkは「今実行すると何が変わるか」を尋ねるモードであり、その答えの精度はモジュールごとに異なります。チェックモードを信頼できるものにする作業は、フラグ1つを付けることではなく、タスクごとにチェックモードでどう振る舞うかを決めてあげる作業です。

なぜ必要なのか

本番サーバーにプレイブックを初めてかける日の恐怖は、具体的です。このプレイブックが20台に何をするのか、誰にもわかりません。誰かが手で直しておいた設定があるかもしれず、半年前の人が入れたタスクが、今は見当違いのことをするかもしれません。かといって、1台ずつ目で見ながら進むわけにもいきません。

--checkは、その場所に置かれた道具です。公式ドキュメントは、このモードをdry runと呼び、やることを1文で定義しています。リモートシステムに変更を加えず、変更が起きそうなら、そのタスクをchangedとして報告します。--diffを一緒に付けると、何がどう変わるのかを行単位で表示します。

ところが、この道具は2回裏切ります。1回目は偽りの安心です。チェックモードでクリーンだったのに、本当に実行すると失敗します。もう1回は偽の失敗です。チェックモードで真っ赤に失敗したのに、本当に実行すると何も問題がありません。2つの裏切りの原因は同じです。チェックモードは、実際には実行せずに結果を推測するモードであり、推測の品質はモジュールごとに異なります。

どう動くのか

チェックモードは、タスクごとに別々に決まります。モジュールがチェックモードをサポートしていれば、実際の変更の代わりに「変わるだろう」を報告します。サポートしていなければ、そのタスクはスキップされます(skipping)。command・shellが代表的です。Ansibleは、そのコマンドが何をするのかを知ることができないので、そもそも実行しません。

ここで、1つ目の事故が起きます。取得だけを行うコマンドのタスクがスキップされると、そのタスクのregister変数が空になり、その値を使う後ろのタスクが、次々と崩れます。そのため、読み取りだけを行うタスクには、check_mode: falseを付けます。そのタスクは、チェックモードでも実際に実行されます。

- name: 현재 설치된 판을 읽는다
  ansible.builtin.command: myapp --version
  register: current
  changed_when: false
  check_mode: false

付ける基準は1つです。このタスクは対象を変更するか。変更しないと確信できる場合にだけ付けます。check_mode: falseを、変更するタスクに付けると、そのタスクはdry runでも本当に実行され、その瞬間、dry runは嘘になります。

反対方向もあります。check_mode: trueを付けたタスクは、普段でも絶対に変更しません。実際の実行でも、「変わるだろう」だけを報告します。まだ有効にしてはいけないタスクをあらかじめ書いておいて、影響だけを見たいとき、または、危険なタスクを人が確認するまで縛っておきたいときに使います。

偽の失敗は、順序のせいで起きます。前のタスクがファイルを作成し、後ろのタスクがそのファイルを直すプレイブックを考えてみましょう。チェックモードでは、前のタスクがファイルを実際には作成しないので、後ろのタスクは、存在しないファイルを直そうとして失敗します。このラボ環境で、lineinfileは、まさにDestination ... does not exist !で失敗します。プレイブックが間違っているのではなく、チェックモードの限界です。

直す方法は3つあり、価値が異なります。1つ目は、後ろのタスクにwhen: not ansible_check_modeを付けて、チェックモードではスキップする方法で、最もよくあり、正直です。その代わり、そのタスクが何を変更するかは、計画から抜けます。2つ目は、前のタスクにcheck_mode: falseを付けて、実際に作成させる方法です。dry runではなくなるので、推奨するとは言いにくいです。3つ目は、そもそもそのファイルを1つのモジュールがまるごと管理するように、設計を変える方法で、最も良いですが、最も大きな変更です。

--diffは、形式が決まっています。--- beforeと+++ afterの下に、変わる行が-と+で出力されます。内容ではなく属性だけが変わる場合には、ファイルの内容の代わりに、属性JSONのdiffが出力されます(ディレクトリのstate: absentがstate: directoryに変わる、という形です)。--diffは、チェックモード専用ではありません。本当の実行に付けると、変更しながら何を変更したのかを示してくれます。事故の調査では、こちらのほうが役に立つことが多いです。

シークレットが含まれるファイルには、no_log: trueを一緒に付けます。そうしないと、diffがその値をそのままログに出力します。

3つの道具は、役割が異なります。

ツール やること 対象に接続するか テンプレートを展開するか
--syntax-check YAMLとプレイブックの構造を読み取る 接続しません 展開しません
--list-tasks 今回の選択で何が動くかの一覧だけを出力する 接続しません 展開しません
--check タスクをチェックモードで実際に実行する 接続します 展開します

実測で確認した違いが、これをはっきり示しています。定義されていない変数を使うプレイブックは、--syntax-checkと--list-tasksを終了コード0で通過します。そのエラーは、--checkで終了コード2として初めて明らかになります。逆に、モジュール名のタイプミスやインデントの事故は、3つの道具がすべて終了コード4で捉えます。そのため、CIは3つを順番にかけます。安価なものから、です。

チェックモードを承認手順として使うときは、人が読む出力物が必要です。画面を流れていく緑・黄色の文字は、承認の根拠として残りません。ansible.posix.jsonコールバックをstdoutコールバックに指定すると、実行全体がJSON1つとして出力され、そこから「変更されるタスクの名前一覧」だけを取り出して、変更リクエストに添付できます。

ANSIBLE_STDOUT_CALLBACK=ansible.posix.json \
  ansible-playbook -i hosts.ini site.yml --check --diff > plan.json

さらに一歩進むと、ゲートになります。収束が終わったあと、チェックモードでもう一度実行して、変わるものが1つでもあれば失敗で終了するスクリプトをCIにかけておけば、誰かが手で直したサーバーが、次のデプロイの前に明らかになります。チェックモードが「見るための道具」から「守るための道具」に変わる場所です。

現場での姿

1つ目は、チェックモードがクリーンなのに、本当の実行が失敗することです。たいていは、シェルのタスクがスキップされたせいです。チェックモードでskippedが多ければ、そのプレイブックのdry runは、その分だけ見ていないことになります。PLAY RECAPのskippedの数字を、計画の信頼度として読む習慣が役に立ちます。

2つ目は、チェックモードが赤いのに、実は問題がないことです。導入したばかりのチームが、ここで最も多く諦めます。「うちのプレイブックは--checkができない」という結論に至り、dry runをまったく使わなくなります。実際には、タスク2、3個にガードを付ければ済む話です。

3つ目は、diffがシークレットをログに漏らすことです。証明書のキーやDBのパスワードをテンプレートでデプロイしながら--diffをかけると、CIのログに値がそのまま残ります。no_log: trueを付けるルールを、最初から立てておくほうがよいです。

4つ目は、承認が画面キャプチャでやり取りされることです。dry runの結果を画像として貼り付けるチームは、半年後に「あのとき何を承認したのか」を見つけられません。JSONの計画の出力物が1つあれば、承認の履歴が、検索可能なテキストとして残ります。

5つ目は、このラボ環境の正直な限界です。監査ログや外部の承認システムは、このPodにありません。そのため、「承認手順」は、計画の出力物をファイルとして残すところまでを扱い、そのファイルをどこに貼り付けるかは、読み物でのみ話します。サービスの再起動のようなタスクも、capabilityがないので扱えないので、チェックモードの対象は、ファイルとディレクトリに限定します。

参考ドキュメント

次のラボですること

プレイブックを作成したらすぐに--checkで先に実行して、何も作られていないことを確認し、一度収束させたあと、値を3つ変えて--check --diffで、内容・パーミッション・1行の3種類のdiffを出力します。読み取り専用のコマンドタスクが、チェックモードでスキップされるのを見て、check_mode: falseでよみがえらせ、逆にcheck_mode: trueで、実際の実行でも絶対に変更しないタスクを作成します。チェックモードの偽の失敗をわざと作ってみて、ansible_check_modeガードで直します。定義されていない変数を使うプレイブックに、--syntax-check・--list-tasks・--checkを順にかけて、終了コードがどこで最初に分かれるかを表として残し、JSONコールバックで承認用の計画の出力物を作成したあと、変わるものがあれば1で終了するドリフトゲートのスクリプトを、自分で作成します。