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.
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.ymltasks/main.ymllà đ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.ymlchứ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.ymlchứ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.ymlchứa các task được kích hoạt bởinotify. 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 modulecopycopy nguyên trạng, còntemplates/chứa các template Jinja2 được moduletemplaterender. 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.ymlkhai 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 commonLệ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=localansible-playbook -i inventory.ini site.ymlPlay 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-passwordCó 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.ymlnằ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.ymlcó mức ưu tiên cao hơnhost_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-etrê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-cliLầ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.ymlRecap ở 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=0changed=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.txtChạ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/lonelyThô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.ymlCó 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.0ansible-galaxy install -r requirements.yml -p galaxy_rolesLuô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_rolesCá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.