SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-07

Ansible playbook 與 role 怎麼選?差異與時機

了解何時使用扁平 Ansible playbook、何時改用 role,包含 7 個角色目錄、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 會依名稱呼叫它。兩者內的 task 語法完全相同,因此這不是功能表達能力的問題,而是重複使用的問題。

先從扁平的 playbook 開始。對第一次進行自動化而言,單一 site.yml 搭配 tasks: 清單是適當的結構,而且適用時間通常比多數人預期的更久。當同一組 tasks 必須對第二組主機執行,或檔案超過約 100 行、無法再透過捲動尋找 task 時,再轉換為 role。

如果你還沒有撰寫過 playbook,請先針對單一 VPS 開始撰寫第一個 playbook,等它開始變大後再回來。

扁平 playbook 適用的情況

工作只執行一次、只針對一台主機,或不會有人讀取時,扁平 playbook 就是正確選擇。例如佈建單一應用程式伺服器,或在維護時段前修補一台主機;這些情況都不需要建立目錄樹。role 會增加 7 個目錄和一層間接關係。如果唯一的呼叫端就是旁邊的 playbook,這層間接關係沒有帶來任何好處,卻讓你每次想查看實際執行內容時,都必須多跳轉一次。

扁平 playbook 不再適用的時機很明確,也很容易辨識。當你把一段 task 複製到第 2 個 playbook 時,就代表已經出現這個訊號。從那一刻起,每項修正都必須做 2 次,而總有一天你只會做 1 次。

角色目錄實際包含的內容

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 結束時執行一次,無論有多少工作通知它。
  • files/ 存放由 copy 模組原樣複製的檔案,templates/ 存放由 template 模組呈現的 Jinja2 範本。在角色內參照這兩者時,只需使用不含路徑的檔名,因為 Ansible 會先搜尋角色自己的目錄。
  • meta/main.yml 宣告角色相依性,以及 Ansible Galaxy 讀取的中繼資料。

這種配置不是風格偏好。Ansible 會在這些確切路徑中搜尋,因此放在 roles/common/template/(單數形式)中的範本根本不會被找到。

使用 ansible-galaxy init 建立 common role

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

這會在 roles/common 下建立完整的骨架,其中包含不會使用的目錄,以及只含 ---main.yml stub。刪除會保持空白的項目。空的 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

如何呼叫 role

# 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 應在摘要中以 failed=0 結尾。以展開形式在呼叫位置傳入參數;同一個 role 可透過這種方式服務兩組主機:

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

有一項執行順序規則幾乎總會讓人意外。Play 可以包含 pre_tasksrolestaskspost_tasks,而 Ansible 會依該順序執行,不論你在檔案中採用什麼排列順序。即使將 tasks: 放在 roles: 上方,roles 仍會先執行。因此,如果某項工作必須在 role 之前執行,就應放在 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

若要在 task 清單內呼叫 role,而不是使用 roles: 鍵,請使用 import_roleinclude_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 會在解析時讀取 role,其 tasks 會成為 play 的一部分,因此 ansible-playbook --list-tasks site.yml 會列出這些 tasks,而套用在 import 上的標籤也會套用到其中每個 task。include_role 是動態方式。直到 task 執行時才會讀取內容,因此可以透過變數或迴圈指定 role 名稱。代價是,這些 tasks 不會顯示在 --list-tasks--start-at-task 中。

這裡有一個容易踩到的陷阱。include_role task 上的 when:,會在被包含 role 的 defaults/main.yml 進入作用域之前進行評估。如果在 include 上寫入 when: common_packages | length > 0,執行會以 'common_packages' is undefined 停止,即使該變數就在你要包含的 role 中定義。解法是將此切換設定移出 role:放在 group_vars/all.yml,讓它在所有位置都處於作用域內,並將 role 的 defaults 留給 role 自身使用的值。

哪個變數優先:預設值、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=0

changed=0 表示每個模組都檢查了目前狀態,並確認所需工作已完成。第二次執行若出現 changed=2,表示有兩個工作無法判斷目前狀態,因此會持續重寫檔案並重新啟動服務。最常見的原因是 commandshell,因為 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 則會列出 template 將重寫的確切行。解讀輸出時請注意:shellcommand 工作會在 check mode 中跳過,因此看似乾淨的執行計畫仍可能隱藏尚未執行的工作。

為什麼 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.ymlroles/ 已經不一致,訊息也會列出 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_pathinventory 設定會在不提示的情況下失效,role lookup 也會因為與 role 無關的原因失敗。ansible --version 會顯示實際載入的 config fileansible-config dump --only-changed 則會列出所有與內建預設值不同的設定。每當執行結果顯示設定檔似乎不存在時,都應檢查這兩項。

分享角色:requirements.yml 與固定版本

其他人撰寫的角色會以安裝方式取得,不是直接複製。請宣告一次:

# 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 外,系統也一律會搜尋該路徑。因此,自有角色可以提交至儲存庫並接受審查;第三方角色則是固定在 tag 的可重現下載內容。

角色不再是解法的情況

角色是單次 Ansible 執行中的可重用單位。它不會在供應商端建立伺服器或 DNS 記錄;若試圖讓角色負責這些工作,playbook 最終會變成無人願意維護的內容。開始前,建議先閱讀 Ansible 與 Terraform 的工作分工。角色也不能取代 inventory 設計:當機器數量超過少數幾台後,如何分組並連線到這些伺服器比工作項目的檔案歸檔方式更重要。

這個 common 角色安裝的強化設定也需要個別決策。上述 drop-in 只設定兩個指令,因此在決定哪些內容應套用到你管理的每台主機前,請先閱讀 哪些 SSH 設定實際值得修改,以及 如何讓 Ubuntu 自動套用安全更新

FAQ

何時應該將 Ansible playbook 改成 role?

當同一組工作需要在第二個 play 中執行,或需要針對第二組主機執行時,就應該這麼做。在不同 playbook 之間複製工作就是明確訊號,因為從那一刻起,每次修正都必須套用兩次,而某一天可能只套用一次。若單一 playbook 大約少於 100 行,且永遠只針對一組主機,使用 role 不會帶來好處;額外的目錄反而會降低可讀性。

Role 會在同一個 play 的 tasks 之前執行嗎?

會。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_varshost_vars,因此 inventory 無法覆寫它。請將變數移至 defaults/main.yml;它位於優先順序的底部附近,適合放置呼叫端應能變更的內容。若要確認原因確實是優先順序,而不是拼字錯誤,請使用 -e name=value 執行一次;它的優先權高於其他所有來源。

為什麼 Ansible 顯示找不到 role?

搜尋會從 playbook 檔案所在位置開始,因此 site.ymlroles/ 必須位於同一個目錄中。錯誤訊息會列出 Ansible 嘗試過的路徑,例如 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 中哪些檔案實際上會執行。

#ansible#roles#playbook#structure#automation