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

参考

docs.ansible.com

jinja.palletsprojects.com

以下蛇足。

はじめに

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/barmodeは空ですが、変数としては空文字""が定義されています。したがって、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/barmodeが、""でなくnullになっています。これは、ternaryフィルタで評価した結果、omitが利いていることを示します。ぶっちゃけfileモジュールはmodeパラメータの指定が""でも動作しますが、空文字の指定が許されないパラメータを持つモジュールでは有用なシーンがあります。

参考

docs.ansible.com

Ansible Vaultのベストプラクティス

Ansible Vaultのベストプラクティス

はじめに

Ansible Vaultは、パスワード等の機密情報を暗号化できます。暗号化は、ファイル単位、変数単位のどちらにも対応していますが、単純に使うと、いずれも下記のデメリットが発生します。

  • ファイル単位

    ファイル全体が暗号化されてしまうため、メンテナンス時にgrepなどで探してもヒットしない

  • 変数単位

    ansible-vault encrypt_stringコマンドで対象の文字列を暗号化する必要があるため、historyに機密情報が残る

これらを解決するために、公式ドキュメントでは、間接的な層を使って暗号化することを推奨しています。

docs.ansible.com

ディレクトリ構成

間接的な層を使うとは、暗号化したファイルとは別に、暗号化したファイルを参照するファイルを作成しておくことです。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の認証情報機能を利用するケースが多いと思いますが、メンテナンス性を意識した変数暗号化のあり方は、参考になるかもしれません。

docs.ansible.com

netboxのLocal Contextを更新する

初めに

netboxのLocal ContextをAnsibleのnetbox.netbox.netbox_deviceモジュールで更新する場合、local_context_dataパラメータに指定した値で全て上書きされてしまいます。このため、既に何らかの値がLocal Contextに設定されていて、そこに別の値を追加したい場合、下記のステップを踏む必要があります。

  1. Ansible側で既存のLocal Contextを取得
  2. 取得したLocal Contextに追加したい値をマージ
  3. マージした値をLocal Contextに登録

今回はkey1、key2が登録された状態から、key3を追加するPlaybookを書きました。

netbox上のイメージ

f:id:ikiri96hyo:20201115145657p:plain
before

f:id:ikiri96hyo:20201115145710p:plain
after

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で設定した変数は、ホスト変数のようには扱えません。

github.com

しかし、ホストに紐づく変数を動的に生成して、後続のジョブテンプレートに持ち越したい、というケースはあるかと思います。そこで、下記のように自ホスト名をキーにすることで、なんちゃってホスト変数的な仕組みを考えました。

ホスト名:
  変数1:変数2:変数3:

取り出すときはlookupプラグインでキーを特定します。

ワークフロー

文字通り、set_statsジョブテンプレートでset_statsして、debug_statsジョブテンプレートで、設定したstatsを確認します。

f:id:ikiri96hyo:20201010183225p:plain

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   

画面左下の追加変数欄を見ると、想定通り、ホスト名をキーとして各変数が格納されていることがわかります。

f:id:ikiri96hyo:20201010190013p:plain

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   

参考

docs.ansible.com

リストにディクショナリを追加する

はじめに

pythonappendメソッド的なことを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