SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

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] は、タスクが実行され、変更がなかったことを示します。apt や template のように状態を管理するモジュールでは、状態を確認した結果、すでに一致していたという意味です。debug や set_fact では、タスクが実行されたという意味だけになります。
  • changed: [web1] は、モジュールが何かを書き込むと予測したことを示します。--diff を指定すると、その上の行に内容が表示されます。ほとんどのモジュールは正確に予測します。ただし、check mode のサポートが一部に限られるモジュールについては、公式ドキュメントに注意事項が記載されています。get_url は check mode では HEAD リクエストだけを送信し、変更状態を誤って報告すると説明しています。
  • skipping: [web1] は、タスクが実行されなかったことを示します。when が評価されて false になったか、モジュールが check mode で実行できないことが原因です。
  • fatal: [web1] は、確認中にタスクが失敗したことを示します。playbook に問題があると判断する前に、メッセージを確認してください。

--diff は、file モジュールについて unified diff を出力します。削除された行には -、追加された行には + が付きます。ヘッダーの行は --- before と +++ after で始まり、宛先パスを示します。ファイルを書き込まないモジュールは、独自の変更前と変更後を出力します。そのため ansible.builtin.user には、ファイル内容ではなく、変更される属性が表示されます。

ansible.cfg で diff を常に有効にすると、フラグの指定を忘れません。

[diff]
always = true
context = 5

check 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: true

aptタスクはchangedと報告します。これは正しい結果です。パッケージが存在しないため、通常の実行ならインストールされます。check modeではインストールされません。続くtemplateタスクは、/etc/nginx/conf.d/がこのホストに存在せず、作成もされていないため失敗します。serviceタスクも、照会対象となるnginx unitが存在しないため失敗します。どちらの失敗もplaybookのバグではありません。dry runでは必要な状態がまだ用意されていなかったためです。ドキュメントが、入力が前のタスクによる変更に依存するタスクではcheck modeが有用な出力を生成できないと警告しているのは、このためです。

このルールを正確に言い換えると、check modeはplaybookの適用が完了したホストに対して最も正確で、新しいホストに対してはノイズが多くなります。すべてのタスクがokを報告する--checkの実行は、収束済みのホストについての有効な結果です。これは、何も変更されないことを意味します。ただし、どのホストでも次の2点には注意が必要です。スキップされたcommandタスクは回答ではなく、空白の結果です。また、get_urlのようにcheck modeを部分的にしかサポートしないmoduleは、changed状態を誤って報告することがあります。新しいホストでは、--checkは主にそのホストが新規であることを示します。VPSに対する最初のAnsible playbookを書くときは、最初のdry runが赤いエラーで埋まることを想定してください。そして、playbookの評価は2回目の実行で行ってください。

check mode で command と shell のタスクがスキップされる理由

ansible.builtin.command と ansible.builtin.shell は、実行するコマンドの内容を把握していません。任意のバイナリを読み取り専用で実行する方法はないため、check mode ではモジュールが実行を拒否します。タスクの結果には skipped: true が含まれ、Command would have run if not in check mode というメッセージが表示されます。また、出力には skipping: [web1] が示されます。

モジュールのドキュメントでは、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 で空白になります。

この影響は、単に空白になるだけではありません。スキップされた command タスクも結果を登録します。その結果には stdout が含まれますが、実行されていないため値は空文字列です。次のタスクの条件はその空文字列に対して評価され、誤った分岐が静かに選択されます。バージョン確認ではバージョンが見つからず、'active' in svc.stdout のようなテストも false になります。実際の実行と dry run で playbook の結果が異なるにもかかわらず、原因を示すエラーは発生しません。この機能で最も混乱しやすい失敗です。エラー 'dict object' has no attribute 'stdout' は別の種類のスキップに関するものです。when が false だったためにスキップされたタスクでは、stdout キーを持たない結果が登録されます。

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_mode

ansible_check_mode は、check run 中に Ansible が true に設定する特殊な変数です。逆のキーワードもあります。check_mode: true は、実際の実行中であってもタスクを常に check mode に固定します。これにより、タスクとの差分を調べるタスクになります。結果を register し、changed のレポートが出た場合は、そのホストがタスクの要求する状態と一致していません。

タスクが毎回 changed と報告される理由

間を空けずに playbook を 2 回実行します。2 回目は、すべてのタスクが ok を報告するはずです。それでも changed を報告するタスクは、次のどちらかを示しています。モジュールが管理対象の状態を確認できないか、渡している入力が安定していません。どちらも修正できます。無視してよいものではありません。

  • command と shell に creates、removes、changed_when のいずれも指定しない場合、モジュールには変更の有無を判断する方法がないため、毎回 changed を報告します。creates を追加するか、出力内の文字列に対して changed_when を設定します。
  • ansible.builtin.file に state: touch を指定すると、ファイルのタイムスタンプが更新されるため、設計上、毎回 changed を報告します。所有者または mode の設定だけが目的なら、state: file を使用します。
  • レンダリング結果が変化する template は、毎回ファイルを書き換えます。ansible_date_time のタイムスタンプ、now() の呼び出し、毎回新しく生成するパスワードはいずれも異なるバイト列を生成するため、モジュールは正しく変更を報告します。変化する値をテンプレートから取り除きます。
  • password: "{{ pw | password_hash('sha512') }}" を使用する ansible.builtin.user は毎回変化します。password_hash は呼び出すたびにランダムな salt を選ぶため、生成される hash が /etc/shadow にある値と一致しません。安定した値から明示的に導出した salt を渡します。
  • package module の 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

チェックモードでは、何が変更されるかを確認できます。これらのフラグは、一度に実行結果を受け取るマシンの台数を決めます。

--limit は、インベントリの一部にプレイの対象を絞ります。hosts: と同じパターンを指定できるため、--limit web1 と --limit 'webservers:!web3' はどちらも使用できます。パターンは引用符で囲んでください。対話的な bash セッションで引用符なしの ! を使うと、感嘆符で履歴展開が発生し、Ansible がコマンドを受け取る前にシェルが書き換えてしまいます。

パターンを信頼する前に、必ず確認してください。ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts は一致したホストを表示し、どのホストにも接続せずに終了します。何も一致しないパターンは安全です。Ansible がインベントリ全体にフォールバックすることはありません。一致するホストパターンがないことを警告し、インベントリ、ホストパターン、--limit の条件では対象ホストが残らないというエラーを表示して終了します。インベントリファイルでグループがどのように定義されているかを把握しておくことが、パターンを予測可能にする前提です。

--tags deploy はタグ付けされたタスクだけを実行し、--skip-tags packages はそれ以外のすべてを実行します。--list-tags は利用可能なタグを表示します。プレイが大きくなり、全体を実行したくなくなった段階で、タグが役立ちます。これは、長い playbook をロールに分割する理由の1つでもあります。

--start-at-task "Write the site config" は、失敗した実行を指定したタスクから再開します。復旧に使用できますが、代償を理解しておく必要があります。そのタスクより前にあるすべてのタスクがスキップされます。後続のタスクが読み取る fact の設定や変数の登録も含まれます。

--step は各タスクの前に確認を求め、yes、no、continue のいずれかの入力を待ちます。処理は遅くなりますが、破壊的な処理を初めて実行するときに適した方法です。20 個のタスクの後ではなく、2 つのタスクの間で停止できます。

直列で変更を段階的に適用する

デフォルトでは、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 件でも 0 を超えるためです。1 台ずつ確認する保証はこの設定によるものではなく、最初のバッチによるものです。全ホストで問題が発生するリリースでも、batch one の単一ホストで失敗し、そこで停止します。5 台のバッチでのみ発生する障害は、そのバッチ内の他のホストにもすでに到達しています。Ansible は次のタスクを開始する前に、バッチ全体に対して各タスクを実行するためです。any_errors_fatal: true は、最初のホストで失敗した時点で全ホストに対する play を終了する、より強制的な設定です。

最初に 1 台のホストで実行することは、過剰な警戒ではありません。理由は明確です。Inventory のグループ構成は変化します。他のホストが追加されてから 6 か月後に追加されたサーバーは、異なるディストリビューションのリリースを実行している場合があります。誰かが手動でインストールしたサービスを持っている場合や、ディスク構成が異なる場合もあります。playbook はグループ全体には正しくても、その 1 台には適合しないことがあります。構成が揃ったホストに対する dry run では、この問題は見つかりません。Linux サーバー群の管理 の中心は、変更によって問題が発生する前に、このような例外的なホストを見つけることです。

実行する順序

  1. ansible-playbook site.yml --syntax-check はネットワーク接続なしで YAML と構造の誤りを検出します。
  2. ansible-playbook site.yml --limit web1 --list-hosts により、パターンが想定どおりに一致することを確認します。
  3. ansible-playbook site.yml --limit web1 --check --diff はドライランです。差分を確認します。
  4. ansible-playbook site.yml --limit web1 --diff をその 1 台のホストに適用します。
  5. 手順 4 をもう一度実行します。すべてが ok と報告されるはずです。changed のままの項目は、残りのホスト群に適用する前に修正すべきタスクです。
  6. これでインベントリ全体に対する 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 を検索し、該当箇所が state の読み取りだけを行う task であることを確認してください。

--check と --diff の違いは何ですか?

--check は、実際に実行するかどうかを決めます。--diff は、表示する詳細度を決めます。--check だけを指定すると、file が変更されることだけが分かります。--diff だけを指定すると、変更を適用し、変更した行を表示します。実際に読みやすい dry run にするには、両方を組み合わせます。通常の実行でも --diff を有効にするには、ansible.cfg の [diff] に always = true を設定します。

Ansible task が毎回 changed と報告されるのはなぜですか?

module が管理対象の state を確認できないか、渡している値が毎回異なるためです。command と shell は、creates または changed_when を追加しない限り、常に changed を報告します。state: touch を指定した file は、設計上変更されます。timestamp や新しく生成した password を出力する template は、実行するたびに異なる byte 列を生成するため、file は実際に書き換えられます。playbook を続けて 2 回実行してください。2 回目も changed のままのものが、修正すべき task です。

dry run で command と shell の task がスキップされるのはなぜですか?

任意の command を read-only で実行する方法がないためです。check mode では、command module が skipped: true を設定し、Command would have run if not in check mode という message を返します。creates または removes を追加すると、check mode で file test を評価できます。state の読み取りだけを行う task では、changed_when: false とともに check_mode: false を設定してください。これにより、dry run 中も registered result に実際の output が入り、それを利用する condition が機能します。

新しいサーバーでは check mode が失敗し、既存のサーバーでは成功するのはなぜですか?

check mode は、後続の task が依存する state を作成しないためです。nginx がない host に対する dry run では、install が changed と報告された後、/etc/nginx/conf.d/ に書き込む task で失敗します。その directory は作成されていないためです。これは想定された動作です。check mode は、playbook の適用が完了している host を対象とした drift detector です。初回実行を検証することはできません。新しい host では、まず 1 台に playbook を適用し、2 回目の実行結果を確認してください。

#ansible#check-mode#idempotency#automation#safety