Ansible playbook 與 role:何時該用哪個?
了解 Ansible 扁平 playbook 何時已足夠,以及何時應改用 role;涵蓋 role 目錄結構、ansible-galaxy init、role 呼叫與變數優先順序。
Ansible playbook 與 role:有何差異
Ansible playbook 是使用 ansible-playbook 執行的檔案。它會將一組主機對應至這些主機需要執行的工作。Ansible role 是採用固定目錄結構的目錄,用來存放工作、範本、處理常式與預設變數,而 playbook 會依名稱呼叫它。兩者內部的工作語法完全相同,因此問題不在於能表達哪些內容,而在於是否能重複使用。
先從扁平的 playbook 開始。第一個自動化專案使用一個包含 tasks: 清單的 site.yml 是正確的結構,而且這種結構適用的時間通常比多數人預期更久。當同一組工作必須對第二組主機執行,或檔案超過約 100 行,導致無法再透過捲動尋找工作時,再將它轉換為 role。
如果你尚未撰寫 playbook,請先針對單一 VPS 開始撰寫第一個 playbook,等它開始變大後再回來。
扁平 playbook 適用的情況
在工作只執行一次、只針對一台主機,或不會有其他人閱讀時,扁平 playbook 是正確的選擇。例如佈建單一應用程式伺服器,或在維護時段前為主機套用修補程式,都不需要建立目錄樹。role 會增加 7 個目錄和一層間接引用。如果唯一的呼叫端就是旁邊的 playbook,這層間接引用沒有帶來任何好處,卻讓你每次想查看實際執行內容時,都必須多跳轉一次。
扁平 playbook 在某個明確時刻就不再適用,而且很容易辨認。當你把一段 tasks 複製到第二個 playbook 時,這就是訊號。從那一刻起,每項修正都必須套用 2 次,而某一天你只會套用 1 次。
實際上,role 目錄包含哪些內容
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是進入點。呼叫 role 時,Ansible 會執行這個檔案,其他目錄則全部是選用項目。defaults/main.yml存放預期由呼叫端覆寫的變數。它是 Ansible 中優先順序最低的來源,因此幾乎所有其他來源都會覆寫它。vars/main.yml存放預期不由呼叫端覆寫的變數。它的優先順序高於 inventory,因此這是需要審慎使用的設定。請少量使用。handlers/main.yml存放由notify觸發的工作。handler 會在 play 結束時執行一次,不論有多少工作通知它。files/存放由copymodule 原樣複製的檔案,templates/則存放由templatemodule 渲染的 Jinja2 template。在 role 內參照這兩者時,使用不含路徑的檔名即可,因為 Ansible 會優先搜尋 role 自身的目錄。meta/main.yml宣告 role 相依性,以及 Ansible Galaxy 讀取的中繼資料。
這種目錄配置不是風格偏好。Ansible 只會搜尋這些確切路徑,因此放在 roles/common/template/(單數形式)中的 template 根本不會被找到。
使用 ansible-galaxy init 建立 common role
mkdir -p ~/infra/roles
cd ~/infra
ansible-galaxy init --init-path roles common這會在 roles/common 下建立完整的骨架,包括不會使用的目錄,以及只包含 --- 的 main.yml 樣板。刪除會保持空白的項目。空的 vars/main.yml 對 Ansible 沒有影響,但會讓人難以判斷 role 中哪些檔案實際上重要。
現在填入實際執行工作的檔案。先處理 defaults,因為它們是 role 的公開介面。
# 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。如果 handler 指定了錯誤的 unit,只有在範本確實發生變更時才會失敗,因此通常要到數週後才會暴露。
工作中的 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。
角色如何由 play 呼叫
# 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 應在摘要中以 failed=0 結束。在呼叫位置使用展開形式傳遞參數,這樣同一個角色就能服務兩組主機:
roles:
- role: common
common_admin_group: ops
common_permit_root_login: prohibit-password有一項排序規則幾乎會讓所有人感到意外。Play 可以包含 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: 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 會列出這些工作,而匯入項目上的標籤會套用至其中每項工作。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 個預設值和 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 變數。第二次執行會輸出 internal=from-cli,因為 extra vars 位於最頂層,下面的任何設定都無法覆寫它。這也是 -e 適合一次性執行,卻不適合放在持續使用的 script 中的原因:它會在不明顯的情況下,優先於 repository 中所有經過考量的設定。
實務規則是:如果希望某個值可由使用者設定,就應放在 defaults/ 中。將它放入 vars/,等於告訴日後所有使用此 role 的人,inventory 不得變更它。這有時確實是你的目的,但通常只是意外。
確認角色具備冪等性:執行兩次
值得信任的 Ansible 執行結果,在第二次執行時應與第一次相同,並回報沒有任何變更。執行兩次 playbook,然後查看摘要。
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml第二次執行的摘要應如下:
PLAY RECAP *********************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0changed=0 表示每個模組都檢查了目前狀態,並確認工作已完成。第二次執行若出現 changed=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 兩次,然後計算包含 wc -l /tmp/grow.txt /tmp/guarded.txt 的行數。/tmp/grow.txt 會包含兩行,/tmp/guarded.txt 會包含一行。第二次執行時,受條件保護的工作完全沒有執行,其結果會帶有 skipped, since /tmp/guarded.txt exists 訊息,因為 creates 會先讓模組尋找可見的產物。若命令不會留下這類產物,請註冊其輸出,並使用 changed_when 自行判斷。
ansible-playbook --check --diff site.yml 可在不套用變更的情況下預測變更,--diff 則會列出範本將重寫的確切行內容。解讀輸出時,請注意一項限制:shell 和 command 工作在 check mode 中會被略過,因此看似乾淨的執行計畫仍可能隱藏待處理的工作。
摘要中還有一個欄位需要同樣謹慎解讀:Ansible 無法連線的主機會計入 unreachable,而不是 failed,且該主機的任何工作都不會執行。因此,在將此角色套用至超過幾台機器之前,請先預先決定單一無法連線的主機是否應中止整個執行。
Ansible 為何表示找不到 role
Ansible 會先在 playbook 檔案旁尋找 roles/ 目錄,接著才搜尋 roles_path。搜尋依據的是 playbook,而不是您的 shell。
ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely這表示 site.yml 與 roles/ 的位置不一致,訊息也會列出 Ansible 嘗試過的路徑。請將兩者放在同一個目錄中。從父目錄執行也沒有問題,因為實際依據的是 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 與固定版本
其他人撰寫的角色應透過安裝取得,而不是直接複製。請宣告一次:
# 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。未設定時,指令會取得執行當日預設分支中的內容。因此,上個月還能正常執行的部署,可能在自己的 repository 完全未變更的情況下失敗。將 roles_path 指向下載目錄,並將該目錄排除在 git 之外:
# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_rolesplaybook 旁 roles/ 中的角色仍會被找到,因為除了 roles_path 之外,系統也一定會搜尋該路徑。因此,您自己的角色可持續提交並接受審查;第三方角色則是固定至標籤且可重現的下載內容。
角色不再是解答的情況
角色是單次 Ansible 執行中的重複使用單位。它不會在您的供應商平台建立伺服器或 DNS 記錄。若試圖讓角色負責這些工作,playbook 很快就會變成無人願意維護的內容。開始之前,建議先閱讀 Ansible 與 Terraform 的工作分工。角色也不能取代 inventory 設計:管理的機器超過少數幾台後,如何分組及連線到這些伺服器,比任務如何歸檔更重要。
這個 common 角色安裝的強化設定,也需要個別決策。上述 drop-in 只設定兩個指令,沒有其他設定。因此,在決定哪些內容應套用到您管理的每台主機之前,請先閱讀 哪些 SSH 設定確實值得修改,以及 如何讓 Ubuntu 自動套用安全更新。
FAQ
何時應將 Ansible playbook 轉換為 role?
當相同的工作項目區塊必須在第二個 play 中執行,或必須套用至第二個主機群組時,就應考慮轉換。需要在不同 playbook 之間複製工作項目,就是明確訊號。從那一刻起,每次修正都必須套用兩次,而某一天可能只套用一次。若單一 playbook 約少於 100 行,且永遠只針對一個群組,使用 role 不會帶來好處,額外的目錄反而會降低可讀性。
Role 會在相同 play 中的工作項目之前執行嗎?
會。Ansible 會先執行 pre_tasks,接著執行 roles: 下列出的所有內容,再執行 tasks:,最後執行 post_tasks:;這個順序不受檔案中各索引鍵的排列順序影響。將 tasks: 寫在 roles: 上方,不會讓這些工作項目先執行。若某項工作必須在 role 之前完成,請將它放在 pre_tasks:。
為什麼我的 group_vars 值無法覆寫 role 中的值?
請確認該變數是否設定在 role 的 vars/main.yml,而不是 defaults/main.yml。在 Ansible 的優先順序中,vars/ 高於 group_vars 和 host_vars,因此 inventory 無法覆寫它。請將變數移至 defaults/main.yml。它位於優先順序較低的位置,適合放置任何呼叫端應能變更的值。若要確認原因是優先順序而不是拼字錯誤,請使用 -e name=value 執行一次。它的優先順序高於其他所有來源。
為什麼 Ansible 顯示找不到 role?
搜尋會從 playbook 檔案所在位置開始,因此 site.yml 和 roles/ 必須位於同一個目錄。錯誤訊息會列出嘗試過的路徑,例如 the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely。從上層目錄執行 playbook 沒有問題,因為搜尋依循 playbook 路徑,而不是 shell 的目前工作目錄。若依賴 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 中哪些檔案實際上會執行。