Ansibleの実行結果の出力をデフォルト出力 + JSONファイルにしたい

## Ansibleの実行結果をJSONで扱いたい！　が、コンソール出力が微妙！

通常、Ansibleは実行結果を段階ごとに下記のようにコンソールに出力していきますが、

```console
$ ansible-playbook -i hosts playbook.yml

PLAY [all] ******************************************************************************************************

TASK [Gathering Facts] ******************************************************************************************
ok: [web_server]
ok: [db_server]

....

PLAY RECAP ******************************************************************************************************
db_server                  : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
web_server                 : ok=8    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```

この実行結果を他のツールで扱いたい場合、構造化されたデータ（JSONなど）が欲しくなってくると思います。（もしくは、このままコンソール出力を解析してもいいですが、かなり辛いです。）

そういった場合は　`ANSIBLE_STDOUT_CALLBACK=json` を環境変数として与えてあげればいいのですが、下記のような長いJSON出力がプレイブック実行の **最後にドバッと一気に出ます。**

```console
$ ANSIBLE_STDOUT_CALLBACK=json ansible-playbook -i hosts playbook.yml
{
    "custom_stats": {},
    "global_custom_stats": {},
    "plays": [
        {
            "play": {
                "duration": {
                    "end": "2022-05-26T04:22:39.631342Z",
                    "start": "2022-05-26T04:22:37.698472Z"
                },

... (4200行ののち)

    "stats": {
        "db_server": {
            "changed": 0,
            "failures": 0,
            "ignored": 0,
            "ok": 5,
            "rescued": 0,
            "skipped": 0,
            "unreachable": 0
        },
        "web_server": {
            "changed": 0,
            "failures": 0,
            "ignored": 0,
            "ok": 8,
            "rescued": 0,
            "skipped": 0,
            "unreachable": 0
        }
    }
}
# 上記が ansible-playbook が実行終了のタイミングでドバっと一気に出る
```

これだと今どこまで処理が進んでいるのかわかりませんね。あと単純に見づらいです。

かといって、JSONをファイルに出力して、標準出力の方はデフォルトの出力方法にする、といった便利な方法はありません。


## Ansibleの実行結果出力を自由にしたければ、自作callbackプラグインを作ればOK

ではどうすればいいかというと、**Ansibleのコンソール出力をカスタマイズする自作callbackプラグインを作ればOKです。**

AnsibleはPythonで出来ており、自分でPythonスクリプトを書くことでAnsibleに自由にプラグインを追加することができます。

そのプロジェクトに閉じたプラグイン(ここでは `json_export` とします)は下記のように構成すれば作成できます。

```ini:ansible.cfg
[defaults]
# ./callback_plugins/json_export.py に自作callbackプラグインを置く
# (ansible-playbook に与える引数のプレイブックからの相対パスになる)
callback_plugins = ./callback_plugins
callbacks_enabled = json_export

# デフォルトのcallbackプラグインの指定
# 別に ANSIBLE_STDOUT_CALLBACK=json_export のように実行時に環境変数として指定してもいい
stdout_callback = json_export
```

```python:callback_plugins/json_export.py
import os
import json
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback.default import CallbackModule as Base

DOCUMENTATION = '''
    name: json_export
    type: stdout
    short_description: Also export results into a JSON file if specified.
    version_added: historical
    description:
        - Export results into a JSON file (specified at JSON_EXPORT_PATH).
    extends_documentation_fragment:
      - default_callback
    requirements:
      - set as stdout in configuration
'''

# 基本的にはデフォルトの出力が欲しいので、デフォルトのcallbackモジュールを拡張したものを作成する
class CallbackModule(Base):

    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'json_export'

    def __init__(self):
        super(CallbackModule, self).__init__()
        self._results = []
        self._servers = {}
        self._output_to = os.getenv('JSON_EXPORT_PATH', '')

    # v2_* メソッドをオーバーライドしてデフォルトの動作に処理を追加していく

    def v2_playbook_on_task_start(self, task, is_conditional):
        super(CallbackModule, self).v2_playbook_on_task_start(task, is_conditional)
        self._results.append({ 'name': task.get_name(), 'items': [] })
    
    def v2_runner_on_ok(self, result):
        super(CallbackModule, self).v2_runner_on_ok(result)
        self._append_result('ok', result)

    def v2_runner_on_failed(self, result, ignore_errors=False):
        super(CallbackModule, self).v2_runner_on_failed(result)
        self._append_result('failed', result)

    def v2_runner_on_skipped(self, result):
        super(CallbackModule, self).v2_runner_on_skipped(result)
        self._append_result('skipped', result)

    def v2_runner_on_unreachable(self, result):
        super(CallbackModule, self).v2_runner_on_unreachable(result)
        self._append_result('unreachable', result)

    def _append_result(self, result_type, result):
        lastResults = self._results[-1]['items']
        host_name = result._host.get_name()

        rawResult = result._result
        essentialResult = {}
        for key in ["changed", "start", "end", "stdout", "stdout_lines", "stderr", "stderr_lines"]:
            if key in rawResult:
                essentialResult[key] = rawResult[key]

        lastResults.append({
            'host_name': host_name,
            'type': result_type,
            'result': essentialResult
        })
        self._servers[host_name] = True

    # 実行結果出力時（実行の最後）にJSONファイルを出力する
    def v2_playbook_on_stats(self, stats):
        super(CallbackModule, self).v2_playbook_on_stats(stats)
        if self._output_to:
            with open(self._output_to, "w") as f:
                json.dump({
                    'results': self._results,
                    'host_names': list(self._servers.keys())
                }, f, cls=AnsibleJSONEncoder, indent=2, ensure_ascii=False, sort_keys=True)
```

`ansible.cfg` でプラグインのサーチパスと許可するプラグインの名前を追加したのち、それを実行時に使うプラグインに設定するだけです。

上記自作プラグインで要求している `JSON_EXPORT_PATH` を与えてあげれば、実行の最後にJSONファイルが出力されます。

```console
$ JSON_EXPORT_PATH=ansible_result.json ansible-playbook -i hosts playbook.yml
PLAY [all] *************************************************************************************************************************

TASK [Gathering Facts] *************************************************************************************************************
ok: [web_server]
ok: [db_server]

...

PLAY RECAP *************************************************************************************************************************
db_server                  : ok=5    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
web_server                 : ok=8    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
```

```json:ansible_result.json
{
  "host_names": [
    "db_server",
    "web_server"
  ],
  "results": [
    {
      "items": [
        {
          "host_name": "db_server",
          "result": {
            "changed": false
          },
          "type": "ok"
        },
...
```

## まとめ

Ansibleで実行結果の出力を自由にカスタマイズしたい場合はcallbackプラグインを作れば何でもできます。

これを応用して、たとえば、Ansibleの全実行結果を強制的に監査用のリポジトリにアップロードするといったことも可能です。