with_items を使って書いたら、実行時に非推奨の警告が出てきた」こういった場面に直面したことのある方は多いはずです。Ansible 2.5(2018年リリース)から
with_* 系のループ構文は非推奨(deprecated)となり、現在は loop: キーワードに統一されています。古い書き方でも一応動くため、気づかないまま使い続けているケースが現場でも少なくありません。この記事では、
loop の基本的な書き方から始め、loop_control によるログ制御、register 変数でループ結果を受け取り条件分岐に活用する設計パターンまでを解説します。RHEL 9.4 / Ubuntu 24.04 LTS + Ansible 2.14 で動作確認済みです。この記事のポイント
・with_items は非推奨。loop: キーワードが現在の標準的な繰り返し構文
・loop_control の label でログ出力を短縮し、index_var でインデックス番号を取得できる
・register 変数はループ全体の結果を .results リストとして保持する
・register + when の組み合わせで「失敗したアイテムにだけ再処理をかける」設計が可能
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
Ansibleのloopとは——with_itemsが廃止になった背景
Ansible初期には、繰り返し処理のためにwith_items、with_dict、with_nested など用途ごとに異なるキーワードが存在しました。実装が複数に分散してメンテナンスコストが増大したため、Ansible 2.5 で loop: キーワードに一本化されました。まず、現在の環境でのAnsibleバージョンを確認します。
$ ansible --version ansible [core 2.14.17] config file = /etc/ansible/ansible.cfg configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules'] ansible python module location = /usr/lib/python3.11/site-packages/ansible ansible collection location = /root/.ansible/collections:/usr/share/ansible/collections executable location = /usr/bin/ansible python version = 3.11.7 (main, Feb 12 2024, 11:00:00) jinja version = 3.1.2 libyaml = True
with_items 形式で書いたPlaybookを実行すると、次のような警告が出ます。[DEPRECATION WARNING]: with_items is deprecated. You can use the 'loop' keyword instead. For more details refer to the following URL: https://docs.ansible.com/ansible-core/2.14/playbook_guide/playbooks_loops.html
loop: で書くことを推奨します。loopの基本的な書き方——リストと辞書の2パターン
1. シンプルなリストをループする
もっとも基本的な使い方です。インストールするパッケージ名のリストをloop: に渡します。# install-packages.yml - name: 開発ツールをまとめてインストールする hosts: webservers become: yes tasks: - name: 必要パッケージを導入する ansible.builtin.dnf: name: "{{ item }}" state: present loop: - git - vim - wget - unzip
{{ item }} が各ループ回で現在のアイテムに置き換わります。実行すると次のように表示されます。TASK [必要パッケージを導入する] *** changed: [192.0.2.10] => (item=git) changed: [192.0.2.10] => (item=vim) ok: [192.0.2.10] => (item=wget) changed: [192.0.2.10] => (item=unzip)
ok は「すでにインストール済み・変更なし」を意味します。Ansibleのべき等性が正しく機能している証拠です。changed は今回新規にインストールされたことを示します。2. 辞書(ハッシュ)をループする
複数の属性をまとめて渡したいときは、辞書形式でアイテムを定義します。item.キー名 のドット記法でアクセスします。- name: アプリケーションユーザーを作成する ansible.builtin.user: name: "{{ item.name }}" uid: "{{ item.uid }}" groups: "{{ item.groups }}" create_home: yes shell: /bin/bash loop: - { name: deploy, uid: 1100, groups: "wheel" } - { name: monitor, uid: 1101, groups: "nobody" } - { name: appuser, uid: 1102, groups: "wheel" }
item.name、item.uid のように属性を参照します。item['name'] のブラケット記法でも同じ結果になります。loop_controlで繰り返し動作を制御する
loop_control: はループの動作をカスタマイズするためのセクションです。主に3つのオプションをよく使います。1. labelでログ出力を読みやすくする
辞書形式のアイテムをループすると、デフォルトのログには辞書全体が出力されて非常に読みにくくなります。# label未設定の場合のログ(見づらい) changed: [192.0.2.10] => (item={'name': 'deploy', 'uid': 1100, 'groups': 'wheel'}) changed: [192.0.2.10] => (item={'name': 'monitor', 'uid': 1101, 'groups': 'nobody'})
label: を設定すると、ログに表示される情報を絞り込めます。- name: アプリケーションユーザーを作成する ansible.builtin.user: name: "{{ item.name }}" uid: "{{ item.uid }}" groups: "{{ item.groups }}" loop: - { name: deploy, uid: 1100, groups: "wheel" } - { name: monitor, uid: 1101, groups: "nobody" } loop_control: label: "{{ item.name }}"
# label設定後のログ(スッキリ読みやすい) changed: [192.0.2.10] => (item=deploy) changed: [192.0.2.10] => (item=monitor)
label: は運用初日から設定しておくことを強くすすめます。2. index_varでインデックスを取得する
ループの実行回数(0始まりのインデックス)を変数として取得できます。設定ファイルに連番を付けたい場合などに使います。- name: 設定ファイルを番号付きで配置する ansible.builtin.template: src: "app-config.j2" dest: "/etc/app/config_{{ idx + 1 }}.conf" owner: appuser group: appuser mode: "0640" loop: "{{ config_targets }}" loop_control: label: "{{ item }}" index_var: idx
idx は0から始まるため、{{ idx + 1 }} とすれば1始まりの連番が取得できます。3. pauseでAPI制限に対応する
クラウドAPIや外部サービスへの連続リクエストがRate Limitに引っかかる場合、ループ間に待機時間を挿入できます。- name: EC2インスタンスにタグを付与する(API制限対応) amazon.aws.ec2_tag: region: "{{ aws_region }}" resource: "{{ item.instance_id }}" tags: Environment: production ManagedBy: ansible loop: "{{ ec2_instances }}" loop_control: label: "{{ item.instance_id }}" pause: 2
pause: 2 で各アイテム処理の後に2秒待機します。単位は秒です。registered変数でループ結果を受け取る設計
1. registerの基本と.results構造を理解する
loop と register: を組み合わせると、各アイテムの実行結果がまとめて格納されます。重要なのは、通常の register と違い、ループ時の結果は必ず .results リストの中に入るという点です。- name: サービスの稼働状態を確認する ansible.builtin.command: cmd: "systemctl is-active {{ item }}" register: service_check loop: - nginx - mariadb - crond ignore_errors: yes
debug モジュールで service_check の中身を確認すると次の構造になっています。ok: [192.0.2.10] => { "service_check": { "results": [ { "item": "nginx", "rc": 0, "stdout": "active", "stderr": "", "failed": false, "changed": true }, { "item": "mariadb", "rc": 0, "stdout": "active", "stderr": "", "failed": false, "changed": true }, { "item": "crond", "rc": 3, "stdout": "inactive", "stderr": "", "failed": false, "changed": true } ] } }
・item:元のループアイテム(nginx / mariadb / crond)
・rc:コマンドの終了コード(0=成功、0以外=失敗)
・stdout:標準出力の内容
・failed:タスクが失敗扱いかどうか(boolean)
2. registerとwhenを組み合わせた条件制御
前のタスクで取得したservice_check.results を別のタスクでループし、停止しているサービスだけを起動する設計パターンです。これが register の最も実務的な使い方です。- name: 停止しているサービスを起動する ansible.builtin.service: name: "{{ item.item }}" state: started enabled: yes loop: "{{ service_check.results }}" when: item.rc != 0 loop_control: label: "{{ item.item }}"
1つ目は
item.item という記法です。外側の item がループの現在の要素(resultsリストの1エントリ)を指し、そのエントリの中に元のループアイテム名が item フィールドとして格納されているため、item.item という形になります。2つ目は
when: item.rc != 0 です。systemctl is-active は稼働中のサービスに対して終了コード0を返し、停止中のサービスには3を返します。この条件で「停止中のアイテムにだけ service タスクを実行する」という絞り込みができます。TASK [停止しているサービスを起動する] *** skipping: [192.0.2.10] => (item=nginx) skipping: [192.0.2.10] => (item=mariadb) changed: [192.0.2.10] => (item=crond)
skipping、crond だけが changed(起動処理を実行)になりました。
Ansible実践ハンズオンの詳細を見る >>
複数リストのネスト処理——subelementsの実践
1人のユーザーに複数のSSH公開鍵を配置するような「1対多」の構造には、subelements ルックアッププラグインを使います。まず変数定義を
group_vars/all.yml に書きます。# group_vars/all.yml users: - name: deploy uid: 1100 ssh_keys: - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAbcdefghijk deploy@pc" - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIXyzabcdefgh deploy@work-pc" - name: monitor uid: 1101 ssh_keys: - "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMonitorKey1 monitor@ops-pc"
subelements を使います。- name: ユーザーの公開鍵を配置する ansible.posix.authorized_key: user: "{{ item.0.name }}" key: "{{ item.1 }}" state: present loop: "{{ users | subelements('ssh_keys') }}" loop_control: label: "{{ item.0.name }}"
subelements('ssh_keys') は users リストと ssh_keys サブリストの組み合わせを生成します。item.0 が親オブジェクト(ユーザー辞書)、item.1 がサブアイテム(鍵文字列)です。TASK [ユーザーの公開鍵を配置する] *** changed: [192.0.2.10] => (item=deploy) changed: [192.0.2.10] => (item=deploy) changed: [192.0.2.10] => (item=monitor)
with_*からloopへの移行パターンと注意点
1. with_itemsの落とし穴——フラット化の挙動変化
with_items はネストしたリストを自動でフラット化しますが、loop はしません。この違いを知らずに機械的に置き換えると、意図しない動作になります。# with_itemsの場合: [1, 2, 3, 4] にフラット化される - with_items: - [1, 2] - [3, 4] # loopの場合: [[1, 2], [3, 4]] のままでネストが維持される - loop: - [1, 2] - [3, 4]
loop でフラット化するには flatten フィルターを明示的に使います。- loop: "{{ [[1, 2], [3, 4]] | flatten(1) }}"
flatten(1) の数値は展開の深さです。1でネスト1段分だけを展開します。2. with_dictの代替——dict2itemsフィルター
辞書をループするために使っていたwith_dict は、dict2items フィルターで置き換えます。# vars定義 vars: service_ports: nginx: 80 mariadb: 3306 redis: 6379 # with_dictの場合(非推奨) - with_dict: "{{ service_ports }}" # loopの場合(現在の標準) - loop: "{{ service_ports | dict2items }}"
dict2items 変換後は item.key でキー(nginx / mariadb / redis)、item.value で値(80 / 3306 / 6379)にアクセスします。- name: firewalldでポートを開放する ansible.posix.firewalld: port: "{{ item.value }}/tcp" permanent: yes state: enabled loop: "{{ service_ports | dict2items }}" loop_control: label: "{{ item.key }}:{{ item.value }}"
3. with_nestedの代替——productフィルター
複数のリストを掛け合わせてすべての組み合わせをループしたい場合、with_nested の代わりに product フィルターを使います。# 全環境×全サービスの組み合わせをループ - name: 環境ごとにサービスを確認する ansible.builtin.debug: msg: "環境: {{ item.0 }} / サービス: {{ item.1 }}" loop: "{{ ['dev', 'stg', 'prod'] | product(['nginx', 'mariadb']) | list }}"
ok: [192.0.2.10] => (item=['dev', 'nginx']) => {"msg": "環境: dev / サービス: nginx"} ok: [192.0.2.10] => (item=['dev', 'mariadb']) => {"msg": "環境: dev / サービス: mariadb"} ok: [192.0.2.10] => (item=['stg', 'nginx']) => {"msg": "環境: stg / サービス: nginx"} ok: [192.0.2.10] => (item=['stg', 'mariadb']) => {"msg": "環境: stg / サービス: mariadb"} ok: [192.0.2.10] => (item=['prod', 'nginx']) => {"msg": "環境: prod / サービス: nginx"} ok: [192.0.2.10] => (item=['prod', 'mariadb']) => {"msg": "環境: prod / サービス: mariadb"}
本記事のまとめ
| やりたいこと | 書き方 |
|---|---|
| シンプルなリストをループ | loop: [a, b, c] |
| 辞書をループ(属性アクセス) | loop: [{key: val}, ...]、item.key でアクセス |
| 辞書変数をループ | loop: "{{ mydict | dict2items }}"、item.key / item.value |
| ネストリストの展開 | loop: "{{ nested_list | flatten(1) }}" |
| 複数リストの直積 | loop: "{{ [list1, list2] | product | list }}" |
| 1対多構造のループ | loop: "{{ users | subelements('ssh_keys') }}" |
| ログを短くする | loop_control: label: "{{ item.name }}" |
| インデックスを取得 | loop_control: index_var: idx |
| ループ結果を次のタスクで使う | register: result、次のタスクで loop: "{{ result.results }}" |
| 失敗アイテムのみ再処理 | loop: "{{ result.results }}" + when: item.rc != 0 |
loop と register の組み合わせは、Playbookの複雑な条件制御を実現するうえで欠かせない設計パターンです。まずは loop + loop_control.label のセットをすべてのループタスクに適用することから始めてください。register + when による条件制御は、サービス再起動が必要なノードのみに処理をかけるケースなど、現場のサーバー管理で頻繁に登場します。Ansible実践ハンズオンの詳細を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:AnsibleでDockerコンテナを管理する方法|community.dockerコレクションでコンテナのデプロイ・停止・削除を自動化する手順
- この記事の属するカテゴリ:Ansibleへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます