Ansible playbookとroleの使い分けは?
Ansible playbookとroleの違いを整理します。約100行やタスクのコピーが切り替えの目安です。roleのディレクトリ構成、ansible-galaxy init、呼び出し方、変数の優先順位を解説します。
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.ymltasks/main.ymlはエントリーポイントです。Ansible はロールが呼び出されるとこのファイルを実行します。その他のディレクトリはすべて任意です。defaults/main.ymlには、呼び出し側が上書きすることを想定した変数を格納します。Ansible で最も優先度が低い変数ソースであるため、ほぼすべての別の設定が優先されます。vars/main.ymlには、呼び出し側が上書きすることを想定していない変数を格納します。インベントリより優先度が高く、強い指定です。使用は慎重にしてください。handlers/main.ymlには、notifyによって実行されるタスクを格納します。handler は play の最後に 1 回だけ実行されます。通知するタスクの数は関係ありません。files/にはcopymodule が内容を変更せずにコピーするファイルを格納し、templates/にはtemplatemodule がレンダリングする 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=localansible-playbook -i inventory.ini site.ymlプレイの最後には、要約に failed=0 が表示されるようにします。呼び出し時に展開形式でパラメーターを渡すと、1 つのロールを 2 つのホストグループで使用できます。
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-passwordほとんどの人が驚く実行順序の規則があります。プレイには pre_tasks、roles、tasks、post_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: postroles: キーではなくタスクリストの中からロールを呼び出すには、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.ymlはhost_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.yml2 回目の recap は次のようになります。
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=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 はテンプレートが書き換える正確な行を出力します。出力は、次の点に注意して読み取ります。shell と command のタスクは 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.yml と roles/ の場所が一致していないことを示しています。試行したパスも表示されます。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_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.0ansible-galaxy install -r requirements.yml -p galaxy_roles必ず version を設定してください。これを指定しないと、コマンドを実行した時点のデフォルトブランチの内容が取得されます。そのため、前月に動作したデプロイが、自分のリポジトリを変更していないのに失敗することがあります。roles_path にはダウンロード先のディレクトリを指定し、そのディレクトリは git の管理対象から除外します。
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesPlaybook と同じ場所にある 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_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:/home/deploy/lonely のように試行したパスが表示されます。親ディレクトリから playbook を実行しても問題ありません。検索はシェルの作業ディレクトリではなく、playbook のパスに従うためです。ansible.cfg の roles_path に依存している場合は、ansible --version でそのファイルが読み込まれたことを確認してください。world writable の作業ディレクトリでは、Ansible がそのファイルを無視するためです。
role の作成に ansible-galaxy init は必要ですか?
いいえ。role は、決められた名前のディレクトリで構成されるため、mkdir -p roles/common/tasks と tasks/main.yml があればすでに動作します。ansible-galaxy init --init-path roles common を使うと入力を減らせ、meta/main.yml と README のひな形を含む完全な骨格も作成できます。空のままにするディレクトリは削除してください。空の vars/main.yml があると、role 内で実際に機能するファイルが分かりにくくなるためです。