Ruby
chef
YAML
Ansible

ChefとAnsibleでのhash|mappingデータに対する多重loopの比較

hash|mappingの構造に沿って多重ループを行う例をChefとAnsibleで示す。

Chefの例

レシピ

recipes/default.rb
node.default['var_test']['a']['aa']['aaa'] = 'AAA'
node.default['var_test']['a']['aa']['aab'] = 'AAB'
node.default['var_test']['a']['ab']['aba'] = 'ABA'
node.default['var_test']['b']['ba']['baa'] = 'BAA'
node.default['var_test']['c']['ca']['caa'] = 'CAA'
node.default['var_test']['c']['cb']['cba'] = 'CBA'

node['var_test'].each do |key1, val1|
  val1.each do |key2, val2|
    val2.each do |key3, val3|
      log "key1:#{key1} key2:#{key2} key3:#{key3} val:#{val3}"
    end
  end
end

hashに対する.eachを用いて多重ループをシンプルに記述可能。
なお、attributeは以下の様な書き方やJSON渡しも可能。

node.default['var_test'] = {
  'a' => { 
    'aa' => { 
      'aaa' => 'AAA',
      'aab' => 'AAB',
    },
...

実行結果

$ kitchen conv
-----> Starting Kitchen (v1.21.2)
-----> Converging <chef-loop-centos-7>...
       Preparing files for transfer
       Preparing dna.json
       Resolving cookbook dependencies with Berkshelf 7.0.2...
       Removing non-cookbook files before transfer
       Preparing validation.pem
       Preparing client.rb
-----> Chef Omnibus installation detected (install only if missing)
       Transferring files to <chef-loop-centos-7>
       Starting Chef Client, version 14.3.37
       resolving cookbooks for run list: ["chef_loop::default"]
       Synchronizing Cookbooks:
         - chef_loop (0.1.0)
       Installing Cookbook Gems:
       Compiling Cookbooks...
       Converging 6 resources
       Recipe: chef_loop::default
         * log[key1:a key2:aa key3:aaa val:AAA] action write

         * log[key1:a key2:aa key3:aab val:AAB] action write

         * log[key1:a key2:ab key3:aba val:AAA] action write

         * log[key1:b key2:ba key3:baa val:BAA] action write

         * log[key1:c key2:ca key3:caa val:CAA] action write

         * log[key1:c key2:cb key3:cba val:CBA] action write


       Running handlers:
       Running handlers complete
       Chef Client finished, 6/6 resources updated in 01 seconds
       Downloading files from <chef-loop-centos-7>
       Finished converging <chef-loop-centos-7> (0m7.12s).
-----> Kitchen is finished. (0m10.56s)

簡潔に実行されている
実行順序もデータ順が保たれている。
Chefでは、構造化されたデータをループで階層毎に処理していくことで、可読性、メンテナンス性、可搬性、抽象化といったメリットが得られていると思える。

Ansibleの例

Ansibleのplaybookはプログラミング言語では無く、データ構造を用いて無理やりロジックを実装しているので、ロジックが必要なケースではとても読みにくい記述となってしまう。

playbook

recipes/default.yml
---
- hosts: all
  vars:
    var_test:
      a:
        aa:
          aaa: AAA
          aab: AAB
        ab:
          aba: ABA
      b:
        ba:
          baa: BAA
      c:
        ca:
          caa: CAA
        cb:
          cba: CBA

  tasks:
    - name: "loop0"
      include: loop1.yml
      with_dict : "{{ vars.var_test }}"

データはChefの例と同じ構造体
ハッシュ|マッピングに対するループはwith_dictを使用するが、内容にブロックを指定できず、includeでループ内容を別ファイル(ここではloop1.yml)に分ける必要がある。
その為、可読性、保守性が低い。

参照:Ansible block単位でLoopしたい!でも、出来ない ので ワークアラウンド

recipes/loop1.yml
---
- name: loop1
  include: loop2.yml
  with_dict: "{{ item.value }}"
  loop_control:
    loop_var: item1

ここから更にループさせるには、更にファイルを分ける必要がある。(ここではloop2.yml)
なお、単純に2重ループ目を作るとitemという内容を指定するキーワードが重複する為、item1という別名を使用している。

recipes/loop2.yml
---
- name: loop2
  debug: msg="key1:{{ item.key }} key2:{{ item1.key }} key3:{{ item2.key }} val:{{ item2.value }}"
  with_dict: "{{ item1.value }}"
  loop_control:
    loop_var: item2

ループの最後で処理の本体。
キーワードの重複を避ける為、item2という別名を使用している。

実行結果

$ kitchen conv
-----> Starting Kitchen (v1.21.2)
-----> Converging <ansible-loop-centos-7>...
       Preparing files for transfer
       *************** AnsiblePush install_command ***************
       Ansible push config validated
       Transferring files to <ansible-loop-centos-7>
       *************** AnsiblePush run ***************

PLAY [all] ******************************************************************************************

TASK [Gathering Facts] ******************************************************************************
ok: [ansible-loop-centos-7]

TASK [loop0] ****************************************************************************************
included: /Users/aa220269/repo/repo-test/cookbooks/ansible_loop/recipes/loop1.yml for ansible-loop-centos-7
included: /Users/aa220269/repo/repo-test/cookbooks/ansible_loop/recipes/loop1.yml for ansible-loop-centos-7
included: /Users/aa220269/repo/repo-test/cookbooks/ansible_loop/recipes/loop1.yml for ansible-loop-centos-7

TASK [loop1] ****************************************************************************************
included: /Users/aa220269/repo/repo-test/cookbooks/ansible_loop/recipes/loop2.yml for ansible-loop-centos-7
included: /Users/aa220269/repo/repo-test/cookbooks/ansible_loop/recipes/loop2.yml for ansible-loop-centos-7

TASK [loop2] ****************************************************************************************
ok: [ansible-loop-centos-7] => (item={'value': u'AAA', 'key': u'aaa'}) => {
    "msg": "key1:a key2:aa key3:aaa val:AAA"
}
ok: [ansible-loop-centos-7] => (item={'value': u'AAB', 'key': u'aab'}) => {
    "msg": "key1:a key2:aa key3:aab val:AAB"
}

TASK [loop2] ****************************************************************************************
ok: [ansible-loop-centos-7] => (item={'value': u'ABA', 'key': u'aba'}) => {
    "msg": "key1:a key2:ab key3:aba val:ABA"
}

TASK [loop1] ****************************************************************************************
included: /Users/aa220269/repo/repo-test/cookbooks/ansible_loop/recipes/loop2.yml for ansible-loop-centos-7
included: /Users/aa220269/repo/repo-test/cookbooks/ansible_loop/recipes/loop2.yml for ansible-loop-centos-7

TASK [loop2] ****************************************************************************************
ok: [ansible-loop-centos-7] => (item={'value': u'CBA', 'key': u'cba'}) => {
    "msg": "key1:c key2:cb key3:cba val:CBA"
}

TASK [loop2] ****************************************************************************************
ok: [ansible-loop-centos-7] => (item={'value': u'CAA', 'key': u'caa'}) => {
    "msg": "key1:c key2:ca key3:caa val:CAA"
}

TASK [loop1] ****************************************************************************************
included: /Users/aa220269/repo/repo-test/cookbooks/ansible_loop/recipes/loop2.yml for ansible-loop-centos-7

TASK [loop2] ****************************************************************************************
ok: [ansible-loop-centos-7] => (item={'value': u'BAA', 'key': u'baa'}) => {
    "msg": "key1:b key2:ba key3:baa val:BAA"
}

PLAY RECAP ******************************************************************************************
ansible-loop-centos-7      : ok=14   changed=0    unreachable=0    failed=0   

       *************** AnsiblePush end run *******************
       Downloading files from <ansible-loop-centos-7>
       Finished converging <ansible-loop-centos-7> (0m5.49s).
-----> Kitchen is finished. (0m8.37s)

ログは読みにくく保守性に欠ける。
実行順序が保障されていない。