AnsibleでPythonカスタムモジュールを開発する方法|べき等性を持つ独自操作をroleに組み込む設計入門

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOME > Linux技術 リナックスマスター.JP(Linuxマスター.JP) > Ansible > AnsibleでPythonカスタムモジュールを開発する方法|べき等性を持つ独自操作をroleに組み込む設計入門
「標準モジュールでは書けない処理があって、shellモジュールに changed_when を何重にも書いているけど、これで本当にいいのか...」

こういう状況になったことはありませんか。shellモジュールは確かに何でもできますが、べき等性を確保するには「現在の状態を毎回調べて変化があったかどうかを判定するロジック」を Playbook 側に書き続ける必要があります。そのロジックが複雑になるほど、Playbook は読みにくくなり、テストも難しくなります。

Ansible には、その問題を解決する仕組みがあります。Python でカスタムモジュールを作ることで、べき等性の判定ロジックをモジュール側に隠蔽し、Playbook をシンプルに保つことができます。

この記事では、Ansibleカスタムモジュールの開発方法を基礎から解説します。AnsibleModuleクラスの仕組み・changed/failed判定の設計・roleへの組み込みまで、RHEL 9.4 / Ubuntu 24.04 LTSで実際に動かしたコードを使って説明します。

この記事のポイント

・AnsibleModuleクラスを使えばJSON入出力を抽象化してべき等性のある独自操作を設計できる
・changed=Trueを返す条件を明示することでdry-run(--check)が正しく動くモジュールになる
・library/ディレクトリへの配置だけでroleから透過的に呼び出せる
・shellモジュールのchanged_when地獄を卒業する設計方針が得られる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜshellモジュールでなくカスタムモジュールが必要なのか

Ansible の shellモジュールは「何でもできる」という意味では最強ですが、べき等性を担保するのが難しいという欠点があります。たとえば「/opt/myapp/conf.d/ 以下にあるすべての .conf ファイルに特定の設定行を追記する」という操作を考えてみてください。

shell モジュールで書こうとすると、次のようになります。

- name: confファイルに設定行を追記(shell版) shell: | for f in /opt/myapp/conf.d/*.conf; do grep -q "timeout=30" "$f" || echo "timeout=30" >> "$f" done changed_when: ??? register: result

changed_when に何を書けばいいかが不明確です。シェルの戻り値だけでは「変更が実際に起きたかどうか」が分かりません。「何件追記したか」を数えて出力する工夫をしても、Playbook 側のロジックが膨らみ続けます。

カスタムモジュールを使うと、この問題が解決します。モジュール側で「変更前後の状態比較 → changed フラグの設定」を完結させるので、Playbook はシンプルな呼び出しだけになります。

- name: confファイルに設定行を追記(カスタムモジュール版) my_conf_line: path: /opt/myapp/conf.d/ line: "timeout=30" state: present

タスクの意味が一目で分かり、changed=True/False の判定はモジュール内で正しく処理されます。

Ansibleモジュールのアーキテクチャ — JSON入出力の仕組み

カスタムモジュールを作る前に、Ansible がモジュールをどのように実行しているかを理解しておきましょう。

Ansible がモジュールを呼び出す流れは以下のとおりです。

・Ansible コントロールノードが Python スクリプト(モジュール)をターゲットホストへ転送する
・ターゲットホスト上で python3 モジュール名.py が実行される
・モジュールは標準出力に JSON 文字列を出力して終了する
・Ansible がその JSON を解析して changed/failed/msg 等のフィールドを取り出す

つまり、Ansible モジュールの正体は「JSON を stdout に出力する Python スクリプト」です。最低限の実装をすると次のようになります。

#!/usr/bin/python3 import json, sys result = {"changed": False, "msg": "nothing to do"} print(json.dumps(result)) sys.exit(0)

もちろん、引数の受け取り・エラー処理・check_mode 対応などを自力で実装するのは大変です。そのために AnsibleModule クラスが用意されています。

AnsibleModuleクラスで最小モジュールを作る

AnsibleModule クラスは Ansible 付属のユーティリティで、引数の定義・検証・JSON 出力・check_mode 対応などを自動で処理してくれます。実務では、ほぼすべてのカスタムモジュールがこのクラスを使います。

1. モジュールの配置場所

最も簡単な配置方法は、Playbook または role の library/ ディレクトリに Python ファイルを置くことです。

# Playbook と同じディレクトリに置く場合 . ├── site.yml └── library/ └── my_conf_line.py # role に同梱する場合 roles/ └── myapp/ ├── library/ │ └── my_conf_line.py └── tasks/ └── main.yml

library/ ディレクトリはデフォルトのモジュール検索パスに含まれるため、ansible.cfg に追加設定なしで Playbook から呼び出せます。

2. AnsibleModuleクラスを使った基本構造

ファイル名と同じ名前がモジュール名になります。ここでは my_conf_line.py というファイルを作ります。

#!/usr/bin/python3 # -*- coding: utf-8 -*- from ansible.module_utils.basic import AnsibleModule import os def main(): # ① 引数の定義 module_args = dict( path=dict(type='str', required=True), line=dict(type='str', required=True), state=dict(type='str', default='present', choices=['present', 'absent']), ) # ② AnsibleModuleの初期化 module = AnsibleModule( argument_spec=module_args, supports_check_mode=True, # --checkフラグを有効にする ) path = module.params['path'] line = module.params['line'] state = module.params['state'] # ③ ファイルが存在しなければエラー if not os.path.exists(path): module.fail_json(msg=f"ファイルが見つかりません: {path}") # ④ 現在の状態を取得 with open(path, 'r') as f: lines = f.readlines() line_exists = any(line.rstrip('\n') == l.rstrip('\n') for l in lines) # ⑤ 変更が不要なら即 exit if state == 'present' and line_exists: module.exit_json(changed=False, msg="行はすでに存在します") if state == 'absent' and not line_exists: module.exit_json(changed=False, msg="行は存在しません(対応不要)") # ⑥ check_mode では実際の変更をスキップ if module.check_mode: module.exit_json(changed=True, msg="[check] 変更が予定されています") # ⑦ 実際の変更処理 if state == 'present': with open(path, 'a') as f: f.write(line + '\n') module.exit_json(changed=True, msg=f"行を追記しました: {line}") else: new_lines = [l for l in lines if l.rstrip('\n') != line] with open(path, 'w') as f: f.writelines(new_lines) module.exit_json(changed=True, msg=f"行を削除しました: {line}") if __name__ == '__main__': main()

コードのポイントを説明します。

・argument_spec で引数の型・デフォルト値・選択肢を定義する。AnsibleModule が自動でバリデーションを行う
・supports_check_mode=True を指定すると module.check_mode が True になる(--check 実行時)
・module.fail_json() はエラー終了。msg が Ansible に伝わり、タスクが FAILED になる
・module.exit_json() は正常終了。changed= の値が Ansible の changed カウントに反映される

3. Playbookから呼び出して実行してみる

# site.yml --- - hosts: webservers tasks: - name: タイムアウト設定を追記 my_conf_line: path: /etc/myapp/app.conf line: "timeout=30" state: present

初回実行時(行が存在しない状態):

$ ansible-playbook -i hosts site.yml PLAY [webservers] ********************** TASK [タイムアウト設定を追記] ***** changed: [web01.example.internal] PLAY RECAP *************************** web01.example.internal : ok=1 changed=1 unreachable=0 failed=0

2回目以降(行がすでに存在する状態):

$ ansible-playbook -i hosts site.yml TASK [タイムアウト設定を追記] ***** ok: [web01.example.internal] PLAY RECAP *************************** web01.example.internal : ok=1 changed=0 unreachable=0 failed=0

2回目は changed=0 になっています。べき等性が正しく機能しています。

べき等性の実装パターン — changed判定の設計

カスタムモジュールで最も重要なのは、「いつ changed=True を返すか」の設計です。ここを誤ると、毎回 changed になる(実際は変化していないのに)か、変更しても changed=False になる(Playbook の通知が機能しない)かのどちらかになります。

1. 「現在の状態 → 目標状態」の比較モデルを作る

べき等性の基本は「現在の状態と目標状態を比較し、差分があれば変更する」という思考モデルです。

・現在の状態: ファイルの内容・プロセスの起動状態・設定値など、実際にシステムから取得するもの
・目標の状態: Playbook でパラメーターとして渡す「こうなってほしい状態」
・差分チェック: 現在 ≠ 目標 のとき changed=True で変更する

この 3ステップを常に意識することで、べき等なモジュールが自然と書けます。

2. check_modeへの対応が運用品質を決める

supports_check_mode=True を指定し、module.check_mode が True のときは実際の変更処理(ファイル書き込み・コマンド実行)を スキップして changed=True だけを返す実装が必要です。

check_mode に対応していないモジュールは ansible-playbook --check を実行した際に WARNING: This module does not support check mode が表示され、実際の変更が走ってしまいます。本番前のドライラン確認が機能しなくなるため、必ず対応してください。

より実践的なAnsible設計パターンを体系的に学びたい方は、Ansible構成管理入門コースも参考にしてください。ハンズオン形式でrole設計からカスタムモジュールまで扱っています。

カスタムモジュールをroleに組み込む

カスタムモジュールは role の library/ ディレクトリに置くことで、その role を利用するすべての Playbook から参照できるようになります。

1. roleのディレクトリ構成

roles/ └── myapp_config/ ├── library/ │ └── my_conf_line.py # カスタムモジュール ├── tasks/ │ └── main.yml ├── defaults/ │ └── main.yml └── meta/ └── main.yml

2. roleのtasks/main.ymlから呼び出す

# roles/myapp_config/tasks/main.yml --- - name: タイムアウト設定を反映 my_conf_line: path: "{{ myapp_conf_path }}" line: "timeout={{ myapp_timeout }}" state: present notify: restart myapp - name: 古い設定行を削除 my_conf_line: path: "{{ myapp_conf_path }}" line: "old_setting=deprecated" state: absent

# roles/myapp_config/defaults/main.yml --- myapp_conf_path: /etc/myapp/app.conf myapp_timeout: 30

カスタムモジュールがべき等性を持っているので、handler の notify も意図通り動きます。changed=True のときだけ restart myapp が発動し、不必要な再起動が起きません。

3. 複数のroleで共用する場合

複数の role で同一のカスタムモジュールを使いたい場合は、Playbook ルートの library/ に置くか、ansible.cfg の library パラメーターで共通ディレクトリを指定します。

# ansible.cfg [defaults] library = ./library:~/.ansible/plugins/modules

デバッグとテストの方法

カスタムモジュールの動作確認には 2つのアプローチがあります。

1. Playbookから-vvvオプションで実行ログを確認する

ansible-playbook -vvv を使うと、モジュールへの引数・返却された JSON・転送されたスクリプト内容が表示されます。

$ ansible-playbook -i hosts site.yml -vvv ... TASK [タイムアウト設定を追記] ***** task path: /home/ansible/site.yml:4 ... {"path": "/etc/myapp/app.conf", "line": "timeout=30", "state": "present"} ... {"changed": true, "msg": "行を追記しました: timeout=30", ...}

2. ローカルで直接引数ファイルを渡してテストする

開発中は Playbook を使わず、引数を JSON ファイルに書いて直接 Python を実行できます。

# args.json(テスト用引数ファイル) {"ANSIBLE_MODULE_ARGS": { "path": "/tmp/test.conf", "line": "timeout=30", "state": "present" }} # 実行 $ python3 library/my_conf_line.py args.json {"changed": true, "msg": "行を追記しました: timeout=30", "invocation": {"module_args": {...}}}

ターゲットホストに転送せずにローカルで動かせるため、開発サイクルが大幅に短縮されます。ansible.builtins に依存しているため、実行環境に ansible-core がインストールされていれば動作します。

3. Moleculeでrole単体テストに組み込む

カスタムモジュールを含む role は、Molecule を使って role 全体としての結合テストが可能です。molecule test を実行することで、Docker コンテナ上で Playbook を実行し、べき等性(2回目の実行で changed=0 になるか)を自動検証できます。

# molecule/default/verify.yml(べき等性検証の例) --- - name: Verify idempotency hosts: all tasks: - name: 2回目の実行(changed=0 になるはず) my_conf_line: path: /etc/myapp/app.conf line: "timeout=30" state: present register: result - name: changedがFalseであることを確認 assert: that: - result.changed == false fail_msg: "べき等性が壊れています"

よくあるエラーと対処法

カスタムモジュール開発でつまずきやすいポイントと対処法をまとめます。

「ModuleNotFoundError: No module named 'ansible'」が出る

ローカルテスト(python3 モジュール.py args.json)時に、ターゲット環境に ansible-core がインストールされていない場合に発生します。

・対処: pip3 install ansible-core を実行してから再試行してください
・注意: ターゲットホスト上での実行はAnsibleが管理するため問題ありません。このエラーはローカルテスト時のみ発生します

毎回 changed=True になってしまう

変更前後の状態比較が正しくできていないケースです。特に文字列の末尾改行(\n)の扱いを見落としがちです。

・よくある原因: ファイルから読んだ行と比較文字列の末尾 \n が一致していない
・対処: 比較前に .rstrip('\n') や .strip() で正規化してから比較する

# NG: 末尾\nが食い違って毎回 changed=True になる line_exists = line in lines # OK: 正規化してから比較する line_exists = any(line.rstrip('\n') == l.rstrip('\n') for l in lines)

--checkで「This module does not support check mode」が出る

supports_check_mode=True を AnsibleModule の初期化に指定していない場合に発生します。

・対処: AnsibleModule(argument_spec=..., supports_check_mode=True) に修正する
・警告: check_mode に対応していないモジュールは本番前のドライランが機能せず、意図しない変更が本番に入るリスクがあります。必ず対応してください

モジュールがターゲットホストで見つからない

role に組み込んだモジュールが「No module named ...」ではなく「ERROR! couldn't resolve module/action」と出る場合は配置場所を確認します。

・role の library/ 以外の場所に置いた可能性がある
・ansible.cfg の library パスが通っていない
・Ansible Galaxy からインストールした role は ~/.ansible/roles/ 配下に展開されるため、library/ の参照パスが変わる場合がある

本記事のまとめ

Ansible カスタムモジュールの開発ポイントをまとめます。
やりたいこと 実装方法
引数を定義して自動バリデーション AnsibleModule(argument_spec=...)
変更ありとして終了 module.exit_json(changed=True, msg=...)
変更なしとして終了 module.exit_json(changed=False, msg=...)
エラーとして終了 module.fail_json(msg=...)
--checkフラグへの対応 supports_check_mode=True + if module.check_mode: exit
roleに同梱する roles/role名/library/モジュール名.py
複数roleで共用する Playbookルートの library/ または ansible.cfg の library パラメーター
ローカル単体テスト JSON 引数ファイルを渡して python3 モジュール.py args.json
カスタムモジュールを作る最大のメリットは、Playbook の可読性と保守性を高めながら、べき等性を確実に担保できる点です。shellモジュールに changed_when を書き続けるのではなく、操作の意味を抽象化したモジュールとして切り出すことで、role の再利用性も格段に上がります。

標準モジュールでは対応できない処理が出てきたとき、まずカスタムモジュール化を検討してみてください。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、
Ansibleを使ったサーバー自動化の設計パターンを体系的に学べる講座を用意しています。role設計・カスタムモジュール・inventory設計まで、実機を使ったハンズオンで習得できます。
>> Ansible構成管理入門を見てみる

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。