SSD Nodes Learn 🎉 VPS từ $5.50/tháng
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-07

Ansible playbook hay role: Khi nào dùng mỗi loại?

Biết khi nào playbook phẳng là đủ và khi nào nên dùng role qua layout thư mục, lệnh 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.

Playbook và role trong Ansible: khác nhau như thế nào

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 task, template, handler và biến mặc định. Playbook gọi role bằng tên. Cú pháp task trong cả hai đều giống nhau, nên vấn đề không nằm ở việc bạn có thể biểu diễn những gì. Vấn đề là khả năng tái sử dụng.

Hãy bắt đầu bằng một playbook phẳng. Một site.yml chứa danh sách tasks: là cấu trúc phù hợp cho automation đầu tiên của bạn. Cấu trúc này cũng thường phù hợp lâu hơn bạn nghĩ. Hãy chuyển sang role khi cùng một block task phải chạy trên nhóm host thứ hai, hoặc khi file dài hơn 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 đúng

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ó người khác đọc nó. Provisioning một application server hoặc patch một máy trước maintenance window không cầ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 caller duy nhất là playbook nằm ngay bên cạnh nó, lớp indirection đó không mang lại lợi ích gì và khiến bạn phải chuyển qua lại mỗi lần muốn xem chính xác những gì được 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 đó 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 hai lần, và đến một ngày nào đó bạn sẽ chỉ sửa một bản.

Thư mục 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à entry point. Ansible chạy file này khi role được gọi, còn mọi thư mục 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 có priority cao hơn.
  • 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 ràng buộc mạnh. Chỉ nên dùng nó trong trường hợp hiếm.
  • 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 bare filename, không kèm path, vì Ansible luôn tìm trong các thư mục riêng của role trước.
  • meta/main.yml khai báo dependencies của role và metadata mà Ansible Galaxy đọc.

Cấu trúc này không phải là vấn đề về style. Ansible tìm trong đúng các path này, nên template bạn đặt trong roles/common/template/ (số ít) sẽ không bao giờ đượ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 ---. 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. Bắt đầu với defaults, vì đây là interface public 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""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 giá trị đó. Dấu ngoặc kép giữ giá trị ở dạng chuỗi.

# 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, unit có tên sshd. Handler ghi 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. Ansible chỉ thay file đích nếu command thoát với mã 0. Hãy thêm một directive không hợp lệ vào template rồi chạy lại: task sẽ fail với failed to validate, /etc/ssh/sshd_config.d/99-hardening.conf thực tế vẫn nguyên vẹn, và bạn vẫn có thể đăng nhập vào server. Lưu ý rằng bước kiểm tra này không chỉ kiểm tra 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 kết luận template bị lỗi.

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 với failed=0 trong phần tổng kết. Truyền tham số ngay tại vị trí gọi bằng dạng đầy đủ. Đây là cách dùng một role cho hai nhóm host:

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

Có một quy tắc về 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, taskspost_tasks. Ansible luôn chạy chúng theo đúng 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: cũng không thay đổi việc role chạy trước. Vì vậy, nếu một việc phải xảy ra trước role, hãy đặt việc đó 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 role từ bên trong danh sách task thay vì dùng khóa 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à dạng tĩnh. Ansible đọc role tại thời điểm phân tích cú pháp, 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 này và tag đặt trên lệnh import sẽ áp dụng cho mọi task bên trong. include_role là dạng động. 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 chạy role trong một vòng lặp. Đổi lại, các task đó không hiển thị trong --list-tasks--start-at-task.

Có một lỗi dễ gặp ở đây. when: trên task include_role được đánh giá trước khi defaults/main.yml của role được include nằm trong phạm vi. Nếu viết when: common_packages | length > 0 trên lệnh include, lần chạy sẽ dừng với 'common_packages' is undefined, dù biến đó được định nghĩa ngay trong role mà bạn đang include. Cách sửa là đưa biến điều khiển ra ngoài role: đặt nó trong group_vars/all.yml, nơi biến có phạm vi ở mọi chỗ, và chỉ để defaults của role chứa 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 hai mươi cấp độ ưu tiên biến. Bốn cấp độ 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 danh sách. Gần như mọi giá trị bạn đặ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/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ố role 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ố role.

Bạn có thể quan sát quá trình này trong khoảng một phút. Tạo một role nhỏ với một giá trị mặc định và một role var, rồi đặt cùng các tê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ứ hai in ra internal=from-cli vì extra vars nằm ở cấp cao nhất và không có cấp nào thấp hơn có thể ghi đè nó. Đâ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 liên quan trong repository của bạn.

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

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

Một lần chạy Ansible đáng tin cậy sẽ tạo ra cùng một 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 tổng kết.

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

Phần tổng kết lần chạy thứ hai sẽ có dạng sau:

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

changed=0 có nghĩa là mọi module đã kiểm tra trạng thái hiện tại và thấy công việc đã được thực hiện. changed=2 ở lần chạy thứ hai có nghĩa là 2 task không thể phân biệt trạng thái hiện tại, nên chúng sẽ liên tục ghi đè file và restart service. 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 những 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 chứa 2 dòng và /tmp/guarded.txt chứa 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 nó có thông báo skipped, since /tmp/guarded.txt exists, vì creates cung cấp cho module một sản phẩm hiện hữu để tìm trước. Khi một command không tạo ra 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 áp dụng chúng, còn --diff in chính xác các dòng mà một template sẽ ghi đè. Khi đọc output, hãy nhớ một điểm: các task shellcommand bị bỏ qua trong check mode, nên một kế hoạch trông có vẻ sạch vẫn có thể che giấu các thay đổi.

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 của bạn.

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

Thông báo này có nghĩa là site.ymlroles/ không còn khớp với 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 của 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ể khó nhận biết hơn của cùng vấn đề này. Ansible bỏ qua 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 một file cấu hình 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_pathinventory của bạn sẽ âm thầm không được áp dụng, còn việc tìm role sẽ 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 lệnh này mỗi khi lần chạy có biểu hiện như thể cấu hình của bạn không tồn tại.

Chia sẻ role: requirements.yml và phiên bản cố định

Một 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 nội dung của branch mặc định tại thời điểm chạy command. Vì vậy, một deployment từng hoạt động 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 tải xuống 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ì đường dẫn đó luôn được tìm kiếm cùng với roles_path. Nhờ vậy, các role do bạn viết vẫn được commit và review, còn role của bên thứ ba là các gói tải xuống có thể tái lập và được cố định theo 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 bản ghi DNS tại nhà cung cấp của bạn. Cố bắt role thực hiện 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á một 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 đổicá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 cần đư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 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 hai lần, và sẽ có ngày bạn chỉ áp dụng một lần. Một playbook đơn lẻ dài khoảng dưới 100 dòng, chỉ luôn chạy trên một nhóm host, không được lợi gì từ role. Các thư mục bổ sung còn khiến playbook 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 đó chạy mọi nội dung được liệt kê dưới roles:, rồi đến tasks:post_tasks:. Ansible không quan tâm thứ tự các key này xuất hiện trong file. 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:.

Tại 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ó precedence cao hơn group_varshost_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ó precedence cao hơn mọi nguồn khác.

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

Ansible bắt đầu tìm ngay cạnh file playbook, nên site.ymlroles/ 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 working directory có quyền ghi cho mọi user.

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 đúng 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 gõ và tạo toàn bộ skeleton, gồm cả meta/main.yml và 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 việc file nào trong role thực sự có tác dụng.

#ansible#roles#playbook#structure#automation