default(omit)したいのに空文字が定義されてしまうケースをternaryで解決する
2021/05/31追記
ternary
フィルターを使わなくても、default
フィルターの標準機能でやりたいことが実現できました。default
フィルターの引数にtrue
を指定することで、対象の変数がfalse
と評価される場合にdefault
を適用することができます。
以下、サンプルコードと実行結果。
--- - hosts: localhost gather_facts: false connection: local vars: foo: "" tasks: - name: debug debug: msg: "{{ item }}" loop: - "{{ foo | bool }}" - "{{ foo | default(omit) }}" - "{{ foo | default(omit,true) }}"
PLAY [localhost] ********************************************************************************************************************************************************************** TASK [debug] ************************************************************************************************************************************************************************** ok: [localhost] => (item=False) => msg: false ok: [localhost] => (item=) => msg: '' ok: [localhost] => (item=__omit_place_holder__34382e50c55cdbf84ac5c85dfb41cba3629ba8f8) => msg: Hello world! PLAY RECAP **************************************************************************************************************************************************************************** localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
参考
以下蛇足。
はじめに
default
フィルターを使用することで、変数が定義されていない場合のデフォルト値を指定できます。さらに、デフォルト値にomit
を指定することで、パラメータそのものを省略することができます。
ただし、default(omit)
で省略されるのは、変数が未定義であった場合です。例えば、read_csv
モジュールで空のフィールドを含むCSVファイルを読み込んで、debug
モジュールで出力した場合、下記のようになります。
path,mode /tmp/foo, /tmp/bar, /tmp/baz,0444
ok: [localhost] => { "res_csv": { "ansible_facts": { "discovered_interpreter_python": "/usr/libexec/platform-python" }, "changed": false, "dict": {}, "failed": false, "list": [ { "mode": "", "path": "/tmp/foo" }, { "mode": "", "path": "/tmp/bar" }, { "mode": "0444", "path": "/tmp/baz" } ] } }
CSVファイル上、/tmp/foo
と/tmp/bar
のmode
は空ですが、変数としては空文字""
が定義されています。したがって、default
フィルタは動作しません。上記のケースで空文字""
をomit
するため、ternary
フィルタを利用します。
Playbook
- hosts: localhost gather_facts: false connection: local tasks: - name: Read CSV file community.general.read_csv: path: file.csv register: res_csv - name: Touch files with an optional mode ansible.builtin.file: path: "{{ item.path }}" state: touch mode: "{{ (item.mode == '') | ternary(omit, item.mode) }}" loop: "{{ res_csv.list }}" register: res_file - name: Debug res_file ansible.builtin.debug: msg: "{{ item.invocation.module_args }}" loop: "{{ res_file.results }}" loop_control: label: "{{ item.invocation.module_args.path }}"
実行結果
PLAY [localhost] ***************************************************************************************************************************** TASK [Read CSV file] ************************************************************************************************************************* ok: [localhost] TASK [Touch files with an optional mode] ***************************************************************************************************** changed: [localhost] => (item={'path': '/tmp/foo', 'mode': ''}) changed: [localhost] => (item={'path': '/tmp/bar', 'mode': ''}) changed: [localhost] => (item={'path': '/tmp/baz', 'mode': '0444'}) TASK [Debug res_file] ************************************************************************************************************************ ok: [localhost] => (item=/tmp/foo) => { "msg": { "_diff_peek": null, "_original_basename": null, "access_time": null, "access_time_format": "%Y%m%d%H%M.%S", "attributes": null, "follow": true, "force": false, "group": null, "mode": null, ★ "modification_time": null, "modification_time_format": "%Y%m%d%H%M.%S", "owner": null, "path": "/tmp/foo", "recurse": false, "selevel": null, "serole": null, "setype": null, "seuser": null, "src": null, "state": "touch", "unsafe_writes": false } } ok: [localhost] => (item=/tmp/bar) => { "msg": { "_diff_peek": null, "_original_basename": null, "access_time": null, "access_time_format": "%Y%m%d%H%M.%S", "attributes": null, "follow": true, "force": false, "group": null, "mode": null, ★ "modification_time": null, "modification_time_format": "%Y%m%d%H%M.%S", "owner": null, "path": "/tmp/bar", "recurse": false, "selevel": null, "serole": null, "setype": null, "seuser": null, "src": null, "state": "touch", "unsafe_writes": false } } ok: [localhost] => (item=/tmp/baz) => { "msg": { "_diff_peek": null, "_original_basename": null, "access_time": null, "access_time_format": "%Y%m%d%H%M.%S", "attributes": null, "follow": true, "force": false, "group": null, "mode": "0444", ★ "modification_time": null, "modification_time_format": "%Y%m%d%H%M.%S", "owner": null, "path": "/tmp/baz", "recurse": false, "selevel": null, "serole": null, "setype": null, "seuser": null, "src": null, "state": "touch", "unsafe_writes": false } } PLAY RECAP *********************************************************************************************************************************** localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
実行結果のmodule_args
を見ると、/tmp/foo
と/tmp/bar
のmode
が、""
でなくnull
になっています。これは、ternary
フィルタで評価した結果、omit
が利いていることを示します。ぶっちゃけfile
モジュールはmode
パラメータの指定が""
でも動作しますが、空文字の指定が許されないパラメータを持つモジュールでは有用なシーンがあります。
参考
Ansible Vaultのベストプラクティス
Ansible Vaultのベストプラクティス
はじめに
Ansible Vaultは、パスワード等の機密情報を暗号化できます。暗号化は、ファイル単位、変数単位のどちらにも対応していますが、単純に使うと、いずれも下記のデメリットが発生します。
ファイル単位
ファイル全体が暗号化されてしまうため、メンテナンス時にgrepなどで探してもヒットしない
変数単位
ansible-vault encrypt_string
コマンドで対象の文字列を暗号化する必要があるため、historyに機密情報が残る
これらを解決するために、公式ドキュメントでは、間接的な層を使って暗号化することを推奨しています。
ディレクトリ構成
間接的な層を使うとは、暗号化したファイルとは別に、暗号化したファイルを参照するファイルを作成しておくことです。group_varsの場合、下記のような構成を指します。
group_vars/ `-- db |-- vars.yml # 暗号化しない。vault.ymlを参照。 `-- vault.yml # 暗号化する。
グループ変数ファイルの中身
vault.yml
ansible-vault
コマンドで暗号化する想定です。機密情報を記載します。機密情報の変数は、わかりやすいようにvault_
プレフィックスを付けて定義します。
--- vault_db_password: dbpass
vars.yml
こちらは暗号化しません。機密情報にあたる変数のみ、vault.yml
を参照するよう定義します。
--- db_user: dbuser db_password: "{{ vault_db_password }}"
Playbook
変数を参照する際は、vars.yml
に記載した変数名を指定します。
- hosts: db gather_facts: false connection: local tasks: - debug: msg: "{{ item }}" loop: - "{{ db_user }}" - "{{ db_password }}"
実行結果
# ansible-vault encrypt group_vars/db/vault.yml New Vault password: Confirm New Vault password: Encryption successful # ansible-playbook -i inventory.ini vault_test.yml --ask-vault-pass Vault password: PLAY [db] *********************************************************************************** TASK [debug] ******************************************************************************** ok: [db01] => (item=dbuser) => msg: dbuser ok: [db01] => (item=dbpass) => msg: dbpass PLAY RECAP ********************************************************************************** db01 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
おわりに
商用環境の場合、この手の暗号化はAnsible Towerの認証情報機能を利用するケースが多いと思いますが、メンテナンス性を意識した変数暗号化のあり方は、参考になるかもしれません。
netboxのLocal Contextを更新する
初めに
netboxのLocal ContextをAnsibleのnetbox.netbox.netbox_device
モジュールで更新する場合、local_context_data
パラメータに指定した値で全て上書きされてしまいます。このため、既に何らかの値がLocal Contextに設定されていて、そこに別の値を追加したい場合、下記のステップを踏む必要があります。
- Ansible側で既存のLocal Contextを取得
- 取得したLocal Contextに追加したい値をマージ
- マージした値をLocal Contextに登録
今回はkey1、key2が登録された状態から、key3を追加するPlaybookを書きました。
netbox上のイメージ
Playbook
- hosts: localhost gather_facts: false connection: local tasks: - name: Fetch local context set_fact: current_local_context_data: "{{ query('netbox.netbox.nb_lookup', 'devices', api_endpoint=netbox_url, api_filter='name=device01', token=netbox_token).0.value.local_context_data }}" - name: Debug current_local_context_data debug: var: current_local_context_data - name: Update local context data netbox.netbox.netbox_device: netbox_token: "{{ netbox_token }}" netbox_url: "{{ netbox_url }}" data: name: device01 local_context_data: "{{ current_local_context_data | combine({'key3':'value3'}) }}" - name: Debug local_context_data debug: msg: "{{ query('netbox.netbox.nb_lookup', 'devices', api_endpoint=netbox_url, api_filter='name=device01', token=netbox_token).0.value.local_context_data }}"
実行結果
PLAY [localhost] **************************************************************************************************************************************************************************** TASK [Fetch local context] ****************************************************************************************************************************************************************** ok: [localhost] TASK [Debug current_local_context_data] ***************************************************************************************************************************************************** ok: [localhost] => current_local_context_data: key1: value1 key2: value2 TASK [Update local context data] ************************************************************************************************************************************************************ changed: [localhost] TASK [Debug local_context_data] ************************************************************************************************************************************************************* ok: [localhost] => msg: key1: value1 key2: value2 key3: value3 PLAY RECAP ********************************************************************************************************************************************************************************** localhost : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
set_statsのper_hostが使えないときの代替手段
はじめに
awxにて、動的に生成した変数を、ジョブテンプレートを跨いで使用したい場合、set_stats
モジュールで実現できます。
ただし、awxの制約事項として、per_host
パラメータはno
である必要があります。したがって、set_stats
で設定した変数は、ホスト変数のようには扱えません。
しかし、ホストに紐づく変数を動的に生成して、後続のジョブテンプレートに持ち越したい、というケースはあるかと思います。そこで、下記のように自ホスト名をキーにすることで、なんちゃってホスト変数的な仕組みを考えました。
ホスト名: 変数1: 値 変数2: 値 変数3: 値
取り出すときはlookupプラグインでキーを特定します。
ワークフロー
文字通り、set_statsジョブテンプレートでset_statsして、debug_statsジョブテンプレートで、設定したstatsを確認します。
Playbook
set_stats
--- - hosts: all gather_facts: false tasks: - name: Set a random integer set_fact: my_integers: int1: "{{ 655535 | random }}" int2: "{{ 655535 | random }}" int3: "{{ 655535 | random }}" - name: Debug my_integers debug: var: my_integers - name: Set stats set_stats: data: "{{ {inventory_hostname:my_integers } }}"
debug_stats
--- - hosts: all gather_facts: false tasks: - name: debug stats debug: msg: "{{ item }}" with_items: - "int1 is {{ lookup('vars', inventory_hostname).int1 }}" - "int2 is {{ lookup('vars', inventory_hostname).int2 }}" - "int3 is {{ lookup('vars', inventory_hostname).int3 }}"
実行結果
set_stats
PLAY [all] ********************************************************************* TASK [Set a random integer] **************************************************** ok: [sv1] ok: [sv2] TASK [Debug my_integers] ******************************************************* ok: [sv1] => { "my_integers": { "int1": "164897", "int2": "560957", "int3": "302017" } } ok: [sv2] => { "my_integers": { "int1": "325519", "int2": "21610", "int3": "105839" } } TASK [Set stats] *************************************************************** ok: [sv1] ok: [sv2] PLAY RECAP ********************************************************************* sv1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 sv2 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
debug_stats
PLAY [all] ********************************************************************* TASK [debug stats] ************************************************************* ok: [sv1] => (item=int1 is 164897) => { "msg": "int1 is 164897" } ok: [sv1] => (item=int2 is 560957) => { "msg": "int2 is 560957" } ok: [sv1] => (item=int3 is 302017) => { "msg": "int3 is 302017" } ok: [sv2] => (item=int1 is 325519) => { "msg": "int1 is 325519" } ok: [sv2] => (item=int2 is 21610) => { "msg": "int2 is 21610" } ok: [sv2] => (item=int3 is 105839) => { "msg": "int3 is 105839" } PLAY RECAP ********************************************************************* sv1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 sv2 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
画面左下の追加変数欄を見ると、想定通り、ホスト名をキーとして各変数が格納されていることがわかります。
failedになっても最後まで実行し、最後にassertで判定する
はじめに
Ansibleはfail-fastな設計のため、Taskが失敗した時点でPlaybookは異常終了します。 しかし、assertモジュールによるバリデーション等は、エラーがあっても ある程度まとめて実行したいケースがあります。
そこで、下記の条件を満たすPlaybookを簡単に書いてみました。
条件
- Assert 1、Assert 2、Assert 3という3つのタスクがある
- 3つすべてがokの場合、後続を実行する
- 1つでもfailedの場合、後続は実行しない
- 3つのタスクは、評価結果の真偽に関わらず、すべて実行する
3つすべてokの場合
Playbook
--- - hosts: localhost gather_facts: false connection: local tasks: - name: Assertions ignore_errors: true ########################################################################## # block start block: - name: Assert 1 assert: that: true register: res_assert1 - name: Assert 2 assert: that: true register: res_assert2 - name: Assert 3 assert: that: true register: res_assert3 # block end ########################################################################## - name: Assert all assert: that: item.failed == false loop: - "{{ res_assert1 }}" - "{{ res_assert2 }}" - "{{ res_assert3 }}" - name: Next task debug: msg: Next task
実行結果
PLAY [localhost] ****************************************************************************************************************************************************************************** TASK [Assert 1] ******************************************************************************************************************************************************************************* ok: [localhost] => { "changed": false, "msg": "All assertions passed" } TASK [Assert 2] ******************************************************************************************************************************************************************************* ok: [localhost] => { "changed": false, "msg": "All assertions passed" } TASK [Assert 3] ******************************************************************************************************************************************************************************* ok: [localhost] => { "changed": false, "msg": "All assertions passed" } TASK [Assert all] ***************************************************************************************************************************************************************************** ok: [localhost] => (item={'changed': False, 'msg': 'All assertions passed', 'failed': False}) => { "ansible_loop_var": "item", "changed": false, "item": { "changed": false, "failed": false, "msg": "All assertions passed" }, "msg": "All assertions passed" } ok: [localhost] => (item={'changed': False, 'msg': 'All assertions passed', 'failed': False}) => { "ansible_loop_var": "item", "changed": false, "item": { "changed": false, "failed": false, "msg": "All assertions passed" }, "msg": "All assertions passed" } ok: [localhost] => (item={'changed': False, 'msg': 'All assertions passed', 'failed': False}) => { "ansible_loop_var": "item", "changed": false, "item": { "changed": false, "failed": false, "msg": "All assertions passed" }, "msg": "All assertions passed" } TASK [Next task] ****************************************************************************************************************************************************************************** ok: [localhost] => { "msg": "Next task" } PLAY RECAP ************************************************************************************************************************************************************************************ localhost : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Assert 2のみエラーの場合
Playbook
--- - hosts: localhost gather_facts: false connection: local tasks: - name: Assertions ignore_errors: true ########################################################################## # block start block: - name: Assert 1 assert: that: true register: res_assert1 - name: Assert 2 assert: that: false register: res_assert2 - name: Assert 3 assert: that: true register: res_assert3 # block end ########################################################################## - name: Assert all assert: that: item.failed == false loop: - "{{ res_assert1 }}" - "{{ res_assert2 }}" - "{{ res_assert3 }}" - name: Next task debug: msg: Next task
実行結果
PLAY [localhost] ****************************************************************************************************************************************************************************** TASK [Assert 1] ******************************************************************************************************************************************************************************* ok: [localhost] => { "changed": false, "msg": "All assertions passed" } TASK [Assert 2] ******************************************************************************************************************************************************************************* fatal: [localhost]: FAILED! => { "assertion": false, "changed": false, "evaluated_to": false, "msg": "Assertion failed" } ...ignoring TASK [Assert 3] ******************************************************************************************************************************************************************************* ok: [localhost] => { "changed": false, "msg": "All assertions passed" } TASK [Assert all] ***************************************************************************************************************************************************************************** ok: [localhost] => (item={'changed': False, 'msg': 'All assertions passed', 'failed': False}) => { "ansible_loop_var": "item", "changed": false, "item": { "changed": false, "failed": false, "msg": "All assertions passed" }, "msg": "All assertions passed" } failed: [localhost] (item={'failed': True, 'evaluated_to': False, 'assertion': False, 'msg': 'Assertion failed', 'changed': False}) => { "ansible_loop_var": "item", "assertion": "item.failed == false", "changed": false, "evaluated_to": false, "item": { "assertion": false, "changed": false, "evaluated_to": false, "failed": true, "msg": "Assertion failed" }, "msg": "Assertion failed" } ok: [localhost] => (item={'changed': False, 'msg': 'All assertions passed', 'failed': False}) => { "ansible_loop_var": "item", "changed": false, "item": { "changed": false, "failed": false, "msg": "All assertions passed" }, "msg": "All assertions passed" } PLAY RECAP ************************************************************************************************************************************************************************************ localhost : ok=3 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=1
ディクショナリにディクショナリを追加する
はじめに
ディクショナリをディクショナリに追加する、ただそれだけのメモです。
Playbook
--- - hosts: localhost connection: local gather_facts: false tasks: - name: Create an IP address list set_fact: ip_address_list: {} - name: Append IP addresses set_fact: ip_address_list: "{{ ip_address_list | combine({item.use: item.ip_address}) }}" loop: - use: service ip_address: '192.168.10.0/24' - use: management ip_address: '192.168.20.0/24' - name: Debug the IP address list debug: var: ip_address_list
実行結果
# ansible-playbook -i inventory.ini append.yml PLAY [localhost] ****************************************************************************************************************************************************************************** TASK [Create an IP address list] ************************************************************************************************************************************************************** ok: [localhost] TASK [Append IP addresses] ******************************************************************************************************************************************************************** ok: [localhost] => (item={'use': 'service', 'ip_address': '192.168.10.0/24'}) ok: [localhost] => (item={'use': 'management', 'ip_address': '192.168.20.0/24'}) TASK [Debug the IP address list] ************************************************************************************************************************************************************** ok: [localhost] => { "ip_address_list": { "management": "192.168.20.0/24", "service": "192.168.10.0/24" } } PLAY RECAP ************************************************************************************************************************************************************************************ localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
参考
リストにディクショナリを追加する
はじめに
pythonのappend
メソッド的なことをansibleでやってみたメモです。
Playbook
--- - hosts: localhost connection: local gather_facts: false tasks: - name: Create an IP address list set_fact: ip_address_list: [] - name: Append IP addresses set_fact: ip_address_list: "{{ ip_address_list + [{item.use: item.ip_address}] }}" loop: - use: service ip_address: '192.168.10.0/24' - use: management ip_address: '192.168.20.0/24' - name: Debug the IP address list debug: var: ip_address_list
実行結果
# ansible-playbook -i inventory.ini append.yml PLAY [localhost] ****************************************************************************************************************************************************************************** TASK [Create an IP address list] ************************************************************************************************************************************************************** ok: [localhost] TASK [Append IP addresses] ******************************************************************************************************************************************************************** ok: [localhost] => (item={'use': 'service', 'ip_address': '192.168.10.0/24'}) ok: [localhost] => (item={'use': 'management', 'ip_address': '192.168.20.0/24'}) TASK [Debug the IP address list] ************************************************************************************************************************************************************** ok: [localhost] => { "ip_address_list": [ { "service": "192.168.10.0/24" }, { "management": "192.168.20.0/24" } ] } PLAY RECAP ************************************************************************************************************************************************************************************ localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0