以下の内容はhttps://usage-automate.hatenablog.com/entry/2025/12/16/085853より取得しました。


Ansibleにプルリクチャレンジをして得た知見

この記事はAnsible Advent Calendar 2025 16日目の記事です。

qiita.com

はじめに

この記事は日頃Ansibleを利用しているしがない男がプルリクエストというちょっと高いハードルに挑戦した時(挑戦中)の記録です。
同じように「いつかOSSに貢献してみたい」と思っている方の背中を押せれば幸いです。

得たこと

今回実施したプルリクエストはまだマージされておらず、まだ対応を続けている最中です。
そんな中でも今回の挑戦で以下のような発見がありました。
→無事にマージされました

  1. ansible-coreのテスト方法の学び
    今回改修したcallback_plugindefaultはゴールデンテスト方式でテストを実施していました。
    github.com
    実行結果と一致する標準出力と標準エラー(ゴールデンファイル)を用意し、ansible-playbookの実行と結果の突合をrunme.shというシェルスクリプトで実行していました。
    ansible-playbookを実行するテストをどのようにしようかと悩むこともありましたが、想定の出力とエラーを用意して比較すればテストできる、という発想を持てるようになりました。

  2. doc_fragmentすごい ドキュメントの共通部分を断片(fragment)として定義し、各プラグインで使い回せる仕組みになっていました。メンテナンス性が考えられていて感動しました。
    ansible/lib/ansible/plugins/doc_fragments/default_callback.py at devel · ansible/ansible · GitHub

  3. メンテナーは忙しい、ほどほどに待とう
    勇気を出してPRを送っても、すぐに反応があるとは限りません。
    世界中のユーザーがIssueを立て、PRを送ってくる中で、メンテナーの方々は限られた時間で対応しています。

きっかけ

  1. 改修のチャンス到来

    以前から「いつかOSS活動に参加してみたいなぁ」とぼんやり考え、チャンスを虎視眈々と狙っていました。
    そんなある日、Ansibleでinclude_roleを使用する場面があったのですが、実行ログを見ていてあることに気づきます。
    includedの表示が多い.......」
    skipを非表示にする機能あったけどincludedの表示って消せないのかな。
    (↑実際のログ。このようにincludedという表示が出る。)
    調べてみるとincluded ログは非表示にする設定がないことがわかりました。
    ※この表示はdefaultstdout callbackが出しているログなので、別のstdout callback(yaml など)を使っている場合は前提が変わります。

  2. Issueを探そう

    同じ悩み抱えている人いないかなぁとIssueを検索してみると……ドンピシャのIssueがありました。
    github.com
    このIssueの中では、消したいならカスタムコールバックプラグインを作ればいいんじゃないかと会話をしているようでした。

  3. 修正箇所のあたりをつけよう

    callback_pluginのdefaultが関係していそうだとあたりがついていたので、
    思い切ってIssueに「こんな風に修正すれば実現できそうじゃないですか?」とコメントを書き込んでみました。
    いきなりPRをしていいものかとも思ったので、よさそうだったらPR出すよと添えて。

  4. 見知らぬ人からの「PR出せばええんやない?」

    このコメントには「ここで話してないでPR出せばいいんじゃない?」といったニュアンスのメッセージが付き、よし、思い切って出してみるかと挑戦をしてみました。

ここからは実際に行ったことを紹介します

開発

  1. Forkする

    まずは開発方法を調べました。
    ansible-coreの実装は ansible/ansible リポジトリで開発されています。
    Ansibleには開発者向けのDeveloper Guideがあるので読んでみましたが情報量が多すぎてわかりませんでした。
    なのでとりあえずほかの人のPRを見てみました。

    マージ済みのPRを何個か見てみるとForkした個人リポジトリで開発をしてプルリクエストをしているようでした。

    では早速Forkをぽちっとして準備を開始しました。

  2. 開発を整えよう 自分のアカウントにForkされたAnsibleのリポジトリができたことを確認し、 開発環境を整えることにしました。

    今回はWSLのAlmalinuxとuvを使って開発しています。
    WSLの起動とuvのインストールは割愛します。
    ForkしたAnsibleリポジトリをgit cloneで手元に持ってきます。
    uvを使っているので、pyproject.tomlに定義された依存関係をuv syncで同期します。

     cd ansible
     uv sync
     source ./hacking/env-setup
    

    ansibleコマンドで起動するときにディレクトリ内のソースを使えるようにする場合はhacking/env-setupを実行します。
    uv syncuv.lockが生成される場合があります。Ansible本体はuv.lockを管理していないため、PRには含めないよう(ローカルでignoreする等)注意します。

  3. 開発をする
    変更したのは下記2ファイル

    1. lib/ansible/plugins/callback/default.py
      callback_pluginのdefaultのソースファイルです。
      CallbackModuleの中にもともとあったv2_playbook_on_includeをオプションに応じてメッセージを出さずに帰すように変更しました。

       def v2_playbook_on_include(self, included_file):
           if not self.get_option("display_included_hosts"):  # ここと
               return  # ここ
      
           msg = 'included: %s for %s' % (included_file._filename, ", ".join([h.name for h in included_file._hosts]))
           label = self._get_item_label(included_file._vars)
           if label:
               msg += " => (item=%s)" % label
           self._display.display(msg, color=C.COLOR_INCLUDED)
      

      2行だけの追加でした。
      はじめは、オプションが存在するときにはメッセージを出して~といった処理にしていました。
      メンテナーの方に、「条件を反転させれば無駄インデント消せるよ」とコメントをもらって上記の形になりました。

    2. lib/ansible/plugins/doc_fragments/default_callback.py
      callback_pluginのdefaultのオプションをドキュメントとして定義してあるファイルです。
      ModuleDocFragmentクラスのDOCUMENTATIONdisplay_included_hostsのオプションを追加しました。
      デフォルト値はyesで表示するようにし、明示的にfalseにした場合に表示を消すようにしました。
      DOCUMENTATIONに文字列として定義したのに実際のオプションになる仕組みも気になっていますが、そこはあまり追えていません。
      素直に便利だなぁ。と感動しました。
      この定義が ansible-doc の表示にも反映されるのが便利でした。

テスト

開発を終えテストについて考えました。
前にOSSの開発をしたときはテストコードを書いてPRを投げました。
しかし、今回追加したオプションと類似のオプションをチェックするUnitテストがtestsに見当たりませんでした。
そんなものかなととりあえず追加した部分が手元で動いているかをチェックすることにしました。

  1. ansible-docで確認してみる
    今回追加したANSIBLE_DISPLAY_INCLUDED_HOSTSのオプションがdefaultのコールバックプラグインに追加されているかをansible-docコマンドで確認しました。

     ansible-doc -t callback ansible.builtin.default
    


    ちゃんと出てきましたね。

  2. 動作確認用Playbookを作る
    次はオプションが意図通りに動くかを確認します。
    とりあえずテスト用のPlaybookを作成してみました。ansibleのリポジトリ外に任意のリポジトリを作ってそこに配置しました。

    テスト用のプレイブックが、include_roleモジュールでロールを呼び出します。

     test_playbook
     ├── ansible.cfg
     ├── include_test.yml
     ├── README.md
     └── roles
         ├── role1
         │   └── tasks
         │       └── main.yml
         └── role2
             └── tasks
                  └── main.yml
    

    テスト用のプレイブック(include_test.yml)
    role1, role2, role1 の順で呼び出す

     ---
     - name: display_included_optionの動作確認
       hosts: localhost
       gather_facts: false
       tasks:
         - name: 呼び出すロールのセット
           ansible.builtin.set_fact:
             execute_role_list:
               - "role1"
               - "role2"
               - "role1"
         - name: ロールの呼び出し(include)
           ansible.builtin.include_role:
             name: "{{ role_name }}"
           loop: "{{ execute_role_list }}"
           loop_control:
             loop_var: role_name
    

    テスト用のロールその1(role1/tasks/main.yml)

     ---
     - name: role1
       ansible.builtin.debug:
         msg: "これはロール1です"
    

    テスト用のロールその2(role2/tasks/main.yml)

     ---
     - name: role2
       ansible.builtin.debug:
         msg: "これはロール2です"
    

    実行ログ、非表示
    ANSIBLE_DISPLAY_INCLUDED_HOSTSを0にして実行をします。
    変更通りにincludedから始まる行が表示されなくなっています。

     3e93:~/test_playbook $ ANSIBLE_DISPLAY_INCLUDED_HOSTS=0 ansible-playbook include_test.yml
     [WARNING]: You are running the development version of Ansible. You should only run Ansible from "devel" if you are modifying the Ansible engine, or trying out features under development. This is a rapidly changing source of code and can become unstable at any point.
     [WARNING]: No inventory was parsed, only implicit localhost is available
     [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
    
     PLAY [display_included_optionの動作確認] *****************************************************************************
    
     TASK [呼び出すロールのセット] ****************************************************************************************
     ok: [localhost]
    
     TASK [ロールの呼び出し(include)] *************************************************************************************
    
     TASK [role1 : role1] *************************************************************************************************
     ok: [localhost] => {
         "msg": "これはロール1です"
     }
    
     TASK [role2 : role2] *************************************************************************************************
     ok: [localhost] => {
         "msg": "これはロール2です"
     }
    
     TASK [role1 : role1] *************************************************************************************************
     ok: [localhost] => {
         "msg": "これはロール1です"
     }
    
     PLAY RECAP ***********************************************************************************************************
     localhost                  : ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    

    実行ログ、表示(デフォルトの挙動)
    ANSIBLE_DISPLAY_INCLUDED_HOSTSを1にして実行をします。
    included~~の行が表示されています。

     3e93:~/test_playbook $ ANSIBLE_DISPLAY_INCLUDED_HOSTS=1 ansible-playbook include_test.yml
     [WARNING]: You are running the development version of Ansible. You should only run Ansible from "devel" if you are modifying the Ansible engine, or trying out features under development. This is a rapidly changing source of code and can become unstable at any point.
     [WARNING]: No inventory was parsed, only implicit localhost is available
     [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
    
     PLAY [display_included_optionの動作確認] *****************************************************************************
    
     TASK [呼び出すロールのセット] ****************************************************************************************
     ok: [localhost]
    
     TASK [ロールの呼び出し(include)] *************************************************************************************
     included: role1 for localhost => (item=role1)
     included: role2 for localhost => (item=role2)
     included: role1 for localhost => (item=role1)
    
     TASK [role1 : role1] *************************************************************************************************
     ok: [localhost] => {
         "msg": "これはロール1です"
     }
    
     TASK [role2 : role2] *************************************************************************************************
     ok: [localhost] => {
         "msg": "これはロール2です"
     }
    
     TASK [role1 : role1] *************************************************************************************************
     ok: [localhost] => {
         "msg": "これはロール1です"
     }
    
     PLAY RECAP ***********************************************************************************************************
     localhost                  : ok=7    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    

    意図通りに動いたので、ここで一度目のPRを実施してみました。
    すぐにメンテナーがコメントをしてくれてソースとドキュメントの軽微な修正を行いました。

    この分だとすぐマージされちゃうのかも!とこの時はわくわくしていました。
    ちなみにansible.cfgで設定の有効/無効を制御する場合はこのような書き方になります。

    無効化(非表示)

     [defaults]
     display_included_hosts=false
    

    有効化(表示)

     [defaults]
     display_included_hosts=true
    
  3. テストコードを書こう
    数日待つとPRにコメントが付きました。
    「テストがないよ」と。
    ここで、手元で動作確認しただけじゃ足りないとわかりました。
    正直どこでテストしているかもわからず、unitテストでは改修した範囲に関連するテストも見当たらなかったのでそのままPRを出してしまっていました。
    メンテナーにどんなテストをしたらいいか質問したら、integrationテストでしていることを教えてもらいました。
    github.com

    callback_defaultではrunme.shが実行されてansible-playbookが呼び出され、 playbook実行時の標準出力(stdout)と標準エラー(stderr)を用意、完全に一致するように準備しテストする、ゴールデンテストをやっていることがわかりました。
    github.com
    とりあえず見様見真似で標準出力と標準エラーのファイルを追加してみました。
    もともとテストにはANSIBLE_DISPLAY_SKIPPED_HOSTSANSIBLE_DISPLAY_OK_HOSTSなど今回の追加したオプションと近い動きをするテストが書かれていたためあまり悩まずに実装ができました。
    ※ゴールデンテストは出力の完全一致が前提なので、環境差分で失敗しやすい点は注意です。

    integrationがすべてゴールデンテストをやっているわけではなさそうでしたのでそこは補足しておきます。

  4. テストの実行
    追加したテストを実行します。
    テストはansible-test integration callback_defaultで実行できます。 実行前に、hacking/env-setupの有効化を忘れずに実施します。 テストを実行すると、shellが呼び出され定義されたplaybookコマンドが順次実行されている様子がわかります。
    正常終了するとプロンプトが戻ってきます。
    ちなみに失敗すると途中で止まります。

    こんな感じ

    テストも無事に通ったので再度Push Github上のCIもPassして後は指摘かマージされるのを待つだけになりました。

そして....

まだマージされていません・・(笑)




以上の内容はhttps://usage-automate.hatenablog.com/entry/2025/12/16/085853より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14