SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor

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 dry run: --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. --limit là có chủ ý. Diff của một host là thứ bạn có thể đọc. Diff của 20 host là thứ bạn sẽ cuộn qua.

Bốn từ kết quả chứa toàn bộ báo cáo.

  • ok: [web1] nghĩa là task đã chạy và không báo thay đổi. Với module quản lý state, chẳng hạn apt hoặc template, điều đó nghĩa là module đã kiểm tra và state đã khớp. Với debug hoặc set_fact, điều đó chỉ có nghĩa là task đã chạy.
  • changed: [web1] nghĩa là module dự đoán nó sẽ ghi thay đổi. Khi dùng --diff, các dòng bên trên cho biết thay đổi đó là gì. Hầu hết module dự đoán chính xác, nhưng một số module chỉ hỗ trợ check mode một phần và cảnh báo điều này trong tài liệu riêng: get_url chỉ gửi request HEAD trong check mode và cho biết nó sẽ báo trạng thái changed không chính xác.
  • skipping: [web1] nghĩa là task không chạy. Hoặc when của task đã được đánh giá và cho kết quả false, hoặc module không thể chạy trong check mode.
  • fatal: [web1] nghĩa là task bị lỗi trong lúc kiểm tra. Hãy đọc message trước khi kết luận playbook bị hỏng.

--diff in unified diff cho các file module. Các dòng bị xóa được đánh dấu bằng - và các dòng được thêm được đánh dấu bằng +. Phần này nằm dưới header có các dòng bắt đầu bằng --- before và +++ after, đồng thời ghi rõ path đích. Các module không ghi file sẽ in phần before và after riêng. Vì vậy, ansible.builtin.user hiển thị các thuộc tính mà module sẽ thay đổi thay vì nội dung file.

Bật diff vĩnh viễn trong ansible.cfg để không bao giờ quên flag này:

[diff]
always = true
context = 5

Có hai bước kiểm tra nhẹ hơn nên chạy trước check mode. ansible-playbook site.yml --syntax-check phân tích YAML và cấu trúc play mà không kết nối đến bất kỳ host nào. ansible-playbook site.yml --list-tasks in ra các task sẽ chạy. Đây là cách bạn phát hiện một role mà bạn nghĩ đã được gắn tag thực ra không được gắn tag. Cả hai lệnh đều không kết nối, nên chạy gần như ngay lập tức.

Bản thân check mode có kết nối. Nó mở SSH đến mọi host trong pattern và thu thập facts, nên host đang tắt sẽ làm dry run thất bại. Đây là một tín hiệu hữu ích, đồng thời cũng là lý do bạn cần quyết định playbook nên xử lý các host không thể truy cập như thế nào trước khi đưa dry run vào CI.

Chế độ check thất bại trên server mới

Play này đúng. Chạy nó với --check trên một server chưa có nginx, phần lớn các task 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: true

Task apt báo cáo changed, và điều đó là đúng: package chưa có, nên lần chạy thật sẽ cài đặt package này. Chế độ check không cài đặt package. Sau đó task template thất bại vì /etc/nginx/conf.d/ chưa tồn tại trên host này và chưa có task nào tạo nó. Task service cũng thất bại vì không có unit nginx để truy vấn. Cả hai lỗi này đều không phải lỗi trong playbook. Dry run không có đủ state cần thiết để tiếp tục. Đây là ý của tài liệu khi cảnh báo rằng chế độ check không thể tạo output hữu ích cho task có input phụ thuộc vào thay đổi của task trước đó.

Vì vậy, cách diễn đạt chính xác hơn là: chế độ check cho kết quả chính xác nhất trên host mà playbook đã converge, và tạo nhiều cảnh báo trên host mới. Một lần chạy --check trong đó mọi task đều báo ok là thông tin thực tế về một host đã converge, vì điều đó có nghĩa là sẽ không có gì thay đổi. Tuy nhiên, có hai ngoại lệ áp dụng cho mọi host: task command bị bỏ qua chỉ là một khoảng trống, không phải câu trả lời; và module chỉ hỗ trợ một phần chế độ check, như get_url, vẫn có thể báo sai trạng thái changed. Trên host hoàn toàn mới, --check chủ yếu cho biết host đó là host mới. Khi viết playbook Ansible đầu tiên cho một VPS, hãy dự kiến dry run đầu tiên sẽ đầy lỗi màu đỏ và đánh giá playbook dựa trên lần chạy thứ hai.

Tại sao các task command và shell bị bỏ qua trong check mode

ansible.builtin.command và ansible.builtin.shell không biết lệnh của bạn thực hiện việc gì. Không có cách chỉ đọc để chạy một binary tùy ý, nên module từ chối chạy nó trong check mode. Kết quả task chứa skipped: true và thông báo Command would have run if not in check mode, còn output hiển thị skipping: [web1].

Tài liệu của module gọi mức hỗ trợ check mode là “partial”, và workaround được nêu là creates và removes. Cung cấp cho task đường dẫn creates để 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/app

Nếu /opt/app/bin/app đã tồn tại, check mode báo Would not run command since '/opt/app/bin/app' exists. Đây là một kết quả thực sự. 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 kết quả thực sự. Không có creates, task đó chỉ để lại một khoảng trống trong dry run.

Hệ quả kéo theo còn tệ hơn khoảng trống này. Một task command vẫn đăng ký một result, và result đó có chứa stdout, nhưng giá trị là chuỗi rỗng vì task chưa chạy. Điều kiện của task tiếp theo được đánh giá dựa trên chuỗi rỗng đó và âm thầm chọn sai nhánh: việc kiểm tra version không tìm thấy version, còn một biểu thức như 'active' in svc.stdout cho kết quả false. Playbook của bạn cho một kết quả khi chạy thật và một kết quả khác trong dry run, nhưng không có lỗi nào để lần theo. Đây là lỗi khó hiểu nhất trong toàn bộ tính năng này. Lỗi 'dict object' has no attribute 'stdout' thuộc về một kiểu skip khác: task bị bỏ qua vì when có giá trị false sẽ đăng ký một result không có key stdout.

check_mode: false và nơi duy nhất nên dùng nó

check_mode: false trên một task có nghĩa là “chạy task này thật, kể cả khi đang ở --check”. Đây là cách sửa lỗi lệnh bị bỏ qua, và chỉ an toàn với task chỉ đọc.

- name: Read the installed app version
  ansible.builtin.command: /usr/local/bin/app --version
  register: app_version
  check_mode: false
  changed_when: false

Task đó hoạt động đúng trong cả hai mode. Task này đọc phiên bản và không ghi dữ liệu, changed_when: false ngăn Ansible báo có thay đổi dù task không thực hiện thay đổi nào, còn check_mode: false giúp app_version.stdout chứa phiên bản thực trong dry run thay vì chuỗi rỗng. Vì vậy, các điều kiện dựa trên biến này vẫn được đánh giá chính xác.

Hãy đọc đúng nghĩa của keyword này trước khi dán nó vào nơi khác. Task có check_mode: false sẽ ghi vào server của bạn trong lúc chạy ansible-playbook --check. Đặt nó trên task apt hoặc task template chỉ để dry run trông gọn hơn sẽ khiến dry run không còn là dry run. Khi không thể làm cho một task ghi dữ liệu trở nên an toàn, hãy dùng điều kiện để kiểm soát task đó:

- name: Apply the database migration
  ansible.builtin.command: /usr/local/bin/app migrate --apply
  when: not ansible_check_mode

ansible_check_mode là magic variable mà Ansible đặt thành true trong một check run. Cũng có keyword ngược lại. check_mode: true luôn buộc task chạy ở check mode, kể cả trong một lần chạy thật. Khi đó task trở thành một drift probe: hãy register kết quả, và báo cáo changed có nghĩa là host không còn khớp với trạng thái mà task yêu cầu.

Vì sao task báo changed ở mọi lần chạy

Chạy playbook 2 lần liên tiếp, không thay đổi gì ở giữa. Mọi task phải báo ok ở lần chạy thứ 2. Bất kỳ task nào vẫn báo changed đều cho thấy một trong 2 vấn đề: module không nhìn thấy state mà nó quản lý, hoặc input bạn truyền vào không ổn định. Cả 2 vấn đề đều sửa được. Không nên coi chúng là nhiễu rồi bỏ qua.

  • command và shell không có creates, removes hoặc changed_when sẽ báo changed ở mọi lần chạy, vì module không có cách biết đã có thay đổi hay chưa. Thêm creates hoặc đặt changed_when để kiểm tra một chuỗi trong output.
  • ansible.builtin.file với state: touch sẽ báo changed ở mọi lần chạy theo đúng thiết kế, vì việc chạm vào file sẽ cập nhật timestamp của file. Dùng state: file nếu mục tiêu của bạn chỉ là đặt owner hoặc mode.
  • Một template có output sau khi render thay đổi sẽ ghi lại file ở mọi lần chạy. Timestamp từ ansible_date_time, một lệnh gọi đến now() hoặc password được tạo mới ở mỗi lần chạy đều tạo ra các byte khác nhau, nên module báo changed là đúng. Đưa giá trị thay đổi đó ra khỏi template.
  • ansible.builtin.user với password: "{{ pw | password_hash('sha512') }}" sẽ thay đổi ở mọi lần chạy, vì password_hash chọn salt ngẫu nhiên mỗi khi được gọi. Vì vậy hash tạo ra không bao giờ khớp với hash đã có trong /etc/shadow. Truyền vào một salt tường minh được tạo từ một giá trị ổn định.
  • state: latest trên một package module sẽ báo changed khi có bản upgrade. Điều đó là chính xác. Đây cũng là lý do state: latest tạo ra playbook có kết quả không thể dự đoán. Dùng state: present và thực hiện upgrade một cách có chủ đích.
  • ansible.builtin.unarchive trỏ đến một URL không có creates sẽ download lại archive ở mọi lần chạy. Với các loại archive, module cũng không thể so sánh với destination mà nó lại giải nén lại. Cung cấp cho nó một path creates.

--diff là cách nhanh nhất để phân biệt các trường hợp này. Nếu task báo changed và diff hiển thị các byte khác nhau, input của bạn không ổn định. Nếu task báo changed nhưng diff không hiển thị gì, module không thể mô tả thay đổi mà nó đã thực hiện. Trường hợp này thường là task command hoặc thao tác chỉ thay đổi metadata, chẳng hạn timestamp.

Đừng dùng changed_when: false chỉ để làm task nhiều output có vẻ yên lặng hơn. Nó ẩn report, nên notify sẽ không bao giờ được kích hoạt và handler khởi động lại service cũng không chạy. Hãy sửa task thay vì che giấu vấn đề.

Thu hẹp phạm vi ảnh hưởng: --limit, --tags và --step

Chế độ kiểm tra cho bạn biết những gì sẽ thay đổi. Các flag này quyết định có bao nhiêu máy thực hiện thay đổi cùng lúc.

--limit giới hạn play vào một phần inventory. Nó nhận cùng các pattern 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. ! không được đặt trong 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, khiến shell sửa lại command trước khi Ansible nhận được.

Hãy xác nhận pattern trước khi tin tưởng nó. ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts in ra các host khớp pattern rồi thoát mà không kết nối đến host nào. Pattern không khớp host nào là an toàn, vì Ansible không tự động quay lại toàn bộ inventory. Ansible in cảnh báo rằng không thể khớp host pattern, rồi thoát với lỗi cho biết inventory, host pattern và --limit không để lại host nào làm target. Hiểu inventory file định nghĩa các group đó như thế nào là điều giúp pattern có kết quả dễ dự đoán ngay từ đầu.

--tags deploy chỉ chạy các task có tag, còn --skip-tags packages chạy mọi task khác. --list-tags in ra các tag hiện có. Tags trở nên hữu ích khi play đã lớn đến mức bạn không còn muốn chạy toàn bộ, cũng là một trong những lý do để tách playbook dài thành các role.

--start-at-task "Write the site config" tiếp tục một lần chạy bị lỗi từ task có tên được chỉ định. Hãy dùng nó để khôi phục, nhưng cần hiểu cái giá phải trả: mọi thứ trước task đó đều bị bỏ qua, bao gồm các task thiết lập facts hoặc register các biến mà những task sau sẽ đọc.

--step hiển thị prompt trước mỗi task và chờ bạn trả lời yes, no hoặc continue. Cách này chậm, nhưng phù hợp khi lần đầu chạy một thao tác có thể gây mất dữ liệu, vì bạn có thể dừng giữa hai task thay vì sau hai mươi task.

Triển khai thay đổi theo từng đợt

Theo mặc định, Ansible chạy một task trên mọi host trong play rồi mới bắt đầu task tiếp theo. Cách này nhanh, nhưng cũng có nghĩa là một task lỗi sẽ chạy trên toàn bộ fleet trong cùng một giây. Đến khi bạn đọc lỗi và nhấn Ctrl-C, thay đổi đã được áp dụng ở mọi nơi.

serial chia play thành các batch. Toàn bộ play chạy trên batch đầu tiên, rồi mới chạy trên batch 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: webapp

Batch đầu tiên có một host. Nếu host này hoạt động bình thường, batch thứ hai có năm host, còn mỗi batch sau đó có 30 phần trăm số host trong play. max_fail_percentage: 0 hủy phần còn lại của play ngay khi có bất kỳ host nào trong một batch bị lỗi, vì ngưỡng phải bị vượt qua và mọi lỗi đều vượt quá 0. Việc bảo đảm chỉ triển khai trên một máy trước tiên đến từ batch đầu tiên, không phải từ setting đó: một release bị lỗi trên toàn bộ hệ thống sẽ fail trên host duy nhất trong batch một và dừng tại đó. Một lỗi chỉ xuất hiện trong batch năm host đã ảnh hưởng đến các host khác trong batch đó, vì Ansible chạy từng task trên toàn bộ batch rồi mới bắt đầu task tiếp theo. any_errors_fatal: true là phiên bản mạnh tay hơn, kết thúc play cho tất cả mọi người ngay khi host đầu tiên bị lỗi.

Chạy trên một host trước tiên không phải là quá thận trọng, và lý do rất cụ thể. Các inventory group sẽ dần khác nhau. Một server được thêm vào sau các server khác sáu tháng có thể chạy một distribution release khác, có một service do ai đó cài thủ công, hoặc có cách bố trí disk khác. Playbook đúng với group nhưng sai với host đó, và không một dry run nào trên host đã đồng nhất với các host khác có thể phát hiện ra điều này. Quản lý fleet các server Linux phần lớn là việc tìm ra host khác biệt trước khi thay đổi tác động đến nó.

Thứ tự thực hiện các tác vụ

  1. ansible-playbook site.yml --syntax-check bắt lỗi YAML và lỗi cấu trúc mà không cần kết nối mạng.
  2. ansible-playbook site.yml --limit web1 --list-hosts xác nhận pattern của bạn khớp với những gì bạn mong đợi.
  3. ansible-playbook site.yml --limit web1 --check --diff là bước chạy thử (dry run). Hãy đọc kỹ phần diff.
  4. ansible-playbook site.yml --limit web1 --diff áp dụng cấu hình cho một host cụ thể đó.
  5. 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áo changed là tác vụ cần sửa trước khi triển khai cho toàn bộ hệ thống.
  6. ansible-playbook site.yml --check --diff trê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ó thay đổi gì trên server không?

Không, ngoại trừ một trường hợp do bạn kiểm soát. Trong check mode, mỗi module được yêu cầu báo cáo thay vì ghi thay đổi; các module không hỗ trợ việc này sẽ không báo cáo và cũng không thực hiện gì. Ngoại lệ là từ khóa task check_mode: false, buộc task đó thực thi thật ngay cả khi chạy bằng --check. Hãy tìm check_mode: false trong playbook và role trước khi tin vào dry run, đồng thời xác nhận mọi kết quả tìm được đều là task chỉ đọc trạng thái.

Sự khác nhau giữa --check và --diff là gì?

--check quyết định có thực thi thay đổi thật hay không. --diff quyết định mức độ chi tiết bạn nhìn thấy. Chỉ dùng --check sẽ cho biết một file sẽ thay đổi. Chỉ dùng --diff sẽ áp dụng thay đổi và hiển thị các dòng đã thay đổi. Hãy dùng chúng cùng nhau để có một dry run dễ đọc, đồng thời bật --diff cho cả các lần chạy thật bằng cách đặt always = true trong [diff] thuộc ansible.cfg.

Tại sao task Ansible của tôi báo changed trong mọi lần chạy?

Vì module không nhìn thấy trạng thái mà nó quản lý, hoặc giá trị bạn truyền vào khác nhau ở mỗi lần chạy. command và shell luôn báo changed, trừ khi bạn thêm creates hoặc changed_when. file cùng với state: touch thay đổi theo thiết kế. Template tạo ra timestamp hoặc password mới sẽ sinh ra các byte khác nhau trong mỗi lần chạy, nên file thực sự được ghi lại. Hãy chạy playbook 2 lần liên tiếp: mọi thứ vẫn ở trạng thái changed trong lần chạy thứ 2 là task cần sửa.

Tại sao các task command và shell của tôi bị bỏ qua trong dry run?

Vì không có cách chỉ đọc để chạy một command tùy ý. Trong check mode, module command đặ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 để check mode có thể đánh giá phép kiểm tra file thay thế. Với task chỉ đọc trạng thái, hãy đặt check_mode: false cùng với changed_when: false để kết quả đã register chứa output thật trong dry run và các điều kiện dựa trên kết quả đó vẫn hoạt động.

Tại sao check mode thất bại trên server mới nhưng chạy được trên server hiện có?

Vì check mode không tạo trạng thái mà các task sau đó phụ thuộc vào. Dry run trên một host chưa có nginx sẽ báo việc cài đặt là changed, rồi thất bại ở task ghi vào /etc/nginx/conf.d/ vì thư mục đó chưa từng được tạo. Đây là hành vi dự kiến. Check mode là công cụ phát hiện drift cho các host mà playbook đã converge. Nó không thể xác thực lần chạy đầu tiên. Trên host mới, hãy áp dụng playbook cho một máy rồi đọc kết quả của lần chạy thứ 2.

#ansible#check-mode#idempotency#automation#safety