Ansibleのネストされたループで「The loop variable item is already in use」エラーを修正する

intermediate🔧 Ansible2026-05-07| Ansible 2.5以降、任意のOS(Linux/macOS)、include_tasksやネストされたループ構造を持つロール内で発生しやすい

Error Message

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_control#loop_var#ロール

シナリオ

デプロイ用のプレイブックが数週間問題なく動いていました。ユーザーのリストをループする新しいロールを追加する — よくある作業です。ところが、プレイ実行の途中で全体がクラッシュしてしまいました:

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の代わりにvhostitemの代わりに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時に原因不明のエラーで失敗するプレイブックの違いを生み出します。

Related Error Notes