Ansibleのloopとregistered変数設計入門|with_items廃止後の繰り返し処理と結果制御パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Ansible > Ansibleのloopとregistered変数設計入門|with_items廃止後の繰り返し処理と結果制御パターン
「Ansibleで複数のパッケージやユーザーに同じ設定を一括で適用したい。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 の組み合わせで「失敗したアイテムにだけ再処理をかける」設計が可能


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Ansibleのloopとは——with_itemsが廃止になった背景

Ansible初期には、繰り返し処理のために with_itemswith_dictwith_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

Playbook自体は動きますが、将来のメジャーバージョンで削除される可能性があるため、新規作成のPlaybookはすべて 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.nameitem.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)

本番のPlaybookでは、ループ対象が増えるほどログが肥大化します。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構造を理解する

loopregister: を組み合わせると、各アイテムの実行結果がまとめて格納されます。重要なのは、通常の 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 }}"

2点だけ注意します。
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)

nginx と mariadb はすでに稼働中のため skipping、crond だけが changed(起動処理を実行)になりました。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
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"

この変数に対してPlaybookで 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)

deploy ユーザーに2本の鍵が配置され、monitor ユーザーに1本が配置されました。ユーザー数×鍵数の組み合わせが自動で展開されます。

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
loopregister の組み合わせは、Playbookの複雑な条件制御を実現するうえで欠かせない設計パターンです。まずは loop + loop_control.label のセットをすべてのループタスクに適用することから始めてください。register + when による条件制御は、サービス再起動が必要なノードのみに処理をかけるケースなど、現場のサーバー管理で頻繁に登場します。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
Ansible実践ハンズオンの詳細を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。