Cách dùng Ansible check mode và --diff để chạy thử
Tìm hiểu cách dùng --check và --diff trong Ansible để kiểm tra thay đổi trước khi áp dụng. Bài viết giải thích tại sao một số module không hỗ trợ check mode gây sai lệch.
Chế độ check mode của Ansible làm gì
Chế độ check mode của Ansible là một lần chạy thử (dry run): ansible-playbook --check kết nối tới mọi host trong play, hỏi từng module xem trạng thái hiện tại đã khớp với trạng thái bạn yêu cầu hay chưa, và báo cáo những gì sẽ thay đổi mà không ghi bất kỳ dữ liệu nào. Thêm --diff vào và nó sẽ in ra nội dung trước và sau của các file mà nó định tác động. Kết hợp lại, chúng trả lời câu hỏi cần thiết trước mỗi lần chạy thực tế: điều gì sắp thay đổi trên các máy chủ này?
Check mode không phải là một mô phỏng playbook của bạn. Không có mô hình nào của máy chủ ở đây cả. Mỗi module chỉ đơn giản được yêu cầu đọc thay vì ghi. Một module có khả năng phản hồi ở chế độ chỉ đọc sẽ báo changed và tiếp tục. Một module không thể phản hồi sẽ không làm gì và không báo cáo gì. Tài liệu của Ansible tóm tắt trong một dòng: "Các module không hỗ trợ check mode sẽ không báo cáo gì và không thực hiện gì cả." Khoảng trống đó là nơi một lần chạy thử đưa ra câu trả lời sai, vì vậy phần lớn hướng dẫn này tập trung vào khoảng trống đó.
Chạy thử nghiệm: --check và --diff
ansible-playbook -i inventory.ini site.yml --check --diff --limit web1-C và -D là dạng viết tắt của hai flag này. --limit là lựa chọn có chủ đích. Diff của một host là thứ bạn có thể đọc được. Diff của hai mươi host là thứ bạn chỉ muốn cuộn qua cho xong.
Bốn từ kết quả truyền tải toàn bộ báo cáo.
ok: [web1]nghĩa là module đã kiểm tra và trạng thái hiện tại đã khớp. Không có gì thay đổi.changed: [web1]nghĩa là module sẽ thực hiện ghi dữ liệu. Với--diff, các dòng phía trên sẽ hiển thị nội dung đó.skipping: [web1]nghĩa là task không được đánh giá. Hoặc làwhentrả về false, hoặc module không hỗ trợ chạy ở chế độ check.fatal: [web1]nghĩa là task bị lỗi trong quá trình kiểm tra. Hãy đọc thông báo lỗi trước khi cho rằng playbook bị hỏng.
--diff in ra một unified diff cho các file module, với các dòng bị xóa đánh dấu - và các dòng được thêm đánh dấu +, nằm dưới một header có các dòng bắt đầu bằng --- before và +++ after, chỉ rõ đường dẫn đích. Các module không ghi file sẽ in ra trạng thái trước và sau của chúng, vì vậy ansible.builtin.user sẽ hiển thị các thuộc tính mà nó sẽ thay đổi thay vì nội dung file.
Bật vĩnh viễn tính năng diff trong ansible.cfg để bạn không bao giờ quên flag này:
[diff]
always = true
context = 5Hai bước kiểm tra nhanh hơn nên được thực hiện trước chế độ check. ansible-playbook site.yml --syntax-check phân tích cú pháp YAML và cấu trúc play mà không cần kết nối tới bất kỳ host nào. ansible-playbook site.yml --list-tasks in ra các task sẽ được thực thi, đây là cách để bạn phát hiện ra một role mà bạn nghĩ là đã được gắn tag thực tế lại không phải vậy. Cả hai đều không kết nối mạng nên tốc độ thực hiện là tức thì.
Bản thân chế độ check có thực hiện kết nối. Nó mở SSH tới mọi host trong pattern và thu thập facts, vì vậy một host đang down sẽ làm thất bại quá trình chạy thử. Bản thân điều đó đã là một tín hiệu hữu ích, và đó cũng là lý do tại sao quyết định playbook nên xử lý thế nào với các host không thể kết nối lại quan trọng trước khi bạn đưa một lần chạy thử vào CI.
Tại sao check mode thất bại trên một server mới
Playbook này hoàn toàn đúng. Hãy chạy nó với --check trên một server chưa cài đặt Nginx, và hầu hết các tác vụ sẽ thất bại.
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Write the site config
ansible.builtin.template:
src: site.conf.j2
dest: /etc/nginx/conf.d/site.conf
- name: Start and enable nginx
ansible.builtin.service:
name: nginx
state: started
enabled: trueTác vụ apt báo cáo changed, và điều đó đúng: gói phần mềm chưa có, nên một lần chạy thực tế sẽ cài đặt nó. Check mode đã không cài đặt gói này. Tác vụ template sau đó thất bại vì /etc/nginx/conf.d/ không tồn tại trên host này và không có gì tạo ra nó cả. Tác vụ service cũng thất bại vì không có unit nginx nào để truy vấn. Cả hai lỗi này không phải là bug trong playbook. Lần chạy thử (dry run) đã thiếu trạng thái cần thiết, đây chính là ý nghĩa của tài liệu khi cảnh báo rằng check mode không thể tạo ra kết quả hữu ích cho một tác vụ mà đầu vào phụ thuộc vào thay đổi của tác vụ trước đó.
Vì vậy, phiên bản trung thực của quy tắc này là: check mode chính xác trên một host đã được cấu hình đồng bộ (converged), và gây nhiễu trên một host mới. Một lần chạy --check mà mọi tác vụ đều báo ok là một khẳng định thực tế về một host đã đồng bộ, vì nó có nghĩa là sẽ không có gì thay đổi. Trên một host hoàn toàn mới, --check chủ yếu cho bạn biết rằng host đó còn mới. Khi bạn viết playbook Ansible đầu tiên cho VPS, hãy chuẩn bị tinh thần cho lần chạy thử đầu tiên sẽ toàn lỗi đỏ, và hãy đánh giá playbook dựa trên lần chạy thứ hai.
Tại sao các tác vụ command và shell bị bỏ qua trong chế độ check mode
ansible.builtin.command và ansible.builtin.shell không thể biết lệnh của bạn thực hiện việc gì. Không có cách nào để chạy một binary tùy ý ở chế độ chỉ đọc (read-only), vì vậy trong chế độ check mode, module sẽ từ chối thực thi. Kết quả của tác vụ sẽ mang giá trị skipped: true cùng thông báo Command would have run if not in check mode, và output của bạn sẽ hiển thị skipping: [web1].
Tài liệu của module gọi hỗ trợ check mode của nó là "một phần" (partial), và giải pháp thay thế được nêu tên là creates và removes. Hãy cung cấp cho tác vụ một đường dẫn creates, khi đó check mode ít nhất có thể đánh giá việc kiểm tra file:
- name: Extract the release bundle
ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
args:
creates: /opt/app/bin/appNếu /opt/app/bin/app đã tồn tại, check mode báo cáo Would not run command since '/opt/app/bin/app' exists, đây là một câu trả lời chính xác. Nếu đường dẫn không tồn tại, bạn nhận được Command would have run if not in check mode, đây cũng là một câu trả lời chính xác. Nếu không có creates, tác vụ đó sẽ là một khoảng trống trong quá trình chạy thử (dry run) của bạn.
Hệ quả kéo theo còn tệ hơn cả khoảng trống đó. Một tác vụ bị bỏ qua vẫn ghi nhận kết quả, nhưng kết quả đó là kết quả skip và nó không có key stdout. Điều kiện của tác vụ tiếp theo sau đó sẽ thất bại khi đang được đánh giá, với lỗi gần giống như 'dict object' has no attribute 'stdout'. Playbook của bạn chạy bình thường trong thực tế nhưng lại hỏng khi chạy thử, đây là lỗi gây khó hiểu nhất trong toàn bộ tính năng này.
check_mode: false, và vị trí duy nhất nó thuộc về
check_mode: false trên một task có nghĩa là "chạy lệnh này thật sự, ngay cả khi đang ở chế độ --check". Đây là cách sửa lỗi cho vấn đề lệnh bị bỏ qua, và nó chỉ an toàn trên các task chỉ thực hiện thao tác đọc.
- name: Read the installed app version
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
check_mode: false
changed_when: falseTask đó trung thực trong cả hai chế độ. Nó đọc một phiên bản và không bao giờ ghi dữ liệu, changed_when: false ngăn nó báo cáo một thay đổi mà nó không thực hiện, và check_mode: false làm cho app_version.stdout tồn tại trong quá trình chạy thử (dry run), để các điều kiện dựa trên nó vẫn được đánh giá.
Hãy đọc kỹ từ khóa trước khi bạn dán nó vào bất kỳ đâu khác. Một task có check_mode: false sẽ ghi dữ liệu vào server của bạn trong quá trình ansible-playbook --check. Đặt nó vào một task apt hoặc task template để làm cho quá trình chạy thử trông gọn gàng hơn, thì quá trình chạy thử của bạn không còn là chạy thử nữa. Khi một task ghi dữ liệu không thể làm cho an toàn, hãy bảo vệ nó bằng cách khác:
- name: Apply the database migration
ansible.builtin.command: /usr/local/bin/app migrate --apply
when: not ansible_check_modeansible_check_mode là một biến ma thuật mà Ansible đặt thành true trong quá trình chạy kiểm tra. Từ khóa ngược lại cũng tồn tại. check_mode: true ghim một task vào chế độ kiểm tra mọi lúc, ngay cả trong quá trình chạy thực tế, điều này biến nó thành một công cụ dò tìm sai lệch (drift probe): ghi lại kết quả, và một báo cáo changed có nghĩa là host không còn khớp với những gì task yêu cầu.
Tại sao một task luôn báo changed trong mỗi lần chạy
Chạy playbook hai lần liên tiếp mà không thay đổi gì cả. Mọi task đều phải báo ok ở lần chạy thứ hai. Bất kỳ task nào vẫn báo changed đều đang cho bạn biết một trong hai điều: module không thể thấy trạng thái mà nó quản lý, hoặc dữ liệu đầu vào bạn cung cấp không ổn định. Cả hai đều có thể sửa được và không phải là nhiễu cần phải loại bỏ.
commandvàshellkhi không cócreates,removeshoặcchanged_whensẽ báochangedmỗi lần chạy, vì module không có cách nào biết được liệu có gì đã xảy ra hay chưa. Hãy thêmcreates, hoặc thiết lậpchanged_whendựa trên một chuỗi trong kết quả đầu ra.ansible.builtin.filevớistate: touchsẽ báochangedtrong mỗi lần chạy theo thiết kế, vì thao tác touch một file sẽ cập nhật timestamp của nó. Hãy dùngstate: filenếu tất cả những gì bạn muốn là thiết lập owner hoặc mode.- Một
templatecó kết quả render thay đổi sẽ ghi đè file trong mỗi lần chạy. Một timestamp từansible_date_time, một lệnh gọinow(), hoặc một mật khẩu được tạo mới mỗi lần đều tạo ra các byte khác nhau, vì vậy module báo changed là chính xác. Hãy loại bỏ giá trị thay đổi đó ra khỏi template. ansible.builtin.uservớipassword: "{{ pw | password_hash('sha512') }}"sẽ thay đổi trong mỗi lần chạy, vìpassword_hashchọn một salt ngẫu nhiên mỗi khi được gọi, nên hash kết quả không bao giờ khớp với hash đã có trong/etc/shadow. Hãy truyền một salt cố định được tạo ra từ một giá trị ổn định.state: latesttrên một module package sẽ báochangedbất cứ khi nào có bản cập nhật. Điều đó là chính xác. Đó cũng là lý do tại saostate: latesttạo ra một playbook mà bạn không thể dự đoán kết quả. Hãy dùngstate: presentvà thực hiện nâng cấp một cách chủ động.ansible.builtin.unarchivetrỏ đến một URL mà không cócreatessẽ tải về và giải nén lại. Hãy cung cấp cho nó một đường dẫncreates.
--diff là cách nhanh nhất để phân biệt các trường hợp này. Nếu một task báo changed và diff cho thấy các byte khác biệt, dữ liệu đầu vào của bạn không ổn định. Nếu nó báo changed và diff không hiển thị gì cả, module không thể diễn giải những gì nó đã thay đổi, điều này thường có nghĩa là một task command hoặc một thao tác ghi chỉ liên quan đến metadata như timestamp.
Đừng dùng changed_when: false để làm im lặng một task ồn ào. Nó sẽ triệt tiêu báo cáo, khiến notify không bao giờ kích hoạt và handler để restart service sẽ không bao giờ chạy. Hãy sửa task đó thay vì dùng cách này.
Thu hẹp phạm vi ảnh hưởng: --limit, --tags và --step
Check mode cho bạn biết những gì sẽ thay đổi. Các flag này quyết định số lượng máy chủ nhận tác động cùng lúc.
--limit giới hạn play trong một tập con của inventory. Nó nhận các pattern giống như hosts:, vì vậy cả --limit web1 và --limit 'webservers:!web3' đều hoạt động. Hãy đặt pattern trong dấu ngoặc kép. Một ! không có dấu ngoặc kép trong phiên bash tương tác sẽ kích hoạt history expansion trên dấu chấm than, và shell của bạn sẽ viết lại lệnh trước khi Ansible kịp nhận diện.
Xác nhận pattern trước khi bạn tin tưởng nó. ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts in ra các host khớp với pattern và thoát mà không kết nối đến bất kỳ host nào. Một pattern không khớp với host nào là an toàn, vì Ansible không tự động quay lại chạy trên toàn bộ inventory. Nó in ra cảnh báo rằng không thể khớp pattern host, sau đó thoát với lỗi thông báo rằng các host và --limit không khớp với bất kỳ host nào. Hiểu cách file inventory định nghĩa các nhóm đó là yếu tố giúp pattern trở nên dễ dự đoán ngay từ đầu.
--tags deploy chỉ chạy các task có tag, và --skip-tags packages chạy tất cả các task còn lại. --list-tags in ra các tag khả dụng. Tags phát huy tác dụng khi một play phát triển đến mức bạn không muốn chạy toàn bộ nó nữa, đây cũng là một trong những lý do để chia một playbook dài thành các roles.
--start-at-task "Write the site config" tiếp tục một lần chạy bị lỗi từ một task được chỉ định. Hãy dùng nó để khôi phục, và hiểu cái giá phải trả: mọi thứ trước task đó đều bị bỏ qua, bao gồm cả các task thiết lập facts hoặc register các biến mà các task sau đó cần đọc.
--step hỏi xác nhận trước mỗi task và đợi bạn trả lời yes, no, hoặc continue. Nó chậm, nhưng là công cụ phù hợp trong lần đầu tiên bạn chạy một thao tác có tính phá hủy, vì bạn có thể dừng lại giữa hai task thay vì sau khi đã chạy xong hai mươi task.
Triển khai thay đổi theo lô với serial
Theo mặc định, Ansible chạy một task trên mọi host trong play trước khi bắt đầu task tiếp theo. Cách này nhanh, nhưng đồng nghĩa với việc một task lỗi sẽ ảnh hưởng đến toàn bộ hệ thống cùng lúc. Khi bạn kịp đọc lỗi và nhấn Ctrl-C, thay đổi đã áp dụng trên mọi nơi rồi.
serial chia play thành các lô. Toàn bộ play sẽ chạy trên lô đầu tiên, sau đó mới đến lô tiếp theo.
- name: Roll out the web tier
hosts: webservers
serial: [1, 5, "30%"]
max_fail_percentage: 0
tasks:
- name: Deploy the release
ansible.builtin.include_role:
name: webappLô đầu tiên là một host. Nếu nó hoạt động ổn, lô thứ hai sẽ là năm host, và các lô sau đó là 30 phần trăm số host trong play. max_fail_percentage: 0 kết thúc play ngay khi bất kỳ host nào trong lô gặp lỗi, vì vậy một bản release lỗi sẽ dừng lại ở một máy duy nhất. any_errors_fatal: true là phiên bản quyết liệt hơn, kết thúc play cho tất cả mọi người ngay khi host đầu tiên thất bại.
Chạy trên một host trước không phải là sự hoang tưởng, và lý do rất cụ thể. Các nhóm inventory thường bị lệch cấu hình theo thời gian. Một server được thêm vào sau sáu tháng so với các server khác có thể chạy bản phân phối khác, hoặc chứa một dịch vụ nào đó do ai đó cài thủ công, hoặc có cách phân vùng ổ đĩa khác biệt. Playbook có thể đúng với nhóm nhưng lại sai với host đó, và không một lần chạy thử (dry run) trên một host đã đồng bộ nào có thể phát hiện ra điều này. Quản lý một hệ thống server Linux phần lớn là công việc tìm ra host khác biệt trước khi thay đổi gây ra sự cố.
Thứ tự thực hiện các tác vụ
ansible-playbook site.yml --syntax-checkbắt lỗi YAML và lỗi cấu trúc mà không cần kết nối mạng.ansible-playbook site.yml --limit web1 --list-hostsxác nhận pattern của bạn khớp với những gì bạn mong đợi.ansible-playbook site.yml --limit web1 --check --difflà bước chạy thử (dry run). Hãy đọc kỹ phần diff.ansible-playbook site.yml --limit web1 --diffáp dụng cấu hình cho một host cụ thể đó.- Chạy lại bước 4. Mọi thứ sẽ báo
ok. Bất kỳ thành phần nào vẫn báochangedlà tác vụ cần sửa trước khi triển khai cho toàn bộ hệ thống. ansible-playbook site.yml --check --difftrên toàn bộ inventory lúc này sẽ trả về kết quả có ý nghĩa, vì các host đã hội tụ (converged) sẽ không hiển thị thay đổi, và những gì còn lại chính là delta thực tế.
Một lưu ý về bước 3. --diff sẽ in nội dung file ra terminal và log của CI job, vì vậy một template render mật khẩu database sẽ làm lộ mật khẩu đó vào log. Hãy thiết lập diff: false cho tác vụ đó để ẩn đầu ra, hoặc dùng no_log: true để ẩn toàn bộ kết quả, đồng thời lưu trữ giá trị đó trong một file Ansible Vault đã mã hóa thay vì để trong repository.
FAQ
ansible-playbook --check có làm thay đổi gì trên server không?
Không, ngoại trừ một trường hợp mà bạn có thể kiểm soát. Ở chế độ check mode, mọi module đều được yêu cầu báo cáo thay vì ghi dữ liệu, và các module không hỗ trợ tính năng này sẽ không báo cáo cũng như không thực hiện gì cả. Ngoại lệ là từ khóa tác vụ check_mode: false, nó buộc tác vụ đó phải thực thi thực tế ngay cả trong quá trình chạy --check. Hãy tìm kiếm trong các playbook và role của bạn từ khóa check_mode: false trước khi tin tưởng vào một lần chạy thử (dry run), và xác nhận mọi kết quả tìm thấy đều là tác vụ chỉ đọc trạng thái.
Sự khác biệt giữa --check và --diff là gì?
--check quyết định liệu có bất kỳ thay đổi thực tế nào được thực hiện hay không. --diff quyết định mức độ chi tiết mà bạn nhìn thấy. --check khi dùng riêng lẻ sẽ cho bạn biết một file sẽ bị thay đổi. --diff khi dùng riêng lẻ sẽ áp dụng thay đổi và hiển thị các dòng đã thay đổi. Hãy kết hợp chúng cho một lần chạy thử mà bạn có thể đọc hiểu được, và luôn bật --diff cho các lần chạy thực tế bằng cách thiết lập always = true dưới mục [diff] trong file ansible.cfg.
Tại sao task Ansible của tôi luôn báo changed trong mỗi lần chạy?
Vì module không thể nhận diện được trạng thái mà nó quản lý, hoặc giá trị bạn cung cấp cho nó khác nhau mỗi lần. command và shell luôn báo changed trừ khi bạn thêm creates hoặc changed_when. file với state: touch thay đổi theo thiết kế. Một template tạo ra timestamp hoặc mật khẩu mới mỗi lần sẽ tạo ra các byte khác nhau, vì vậy file thực sự bị ghi đè. Hãy chạy playbook hai lần liên tiếp: bất kỳ task nào vẫn báo changed ở lần chạy thứ hai chính là task cần sửa.
Tại sao các task command và shell của tôi bị bỏ qua trong quá trình chạy thử?
Vì không có cách nào để chạy một lệnh tùy ý ở chế độ chỉ đọc. Ở chế độ check mode, module command sẽ đặt skipped: true với thông báo Command would have run if not in check mode. Hãy thêm creates hoặc removes để chế độ check mode có thể đánh giá kiểm tra file thay thế. Đối với một task chỉ đọc trạng thái, hãy thiết lập check_mode: false cùng với changed_when: false, để kết quả đã đăng ký (registered result) vẫn tồn tại trong quá trình chạy thử và các điều kiện dựa trên nó vẫn hoạt động.
Tại sao check mode thất bại trên server mới nhưng lại thành công trên server cũ?
Vì check mode không tạo ra trạng thái mà các task sau đó phụ thuộc vào. Một lần chạy thử trên host chưa có nginx sẽ báo việc cài đặt là changed, sau đó thất bại ở task ghi dữ liệu vào /etc/nginx/conf.d/, vì thư mục đó chưa bao giờ được tạo. Đây là hành vi dự kiến. Check mode là công cụ phát hiện sự sai lệch (drift) cho các host đã được playbook cấu hình đồng bộ. Nó không thể xác thực lần chạy đầu tiên. Trên một host mới, hãy áp dụng playbook cho một máy và đọc kết quả của lần chạy thứ hai.