シナリオ
デプロイ用のプレイブックが数週間問題なく動いていました。ユーザーのリストをループする新しいロールを追加する — よくある作業です。ところが、プレイ実行の途中で全体がクラッシュしてしまいました:
The loop variable 'item' is already in use. You should set the `loop_var` value in the `loop_control` option for the task to something different to avoid this error.
外側のプレイブックは何も変更していません。ロールも問題なさそうです。では、何が壊れたのでしょうか?
原因
Ansibleはすべてのloopディレクティブに対して、デフォルトのループ変数としてitemを使用します。2つのタスクが同時にその名前を共有することはできません。
ループするタスクがinclude_tasks(または内部でループするロール)を呼び出すと、外側と内側の両方のタスクがitemを取り合います。Ansible 2.5以降はこの衝突を検出し、外側の値をサイレントに上書きする代わりに処理を停止します。サイレントな上書きは、エラーメッセージなしに誤った結果を生むからです。
ほぼ毎回この問題を引き起こす構成:
- 外側のプレイブックが環境またはユーザーをループしている
- そのループが
include_tasksまたはinclude_roleを呼び出している - インクルードされたファイルまたはロールにも
loopが含まれている
# playbook.yml — 外側のループ
- name: Configure users
include_tasks: setup_user.yml
loop: "{{ users }}"
# setup_user.yml — 内側のループ(競合)
- name: Add SSH keys for user
authorized_key:
user: "{{ item.name }}"
key: "{{ item.key }}"
loop: "{{ item.ssh_keys }}" # 'item' は外側のループで既に使用中!
Ansibleは外側のループがitemを使用していることを認識し、内側のループでも再利用しようとして、処理を中断します。
手っ取り早い修正方法
外側のループ、内側のループ、あるいは両方に、カスタムのloop_varを持つloop_controlを追加します。外側のループをリネームする方法が通常はすっきりします — そのタスクは直接自分で管理しており、わかりやすい名前を自由に付けられるからです。
方法1:外側のループ変数をリネームする
# playbook.yml
- name: Configure users
include_tasks: setup_user.yml
loop: "{{ users }}"
loop_control:
loop_var: user_item
# setup_user.yml — 内側のループは 'item' を自由に使える
- name: Add SSH keys for user
authorized_key:
user: "{{ user_item.name }}"
key: "{{ key }}"
loop: "{{ user_item.ssh_keys }}"
loop_control:
loop_var: key
方法2:内側のループ変数をリネームする
外側のループを変更できない場合 — たとえばサードパーティのロールの内部にある場合 — 代わりに内側のループをリネームします:
- name: Add SSH keys for user
authorized_key:
user: "{{ item.name }}"
key: "{{ ssh_key }}"
loop: "{{ item.ssh_keys }}"
loop_control:
loop_var: ssh_key
ロールへの恒久的な対処法
ロールは再利用されることを前提に作られています。デフォルトのitemを使用するロールは、いつか同じようにループする呼び出し元と衝突します。これは時限爆弾のようなバグです。
対処法:ロールのループ変数には常にロール固有の名前を付けます。一般的な慣習として、ロール名または短い略称をプレフィックスとして付けます — itemの代わりにvhost、itemの代わりにpkg、といった具合です。
# roles/nginx_vhost/tasks/main.yml
- name: Create vhost configs
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ vhost.name }}.conf"
loop: "{{ nginx_vhosts }}"
loop_control:
loop_var: vhost # ロール固有の名前で衝突しない
label: "{{ vhost.name }}"
labelフィールドは便利なおまけです。指定しない場合、Ansibleはイテレーションごとにフルのdictを出力します — JSONの壁になりがちです。指定すると、vhost.nameのように定義した内容だけが表示されます。CIのログで確認するのがずっと楽になります。
ループ内での include_role の扱い
include_roleのループも同じように機能します。include_roleタスク自体にloop_controlを追加してください:
- name: Deploy application per environment
include_role:
name: deploy_app
loop: "{{ environments }}"
loop_control:
loop_var: env
label: "{{ env.name }}"
次にdeploy_appの内部を確認してください — いずれのタスクもenvを自分のループ変数として使用していないことを確かめます。そうしないと元の状態に逆戻りします。
3段階のネスト
3重にネストしたループは珍しいですが、複雑なプロビジョニングロールではそうなることもあります。各レベルには例外なく異なる変数名が必要です:
# レベル1
- include_tasks: per_region.yml
loop: "{{ regions }}"
loop_control:
loop_var: region
# per_region.yml — レベル2
- include_tasks: per_host.yml
loop: "{{ region.hosts }}"
loop_control:
loop_var: host
# per_host.yml — レベル3
- name: Apply firewall rules
ufw:
rule: allow
port: "{{ port }}"
loop: "{{ host.allowed_ports }}"
loop_control:
loop_var: port
修正の確認
実際のホストに対して実行する前に、構文チェックとドライランを行いましょう:
# 静的解析で変数名の衝突を検出する
ansible-playbook playbook.yml --syntax-check
# ドライラン — 詳細出力でループ変数名の動作を確認する
ansible-playbook playbook.yml --check -v
詳細出力では、タスクのラベルにitemの代わりにカスタムのループ変数名が表示されるはずです。labelを設定した場合は、生のオブジェクトではなくその文字列が表示されます。正常な実行結果は次のようになります:
TASK [Add SSH keys for user] ***
ok: [server1] => (item=deploy_key)
ok: [server1] => (item=backup_key)
衝突エラーなし、カスタム名も確認できた — 完了です。
クイックリファレンス
loop_control:
loop_var: my_var # 'item' を 'my_var' にリネーム
label: "{{ my_var.name }}" # ログ出力をわかりやすくする
index_var: idx # オプション:ループインデックス(0, 1, 2...)
pause: 1 # オプション:イテレーション間の待機秒数
覚えておくべきルールは1つ:ロール内のすべてのloopにはloop_varを定義すること。たった1行の追加です。そしてその1行が、正午に正常稼働するプレイブックと、深夜2時に原因不明のエラーで失敗するプレイブックの違いを生み出します。

