Ansibleの実行結果をJSONで扱いたい! が、コンソール出力が微妙!
通常、Ansibleは実行結果を段階ごとに下記のようにコンソールに出力していきますが、
$ 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出力がプレイブック実行の 最後にドバッと一気に出ます。
$ 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
とします)は下記のように構成すれば作成できます。
[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
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_skipped(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ファイルが出力されます。
$ 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
{
"host_names": [
"db_server",
"web_server"
],
"results": [
{
"items": [
{
"host_name": "db_server",
"result": {
"changed": false
},
"type": "ok"
},
...
まとめ
Ansibleで実行結果の出力を自由にカスタマイズしたい場合はcallbackプラグインを作れば何でもできます。
これを応用して、たとえば、Ansibleの全実行結果を強制的に監査用のリポジトリにアップロードするといったことも可能です。