0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Ansibleが理解できない理由はLinuxにあった【Ansible編】第3回:なぜ毎回changedになるのか

0
Last updated at Posted at 2026-06-22

📚 Ansibleが理解できない理由はLinuxにあった【Ansible編】第3回:なぜ毎回changedになるのか

⓪ なぜShellの知識がないとAnsibleは壊れるのか
① なぜshellモジュールは危険なのか
② なぜmoduleを使うと安全になるのか
③ なぜ毎回changedになるのか
④ なぜ条件分岐が壊れるのか(Ansible編)
⑤ なぜ変数が意図通りに扱えないのか
⑥ なぜタスクの順序で壊れるのか
⑦ なぜエラー制御が破綻するのか
⑧ なぜ非同期・待機で失敗するのか
⑨ なぜPlaybookが読めないのか
⑩ なぜAnsibleを正しく設計できるようになるのか


🗺️ 初めての方・シリーズの全体像を知りたい方はこちら

本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。

Ansibleが理解できない理由はLinuxにあった|統合ガイド


📑 【Ansible編】全体のまとめはこちら
Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ ※近日公開予定


📑 連載の移動
前の記事:【Ansible編】第2回 | 次の記事:【Ansible編】第4回 ※近日公開予定


📋 目次

  1. 前回の振り返り(第2回)
  2. 逆引き辞典との連動
  3. はじめに(この記事のスタンス)
  4. 問題定義:なぜ2回目以降の「changed」が問題なのか」が問題なのか
  5. 仕組み:Ansibleが「変更」を判断する仕組み
  6. 理由:なぜshellモジュールは毎回changedになるのか
  7. 設計:changed_whenによるタスクの制御
  8. 本質:真の冪等性(Idempotency)の設計
  9. まとめ
  10. 次回予告
  11. 連載一覧:Ansibleが理解できない理由はLinuxにあった【Ansible編】

※本記事の位置とシリーズ全体の関係を先に確認します。


:map: シリーズ全体構造(学習 × 問題解決)


本シリーズは
「理解(各記事)」と「問題解決(逆引き辞典)」を組み合わせて
スキルを身につける構成になっています。

この図は、どこから学び、どこに進めばよいかを示した“ロードマップ”です。


📍 現在の位置


現在はこの図の「Ansible編の第3回」になります。


1. 前回の振り返り(第2回)

前回は、専用モジュールがPythonスクリプト(Ansiballz)として実行されることで、Shellの「再解釈」を物理的にバイパスし、データを安全に届ける構造を確認しました。
「命令」から「状態」へのシフトこそが、Ansibleにおける安全性の根幹です。


↑ 目次に戻る


2. 逆引き辞典との連動

「設計上の原因」を特定したい場合は、以下の逆引き辞典を活用してください。

Ansibleが理解できない理由はLinuxにあった【Ansible編】:トラブル逆引き辞典(設計パターン版)※近日公開予定

  • まず「逆引き辞典」で該当ステップを特定する
  • 次に「本編(各連載記事)」で仕組みを理解する

↑ 目次に戻る


3. はじめに(この記事のスタンス)

shell モジュールでシェルスクリプトをそのままAnsibleに移植したタスクでは、2回目以降の実行でも毎回タスクが changed と判定され、RECAPの changed カウントが増え続けるケースがあります。エラーにはならないため「動いているから問題ない」と見過ごしがちですが、構成管理の観点では設計上の問題です。

この場合、実行のたびにシステムへの上書き処理が走り、Ansibleの「冪等性(べきとうせい)」が機能していません。規模が大きくなると、予期せぬサービス停止につながるリスクもあります。

本記事では、なぜ shell モジュールで毎回 changed が発生するのか、その仕組みを解説します。あわせて、専用モジュールで代替できない場合に changed_when を使って確実に changed=0 で終わらせるPlaybookの設計方法を整理します。


↑ 目次に戻る


4. 問題定義:なぜ2回目以降の「changed」が問題なのか

第2回で確認した通り、適切なモジュール(lineinfile など)を使っていれば、2回目以降の実行では changed=0 と表示されます。これがAnsibleの本来の挙動です。

shell モジュールを不適切に使うと、変更がないはずの実行でも毎回 changed がカウントされます。この状態が続くと、運用上で以下の問題が起きます。

  • 不要なハンドラー(handlers)の起動
    notifyhandlers でサービス再起動を連携させている場合、changed が毎回返るせいで、設定が変わっていないにもかかわらず本番サービスが再起動(瞬断)し続けます。
  • 実行時間の増大
    スキップできるはずの処理を毎回実行するため、全体の実行時間が長くなります。
  • 予期せぬ変化の見落とし
    常に changed が出る状態に慣れてしまうと、本当に予期せぬ変化(手動による設定変更など)が起きても気づけなくなります。

目指すべき実行結果(RECAP)の基準は以下の通りです。

  • 1回目の実行: changed=1(設定を適用するため)
  • 2回目以降の実行: changed=0ok、または when 条件でスキップされた場合は skipped

↑ 目次に戻る


5. 仕組み:Ansibleが「変更」を判断する仕組み

専用モジュール(copy, template, yum など)は、リモートサーバーの状態を以下の流れで判定しています。

[専用モジュールの実行フロー]
      ↓
1. 現在の状態を調査 (Gather Facts / Current State)
   (例: ファイルのハッシュ値チェック、パッケージの導入有無)
      ↓
2. 定義された「あるべき状態」と比較
      ↓
3. 差分がある場合のみ、変更を実行
      ↓
4. 実行成功後、Ansibleエンジンに "changed: true" を返却

この「調査 → 比較 → 実行」というステップがモジュール内部に実装されているため、変更がない2回目以降の実行では処理がスキップされ、changed=0 を実現できます。


↑ 目次に戻る


6. 理由:なぜshellモジュールは毎回changedになるのか

shell モジュールや command モジュールを実行すると、2回目以降でも毎回 changed がカウントされます。

Ansibleは、任意のコマンド文字列(例: /usr/bin/some_script.sh)がシステムにどのような変化を与えたかを自動的に判断できません。ファイルが書き換わったのか、単に文字列が出力されただけなのかを、モジュール側では区別できないためです。

そのため、shellcommand モジュールは対策を講じない限り、「コマンドが実行された時点で一律に changed と判定する」挙動をとります。これが、無駄な changed が発生する直接の原因です。


↑ 目次に戻る


7. 設計:changed_whenによるタスクの制御

専用モジュールで代替できない場合に shellcommand モジュールを使う際は、changed_when を明示して無駄な changed を防ぐ必要があります。

パターン1:システムを変更しないコマンド(常に changed=0 にする)

設定ファイルの構文チェックや情報の取得といった「調査」目的のコマンドは、システムの状態を書き換えません。このようなタスクは、常に changed=0 と判定させるのが適切です。

🧪 実際の環境における検証(検証1:changed_whenによる制御)

Nginxの構文チェックコマンドを実行し、2回目以降も changed=0 が維持されるかを検証します。

ファイル名: check_nginx_syntax.yml

---
- name: Nginx構文チェックのchanged_when検証
  hosts: all
  gather_facts: false
  become: true # 一般ユーザーでは設定ファイルが読めないため必須

  tasks:
    - name: Nginx設定ファイルの構文チェックを実行
      ansible.builtin.command: nginx -t
      register: nginx_check
      changed_when: false # これがあるため、コマンドが成功してもchanged=0(ok)になる

    - name: 実行結果(デバッグ出力)の確認
      ansible.builtin.debug:
        msg:
          - "STDOUT: {{ nginx_check.stdout }}"
          - "STDERR: {{ nginx_check.stderr }}"

【コントローラーノード側】

実行コマンド

ansible-playbook -i inventory.ini check_nginx_syntax.yml

▼ 1回目の実行結果

PLAY [Nginx構文チェックのchanged_when検証] **************************************************************************************************************************************************

TASK [Nginx設定ファイルの構文チェックを実行] ************************************************************************************************************************************************
ok: [192.168.1.21]

TASK [実行結果(デバッグ出力)の確認] *******************************************************************************************************************************************************
ok: [192.168.1.21] => {
    "msg": [
        "STDOUT: ",
        "STDERR: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /etc/nginx/nginx.conf test is successful"
    ]
}

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

▼ 2回目の実行結果

PLAY [Nginx構文チェックのchanged_when検証] **************************************************************************************************************************************************

TASK [Nginx設定ファイルの構文チェックを実行] ************************************************************************************************************************************************
ok: [192.168.1.21]

TASK [実行結果(デバッグ出力)の確認] *******************************************************************************************************************************************************
ok: [192.168.1.21] => {
    "msg": [
        "STDOUT: ",
        "STDERR: nginx: the configuration file /etc/nginx/nginx.conf syntax is ok\nnginx: configuration file /etc/nginx/nginx.conf test is successful"
    ]
}

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

このように、changed_when: false を明示することで、何回実行しても無駄な changed を発生させず、changed=0 の状態を維持できるようになります。

パターン2:実行結果の出力(stdout)に基づいて判定する

専用モジュールが存在しない独自のインストールスクリプトなどを実行する場合、スクリプトが出力する文字列(stdout)を監視して、実際に変更があったときだけ changed をカウントさせます。

実務でよくある「社内共通ツールが未導入の場合のみ、インストールを実行する」というケースを例に、検証してみましょう。

🧪 実際の環境における検証(検証2:stdoutによる制御)

読者がどのLinux環境でも安全かつ確実に再現できるよう、実際のインストールコマンド(curlbash)の代わりに、ファイルを作成するダミー処理(touch)を用いてロジックの動きを検証します。

ファイル名: install_custom_tool.yml

---
- name: 独自スクリプトのchanged_when検証
  hosts: all
  gather_facts: false

  tasks:
    - name: 独自ツールのインストール(未導入時のみ実行)
      ansible.builtin.shell: |
        # 1. ツールが導入済みか(ダミーファイルがあるか)チェック
        if [ ! -f /tmp/mytool_installed.txt ]; then
          
          # --------------------------------------------------
          # 【実務では】ここに実際のインストールコマンドを記述します
          # 例: curl -sS https://internal.example.com/install.sh | bash
          # --------------------------------------------------
          touch /tmp/mytool_installed.txt # 今回は検証用のダミー処理です
          
          # 2. インストールが実行された目印として文字列を出力
          echo "INSTALLED"
        fi
      register: install_result
      # "INSTALLED" という文字列が出力された時(1回目)だけ changed にする
      # 既に導入済みの2回目以降は何も出力されないため、changed=0(ok)になる
      changed_when: "'INSTALLED' in install_result.stdout"

    - name: 実行結果(デバッグ出力)の確認
      ansible.builtin.debug:
        msg:
          - "STDOUT: {{ install_result.stdout }}"
          - "CHANGED: {{ install_result.changed }}"

【コントローラーノード側】

実行コマンド

ansible-playbook -i inventory.ini install_custom_tool.yml

▼ 1回目の実行結果(ターゲット側にファイルがない状態)


PLAY [独自スクリプトのchanged_when検証] *****************************************************************************************************************************************************

TASK [独自ツールのインストール(未導入時のみ実行)] *****************************************************************************************************************************************
changed: [192.168.1.21]

TASK [実行結果(デバッグ出力)の確認] *******************************************************************************************************************************************************
ok: [192.168.1.21] => {
    "msg": [
        "STDOUT: INSTALLED",
        "CHANGED: True"
    ]
}

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

シェルスクリプト内の条件分岐が成立し、INSTALLED が出力されたため、Ansible側でも正しく CHANGED: True(changed=1) と判定されています。

▼ 2回目の実行結果(すでにファイルが存在する状態)


PLAY [独自スクリプトのchanged_when検証] *****************************************************************************************************************************************************

TASK [独自ツールのインストール(未導入時のみ実行)] *****************************************************************************************************************************************
ok: [192.168.1.21]

TASK [実行結果(デバッグ出力)の確認] *******************************************************************************************************************************************************
ok: [192.168.1.21] => {
    "msg": [
        "STDOUT: ",
        "CHANGED: False"
    ]
}

PLAY RECAP **********************************************************************************************************************************************************************************
192.168.1.21               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

2回目はすでにファイルがあるためシェル内の処理がスキップされ、何も出力されません。changed_when の条件を満たさないため、無駄な更新を発生させずに CHANGED: False(changed=0) で安全に終了します。

set -e(errexit)をスクリプト冒頭に指定している場合、条件不成立ルートで意図せずスクリプトが終了することがあります。changed_when と組み合わせて使う際は戻り値の挙動に注意してください。


↑ 目次に戻る


8. 本質:真の冪等性(Idempotency)の設計

Ansible公式ドキュメントでは、冪等性を以下のように定義しています。

"An operation is idempotent if the result of performing it once is exactly the same as the result of performing it repeatedly without any intervening actions."
(一度実行した結果と、その後何度繰り返しても得られる結果が同一であること)

あるべき状態がすでに実現されている場合、何度Playbookを実行してもシステムの状態は変わらず、2回目以降は各タスクが ok となり、RECAPの changed=0 で完了します。これがAnsibleにおける冪等性が機能している状態です。

なお、when 条件によってタスク自体が実行されなかった場合は skipped と表示されます。これは冪等性とは別の制御(条件分岐)によるものですが、実務のPlaybookでは okskipped が混在する実行結果になることが多いです。

冪等性を実現するためのタスク設計の方針は、主に以下の3つです。

  1. 専用モジュールの利用
    状態比較が内部に実装されているモジュールを優先する。
  2. creates / removes の活用
    command モジュールなどで、条件に応じて処理自体をスキップさせる。
  3. changed_when の明示
    shellcommand モジュールを使う場合に、変更の有無をロジックで明示的に定義する。

🔍 裏付けとなる公式ドキュメント


↑ 目次に戻る


9. まとめ

  • changed が常態化すると何が起きるか
    毎回 changed になるPlaybookでは、本当にシステムに変更を加えたのか、スクリプトが走っただけなのかの区別がつかなくなります。設計段階から意識しておくべき点です。
  • 専用モジュールを優先する理由
    専用モジュールは「現状調査とあるべき状態の比較」を内部で行うため、冪等性が自動的に確保されます。shell モジュールを使う前に、専用モジュールで代替できないかを確認してください。
  • shell モジュールを使う場合は changed_when を必ず書く
    スクリプトの実行が避けられない場合、Ansibleは変更の有無を自動判定できません。戻り値や標準出力を使って「変わったかどうか」を明示的に定義してください。

↑ 目次に戻る


10. 次回予告

本記事では、専用モジュールで代替できない場合に shell モジュールと changed_when を組み合わせて、Ansibleの「状態の変化」を正しく管理する方法を解説しました。

ただ、状態を正確に判定できても、次に課題となるのが「条件分岐」です。Shellスクリプトで扱った「終了ステータス」や「条件式」が、Ansibleの when 条件ではどう評価されるのか、次回はその対応関係を見ていきます。

次回:【Ansible編】第4回:なぜ条件分岐が壊れるのか(Ansible編)※近日公開予定


↑ 目次に戻る


📑 連載の移動
前の記事:【Ansible編】第2回 | 次の記事:【Ansible編】第4回 ※近日公開予定


📑 【Ansible編】全体のまとめはこちら
Ansibleが理解できない理由はLinuxにあった【Ansible編】まとめ ※近日公開予定


🗺️ 初めての方・シリーズの全体像を知りたい方はこちら

本記事は、OSの仕組みからAnsible設計までを繋ぐ連載シリーズの一部です。
「どこから読み始めればいいか」あるいは、「OS/Shell/Ansible編の関係性」 について把握されたい場合は、以下の統合ガイドで整理しています。

Ansibleが理解できない理由はLinuxにあった|統合ガイド


📚11. 連載一覧:Ansibleが理解できない理由はLinuxにあった【Ansible編】

回数とタイトル 内容(概要)
【Ansible編】第0回:なぜShellの知識がないとAnsibleは壊れるのか AnsibleはShellのラッパーとして動作している。Shell編で学んだ構造(Ansible → SSH → Shell → Linux)を再確認し、「なぜmoduleを使うべきなのか」の前提を整理する。
【Ansible編】第1回:なぜshellモジュールは危険なのか shellモジュールはShellの仕様(分割・展開・環境)に依存するため壊れやすい。commandとの違いを理解し、「どこまでShellを許容すべきか」を判断できるようになる。
【Ansible編】第2回:なぜmoduleを使うと安全になるのか file / copy / lineinfile などのmoduleは状態管理を前提としている。「結果」ではなく「状態」を扱うことで、Shell依存を排除し安全な構成を実現する。
【Ansible編】第3回:なぜ毎回changedになるのか shellモジュールは状態を判定できないため、常にchangedになる。changed_whenを使った制御と冪等性(idempotency)の設計を理解する。
【Ansible編】第4回:なぜ条件分岐が壊れるのか(Ansible編) when条件が意図通りに動かない原因は、rc / stdout / stderrの扱いにある。Shell編で学んだ終了ステータスと組み合わせ、正しい条件分岐を設計する。
【Ansible編】第5回:なぜ変数が意図通りに扱えないのか vars / host_vars / group_vars のスコープや、registerの扱いを誤ると値が壊れる。Ansible内での変数管理と評価タイミングを整理する。
【Ansible編】第6回:なぜタスクの順序で壊れるのか Ansibleのタスクはそれぞれ独立したプロセスとして実行される。chdir / environment の制御と、「前の結果に依存する設計」の危険性を理解する。
【Ansible編】第7回:なぜエラー制御が破綻するのか ignore_errors や failed_when の使い方を誤るとエラーが隠蔽または暴発する。Shellのrcと連動させた正しいエラー制御を設計する。
【Ansible編】第8回:なぜ非同期・待機で失敗するのか サービス起動や外部依存処理はタイミング問題を引き起こす。async / poll / wait_for / retries を使い、安定した実行制御を実現する。
【Ansible編】第9回:なぜPlaybookが読めないのか role構成やタスク分割が不適切だと可読性が崩壊する。実務で保守できるPlaybook設計と構造化の原則を理解する。
【Ansible編】第10回:なぜAnsibleを正しく設計できるようになるのか これまでの知識を統合し、Shell編と接続することで「壊れない設計」ができるようになる。Ansibleを実務で使いこなすための設計思想を完成させる。

↑ 目次に戻る


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?