SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

Ansible playbook hay role: khi nào dùng từng loại?

Biết khi nào flat playbook là đủ và khi nào nên dùng role: cấu trúc thư mục, ansible-galaxy init, cách gọi role và thứ tự ưu tiên biến.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

So sánh Ansible playbook và role: khác nhau ở đâu

Ansible playbook là file bạn chạy bằng ansible-playbook. File này ánh xạ một nhóm host với các công việc cần thực hiện trên đó. Ansible role là một thư mục có cấu trúc cố định, chứa tasks, templates, handlers và các biến mặc định; playbook gọi role theo tên. Cú pháp task bên trong cả hai giống nhau, nên vấn đề không nằm ở việc bạn có thể mô tả những gì. Vấn đề là khả năng tái sử dụng.

Hãy bắt đầu bằng một playbook đơn giản. Một site.yml chứa danh sách tasks: là cấu trúc phù hợp cho lần automation đầu tiên, và thường vẫn phù hợp lâu hơn bạn nghĩ. Hãy chuyển sang role khi cùng một nhóm task phải chạy cho nhóm host thứ hai, hoặc khi file dài quá khoảng 100 dòng và bạn không còn tìm được task chỉ bằng cách cuộn.

Nếu bạn chưa viết playbook nào, hãy bắt đầu với playbook đầu tiên trên một VPS duy nhất rồi quay lại khi playbook bắt đầu lớn dần.

Khi flat playbook là lựa chọn phù hợp

Flat playbook phù hợp khi công việc chỉ chạy một lần, chỉ chạy trên một host hoặc không có ai khác đọc nó. Provisioning một application server hoặc patch một máy trước maintenance window không cần đến cả một cây thư mục. Một role thêm 7 thư mục và một lớp indirection. Nếu playbook duy nhất gọi role nằm ngay bên cạnh role đó, indirection này không mang lại lợi ích nào và khiến bạn phải chuyển qua lại giữa các file mỗi khi muốn xem chính xác phần nào đang chạy.

Flat playbook không còn phù hợp tại một thời điểm cụ thể, và thời điểm đó rất dễ nhận ra. Bạn copy một block task vào playbook thứ hai. Bản copy này là dấu hiệu cảnh báo. Từ lúc đó, mọi bản sửa đều phải thực hiện 2 lần, và sẽ có ngày bạn chỉ sửa 1 lần.

Một role thực sự chứa gì

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 là điểm bắt đầu. Ansible chạy file này khi role được gọi, còn mọi directory khác đều là tùy chọn.
  • defaults/main.yml chứa các biến mà caller được dự kiến sẽ override. Đây là nguồn có priority thấp nhất trong Ansible, nên gần như mọi nguồn khác đều ghi đè được.
  • vars/main.yml chứa các biến mà caller không được dự kiến sẽ override. Nó có priority cao hơn inventory, nên đây là một quyết định cần cân nhắc kỹ. Chỉ dùng khi thật cần.
  • handlers/main.yml chứa các task được kích hoạt bởi notify. Handler chạy một lần ở cuối play, bất kể có bao nhiêu task notify nó.
  • files/ chứa các file được module copy copy nguyên trạng, còn templates/ chứa các template Jinja2 được module template render. Bên trong role, bạn tham chiếu cả hai bằng tên file không kèm path, vì Ansible luôn tìm trong các directory riêng của role trước.
  • meta/main.yml khai báo các dependency của role và metadata mà Ansible Galaxy đọc.

Layout này không chỉ là vấn đề style. Ansible tìm trong đúng các path này, nên template bạn đặt trong roles/common/template/ (dạng số ít) sẽ hoàn toàn không được tìm thấy.

Tạo role dùng chung bằng ansible-galaxy init

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

Lệnh này tạo toàn bộ skeleton trong roles/common, bao gồm các thư mục bạn không dùng và các file main.yml chỉ chứa ---. Hãy xóa những thư mục và file bạn để trống. vars/main.yml trống không gây lỗi cho Ansible, nhưng khiến bạn khó biết file nào trong role thực sự quan trọng.

Bây giờ điền nội dung cho các file thực hiện công việc. Hãy bắt đầu với defaults, vì đây là public interface của 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"

Đặt "no" và "yes" trong dấu ngoặc kép. Ansible phân tích YAML bằng PyYAML. PyYAML đọc no không có dấu ngoặc kép thành boolean false, nên dòng cấu hình được render thành PermitRootLogin False và sshd từ chối nó. Dấu ngoặc kép giữ giá trị ở dạng string.

# 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 }}

Trên Debian và Ubuntu, systemd unit có tên ssh. Trên các hệ thống thuộc họ RHEL, tên unit là sshd. Handler dùng sai tên unit chỉ lỗi khi có thay đổi thực sự đối với template. Vì vậy lỗi này thường chỉ xuất hiện sau vài tuần.

Dòng validate là phần hữu ích nhất trong task đó. Ansible render template vào một file tạm, thay %s bằng đường dẫn của file đó rồi chạy command. File đích chỉ được thay thế nếu command thoát với mã 0. Hãy thêm một directive vô nghĩa vào template rồi chạy lại: task sẽ lỗi với failed to validate, /etc/ssh/sshd_config.d/99-hardening.conf thực tế vẫn không bị thay đổi, và bạn vẫn có thể đăng nhập vào server. Lưu ý rằng bước kiểm tra này kiểm tra nhiều hơn cú pháp. Nếu sshd -t không đọc được host key, nó sẽ thoát với sshd: no hostkeys available -- exiting. và Ansible báo cùng failed to validate. Vì vậy, hãy đọc msg của module trước khi quy lỗi cho template.

Cách playbook gọi một 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 phải kết thúc bằng failed=0 trong phần tổng kết. Truyền tham số tại vị trí gọi bằng dạng mở rộng. Nhờ đó, một role có thể phục vụ hai nhóm host:

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

Có một quy tắc thứ tự khiến hầu hết mọi người bất ngờ. Một play có thể chứa pre_tasks, roles, tasks và post_tasks, nhưng Ansible luôn chạy chúng theo thứ tự đó, bất kể bạn viết chúng theo thứ tự nào trong file. Đặt tasks: phía trên roles: thì các role vẫn chạy trước. Vì vậy, nếu một việc phải xảy ra trước một role, hãy đặt nó trong pre_tasks:, không đặt ở đầu 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

Để gọi một role từ bên trong danh sách task thay vì dùng key roles:, hãy dùng import_role hoặc 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 là static. Ansible đọc role tại thời điểm parse, rồi đưa các task của role vào play. Vì vậy, ansible-playbook --list-tasks site.yml liệt kê được các task đó và một tag trên import sẽ áp dụng cho mọi task bên trong. include_role là dynamic. Ansible chỉ đọc role khi task chạy. Nhờ đó, bạn có thể lấy tên role từ một biến hoặc một loop. Đổi lại, các task đó không xuất hiện trong --list-tasks và --start-at-task.

Có một bẫy ở đây. when: trên một task include_role được đánh giá trước khi defaults/main.yml của role được include có trong scope. Nếu viết when: common_packages | length > 0 trên include, lần chạy sẽ dừng với 'common_packages' is undefined, dù biến đó được định nghĩa ngay trong role bạn đang include. Cách sửa là đưa toggle ra ngoài role: đặt nó trong group_vars/all.yml, nơi nó có scope ở mọi vị trí, và giữ defaults của role cho các giá trị mà chính role sử dụng.

Biến nào được ưu tiên: defaults, group_vars, vars, extra vars

Ansible có hơn 20 mức độ ưu tiên của biến. 4 mức trong số đó giải quyết gần như mọi tranh luận thực tế. Dưới đây là thứ tự từ yếu nhất đến mạnh nhất.

  • roles/<name>/defaults/main.yml nằm gần cuối bảng ưu tiên. Gần như mọi giá trị được đặt ở nơi khác đều ghi đè được nó. Vì vậy, đây là nơi phù hợp để đặt các tham số có thể điều chỉnh của role.
  • group_vars/ và host_vars/ nằm ở giữa. Đây là nơi đặt các giá trị riêng của site. Chúng ghi đè rõ ràng lên các giá trị mặc định của role.
  • roles/<name>/vars/main.yml có mức ưu tiên cao hơn host_vars. Giá trị đặt ở đây không thể bị inventory ghi đè. Chỉ dùng nó cho những giá trị mà role cần giữ nhất quán nội bộ, chẳng hạn tên package phải khớp với tên service.
  • Tham số của role được truyền tại vị trí gọi role có mức ưu tiên cao hơn vars/main.yml. Còn -e trên command line có mức ưu tiên cao hơn mọi thứ, kể cả tham số của role.

Bạn có thể quan sát quá trình này trong khoảng 1 phút. Tạo một role nhỏ với 1 giá trị mặc định và 1 role var, sau đó đặt cùng các tên biến đó trong 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

Lần chạy đầu tiên in ra tunable=from-hostvars internal=from-rolevars. Inventory đã ghi đè giá trị mặc định của role nhưng bị role var ghi đè. Lần chạy thứ 2 in ra internal=from-cli, vì extra vars nằm ở mức cao nhất và không có mức nào bên dưới ghi đè được. Đây cũng là lý do -e phù hợp cho một lần chạy riêng lẻ nhưng không phù hợp trong một script được duy trì lâu dài: nó âm thầm có mức ưu tiên cao hơn mọi quyết định đã được cân nhắc trong repository.

Quy tắc thực tế là: nếu muốn cho phép thiết lập một giá trị, hãy đặt nó trong defaults/. Đặt nó trong vars/ có nghĩa là thông báo cho mọi người dùng sau này của role rằng inventory không được thay đổi giá trị đó. Đôi khi đây là điều bạn thực sự muốn, nhưng thường là một lỗi ngoài ý muốn.

Chứng minh role có tính idempotent: chạy 2 lần

Một lần chạy Ansible đáng tin cậy phải cho cùng kết quả ở lần chạy thứ hai và báo rằng không có gì thay đổi. Chạy playbook 2 lần rồi đọc phần recap.

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

Recap ở lần chạy thứ hai phải có dạng như sau:

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

changed=0 nghĩa là mọi module đã kiểm tra trạng thái hiện tại và thấy công việc đã hoàn tất. changed=2 ở lần chạy thứ hai nghĩa là 2 task không nhận biết được sự khác biệt, vì vậy chúng sẽ tiếp tục ghi đè file và restart service vô hạn. Nguyên nhân thường là command hoặc shell, vì Ansible không có cách biết một command tùy ý đã thực hiện gì.

# 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

Chạy playbook đó 2 lần, rồi đếm các dòng có wc -l /tmp/grow.txt /tmp/guarded.txt. /tmp/grow.txt có 2 dòng và /tmp/guarded.txt có 1 dòng. Ở lần chạy thứ hai, task có điều kiện bảo vệ hoàn toàn không chạy, và kết quả của task có thông báo skipped, since /tmp/guarded.txt exists, vì creates cung cấp cho module một sản phẩm có thể kiểm tra trước. Khi một command không để lại sản phẩm như vậy, hãy register output của nó và tự quyết định bằng changed_when.

ansible-playbook --check --diff site.yml dự đoán các thay đổi mà không thực hiện chúng, còn --diff in chính xác các dòng mà một template sẽ ghi đè. Khi đọc output, cần lưu ý một điểm: các task shell và command bị bỏ qua trong check mode, vì vậy một kế hoạch có vẻ không có thay đổi vẫn có thể che giấu công việc cần thực hiện.

Một cột khác trong recap cũng cần được chú ý: host mà Ansible không thể kết nối sẽ được tính dưới unreachable thay vì failed, và không task nào trên host đó được chạy. Vì vậy, hãy quyết định trước một host unreachable có nên dừng toàn bộ lần chạy hay không trước khi áp dụng role này cho hơn một vài máy chủ.

Vì sao Ansible báo không tìm thấy role

Ansible tìm thư mục roles/ bên cạnh file playbook, sau đó tìm trong roles_path. Việc tìm kiếm dựa trên playbook, không dựa trên shell hiện tại.

ERROR! the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely

Thông báo đó có nghĩa là site.yml và roles/ đã không còn khớp nhau. Ansible cũng in ra các đường dẫn mà nó đã thử. Đặt hai thư mục này trong cùng một thư mục. Chạy lệnh từ thư mục cha vẫn được, vì đường dẫn đến playbook mới là yếu tố quyết định:

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

Có một biến thể ít rõ ràng hơn của cùng lỗi này. Ansible bỏ qua file ansible.cfg trong thư mục hiện tại nếu thư mục đó cho phép mọi user ghi, vì bất kỳ user nào trên máy cũng có thể đặt config vào đó và thay đổi hành vi của lần chạy.

[WARNING]: Ansible is being run in a world writable directory (/tmp/infra), ignoring it as an ansible.cfg source.

Khi đó, các thiết lập roles_path và inventory của bạn sẽ âm thầm không được áp dụng. Việc tìm role thất bại vì một lý do không liên quan đến role. ansible --version in ra config file mà Ansible thực sự đã nạp, còn ansible-config dump --only-changed in ra mọi thiết lập khác với giá trị mặc định tích hợp. Hãy kiểm tra cả hai khi lần chạy hoạt động như thể config của bạn không tồn tại.

Chia sẻ role: requirements.yml và phiên bản được pin

Role do người khác viết sẽ được cài đặt, không phải sao chép. Khai báo role đó một lần:

# 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

Luôn đặt version. Nếu không, bạn sẽ nhận branch mặc định tại thời điểm chạy command. Vì vậy, một deployment từng hoạt động vào tháng trước có thể hỏng dù repository của bạn không thay đổi. Trỏ roles_path đến thư mục download và không đưa thư mục đó vào git:

# ansible.cfg
[defaults]
inventory = inventory.ini
roles_path = ./galaxy_roles

Các role trong roles/ nằm cạnh playbook vẫn được tìm thấy, vì path đó luôn được tìm kiếm cùng với roles_path. Nhờ vậy, các role tự viết vẫn được commit và review, còn role của bên thứ ba là các bản download có thể tái lập và được pin vào một tag.

Khi role không còn là câu trả lời

Role là một đơn vị tái sử dụng trong một lần chạy Ansible. Role không tạo server hoặc DNS record tại nhà cung cấp của bạn. Cố bắt role làm việc đó sẽ khiến playbook trở nên khó bảo trì. Bạn nên đọc phần phân chia công việc giữa Ansible và Terraform trước khi bắt đầu. Role cũng không thay thế thiết kế inventory. Khi số lượng máy vượt quá vài máy, cách bạn nhóm và truy cập các server đó quan trọng hơn cách bạn sắp xếp task.

Các thiết lập hardening mà role common này cài đặt cũng cần được quyết định riêng. Drop-in ở trên chỉ đặt 2 directive, không thêm gì khác. Vì vậy, hãy đọc những thiết lập SSH nào thực sự đáng thay đổi và cách để Ubuntu tự áp dụng các bản cập nhật bảo mật trước khi quyết định nội dung nào sẽ đưa vào role cho mọi host bạn quản lý.

FAQ

Khi nào tôi nên chuyển một Ansible playbook thành role?

Khi cùng một nhóm task phải chạy trong play thứ hai hoặc trên nhóm host thứ hai. Việc phải sao chép task giữa các playbook là dấu hiệu rõ ràng, vì từ lúc đó mọi bản sửa đều phải áp dụng 2 lần, và sớm muộn sẽ có lần chỉ được áp dụng 1 lần. Một playbook đơn lẻ dưới khoảng 100 dòng, chỉ luôn nhắm đến 1 nhóm, không thu được lợi ích gì từ role. Các thư mục bổ sung còn khiến file khó đọc hơn.

Role có chạy trước các task trong cùng play không?

Có. Ansible chạy pre_tasks, sau đó mọi mục được liệt kê trong roles:, rồi tasks: và post_tasks:. Nó bỏ qua thứ tự các key này xuất hiện trong file của bạn. Việc viết tasks: phía trên roles: không khiến các task đó chạy trước. Nếu một việc phải xảy ra trước role, hãy đặt việc đó trong pre_tasks:.

Vì sao giá trị trong group_vars không ghi đè được role?

Kiểm tra xem biến có được đặt trong vars/main.yml của role thay vì defaults/main.yml hay không. vars/ có độ ưu tiên cao hơn group_vars và host_vars trong thứ tự precedence của Ansible, nên inventory không thể ghi đè biến này. Chuyển biến sang defaults/main.yml. Vị trí này gần cuối thứ tự precedence và phù hợp cho mọi giá trị mà caller cần có thể thay đổi. Để xác nhận nguyên nhân là precedence chứ không phải lỗi đánh máy, chạy một lần với -e name=value. Tùy chọn này có độ ưu tiên cao hơn mọi nguồn khác.

Vì sao Ansible báo không tìm thấy role?

Ansible bắt đầu tìm bên cạnh file playbook, nên site.yml và roles/ phải nằm trong cùng một thư mục. Lỗi sẽ in ra các path mà Ansible đã thử, như trong the role 'common' was not found in /home/deploy/lonely/roles:/home/deploy/lonely. Chạy playbook từ thư mục cha vẫn được, vì Ansible tìm theo path của playbook chứ không theo working directory của shell. Nếu bạn phụ thuộc vào roles_path từ ansible.cfg, hãy xác nhận file đó đã được load bằng ansible --version, vì Ansible sẽ bỏ qua file này nếu working directory cho phép mọi người ghi.

Tôi có cần ansible-galaxy init để tạo role không?

Không. Role chỉ là các thư mục có tên theo quy ước, nên mkdir -p roles/common/tasks cùng với một tasks/main.yml đã là một role hoạt động được. ansible-galaxy init --init-path roles common giúp giảm thao tác nhập lệnh và tạo đầy đủ skeleton, bao gồm meta/main.yml cùng một README mẫu. Hãy xóa các thư mục bạn để trống, vì vars/main.yml trống sẽ che khuất những file nào trong role thực sự có tác dụng.

#ansible#roles#playbook#structure#automation