BIG-IPでAnsible経由で操作する場合の基本的なお作法をまとめます。Ansibleで情報豊富で実績多数なのはアプリケーションに関わる部分で、ネットワークに関しては情報が少なめです。ネットワーク機器を操作する場合は、connection moduleの特殊な使い方が必要で、connectionに苦戦する人を多く見かけます。
接続方法の説明
BIG-IPへの接続方法を説明します。最もシンプルで簡潔な書き方ではなく、動作原理が分かるように順を追って説明します。
基本形
BIG-IPを操作する時の特徴は、BIG-IP宛ではなくlocalhostを宛先として操作する事です。
以下に「show sys version」の出力するplaybookの例を載せます。hostsに指定するのはlocalhostで、Ansibleモジュールの変数の一部としてBIG-IPのIPアドレスやユーザ名を指定します。
cat << EOF > bigip_show_sys_version.yml - hosts: localhost gather_facts: no tasks: - name: run show sys version bigip_command: commands: show sys version provider: server: 192.168.2.30 password: admin user: admin validate_certs: no register: result - name: display show sys version debug: var: result['stdout'] EOF
実行結果は以下の通りです。
$ ansible-playbook bigip_show_sys_version.yml [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 [localhost] *************************************************************** TASK [run show sys version] **************************************************** ok: [localhost] TASK [display show sys version] ************************************************ ok: [localhost] => { "result['stdout']": [ "Sys::Version\nMain Package\n Product BIG-IP\n Version 16.1.0\n Build 0.0.19\n Edition Final\n Date Tue Jun 22 23:52:22 PDT 2021" ] } PLAY RECAP ********************************************************************* localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
もう少し動作原理を追跡してみましょう。そもそものDevOpsの起源が「10+ Deploys Per Day」と呼ばれるデプロイ頻度が高いようなサービスを意識しているような所もあり、ネットワーク機器の操作はDevOpsの設計思想とは180度反対を向いており、若干の強引な実装になっている所があります。この辺りの思想の「ひずみ」は理解しておくと、デバッグに役立つと思っています。
AnsibleはLinux機を操作する事をメインにしていますので、pythonスクリプトを対象機に転送し、対象機でスクリプトを実行する挙動になっています。ですので、SSHやtelnetで操作せざるを得ないネットワーク機器やAPIで操作する機器はconnection moduleと呼ばれる特殊な拡張をする事によって自動化を実現しています。
BIG-IPの場合はAPIによって操作しますので、APIを実行するスクリプトをlocalhostに転送して、それのスクリプトを実行するような挙動になっています。
動作原理を理解している方ならばお気づきかもしれませんが、実はlocalhostに転送する必要はありません。無駄なネットワーク越しの転送時間を減らすためにlocalhostを使うのが一番有利ですが、pythonが動作するサーバならばlocalhost以外でも問題なく動作します。
この原理を確かめるために、環境変数ANSIBLE_KEEP_REMOTE_FILESを使ってみましょう。これを使うと、転送したpythonスクリプトを消去しないようになり、Ansibleの内部的な挙動を追跡できるようになります。ANSIBLE_KEEP_REMOTE_FILES=1を指定した状態で、再度、playbookを実行してみます。
$ export ANSIBLE_KEEP_REMOTE_FILES=1 $ $ $ ansible-playbook bigip_show_sys_version.yml <omitted> PLAY RECAP ********************************************************************* localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
playbook実行後、~/.ansible/tmp/配下にAnsibleが生成したスクリプトを確認する事ができます。スクリプトの中身を見ると、確かにplaybookで与えたBIG-IPの宛先IPアドレスや認証情報が格納されている事が分かります。
cat /home/administrator/.ansible/tmp/ansible-tmp-1630646509.0181181-185265-141961755123201/AnsiballZ_bigip_command.py <omitted> ANSIBALLZ_PARAMS = '{"ANSIBLE_MODULE_ARGS": {"commands": "show sys version", "provider": {"server": "192.168.2.30", "password": "admin", "user": "admin", "validate_certs": false, "server_port": null, "transport": "rest", "timeout": null, "no_f5_teem": null, "auth_provider": null}, "_ansible_check_mode": false, "_ansible_no_log": false, "_ansible_debug": false, "_ansible_diff": false, "_ansible_verbosity": 0, "_ansible_version": "2.11.4", "_ansible_module_name": "bigip_command", "_ansible_syslog_facility": "LOG_USER", "_ansible_selinux_special_fs": ["fuse", "nfs", "vboxsf", "ramfs", "9p", "vfat"], "_ansible_string_conversion_action": "warn", "_ansible_socket": null, "_ansible_shell_executable": "/bin/sh", "_ansible_keep_remote_files": true, "_ansible_tmpdir": "/home/administrator/.ansible/tmp/ansible-tmp-1630646509.0181181-185265-141961755123201/", "_ansible_remote_tmp": "~/.ansible/tmp"}}' if PY3: ANSIBALLZ_PARAMS = ANSIBALLZ_PARAMS.encode('utf-8') try: # There's a race condition with the controller removing the # remote_tmpdir and this module executing under async. So we cannot # store this in remote_tmpdir (use system tempdir instead) # Only need to use [ansible_module]_payload_ in the temp_path until we move to zipimport # (this helps ansible-test produce coverage stats) temp_path = tempfile.mkdtemp(prefix='ansible_bigip_command_payload_') zipped_mod = os.path.join(temp_path, 'ansible_bigip_command_payload.zip') <omitted>
connection local
前述の書き方は動作原理を理解するための解説であり、実践ではほぼ使う事はないでしょう。前述の書き方では、BIG-IP 1台しか操作できませんが、ansible_connectionやdelegate_to localhostを使うと見通しの良い書き方ができます。まずはansible_connectionを使った書き方を紹介します。
connectionモジュールとして「local」を指定するインベントリファイルを作成します。
cat << EOF > inventory.ini [bigip:vars] ansible_user = 'admin' ansible_password = 'admin' ansible_connection = 'local' [bigip] bigip161 ansible_host=192.168.2.30 bigip162 ansible_host=192.168.2.31 EOF
プレイブックは以下のように記述します。
cat << EOF > bigip_show_sys_version.yml - hosts: bigip gather_facts: no tasks: - name: run show sys version bigip_command: commands: show sys version provider: server: "{{ ansible_host }}" password: "{{ ansible_password }}" user: "{{ ansible_user }}" validate_certs: no register: result - name: display show sys version debug: var: result['stdout'] EOF
実行結果は以下の通りです。
$ ansible-playbook -i inventory.ini bigip_show_sys_version.yml PLAY [bigip] ******************************************************************* TASK [run show sys version] **************************************************** ok: [bigip162] ok: [bigip161] TASK [display show sys version] ************************************************ ok: [bigip161] => { "result['stdout']": [ "Sys::Version\nMain Package\n Product BIG-IP\n Version 16.1.0\n Build 0.0.19\n Edition Final\n Date Tue Jun 22 23:52:22 PDT 2021" ] } ok: [bigip162] => { "result['stdout']": [ "Sys::Version\nMain Package\n Product BIG-IP\n Version 16.1.0\n Build 0.0.19\n Edition Final\n Date Tue Jun 22 23:52:22 PDT 2021" ] } PLAY RECAP ********************************************************************* bigip161 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 bigip162 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
delegate_to localhost
connection localを使う代わりにdelegate_to localhost(ローカルホストに処理を委任する)を使う方法もあります。動作確認のため、connection localをコメントアウトします。
cat << EOF > inventory.ini [bigip:vars] ansible_user = 'admin' ansible_password = 'admin' #ansible_connection = 'local' [bigip] bigip161 ansible_host=192.168.2.30 bigip162 ansible_host=192.168.2.31 EOF
connection localの代わりに”delegate_to: localhost”を指定します。この指定があるタスクはローカルホストで実行されるようになります。
cat << EOF > bigip_show_sys_version.yml - hosts: bigip gather_facts: no tasks: - name: run show sys version bigip_command: commands: show sys version provider: server: "{{ ansible_host }}" password: "{{ ansible_password }}" user: "{{ ansible_user }}" validate_certs: no delegate_to: localhost register: result - name: display show sys version debug: var: result['stdout'] EOF
実行結果は以下の通りです。
# ansible-playbook -i inventory.ini bigip_show_sys_version.yml PLAY [bigip] ******************************************************************* TASK [run show sys version] **************************************************** ok: [bigip162 -> localhost] ok: [bigip161 -> localhost] TASK [display show sys version] ************************************************ ok: [bigip161] => { "result['stdout']": [ "Sys::Version\nMain Package\n Product BIG-IP\n Version 16.1.0\n Build 0.0.19\n Edition Final\n Date Tue Jun 22 23:52:22 PDT 2021" ] } ok: [bigip162] => { "result['stdout']": [ "Sys::Version\nMain Package\n Product BIG-IP\n Version 16.1.0\n Build 0.0.19\n Edition Final\n Date Tue Jun 22 23:52:22 PDT 2021" ] } PLAY RECAP ********************************************************************* bigip161 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 bigip162 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
操作例
showコマンドの実行
ネットワーク機器の操作は、想定外発生時の影響範囲が大きかったり予算制約のため事前検証不能であったりの技術以外の事情で更新系の自動化は困難である事もあります。しかし、参照系はリスクが低いため比較的自動化しやすい要件になります。まずは、最も使用頻度が高いと思われる参照操作の例を紹介します。
まずはインベントリファイルを作成します
cat << EOF > inventory.ini [bigip:vars] ansible_user = 'admin' ansible_password = 'admin' ansible_connection = 'local' [bigip] bigip030 ansible_host=192.168.2.30 bigip031 ansible_host=192.168.2.31 EOF
「show sys version」を実行する例は以下の通りです。
bigip_commandモジュールを使うと、tmosに対してコマンドを実行する事ができます。showコマンドを指定すれば情報取得になりますし、createコマンドやmodifyコマンドを使用すれば設定変更操作になります。
bigip_commandモジュールの実行結果は、Ansibleの戻り値として返されます。以下の例では、戻り値はregister句で
指定した変数に格納されます。戻り値に何が格納されるかはCollection Index / F5 networksを参照ください。
Ansible Moduleのドキュメントは、Ansible 2.9系以前ではModule Indexと呼ばれていましたが、Ansible 2.10以降ではCollection Indexと呼ばれています。
cat << EOF > bigip_show_sys_version.yml - hosts: bigip030 gather_facts: no vars: provider: password: "{{ ansible_password }}" server: "{{ ansible_host }}" user: "{{ ansible_user }}" validate_certs: no tasks: - name: run show sys version bigip_command: commands: show sys version provider: "{{ provider }}" register: result - name: display show sys version debug: var: result['stdout'] EOF
実行結果は以下のようになります。
$ ansible-playbook -i inventory.ini bigip_show_sys_version.yml PLAY [bigip030] **************************************************************** TASK [run show sys version] **************************************************** ok: [bigip030] TASK [display show sys version] ************************************************ ok: [bigip030] => { "result['stdout']": [ "Sys::Version\nMain Package\n Product BIG-IP\n Version 16.1.0\n Build 0.0.19\n Edition Final\n Date Tue Jun 22 23:52:22 PDT 2021" ] } PLAY RECAP ********************************************************************* bigip030 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
bigip_xxxxモジュール
更新系の操作は可能ならばbigip_xxxxxという名前のモジュールを使用しましょう。これらモジュールは可能な限り冪等性が担保されていますし、yaml形式で見通しよく設定を定義する事ができます。
「yaml形式で見通しよく設定を定義する」はAnsible界隈で語られる意見をそのまま転載しました。ネットワーク技術者から見ればyaml形式よりもconfigの方が読みやすい事もあり、必ずしもyamlが優れているとは思えません。
以下、bigip_hostnameモジュールを使用してホスト名を設定し、bigip_device_dnsモジュールを使用してDNSサーバを指定する例を紹介します。
cat << EOF > bigip_modify_services.yml --- - hosts: bigip030 gather_facts: no vars: provider: password: "{{ ansible_password }}" server: "{{ ansible_host }}" user: "{{ ansible_user }}" validate_certs: no tasks: - name: Set the hostname of the BIG-IP bigip_hostname: hostname: bigip030.localhost.localdomain provider: "{{ provider }}" - name: Set the DNS settings on the BIG-IP bigip_device_dns: name_servers: - 192.168.2.1 - 192.168.1.1 search: - localdomain - lab.local provider: "{{ provider }}" EOF
プレイブックを実行すると以下のように出力されます。
$ ansible-playbook -i inventory.ini bigip_modify_services.yml PLAY [bigip030] **************************************************************** TASK [Set the hostname of the BIG-IP] ****************************************** changed: [bigip030] TASK [Set the DNS settings on the BIG-IP] ************************************** changed: [bigip030] PLAY RECAP ********************************************************************* bigip030 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
念の為、tmosで設定が反映されているかを確認します。
root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)# list sys global-settings hostname sys global-settings { hostname bigip030.localhost.localdomain } root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)# root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)# root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)# list sys dns sys dns { name-servers { 192.168.2.1 192.168.1.1 } search { localdomain lab.local } } root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)#
bigip_commandによる更新処理
bigip_xxxxは冪等性担保や可読性担保の可能性で優れていますが、場合によってはcreateコマンドやmodifyコマンドをそのまま自動化しなければならない事もあるかもしれません。例えば、「マイナーな要件でbigip_xxxxxモジュールでは操作できない設定を自動化しなければならない」「政治的な要件により手順書を『改編なしにそのまま』自動化しなければならない」「Ansibleのドキュメントを読んでいる余裕がないくらいの工数不足」などの状況が該当します。
前述のbigip_commandモジュールにcreateコマンドやmodifyコマンドを指定すれば、更新操作が可能です。複数コマンドの同時実行も可能です。以下はvlanとip addressを設定する例です。
cat << EOF > bigip_create_ipv4_address01.yml - hosts: bigip030 gather_facts: no vars: provider: password: "{{ ansible_password }}" server: "{{ ansible_host }}" user: "{{ ansible_user }}" validate_certs: no tasks: - name: create ipv4 address bigip_command: commands: - create net vlan VLAN0020 tag 20 - create net self SELF0020 vlan VLAN0020 address 10.0.20.1/24 provider: "{{ provider }}" EOF
プレイブックを実行すると以下のように出力されます。ただし、bigip_commadsを編集用途で使用するのは非推奨であり、idempotent(冪等)ではない旨の警告が出力されます。
$ ansible-playbook -i inventory.ini bigip_create_ipv4_address01.yml PLAY [bigip030] **************************************************************** TASK [create ipv4 address] ***************************************************** [WARNING]: Using "write" commands is not idempotent. You should use a module that is specifically made for that. If such a module does not exist, then please file a bug. The command in question is "create net vlan VLAN0020 tag 20..." [WARNING]: Using "write" commands is not idempotent. You should use a module that is specifically made for that. If such a module does not exist, then please file a bug. The command in question is "create net self SELF0020 vlan VLAN0020 a..." changed: [bigip030] PLAY RECAP ********************************************************************* bigip030 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
念の為、tmosで設定が反映されているかを確認します。
root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)# list net vlan net vlan VLAN0020 { if-index 144 tag 20 } root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)# list net self net self SELF0020 { address 10.0.20.1/24 traffic-group traffic-group-local-only vlan VLAN0020 }
テンプレートの流し込み
BIG-IPに限らず、ネットワーク機器を操作する多くのAnsible moduleはjinja2テンプレートを流し込む事ができます。
動作確認のために以下のようなテンプレートファイルを作成します。文字通り、jinja2はテンプレートエンジンなので、変数や繰り返し処理に対応しています。jinja2の操作は、BIG-IPに限った話ではなく、pythonの一般論のお話ですので、ここでは説明を省略します。
テンプレートファイルに記載するのは、tmshで指定する構文ではなく、”load sys config from-terminal merge”で指定する構文です。bigip.confやbigip_base.confで記述される構文と同じです。
cat << EOF > bigip_template.j2 net vlan VLAN0030 { tag 30 } net self SELF0030 { address 10.0.30.1/24 vlan VLAN0030 } EOF
テンプレートを流し込むにはbigip_configモジュールを使用します。
cat << EOF > bigip_create_ipv4_address02.yml - hosts: bigip030 gather_facts: no vars: provider: password: "{{ ansible_password }}" server: "{{ ansible_host }}" user: "{{ ansible_user }}" validate_certs: no tasks: - name: create ipv4 address bigip_config: merge_content: "{{ lookup('file', 'bigip_template.j2') }}" provider: "{{ provider }}" EOF
実行結果は以下のようになります。
$ ansible-playbook -i inventory.ini bigip_create_ipv4_address02.yml PLAY [bigip030] **************************************************************** TASK [create ipv4 address] ***************************************************** changed: [bigip030] PLAY RECAP ********************************************************************* bigip030 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
念の為、tmosで設定が反映されているかを確認します。
root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)# list net vlan net vlan VLAN0020 { if-index 144 tag 20 } net vlan VLAN0030 { if-index 160 tag 30 } root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)# list net self net self SELF0030 { address 10.0.30.1/24 traffic-group traffic-group-local-only vlan VLAN0030 } net self SELF0020 { address 10.0.20.1/24 traffic-group traffic-group-local-only vlan VLAN0020 } root@(bigip030)(cfg-sync Standalone)(Active)(/Common)(tmos)#