Ansibleのyumにwith_itemsを指定するときの落とし穴と回避策

以前調べたけど記事にしていなかったので、Ansible 最新版の1.6.6で再度確認した上で書いておくことにしました。

# yumモジュールのname属性

Ansibleのyumモジュールのname属性には以下のものが指定可能です。

* パッケージ名
* name-1.0 のようにパッケージ名にパージョンを追加したもの
* rpmファイルのURL
* rpmファイルのローカルパス

[yumモジュールのマニュアル](http://docs.ansible.com/yum_module.html)に例が出ていますのでご参照ください。

# 落とし穴の例

IIJのミラーからMySQLのクライアント用のrpmをダウンロードしてインストールしようと、以下の様なplaybookを書きました。

```yml:cent65-mysql.yml
- hosts: cent65
  user: vagrant
  sudo: yes
  vars:
    mysql_download_base_url: http://ftp.iij.ad.jp/pub/db/mysql/Downloads/MySQL-5.6
    mysql_download_dest_dir: /usr/local/src
    mysql_common_rpms:
      - MySQL-shared-5.6.19-1.el6.x86_64.rpm
      - MySQL-shared-compat-5.6.19-1.el6.x86_64.rpm
      - MySQL-client-5.6.19-1.el6.x86_64.rpm
      - MySQL-devel-5.6.19-1.el6.x86_64.rpm
  tasks:
    - file: path={{ mysql_download_dest_dir }} state=directory
    - get_url: >
        url={{ mysql_download_base_url }}/{{ item }}
        dest={{ mysql_download_dest_dir }}
      with_items: mysql_common_rpms
    - yum: name="{{ mysql_download_dest_dir }}/{{ item }}" state=present
      with_items: mysql_common_rpms
```

しかし、これはエラーになってしまいます。例によって `ansible-playbook` に `-vvvv` オプションをつけて実行すると、ログに以下のような行がありました。

```shell-session
$ ansible-playbook -vvvv cent65-mysql.yml
…(略)…
<cent65> REMOTE_MODULE yum name="/usr/local/src/MySQL-shared-5.6.19-1.el6.x86_64.rpm,MySQL-shared-compat-5.6.19-1.el6.x86_64.rpm,MySQL-client-5.6.19-1.el6.x86_64.rpm,MySQL-devel-5.6.19-1.el6.x86_64.rpm" state=present
…(略)…
failed: [cent65] => (item=MySQL-shared-5.6.19-1.el6.x86_64.rpm,MySQL-shared-compat-5.6.19-1.el6.x86_64.rpm,MySQL-client-5.6.19-1.el6.x86_64.rpm,MySQL-devel-5.6.19-1.el6.x86_64.rpm) => {"changed": false, "failed": true, "item": "MySQL-shared-5.6.19-1.el6.x86_64.rpm,MySQL-shared-compat-5.6.19-1.el6.x86_64.rpm,MySQL-client-5.6.19-1.el6.x86_64.rpm,MySQL-devel-5.6.19-1.el6.x86_64.rpm", "rc": 0, "results": []}
msg: No Package file matching 'MySQL-shared-compat-5.6.19-1.el6.x86_64.rpm' found on system
```

yumのname属性にカンマ区切りの文字列が指定され、最初の要素は `/usr/local/src/MySQL-shared-5.6.19-1.el6.x86_64.rpm` とフルパスになっているのですが、2つめ以降は `MySQL-shared-compat-5.6.19-1.el6.x86_64.rpm` のようにファイル名のみになっています。

上記のplaybookでは `/usr/local/src/` にrpmをダウンロードしているので、これでは見つからずエラーになっているというわけです。

# yumモジュールにwith_itemsを指定した場合の最適化

これはAnsibleのyumモジュールをwith_itemsと組み合わせて使うと `{{ item }}` の部分をwith_itemsに指定した要素をカンマで連結した値に展開して、yumを実行するようになっています。Ansibleのソースコードで言うと以下の部分です。

```py:https://github.com/ansible/ansible/blob/v1.6.6/lib/ansible/runner/__init__.py#L618-L626
            if len(items) and utils.is_list_of_strings(items) and self.module_name in [ 'apt', 'yum', 'pkgng' ]:
                # hack for apt, yum, and pkgng so that with_items maps back into a single module call
                use_these_items = []
                for x in items:
                    inject['item'] = x
                    if not self.conditional or utils.check_conditional(self.conditional, self.basedir, inject, fail_on_undefined=self.error_on_undefined_vars):
                        use_these_items.append(x)
                inject['item'] = ",".join(use_these_items)
                items = None
```

通常は `with_items` で配列を指定した場合は、要素のそれぞれについてモジュールの処理を実行します。 `apt`, `yum`, `pkgng` の場合は上記のような最適化が入るのです。

# 回避策

`with_items` に文字列の配列ではなくハッシュの配列を指定すれば回避できます。

```yml:cent65-mysql.yml
- hosts: cent65
  user: vagrant
  sudo: yes
  vars:
    mysql_download_base_url: http://ftp.iij.ad.jp/pub/db/mysql/Downloads/MySQL-5.6
    mysql_download_dest_dir: /usr/local/src
    mysql_common_rpms:
      - filename: MySQL-shared-5.6.19-1.el6.x86_64.rpm
      - filename: MySQL-shared-compat-5.6.19-1.el6.x86_64.rpm
      - filename: MySQL-client-5.6.19-1.el6.x86_64.rpm
      - filename: MySQL-devel-5.6.19-1.el6.x86_64.rpm
  tasks:
    - file: path={{ mysql_download_dest_dir }} state=directory
    - get_url: >
        url={{ mysql_download_base_url }}/{{ item.filename }}
        dest={{ mysql_download_dest_dir }}
      with_items: mysql_common_rpms
    - yum: name="{{ mysql_download_dest_dir }}/{{ item.filename }}" state=present
      with_items: mysql_common_rpms
```

こうすると `with_items` の配列の要素それぞれについて `yum` モジュールが実行され、エラーを回避できます。

```shell-session
$ ansible-playbook -vvvv cent65-mysql.yml
…(略)…
<cent65> REMOTE_MODULE yum name="/usr/local/src/MySQL-shared-5.6.19-1.el6.x86_64.rpm" state=present
…(略)…
<cent65> REMOTE_MODULE yum name="/usr/local/src/MySQL-shared-compat-5.6.19-1.el6.x86_64.rpm" state=present
…(略)…
```

# get_urlの使い方の改善

上記のplaybookはさらに改善することができます。
[Ansibleのget_urlはdestをファイル名にしてsha256sumを指定するのがお勧め - Qiita](http://qiita.com/hnakamur/private/8b8870c640205d438826)に書きましたので、そちらをご参照ください。