Ansible playbookとroleの使い分けは?
Ansible playbookとroleの違いを整理します。1回限りならplaybook、タスクを再利用するならrole。ディレクトリ構成、ansible-galaxy init、呼び出し方、変数の優先順位も解説します。
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.ymltasks/main.ymlはエントリーポイントです。Ansible はロールが呼び出されたときにこのファイルを実行します。その他のディレクトリはすべて省略できます。defaults/main.ymlには、呼び出し側が上書きすることを想定した変数を格納します。Ansible では優先度が最も低い変数ソースであるため、ほぼすべての設定がこれを上書きします。vars/main.ymlには、呼び出し側が上書きすることを想定していない変数を格納します。優先度は inventory より高く、この設定は強い意図を示します。使用は慎重にしてください。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 を確認してください。
ロールを呼び出す playbook
# 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.ymlplay は、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: 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 は解析時にロールを読み込み、そのタスクを 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-cli1回目の実行では tunable=from-hostvars internal=from-rolevars と表示されます。inventory の値はロールのデフォルト値より優先されますが、ロール変数には負けます。2回目の実行では internal=from-cli と表示されます。extra vars は最上位に位置し、それより下位の値では上書きできないためです。このため、-e は1回限りの実行には適していますが、継続して使用するスクリプトでは不適切です。リポジトリ内で検討したすべての設定より、暗黙的に優先されるからです。
実務上のルールは単純です。変更可能にしたい値は defaults/ に置きます。vars/ に置くと、そのロールを今後使用するすべてのユーザーに対して、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 によって、モジュールが最初に確認する対象を明示できるためです。そのような対象を残さないコマンドでは、出力を register に保存し、changed_when を使って自分で実行要否を判断します。
ansible-playbook --check --diff site.yml は変更を加えずに変更内容を予測し、--diff は template が書き換える正確な行を表示します。出力を読む際は、次の点に注意してください。shell と command のタスクは check mode ではスキップされます。そのため、変更がないように見える計画にも、実行される処理が隠れている可能性があります。
recap のもう 1 つの列にも同じ注意が必要です。Ansible が接続できなかったホストは failed ではなく unreachable に分類され、そのホストのタスクは 1 つも実行されません。そのため、このロールを数台を超えるマシンに適用する前に、到達不能なホストが 1 台あった場合に実行全体を停止するか、事前に決めておきます。
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/ のパスが一致していないことを示しています。Ansible は試したパスも表示します。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 に加えて常に検索されるためです。これにより、自作のロールはコミットしてレビューでき、サードパーティー製のロールはタグを固定した再現可能なダウンロードとして管理できます。
役割だけでは解決できない範囲
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 で設定されていないか確認してください。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 があれば、すでに動作する role になります。ansible-galaxy init --init-path roles common を使うと入力を減らせ、meta/main.yml と README のひな形を含む完全な骨格も作成できます。空のままのディレクトリは削除してください。空の vars/main.yml があると、role 内で実際に処理を行うファイルが分かりにくくなるためです。