SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-07

Ansible playbookとroleの使い分けは?

Ansible playbookとroleの違いを整理します。約100行やタスクのコピーが切り替えの目安です。roleのディレクトリ構成、ansible-galaxy init、呼び出し方、変数の優先順位を解説します。

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

Ansible playbook と role の違い

Ansible playbook は、ansible-playbook で実行するファイルです。対象ホストのグループと、そのホストで実行する処理を対応付けます。Ansible role は、tasks、templates、handlers、default variables を固定のレイアウトで格納するディレクトリです。playbook から名前で呼び出します。どちらでもタスクの構文は同じです。そのため、表現できる処理の違いではありません。再利用性の違いです。

まずは、単一の playbook から始めます。tasks: のリストを含む site.yml が、最初の自動化には適した構成です。多くの場合、この構成は予想以上に長く使えます。同じタスクのまとまりを別のホストグループでも実行する必要が生じた場合、またはファイルが約 100 行を超えてスクロールしてもタスクを見つけにくくなった場合に、role に変換します。

まだ playbook を作成していない場合は、単一の VPS を対象に最初の playbook を始める から始め、ファイルが大きくなってきたら戻ってきてください。

フラットな playbook が適切な場合

作業を 1 回だけ実行する場合、1 台のホストだけを対象にする場合、または他の人が読まない場合は、フラットな playbook が適しています。単一のアプリケーションサーバーをプロビジョニングする作業や、メンテナンス時間帯の前に 1 台のサーバーへパッチを適用する作業に、ディレクトリツリーは必要ありません。role を使うと、7 個のディレクトリと 1 層の間接化が追加されます。呼び出し元が隣にある playbook だけなら、その間接化には利点がなく、実際に実行される内容を読むたびに移動が必要になります。

フラットな playbook が適切でなくなるタイミングは明確で、簡単に見つけられます。タスクのブロックを 2 つ目の playbook にコピーした時点です。そのコピーがシグナルになります。それ以降、すべての修正を 2 箇所に反映する必要があり、いつか片方にしか反映されなくなります。

実際にロールディレクトリに含まれるもの

roles/common/
  defaults/main.yml
  vars/main.yml
  tasks/main.yml
  handlers/main.yml
  templates/99-hardening.conf.j2
  files/
  meta/main.yml
  • tasks/main.yml はエントリーポイントです。Ansible はロールが呼び出されるとこのファイルを実行します。その他のディレクトリはすべて任意です。
  • defaults/main.yml には、呼び出し側が上書きすることを想定した変数を格納します。Ansible で最も優先度が低い変数ソースであるため、ほぼすべての別の設定が優先されます。
  • vars/main.yml には、呼び出し側が上書きすることを想定していない変数を格納します。インベントリより優先度が高く、強い指定です。使用は慎重にしてください。
  • handlers/main.yml には、notify によって実行されるタスクを格納します。handler は play の最後に 1 回だけ実行されます。通知するタスクの数は関係ありません。
  • files/ には copy module が内容を変更せずにコピーするファイルを格納し、templates/ には template module がレンダリングする Jinja2 template を格納します。ロール内では、どちらもパスを付けずにファイル名だけで参照します。Ansible は最初にロール自身のディレクトリを検索するためです。
  • meta/main.yml にはロールの依存関係を宣言します。また、Ansible Galaxy が読み取るメタデータも格納します。

この構成は単なるスタイル上の選択ではありません。Ansible はこれらの正確なパスを検索するため、roles/common/template/(単数形)に配置した template は見つかりません。

ansible-galaxy init で共通ロールを作成する

mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles common

これにより、roles/common 以下にロールのスケルトン全体が作成されます。使用しないディレクトリや、--- だけを含む main.yml のスタブも作成されます。空のままにするものは削除してください。空の vars/main.yml があっても Ansible の動作には影響しませんが、ロールで実際に必要なファイルが分かりにくくなります。

次に、処理を実行するファイルを記述します。まず defaults から始めます。defaults はロールの公開インターフェースだからです。

# roles/common/defaults/main.yml
---
common_packages:
  - ufw
  - fail2ban
  - unattended-upgrades
common_admin_group: admins
common_permit_root_login: "no"
common_password_authentication: "no"

"no""yes" は引用符で囲んでください。Ansible は PyYAML で YAML を解析します。PyYAML は引用符のない no をブール値の false と解釈するため、生成される設定行が PermitRootLogin False になり、sshd が拒否します。引用符により、値が文字列として保持されます。

# roles/common/tasks/main.yml
---
- name: Install the base packages
  ansible.builtin.apt:
    name: "{{ common_packages }}"
    state: present
    update_cache: true
    cache_valid_time: 3600

- name: Create the admin group
  ansible.builtin.group:
    name: "{{ common_admin_group }}"
    state: present

- name: Install the sshd hardening drop-in
  ansible.builtin.template:
    src: 99-hardening.conf.j2
    dest: /etc/ssh/sshd_config.d/99-hardening.conf
    owner: root
    group: root
    mode: "0644"
    validate: /usr/sbin/sshd -t -f %s
  notify: Restart sshd
# roles/common/handlers/main.yml
---
- name: Restart sshd
  ansible.builtin.service:
    name: ssh
    state: restarted
# roles/common/templates/99-hardening.conf.j2
# Managed by Ansible. Local edits are overwritten on the next run.
PermitRootLogin {{ common_permit_root_login }}
PasswordAuthentication {{ common_password_authentication }}

Debian と Ubuntu では systemd の unit 名が ssh で、RHEL 系システムでは sshd です。誤った unit 名を指定した handler は、テンプレートに実際の変更があった場合にだけ失敗します。そのため、問題が数週間後に初めて発覚することがあります。

このタスクで最も重要なのが validate 行です。Ansible はテンプレートを一時ファイルにレンダリングし、そのファイルのパスを %s に設定してコマンドを実行します。コマンドが 0 で終了した場合にだけ、宛先ファイルが置き換えられます。テンプレートに無効なディレクティブを記述して再実行してみてください。タスクは failed to validate で失敗し、実際の /etc/ssh/sshd_config.d/99-hardening.conf は変更されません。そのため、引き続きサーバーへログインできます。このチェックは構文だけを検証するわけではない点に注意してください。sshd -t がホストキーを読み取れない場合、sshd: no hostkeys available -- exiting. で終了し、Ansible は同じ failed to validate を報告します。テンプレートを原因と判断する前に、モジュールの msg を確認してください。

ロールを呼び出すプレイブック

# site.yml
---
- name: Base configuration for every server
  hosts: all
  become: true
  roles:
    - common
# inventory.ini
[local]
localhost ansible_connection=local
ansible-playbook -i inventory.ini site.yml

プレイの最後には、要約に failed=0 が表示されるようにします。呼び出し時に展開形式でパラメーターを渡すと、1 つのロールを 2 つのホストグループで使用できます。

  roles:
    - role: common
      common_admin_group: ops
      common_permit_root_login: prohibit-password

ほとんどの人が驚く実行順序の規則があります。プレイには pre_tasksrolestaskspost_tasks を記述できますが、ファイル内の記述順に関係なく、Ansible はこの順序で実行します。tasks:roles: より上に記述しても、ロールが先に実行されます。そのため、ロールより前に実行する処理は pre_tasks: に記述し、tasks: の先頭には記述しません。

- name: Ordering demonstration
  hosts: local
  gather_facts: false
  pre_tasks:
    - name: Runs first
      ansible.builtin.debug:
        msg: pre
  roles:
    - common
  tasks:
    - name: Runs after the role
      ansible.builtin.debug:
        msg: task
  post_tasks:
    - name: Runs last
      ansible.builtin.debug:
        msg: post

roles: キーではなくタスクリストの中からロールを呼び出すには、import_role または include_role を使用します。

  tasks:
    - name: Static, read when the playbook is parsed
      ansible.builtin.import_role:
        name: common

    - name: Dynamic, resolved when the task runs
      ansible.builtin.include_role:
        name: postgres
      when: "'db' in group_names"

import_role は静的です。Ansible は解析時にロールを読み込み、そのタスクをプレイの一部にします。そのため、ansible-playbook --list-tasks site.yml にはタスクが一覧表示され、インポートに付けたタグは内部のすべてのタスクに適用されます。include_role は動的です。タスクが実行されるまで何も読み込まれないため、変数やループからロール名を指定できます。ただし、その代わりにこれらのタスクは --list-tasks--start-at-task から見えません。

ここには注意点があります。include_role タスクの when: は、インクルードしたロールの defaults/main.yml がスコープに入る前に評価されます。インクルード側に when: common_packages | length > 0 と記述すると、その変数がインクルード対象のロール内で定義されていても、実行は 'common_packages' is undefined で停止します。対処方法は、切り替え用の値をロールの外に移動することです。どこからでも参照できる group_vars/all.yml に配置し、ロールのデフォルト値はロール自身が使用する値に限定します。

どの変数が優先されるか: defaults、group_vars、vars、extra vars

Ansible には、変数の優先順位が20以上あります。実際の議論のほとんどは、次の4つで決まります。以下では、弱い順に示します。

  • roles/<name>/defaults/main.yml は下位に位置します。ほかの場所で設定した値のほぼすべてに上書きされるため、role の調整可能な値を置く場所として適しています。
  • group_vars/host_vars/ は中間に位置します。サイト固有の値はここに置き、role のデフォルト値を明確に上書きします。
  • roles/<name>/vars/main.ymlhost_vars より上位に位置します。ここに設定した値は inventory から上書きできません。サービス名と一致させる必要があるパッケージ名など、role 内で整合性を保つ必要がある値に限定してください。
  • 呼び出し箇所で渡した role パラメーターは vars/main.yml より優先され、コマンドラインで指定した -e は、role パラメーターを含むすべての値より優先されます。

この動作は、約1分で確認できます。小さな role にデフォルト値と1つの role 変数を設定し、host_vars に同じ名前の値を設定します。

# roles/prec/defaults/main.yml
---
prec_tunable: from-defaults
prec_internal: from-defaults
# roles/prec/vars/main.yml
---
prec_internal: from-rolevars
# host_vars/localhost.yml
---
prec_tunable: from-hostvars
prec_internal: from-hostvars
# roles/prec/tasks/main.yml
---
- name: Show which value survived
  ansible.builtin.debug:
    msg: "tunable={{ prec_tunable }} internal={{ prec_internal }}"
ansible-playbook -i inventory.ini prec.yml
ansible-playbook -i inventory.ini prec.yml -e prec_internal=from-cli

最初の実行では tunable=from-hostvars internal=from-rolevars と表示されます。inventory の値は role のデフォルト値には勝ちますが、role 変数には負けます。2回目の実行では internal=from-cli と表示されます。extra vars は最上位に位置し、それより下位の値は上書きできないためです。このため、-e は一度きりの実行には適していますが、継続して使用するスクリプトでは不適切です。リポジトリ内で検討したすべての決定よりも、暗黙に優先されるためです。

基本原則は、変更可能にしたい値を defaults/ に置くことです。vars/ に置くと、今後その role を使うすべてのユーザーに対して、inventory から変更できない値であることを示します。意図した動作になる場合もありますが、多くの場合は設定ミスです。

ロールが冪等であることを確認する: 2 回実行します

信頼できる Ansible の実行では、2 回目も同じ結果になり、変更がなかったと報告されます。playbook を 2 回実行し、recap を確認します。

ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml

2 回目の recap は次のようになります。

PLAY RECAP *********************************************************************
localhost   : ok=4  changed=0  unreachable=0  failed=0  skipped=0  rescued=0  ignored=0

changed=0 は、すべてのモジュールが現在の状態を確認し、作業がすでに完了していると判断したことを示します。2 回目の実行で changed=2 が表示される場合、2 つのタスクが状態の違いを認識できていないため、ファイルの書き換えとサービスの再起動を繰り返します。通常の原因は command または shell です。Ansible には、任意のコマンドが何を実行したかを判断する方法がないためです。

# traps.yml
---
- name: Command modules do not know what they changed
  hosts: local
  gather_facts: false
  tasks:
    - name: This appends a line on every run
      ansible.builtin.shell: "echo run >> /tmp/grow.txt"

    - name: This appends a line only once
      ansible.builtin.shell: "echo run >> /tmp/guarded.txt"
      args:
        creates: /tmp/guarded.txt

この playbook を 2 回実行し、wc -l /tmp/grow.txt /tmp/guarded.txt が含まれる行数を数えます。/tmp/grow.txt には 2 行、/tmp/guarded.txt には 1 行が含まれます。2 回目の実行では、条件付きのタスクはまったく実行されません。その結果には skipped, since /tmp/guarded.txt exists というメッセージが含まれます。これは、creates によってモジュールが最初に確認できる生成物を指定しているためです。コマンドがそのような生成物を残さない場合は、その出力を登録し、changed_when を使って自分で判断します。

ansible-playbook --check --diff site.yml は変更を行わずに変更内容を予測し、--diff はテンプレートが書き換える正確な行を出力します。出力は、次の点に注意して読み取ります。shellcommand のタスクは check mode ではスキップされるため、変更なしに見える計画でも、実際の作業が隠れている可能性があります。

Ansible で role が見つからないと表示される理由

Ansible は、playbook ファイルと同じディレクトリにある roles/ を確認し、その後で roles_path を確認します。検索基準はシェルのカレントディレクトリではなく、playbook です。

ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely

このメッセージは、site.ymlroles/ の場所が一致していないことを示しています。試行したパスも表示されます。2 つを同じディレクトリに配置してください。親ディレクトリから実行しても問題ありません。基準になるのは playbook のパスです。

ansible-playbook -i infra/inventory.ini infra/site.yml

同じ問題には、気付きにくい別の形もあります。カレントディレクトリが全ユーザーによる書き込み可能な状態だと、Ansible はそのディレクトリにある ansible.cfg を無視します。これは、同じホスト上の任意のユーザーがそこに設定ファイルを置き、実行内容を変更できるためです。

[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.

その結果、roles_pathinventory の設定がひそかに無効になり、role の検索が role とは無関係な理由で失敗します。ansible --version は実際に読み込んだ config file を表示し、ansible-config dump --only-changed は組み込みのデフォルト値と異なるすべての設定を表示します。設定が存在しないかのように実行が動作する場合は、必ず両方を確認してください。

ロールの共有: requirements.yml と固定バージョン

他の人が作成したロールはコピーせずにインストールします。1 回だけ宣言します。

# requirements.yml
---
roles:
  - name: postgres
    src: https://github.com/example/ansible-role-postgres
    scm: git
    version: v1.4.0
ansible-galaxy install -r requirements.yml -p galaxy_roles

必ず version を設定してください。これを指定しないと、コマンドを実行した時点のデフォルトブランチの内容が取得されます。そのため、前月に動作したデプロイが、自分のリポジトリを変更していないのに失敗することがあります。roles_path にはダウンロード先のディレクトリを指定し、そのディレクトリは git の管理対象から除外します。

# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_roles

Playbook と同じ場所にある roles/ 内のロールも見つかります。このパスは roles_path に加えて常に検索されるためです。これにより、自作のロールはリポジトリにコミットしてレビューでき、サードパーティー製のロールはタグでバージョンを固定した再現可能なダウンロードとして管理できます。

ロールが答えにならない範囲

ロールは、1 回の Ansible 実行内で再利用する単位です。プロバイダー側でサーバーや DNS レコードを作成するものではありません。ロールにその処理まで担わせると、誰も保守したくない playbook になってしまいます。開始前に、Ansible と Terraform の作業分担を確認してください。ロールは inventory の設計も置き換えません。数台を超えると、タスクをどのファイルに分けるかよりも、サーバーをどのようにグループ化し、接続するかのほうが重要になります。

この common ロールが適用するセキュリティ強化についても、個別に判断する必要があります。上記の drop-in では 2 つのディレクティブだけを設定しています。所有するすべてのホストでロールに何を含めるか決める前に、実際に変更する価値のある SSH 設定と、Ubuntu にセキュリティ更新を自動適用させる方法を確認してください。

FAQ

Ansible playbook を role にするのはどのような場合ですか?

同じタスクブロックを 2 つ目の play で実行する必要がある場合、または 2 つ目のホストグループに対して実行する必要がある場合です。playbook 間でタスクをコピーし始めたら、その合図です。その時点から、修正を 2 箇所に適用する必要が生じ、いつか 1 箇所にしか適用されなくなります。おおむね 100 行未満で、常に 1 つのグループだけを対象とする単一の playbook であれば、role にしても利点はありません。ディレクトリが増えることで、かえって読みにくくなります。

同じ play のタスクより前に role は実行されますか?

はい。Ansible は pre_tasks、次に roles: に記載されたすべての内容、続いて tasks:、最後に post_tasks: を実行します。ファイル内でこれらのキーが登場する順序は無視されます。tasks:roles: より上に記述しても、そのタスクが先に実行されるわけではありません。role より前に実行する必要がある処理は、pre_tasks: に記述してください。

group_vars の値で role の値が上書きされないのはなぜですか?

変数が defaults/main.yml ではなく、role の vars/main.yml に設定されていないか確認してください。Ansible の優先順位では vars/group_varshost_vars より上位にあるため、inventory では上書きできません。変数を defaults/main.yml に移してください。defaults/main.yml は優先順位の下位にあり、呼び出し元から変更可能にする値を置く場所として適切です。原因が入力ミスではなく優先順位であることを確認するには、-e name=value を指定して 1 回実行します。これは他のすべての設定元より優先されます。

Ansible が role が見つからないと表示するのはなぜですか?

検索は playbook ファイルの隣から始まるため、site.ymlroles/ は同じディレクトリに配置する必要があります。エラーには、the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely のように試行したパスが表示されます。親ディレクトリから playbook を実行しても問題ありません。検索はシェルの作業ディレクトリではなく、playbook のパスに従うためです。ansible.cfgroles_path に依存している場合は、ansible --version でそのファイルが読み込まれたことを確認してください。world writable の作業ディレクトリでは、Ansible がそのファイルを無視するためです。

role の作成に ansible-galaxy init は必要ですか?

いいえ。role は、決められた名前のディレクトリで構成されるため、mkdir -p roles/common/taskstasks/main.yml があればすでに動作します。ansible-galaxy init --init-path roles common を使うと入力を減らせ、meta/main.yml と README のひな形を含む完全な骨格も作成できます。空のままにするディレクトリは削除してください。空の vars/main.yml があると、role 内で実際に機能するファイルが分かりにくくなるためです。

#ansible#roles#playbook#structure#automation