SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

Ansible playbookとroleの使い分けは?

Ansible playbookとroleの違いを整理します。1回限りならplaybook、タスクを再利用するなら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、デフォルト変数を格納し、playbook から名前で呼び出します。両者の内部で使用するタスク構文は同じです。つまり、表現できる内容の違いではありません。再利用性の違いです。

まずはフラットな playbook から始めます。tasks: のリストを持つ 1 つの site.yml は、最初の自動化に適した構成です。多くの場合、その構成は想定以上に長く使えます。同じタスクのまとまりを 2 つ目のホストグループでも実行する必要が生じた場合や、ファイルが約 100 行を超えてスクロールだけではタスクを見つけにくくなった場合に、role へ移行します。

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

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

フラットな playbook が適切なのは、作業を 1 回だけ実行する場合、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 には、呼び出し側が上書きすることを想定していない変数を格納します。優先度は inventory より高く、この設定は強い意図を示します。使用は慎重にしてください。
  • 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 を確認してください。

ロールを呼び出す playbook

# 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

play は、recap に failed=0 が含まれる状態で終了する必要があります。呼び出し箇所では展開形式でパラメーターを渡します。この方法により、1 つのロールを 2 つのホストグループで使用できます。

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

ほとんどの人が驚く順序規則があります。play には pre_tasks、roles、tasks、post_tasks を記述できますが、Ansible はファイル内の記述順に関係なく、この順序で実行します。tasks: を roles: より上に記述しても、ロールが先に実行されます。そのため、ロールより前に実行する必要がある処理は、tasks: の先頭ではなく pre_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 は解析時にロールを読み込み、そのタスクを play の一部にします。そのため、ansible-playbook --list-tasks site.yml にはタスクが一覧表示され、import に付けたタグは内部のすべてのタスクに適用されます。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 にはロール自身が使用する値だけを残します。

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

Ansible には、変数の優先順位が20を超えるレベルで定義されています。実際の議論のほとんどは、次の4つで決まります。以下では、優先度の低い順に示します。

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

この動作は約1分で確認できます。小さなロールにデフォルト値を1つとロール変数を1つ定義し、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

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

実務上のルールは単純です。変更可能にしたい値は defaults/ に置きます。vars/ に置くと、そのロールを今後使用するすべてのユーザーに対して、inventory から変更できない値だと示すことになります。意図した設計である場合もありますが、多くの場合は意図しない設定です。

ロールの冪等性を証明する:2回実行する

信頼に足るAnsibleの実行結果とは、2回目に実行した際も同じ結果を返し、何も変更されなかったと報告するものです。Playbookを2回実行し、リキャップ(要約)を確認してください。

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

2回目のリキャップは以下のようになるはずです。

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

changed=0は、どのタスクも変更を報告しなかったことを意味します。apt、template、groupのようなモジュールにとって、これは現在の状態に対する真のチェックとなります。ただし、この数値は各タスクの変更報告の正確さに依存します。changed_when: falseを持つタスクは、実際に行ったことに関わらず「変更なし」としてカウントされます。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回目の実行では、ガードされたタスクは一切実行されません。createsはモジュールが最初に探すべき目に見える成果物を与えるため、タスクはDid not run command since '/tmp/guarded.txt' existsというメッセージと共にokを報告します。コマンドがそのような成果物を残さない場合は、出力を登録し、changed_whenを使用して自身で判断してください。

ansible-playbook --check --diff site.ymlは変更を加えずに変更内容を予測し、--diffはテンプレートが書き換える正確な行を出力します。出力を読む際は、1点だけ注意してください。単純なshellやcommandのタスクはチェックモードではスキップされます(createsやremovesを使用すると、実行されるかどうかのみが報告されます)。また、check_mode: falseとマークされたタスクは、--checkの下であっても実際に実行されます。そのため、一見クリーンに見える計画であっても作業が隠れている可能性があり、ドライランが常に「ドライ(非破壊)」であるとは限りません。

リキャップ内のもう1つの列にも同様の注意が必要です。Ansibleが接続できなかったホストはfailedではなくunreachableとしてカウントされ、そのホストのタスクは一切実行されません。そのため、このロールを2、3台を超えるマシンに適用する前に、到達不能なホストが全体の実行を停止すべきか事前に決めておく 必要があります。

Ansible が role が見つからないと報告する理由

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

[ERROR]: The role 'common' was not found in: /home/deploy/lonely/roles:/home/deploy/.ansible/roles:/usr/share/ansible/roles:/etc/ansible/roles:/home/deploy/lonely

このメッセージは、site.yml と roles/ の場所が一致していないことを示します。試行したパスも表示されるため、原因を確認できます。これは ansible-core 2.21 の文言です。古いリリースでは、先頭が小文字でコロンのない the role 'common' was not found in と表示されます。そのため、was not found in を条件に照合してください。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. For more information see https://docs.ansible.com/ansible/devel/reference_appendices/config.html#cfg-in-world-writable-dir

その結果、roles_path と inventory の設定が警告なしに読み込まれず、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 に加えて常に検索されるためです。これにより、自作のロールはコミットしてレビューでき、サードパーティー製のロールはタグを固定した再現可能なダウンロードとして管理できます。

役割だけでは解決できない範囲

role は、1 回の Ansible 実行内で再利用する単位です。プロバイダー側でサーバーや DNS レコードを作成するものではありません。role にそれをさせようとすると、誰も保守したくない playbook になりやすくなります。開始前に、Ansible と Terraform の作業分担を読むことをお勧めします。role は inventory の設計に取って代わるものでもありません。管理対象が数台を超えると、タスクをどのように分類するかよりも、サーバーをどのようにグループ化し、接続するかの方が重要になります。

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

FAQ

Ansible playbook を role にするのはいつですか?

同じタスクブロックを 2 つ目の play で実行する必要がある場合、または 2 つ目のホストグループを対象にする必要がある場合です。playbook 間でタスクをコピーし始めたら、その兆候です。その時点から、修正を 2 か所に適用する必要が生じ、ある日、一方にしか適用されなくなります。常に 1 つのグループだけを対象とする、おおむね 100 行未満の単一の 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 に設定されていないか確認してください。vars/ は Ansible の優先順位で group_vars と host_vars より上位にあるため、inventory では上書きできません。変数を defaults/main.yml に移してください。defaults/main.yml は優先順位の下位にあり、呼び出し側で変更可能にする変数を置く場所として適切です。原因が入力ミスではなく優先順位であることを確認するには、-e name=value を付けて 1 回実行してください。これは他のすべての変数ソースより優先されます。

Ansible が role が見つからないと言うのはなぜですか?

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

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

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

#ansible#roles#playbook#structure#automation