てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] 「つまずき Ansible 【Part36】ansible-navigator」ふりかえり

はじめに

2022/03/12 に、YouTube Live で「つまずき Ansible 【Part36】ansible-navigator」という配信をしました。

tekunabe.connpass.com

これまで Playbook の実行といえば、ansible-playbook コマンドでした。 最近 ansible-navigator という新たなツールが出てきました。TUI または CLI のPlaybook 実行ツールです。今回はこれをさわりました。


動画

youtu.be

ansible-navigator とは

個人的に感じている特徴は以下の通りです。

  • TUI を備えている
  • Ansibleの実行環境コンテナ(Execution Environment)を利用できる
  • Playbook 実行だけなくドキュメントの参照などもできる

準備

インストール

今回は pip でインストールします。

pip install ansible-navigator

ansible-runner などの依存パッケージがインストールされます。ansible-navigator から ansible-runner を呼び出すような関係になっています。

$ pip list
Package             Version
------------------- -------
ansible-navigator   1.1.0
ansible-runner      2.1.2
...(略)...

ansibleansible-core はインストールされません。

TUI による Playbook 実行

まずは実行

Playbook の実行にはサブコマンド run を指定します。その後、Playbookファイル名とインベントリファイルを指定します。

$ ansible-navigator run debug.yml -i inventory 

以下のような画面になります。これはPlayの一覧です。

  PLAY NAME    OK  CHANGED   UNREACHABLE    FAILED   SKIPPED   IGNORED   IN PROGRESS     TASK COUNT        PROGRESS
0│localhost     1        0             0         0         0         0             0              1        COMPLETE

左にある番号を入力すると、そのPlay内のタスクの一覧が表示されます。

更に、タスクの番号を入力するとそのタスクの実行結果の詳細が表示されます。

動画を見ていただくほうが、分かりやすいと思います。

このTUIのモードは、設定上は interactive モードと呼ばれています。

ansible-playbook コマンドと同じ形式でもOK

デフォルトは interactive モードですが、ansible-playbook コマンド実行時と同じ形式の stdout モードにも変更できます。

指定は設定ファイル(後述)で行うか、ansible-navigator コマンドで --mode stdout を指定します。

$ ansible-navigator run debug.yml -i inventory --mode stdout

PLAY [localhost] ***************************************************************

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "sakana sakana sakana"
}

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

アーティファクト(ログ)の保存とリプレイ機能

ansible-navigator にはアーティファクト保存機能があります。JSON形式で保存されます。

アーティファクトの例

以下は、ios_command でshow ip route を実行して debug 表示するPlaybookの実行したときのアーティファクトです。

▼クリックして開く

{
    "version": "1.0.0",
    "plays": [
        {
            "playbook": "/home/admin/stumble/ios_show.yml",
            "playbook_uuid": "6b70c8f8-4e21-4288-8fe0-53bc29178723",
            "play": "ios",
            "play_uuid": "a29dee92-8fae-871d-4141-000000000007",
            "play_pattern": "ios",
            "name": "ios",
            "pattern": "ios",
            "uuid": "a29dee92-8fae-871d-4141-000000000007",
            "__play_name": "ios",
            "tasks": [
                {
                    "playbook": "/home/admin/stumble/ios_show.yml",
                    "playbook_uuid": "6b70c8f8-4e21-4288-8fe0-53bc29178723",
                    "play": "ios",
                    "play_uuid": "a29dee92-8fae-871d-4141-000000000007",
                    "play_pattern": "ios",
                    "task": "exec show commands",
                    "task_uuid": "a29dee92-8fae-871d-4141-000000000009",
                    "task_action": "cisco.ios.ios_command",
                    "task_args": "",
                    "task_path": "/home/admin/stumble/ios_show.yml:6",
                    "host": "ios01",
                    "uuid": "8db1e3c2-ab2f-4ff7-917b-cd5bab31cfdb",
                    "__host": "ios01",
                    "__result": "OK",
                    "__changed": false,
                    "__duration": "1s",
                    "__number": 0,
                    "__task": "exec show commands",
                    "__task_action": "cisco.ios.ios_command",
                    "remote_addr": "ios01",
                    "res": {
                        "changed": false,
                        "stdout": [
                            "Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP\n       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area \n       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2\n       E1 - OSPF external type 1, E2 - OSPF external type 2\n       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2\n       ia - IS-IS inter area, * - candidate default, U - per-user static route\n       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP\n       a - application route\n       + - replicated route, % - next hop override, p - overrides from PfR\n\nGateway of last resort is not set\n\n      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks\nC        192.168.1.0/24 is directly connected, GigabitEthernet0/0\nL        192.168.1.11/32 is directly connected, GigabitEthernet0/0"
                        ],
                        "stdout_lines": [
                            [
                                "Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP",
                                "       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area ",
                                "       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2",
                                "       E1 - OSPF external type 1, E2 - OSPF external type 2",
                                "       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2",
                                "       ia - IS-IS inter area, * - candidate default, U - per-user static route",
                                "       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP",
                                "       a - application route",
                                "       + - replicated route, % - next hop override, p - overrides from PfR",
                                "",
                                "Gateway of last resort is not set",
                                "",
                                "      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks",
                                "C        192.168.1.0/24 is directly connected, GigabitEthernet0/0",
                                "L        192.168.1.11/32 is directly connected, GigabitEthernet0/0"
                            ]
                        ],
                        "invocation": {
                            "module_args": {
                                "commands": [
                                    "show ip route"
                                ],
                                "match": "all",
                                "retries": 10,
                                "interval": 1,
                                "wait_for": null,
                                "provider": null
                            }
                        },
                        "ansible_facts": {
                            "discovered_interpreter_python": "/usr/libexec/platform-python"
                        },
                        "_ansible_no_log": false
                    },
                    "start": "2022-03-12T10:48:19.084551",
                    "end": "2022-03-12T10:48:21.031434",
                    "duration": 1.946883,
                    "event_loop": null
                },
                {
                    "playbook": "/home/admin/stumble/ios_show.yml",
                    "playbook_uuid": "6b70c8f8-4e21-4288-8fe0-53bc29178723",
                    "play": "ios",
                    "play_uuid": "a29dee92-8fae-871d-4141-000000000007",
                    "play_pattern": "ios",
                    "task": "debug show commands",
                    "task_uuid": "a29dee92-8fae-871d-4141-00000000000a",
                    "task_action": "ansible.builtin.debug",
                    "task_args": "",
                    "task_path": "/home/admin/stumble/ios_show.yml:12",
                    "host": "ios01",
                    "uuid": "4ee85d46-5d0b-4a99-a806-4be244eb1fed",
                    "__host": "ios01",
                    "__result": "OK",
                    "__changed": false,
                    "__duration": "0s",
                    "__number": 1,
                    "__task": "debug show commands",
                    "__task_action": "ansible.builtin.debug",
                    "remote_addr": "ios01",
                    "res": {
                        "msg": [
                            "Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP",
                            "       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area ",
                            "       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2",
                            "       E1 - OSPF external type 1, E2 - OSPF external type 2",
                            "       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2",
                            "       ia - IS-IS inter area, * - candidate default, U - per-user static route",
                            "       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP",
                            "       a - application route",
                            "       + - replicated route, % - next hop override, p - overrides from PfR",
                            "",
                            "Gateway of last resort is not set",
                            "",
                            "      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks",
                            "C        192.168.1.0/24 is directly connected, GigabitEthernet0/0",
                            "L        192.168.1.11/32 is directly connected, GigabitEthernet0/0"
                        ],
                        "_ansible_verbose_always": true,
                        "_ansible_no_log": false,
                        "changed": false
                    },
                    "start": "2022-03-12T10:48:21.039585",
                    "end": "2022-03-12T10:48:21.544976",
                    "duration": 0.505391,
                    "event_loop": null
                }
            ]
        }
    ],
    "stdout": [
        "",
        "PLAY [ios] *********************************************************************",
        "",
        "TASK [exec show commands] ******************************************************",
        "\u001b[0;32mok: [ios01]\u001b[0m",
        "",
        "TASK [debug show commands] *****************************************************",
        "\u001b[0;32mok: [ios01] => {\u001b[0m",
        "\u001b[0;32m    \"msg\": [\u001b[0m",
        "\u001b[0;32m        \"Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP\",\u001b[0m",
        "\u001b[0;32m        \"       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area \",\u001b[0m",
        "\u001b[0;32m        \"       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2\",\u001b[0m",
        "\u001b[0;32m        \"       E1 - OSPF external type 1, E2 - OSPF external type 2\",\u001b[0m",
        "\u001b[0;32m        \"       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2\",\u001b[0m",
        "\u001b[0;32m        \"       ia - IS-IS inter area, * - candidate default, U - per-user static route\",\u001b[0m",
        "\u001b[0;32m        \"       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP\",\u001b[0m",
        "\u001b[0;32m        \"       a - application route\",\u001b[0m",
        "\u001b[0;32m        \"       + - replicated route, % - next hop override, p - overrides from PfR\",\u001b[0m",
        "\u001b[0;32m        \"\",\u001b[0m",
        "\u001b[0;32m        \"Gateway of last resort is not set\",\u001b[0m",
        "\u001b[0;32m        \"\",\u001b[0m",
        "\u001b[0;32m        \"      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks\",\u001b[0m",
        "\u001b[0;32m        \"C        192.168.1.0/24 is directly connected, GigabitEthernet0/0\",\u001b[0m",
        "\u001b[0;32m        \"L        192.168.1.11/32 is directly connected, GigabitEthernet0/0\"\u001b[0m",
        "\u001b[0;32m    ]\u001b[0m",
        "\u001b[0;32m}\u001b[0m",
        "",
        "PLAY RECAP *********************************************************************",
        "\u001b[0;32mios01\u001b[0m                      : \u001b[0;32mok=2   \u001b[0m changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   "
    ],
    "status": "successful",
    "status_color": 10
}

リプレイ機能で再現表示

そのまま表示するのも良いですが、リプレイ機能が便利です。

ansible-navigator replay アーティファクトファイル名

このように指定して実行すると、ansible-navigator run 実行を再現したような表示になります。Ansible Tower / Automation Controller でいうと過去のジョブ実行結果を後から表示するイメージです。

ansible-navigator runinteractive モードで実行したときのアーティファクトstdout モードで表示することもできますし、その逆もできます。


設定は ansible-navigator.yml

ansible-navigator の挙動を指定する設定ファイルは ansible-navigator.yml というファイルです。

厳密には、YAML でも JSON でも構いません。YAML の場合は、拡張子 は .yml または .yaml です。

  • 環境変数 ANSIBLE_NAVIGATOR_CONFIG で指定したパスのファイル
  • カレントディレクトリの ansible-navigator.yml
  • ~/.ansible-navigator.yml

ansible.cfg と似てますね。

この設定ファイルでは、利用するイメージの指定やモード、アーティファクトファイルの保存パスの指定などができます。設定項目の一覧は、公式ドキュメントに掲載されています。

ansible-navigator.readthedocs.io

なお、配信中にいじっていた設定ファイルは以下のようなものです。

---
ansible-navigator:

  # ansible:
  #   inventories:
  #     - inventory

  execution-environment:
    container-engine: podman
    enabled: true
    # image: quay.io/ansible/creator-ee:v0.2.0
    image: quay.io/ansible/ansible-runner:stable-2.12-latest
    # image: ghcr.io/akira6592/my-ee:latest
    pull-policy: missing

  # mode: stdout

  playbook-artifact:
    enable: True
    save-as: "artifacts/{playbook_name}-{ts_utc}.json"

Ansibleの実行環境コンテナの利用

利用イメージの定義

利用するイメージは、ansible-navigator.yml では以下のように image で指定します。

---
ansible-navigator:
  execution-environment:
    container-engine: podman
    enabled: true
    image: quay.io/ansible/ansible-runner:stable-2.12-latest

たとえば、このイメージに入っていないコレクションのモジュールを使おうとするとエラーになります。

イメージは、できあいの物を利用するか ansible-builder というツールを使って作成したものを利用します。

コンテナを使わなくてもOK

コンテナの実行環境を使わないで、ansible-navigator を実行している自身の環境の Ansible を利用できます。

---
ansible-navigator:
  execution-environment:
    enabled: false    # ここで無効を指定

コマンドのオプションで指定する場合は --ee false です。

Playbook 実行以外にも

ansible-navigator のサブコマンドを run 以外に切り替えることによって、ドキュメントの参照やコンフィグの参照などする機能もあります。

ansible-navigator のサブコマンドと、既存の ansible-* コマンドとの対応を以下のページにまとめられています。

ansible-navigator.readthedocs.io

ドキュメントの参照

ansible-doc コマンドに相当します。

モジュールを指定する場合、コマンド書式は以下のとおりです。

$ ansible-navigator doc モジュール名

おわりに

簡単ですが、ansible-navigator の紹介をしました。

コンテナによる実行環境が主流になってくると、コマンドとしては、ansible-navigator を使う機会も増えてくるかも知れません。

stdout モードが使えるのは気が利いているなと思います。

アーティファクトの保存とリプレイ機能も便利だと感じました。

参考


Part37にむけて

以下のネタを検討中です。気が向いたものをやります。 connpass申込時のアンケートでいただいたものも含めています。

  • ansible-builder
  • connection: local ななにか
  • Windows
  • cli_parse モジュール(Part15 の続き)
  • モジュールのテスト
  • Tower / AWX
  • role と Playbook のリポジトリ分割と読み込み
  • AWXとの共存を念頭に入れたDirectory構成