Ansibleのcheck modeと--diffでdry runする方法
Ansibleの--checkと--diffが実際に確認できる範囲を解説します。check mode非対応moduleは何も報告せず、dry runの結果が誤る理由も説明します。
Ansible の check mode で確認できること
Ansible の check mode は dry run です。ansible-playbook --check は play 内のすべてのホストに接続し、各 module に現在の状態が要求した状態と一致しているかを確認させ、何も書き込まずに変更予定を報告します。--diff を追加すると、変更対象となるファイルの変更前と変更後の内容も表示します。これらを組み合わせると、実行前に確認すべき問いに答えられます。これらのサーバーで何が変更されようとしているのか、という問いです。
check mode は playbook のシミュレーションではありません。サーバーのモデルが用意されているわけではありません。各 module は、書き込む代わりに確認するよう求められるだけです。読み取り専用で判定できる module は、changed を報告して次へ進みます。判定できない module は何も実行せず、何も報告しません。Ansible のドキュメントでは、これを次の1行で説明しています。「check mode をサポートしない module は、何も報告せず、何も実行しません。」この制約があるため、dry run の結果が誤ることがあります。このガイドの大部分では、この制約について説明します。
ドライランを実行する: --check と --diff
ansible-playbook -i inventory.ini site.yml --check --diff --limit web1-C と -D は、2 つのフラグの短縮形です。--limit は意図的に指定しています。1 台のホストの差分なら確認できます。20 台のホストの差分になると、スクロールして見過ごしやすくなります。
4 つの結果ワードで、レポート全体を確認できます。
ok: [web1]は、モジュールが状態を確認し、すでに一致していることを示します。変更はありません。changed: [web1]は、モジュールが何かを書き込むことを示します。--diffが付いている場合は、その上の行に内容が表示されます。skipping: [web1]は、タスクが評価されなかったことを示します。whenが false だったか、モジュールが check mode で実行できません。fatal: [web1]は、確認中にタスクが失敗したことを示します。playbook が壊れていると判断する前に、メッセージを確認してください。
--diff は、file module の統合差分を出力します。削除された行には -、追加された行には + が付きます。ヘッダーの行は --- before と +++ after で始まり、宛先パスを示します。ファイルを書き込まないモジュールは、独自の変更前と変更後を出力します。そのため、ansible.builtin.user にはファイル内容ではなく、変更される属性が表示されます。
ansible.cfg で diff を永続的に有効にすると、フラグの指定を忘れません。
[diff]
always = true
context = 5check mode の前に、より軽量な確認を 2 つ実行できます。ansible-playbook site.yml --syntax-check は、ホストに接続せずに YAML と play の構造を解析します。ansible-playbook site.yml --list-tasks は実行されるタスクを出力します。これにより、タグを付けたつもりの role に実際にはタグが付いていないことを確認できます。どちらも接続を行わないため、すぐに完了します。
check mode 自体は接続を行います。パターンに一致するすべてのホストへ SSH 接続を開き、facts を収集します。そのため、停止中のホストがあるとドライランは失敗します。これは単独でも有用な情報です。また、ドライランを CI に組み込む前に、到達不能なホストに対して playbook でどう処理するかを決めること が重要な理由でもあります。
新規サーバーで check mode が失敗する理由
この play は正しい内容です。まだ nginx がインストールされていないサーバーに対して --check で実行すると、その大部分が失敗します。
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Write the site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: trueapt task は changed と報告します。これは正しい結果です。パッケージが存在しないため、通常の実行ではインストールされます。check mode ではインストールされません。続く template task は失敗します。このホストには /etc/nginx/conf.d/ が存在せず、作成もされていないためです。service task も失敗します。照会対象となる nginx unit が存在しないためです。どちらの失敗も playbook のバグではありません。dry run では、必要な状態を作成する処理が実行されなかったためです。ドキュメントが、入力が先行 task の変更に依存する task では check mode が有用な出力を生成できないと説明しているのは、このためです。
したがって、このルールを正確に言い換えると、check mode は playbook の適用が完了したホストに対しては正確で、新規ホストに対しては大量のエラーを報告します。すべての task が ok と報告する --check 実行は、収束済みのホストに関する実際の状態を示します。何も変更されないという意味だからです。新規ホストでは、--check は主にそのホストが新しいことを示します。VPS に対して最初の Ansible playbook を作成するときは、最初の dry run が赤いエラーで埋まることを想定してください。playbook の評価は、2 回目の実行結果で行います。
check mode で command と shell のタスクがスキップされる理由
ansible.builtin.command と ansible.builtin.shell は、実行するコマンドの内容を把握できません。任意のバイナリを読み取り専用で実行する方法はないため、check mode では module が実行を拒否します。タスク結果には skipped: true が設定され、Command would have run if not in check mode というメッセージが表示されます。出力には skipping: [web1] が示されます。
module のドキュメントでは、check mode のサポートを「partial」と説明しています。記載されている回避策は creates と removes です。タスクに creates のパスを指定すると、check mode でも少なくともファイルの存在確認を評価できます。
- name: Extract the release bundle
ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
args:
creates: /opt/app/bin/app/opt/app/bin/app がすでに存在する場合、check mode は Would not run command since '/opt/app/bin/app' exists を報告します。これは実際の判定結果です。パスが存在しない場合は Command would have run if not in check mode になり、これも実際の判定結果です。creates がなければ、そのタスクは dry run では空白になります。
この影響は、単に空白になるだけではありません。スキップされたタスクも結果を登録しますが、その結果は skip result であり、stdout キーを持ちません。次のタスクの条件を評価するときに失敗し、'dict object' has no attribute 'stdout' に近いエラーになります。実行時には正常に動作する playbook が dry run では失敗します。この機能で最も原因を特定しにくい問題です。
check_mode: false の設定と、適用すべき箇所
タスクの check_mode: false は、「--check 中であっても、実際に実行する」という意味です。これはコマンドがスキップされる問題を解決します。ただし、読み取りだけを行うタスクでのみ安全に使用できます。
- name: Read the installed app version
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
check_mode: false
changed_when: falseこのタスクは、どちらのモードでも正しく動作します。バージョンを読み取るだけで書き込みは行いません。changed_when: false により、実行していない変更を変更済みとして報告することを防ぎます。また、check_mode: false により、ドライラン中でも app_version.stdout が存在するため、それを基にした条件も評価されます。
このキーワードは、別の場所に貼り付ける前に文字どおり確認してください。check_mode: false を指定したタスクは、ansible-playbook --check 中にサーバーへ書き込みます。これを apt タスクまたは template タスクに付けると、ドライランの表示は整って見えますが、ドライランではなくなります。書き込みを行うタスクを安全にできない場合は、条件で制御してください。
- name: Apply the database migration
ansible.builtin.command: /usr/local/bin/app migrate --apply
when: not ansible_check_modeansible_check_mode は、チェック実行中に Ansible が true に設定する特殊変数です。逆のキーワードもあります。check_mode: true は、実際の実行中であってもタスクを常にチェックモードで実行します。これは差分検出に使えます。結果を register し、changed レポートが返った場合、そのホストはタスクが要求する状態と一致しなくなっています。
タスクが毎回の実行で changed と報告される理由
間を空けずに playbook を 2 回実行します。2 回目は、すべてのタスクが ok と報告するはずです。それでも changed と報告するタスクは、次のどちらかを示しています。モジュールが管理対象の状態を確認できないか、渡している入力が安定していません。どちらも修正できます。無視してよい情報ではありません。
commandとshellにcreates、removes、changed_whenがない場合、モジュールには何かが実行されたかどうかを知る方法がないため、毎回changedと報告します。createsを追加するか、出力内の文字列に対してchanged_whenを設定します。state: touchを指定したansible.builtin.fileは、ファイルに触れるとタイムスタンプが更新されるため、設計上、毎回changedと報告します。目的が所有者またはモードの設定だけなら、state: fileを使用します。- レンダリング後の出力が変化する
templateは、毎回ファイルを書き換えます。ansible_date_timeのタイムスタンプ、now()の呼び出し、または実行のたびに新しく生成するパスワードは、いずれも異なるバイト列を生成します。そのため、モジュールが変更を報告するのは正しい動作です。変化する値をテンプレートから取り除きます。 password: "{{ pw | password_hash('sha512') }}"を指定したansible.builtin.userは、毎回変更されます。password_hashは呼び出されるたびにランダムな salt を生成するため、結果の hash が/etc/shadowにある値と一致しません。安定した値から明示的に salt を生成して渡します。- パッケージモジュールでの
state: latestは、アップグレードが利用可能な場合にchangedと報告します。これは正しい報告です。同時に、state: latestでは playbook の結果を予測できません。state: presentを使用し、意図したタイミングでアップグレードします。 createsのない URL を指定したansible.builtin.unarchiveは、毎回再取得して再展開します。createsのパスを指定します。
これらを見分けるには --diff が最も速い方法です。タスクが changed と報告し、diff に異なるバイト列が表示される場合、入力が安定していません。changed と報告され、diff に何も表示されない場合、モジュールは変更内容を表現できません。通常は command タスクか、タイムスタンプのようなメタデータだけを書き込む処理が原因です。
ノイズの多いタスクを静かにするために changed_when: false を使ってはいけません。これは報告を抑制するため、notify が実行されず、サービスを再起動する handler も実行されません。タスク自体を修正します。
影響範囲を絞る: --limit、--tags、--step
Check mode は、何が変更されるかを示します。これらのフラグは、一度に実行結果を受け取るマシンの数を決めます。
--limit は、inventory の一部だけに play の対象を絞ります。hosts: と同じパターンを使用するため、--limit web1 と --limit 'webservers:!web3' のどちらも機能します。パターンは引用符で囲んでください。対話的な bash セッションで引用符のない ! を使うと、感嘆符で履歴展開が発生し、Ansible がコマンドを認識する前に shell が書き換えてしまいます。
信頼する前に、パターンを確認してください。ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts は一致したホストを表示し、どのホストにも接続せずに終了します。何も一致しないパターンは安全です。Ansible が inventory 全体にフォールバックすることはありません。一致する host pattern がないという警告を表示し、その後、hosts と --limit がどのホストにも一致しないというエラーで終了します。そもそもパターンを予測可能にするには、inventory ファイルでそれらのグループがどのように定義されているかを把握する必要があります。
--tags deploy はタグ付けされた task だけを実行し、--skip-tags packages はそれ以外をすべて実行します。--list-tags は利用可能なタグを表示します。play が大きくなり、全体を実行することを避けたい場合に、タグが役立ちます。これは、長い playbook を role に分割する理由の 1 つでもあります。
--start-at-task "Write the site config" は、名前を指定した task から失敗した実行を再開します。復旧に使用できますが、その代償を理解しておく必要があります。その task より前にあるすべての task がスキップされます。後続の task が読み取る fact の設定や変数の register も含まれます。
--step は各 task の前に確認を求め、yes、no、continue のいずれかの入力を待ちます。処理は遅くなりますが、破壊的な操作を初めて実行する場合に適した方法です。20 個後ではなく、2 個の task の間で停止できます。
シリアルで変更を段階的に適用する
デフォルトでは、Ansible は次のタスクを開始する前に、play 内のすべてのホストに対して現在のタスクを実行します。これは高速ですが、問題のあるタスクが同じ秒に全ホストへ到達することも意味します。エラーを読み、Ctrl-C を押す頃には、変更がすでに全ホストへ適用されています。
serial を使うと、play をバッチに分割できます。最初のバッチに対して play 全体を実行し、続いて次のバッチで実行します。
- name: Roll out the web tier
hosts: webservers
serial: [1, 5, "30%"]
max_fail_percentage: 0
tasks:
- name: Deploy the release
ansible.builtin.include_role:
name: webapp最初のバッチは 1 台です。問題なく完了すれば、2 番目のバッチは 5 台になり、それ以降の各バッチは play の対象ホストの 30 percent になります。max_fail_percentage: 0 は、バッチ内のいずれかのホストで失敗すると play を直ちに終了するため、問題のあるリリースを 1 台で止められます。any_errors_fatal: true はより強制的な動作で、最初のホストが失敗した時点で全ホストに対する play を終了します。
最初に 1 台だけを対象に実行するのは、過剰な警戒ではありません。理由は明確です。Inventory のグループ構成は変化します。他のホストを追加してから 6 か月後に登録されたサーバーでは、異なるディストリビューションのリリースが動作していたり、誰かが手動でインストールしたサービスが存在したり、ディスク構成が異なっていたりする可能性があります。playbook はグループ全体には正しくても、その 1 台には適用できないことがあります。構成がそろったホストで dry run を実行しても、この問題は見つかりません。Linux サーバー群を管理するとは、変更を適用する前に、このような例外的なホストを見つける作業でもあります。
実行する順序
ansible-playbook site.yml --syntax-checkはネットワーク接続なしで YAML と構造の誤りを検出します。ansible-playbook site.yml --limit web1 --list-hostsにより、パターンが想定どおりに一致することを確認します。ansible-playbook site.yml --limit web1 --check --diffはドライランです。差分を確認します。ansible-playbook site.yml --limit web1 --diffをその 1 台のホストに適用します。- 手順 4 をもう一度実行します。すべてが
okと報告されるはずです。changedのままの項目は、残りのホスト群に適用する前に修正すべきタスクです。 - これでインベントリ全体に対する
ansible-playbook site.yml --check --diffは意味のある結果を返します。収束済みのホストは静かになり、残っているものが実際の差分だからです。
手順 3 について、1 つ注意があります。--diff はファイルの内容を端末と CI ジョブのログに出力します。そのため、データベースのパスワードを生成するテンプレートでは、そのパスワードもログに出力されます。そのタスクに diff: false を設定して出力を抑制するか、no_log: true を設定して結果全体を非表示にしてください。また、値そのものはリポジトリではなく、暗号化した Ansible Vault ファイルに保存してください。
FAQ
ansible-playbook --check はサーバー上の何かを変更しますか?
いいえ。例外が 1 つあり、それは制御できます。check mode では、各モジュールに対して書き込みではなく報告を行うよう指示します。この処理に対応していないモジュールは、何も報告せず、何も実行しません。例外は check_mode: false task keyword です。これは --check 実行中でも、その task だけを実際に実行します。dry run を信頼する前に、playbook と role で check_mode: false を検索し、該当箇所が状態を読み取るだけの task であることを確認してください。
--check と --diff の違いは何ですか?
--check は、実際の処理を実行するかどうかを決めます。--diff は、表示する詳細度を決めます。--check だけを指定すると、ファイルが変更されることだけが示されます。--diff だけを指定すると、変更を適用し、変更した行を表示します。実際に内容を確認できる dry run には、これらを併用してください。実行時にも --diff を有効にするには、ansible.cfg 内の [diff] で always = true を設定します。
Ansible task が毎回の実行で changed になるのはなぜですか?
モジュールが管理対象の状態を確認できないか、渡している値が毎回異なるためです。command と shell は、creates または changed_when を追加しない限り、常に changed を報告します。state: touch を指定した file は、仕様上変更されます。timestamp や新しく生成した password を出力する template は、実行ごとに異なるバイト列を生成するため、ファイルは実際に書き換えられます。playbook を続けて 2 回実行してください。2 回目にも changed のままの項目が、修正すべき task です。
dry run 中に command と shell の task がスキップされるのはなぜですか?
任意の command を読み取り専用で実行する方法がないためです。check mode では、command module が skipped: true を設定し、Command would have run if not in check mode という message を返します。creates または removes を追加し、check mode でファイルテストを評価できるようにしてください。状態を読み取るだけの task では、changed_when: false と併せて check_mode: false を設定してください。これにより、dry run 中も登録済みの結果が存在し、その結果に基づく条件が機能します。
新しいサーバーでは check mode が失敗し、既存のサーバーでは成功するのはなぜですか?
check mode は、後続の task が依存する状態を作成しないためです。nginx がないホストに対する dry run では、インストールが changed と報告されます。その後、/etc/nginx/conf.d/ に書き込む task で失敗します。そのディレクトリは作成されていないためです。これは想定された動作です。check mode は、playbook の適用が完了しているホストを対象にした差分検出機能です。初回実行の妥当性は検証できません。新しいホストでは、1 台に playbook を適用し、2 回目の実行結果を確認してください。