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

Ansible template và handler: ví dụ reload nginx

Tạo config nginx bằng Jinja2, dùng handler chỉ reload khi file đổi, rồi chạy playbook hai lần để kiểm tra idempotence và tránh reload thừa.

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

Các template và handler của Ansible bổ sung gì cho playbook đầu tiên

Template và handler của Ansible là hai thành phần biến một playbook tĩnh thành playbook hữu ích. Template tạo file cấu hình từ các biến của bạn, nên một file có thể dùng cho mọi host. Handler chỉ chạy khi một task thực sự thay đổi dữ liệu, vì vậy service được reload khi cấu hình thực sự thay đổi và không bị tác động trong những lần còn lại.

Hướng dẫn này tiếp tục đúng từ playbook Ansible đầu tiên của bạn trên VPS. Bạn đã có một play cài package và khởi động service. Toàn bộ nội dung bên dưới chạy trên một máy vì play nhắm đến localhost qua local connection. Bạn không cần server thứ hai để làm theo. Play tương tự có thể chạy trên các host trong inventory thực tế mà không cần thay đổi các task; phần cuối cùng giải thích những điểm cần thay đổi.

Thiết lập thư mục làm việc

sudo apt update
sudo apt install -y ansible nginx
ansible --version
mkdir -p ~/ansible-templates/templates
cd ~/ansible-templates

nginx chỉ được dùng ở đây vì đây là một service thực tế có file cấu hình và lệnh reload, đủ cho ví dụ này. ansible --version in phiên bản ansible-core và Python interpreter mà Ansible sẽ sử dụng. Ghi lại cả hai giá trị. Playbook bên dưới dùng tên module đầy đủ như ansible.builtin.template. Các tên này yêu cầu Ansible 2.10 trở lên, trong khi mọi package hiện tại của các distribution đều đã vượt xa phiên bản đó.

Tạo inventory.ini:

[local]
localhost ansible_connection=local ansible_python_interpreter="{{ ansible_playbook_python }}"

ansible_connection=local yêu cầu Ansible chạy từng task dưới dạng một process cục bộ thay vì mở phiên SSH đến chính máy đó. Thiết lập thứ hai không phải để trang trí. Khi ghi localhost vào inventory file, nó trở thành một host thông thường và không còn nhận interpreter mà Ansible tự động cung cấp cho localhost ngầm định. Khi đó Ansible chuyển sang interpreter discovery và có thể chọn một Python khác với Python đang chạy play. ansible_playbook_python là interpreter đang chạy ansible-playbook ngay lúc này, nhờ đó hai bên luôn dùng cùng interpreter.

Tạo ansible.cfg:

[defaults]
inventory = inventory.ini

Nếu không có file này, bạn phải truyền -i inventory.ini trong mọi command. Khi hoàn toàn không có inventory, Ansible in ra [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all', và play có hosts: all sẽ không match host nào. Còn một điểm cần lưu ý về ansible.cfg: Ansible bỏ qua file này nếu nó nằm trong thư mục có quyền ghi cho mọi người, vì vậy hãy đặt project trong home directory của bạn. Inventory file chứa nhiều thông tin hơn một danh sách host, và đây là file nhỏ nhất đủ để hoạt động.

template và copy, và khi nào dùng từng loại

ansible.builtin.copy truyền file nguyên trạng. ansible.builtin.template chạy file qua Jinja2 trước rồi truyền kết quả. Tài liệu module mô tả template là “một module ảo được triển khai hoàn toàn dưới dạng action plugin và chạy trên controller”. Điều này dẫn đến một điểm cần nhớ: quá trình render diễn ra trên máy nơi bạn nhập ansible-playbook. Target host không bao giờ thấy các biến của bạn và không cần cài Jinja2.

Dùng copy khi file giống hệt nhau trên mọi host. Dùng template ngay khi một giá trị khác nhau trên từng host, hoặc khi cần vòng lặp {% for %} hay block {% if %}. copy có tham số content:, và các biến bên trong tham số này được thay thế như mọi task argument khác. Tuy nhiên, tham số này không hỗ trợ vòng lặp hoặc điều kiện, nên mọi nội dung có cấu trúc đều phải đặt trong template. Cả hai module nhận cùng các tùy chọn file vì cùng dùng các đoạn tài liệu giống nhau. Do đó, owner, group, mode, backupvalidate hoạt động giống nhau trong cả hai module.

Viết template: một biến, một vòng lặp

Lưu nội dung sau thành templates/app.conf.j2:

# {{ ansible_managed }}
upstream {{ app_name }}_backend {
{% for backend in app_backends %}
    server {{ backend.host }}:{{ backend.port }} weight={{ backend.weight }};
{% endfor %}
}

server {
    listen {{ app_listen_port }};
    server_name {{ app_server_name }};

    location / {
        proxy_pass http://{{ app_name }}_backend;
        proxy_set_header Host $host;
    }
}

Hai loại Jinja2 tag thực hiện phần việc này. {{ ... }} là expression và in ra giá trị của nó. {% ... %} là statement và không tự in gì. app_backends là một danh sách các dictionary, nên backend.host đọc một key từ mỗi entry, còn vòng lặp ghi một dòng server cho mỗi entry, với số lượng entry tùy bạn định nghĩa.

Có một chi tiết về khoảng trắng dễ gây bất ngờ cho những người đã biết Jinja2 từ nơi khác. Theo mặc định, Ansible đặt trim_blocks thành yes, còn bản thân Jinja2 thì không. Vì vậy, newline ngay sau tag {% ... %} sẽ bị xóa và vòng lặp không để lại một dòng trống phía sau. Ansible giữ lstrip_blocksno, nên mọi khoảng trắng bạn đặt trước tag {% sẽ được giữ lại và xuất hiện trong file đã render. Nếu output có indentation thừa, hãy đặt lstrip_blocks: true trên task template.

Theo mặc định, {{ ansible_managed }} render thành văn bản nguyên văn Ansible managed. Hãy giữ nguyên như vậy. Nhiều người thường định nghĩa lại ansible_managed trong ansible.cfg để thêm ngày tháng. Khi đó, file đã render sẽ khác sau mỗi lần chạy, task báo có thay đổi sau mỗi lần chạy và service reload sau mỗi lần chạy. Chỉ một thiết lập đó cũng phá hỏng tính chất mà phần còn lại của hướng dẫn này hướng đến. Extension .j2 chỉ là một quy ước và Ansible không kiểm tra nó.

Playbook

Lưu nội dung này vào site.yml:

- name: Render an nginx site from a template
  hosts: local
  become: true

  vars:
    app_name: learn
    app_listen_port: 8080
    app_server_name: learn.example.com
    app_backends:
      - host: 127.0.0.1
        port: 9001
        weight: 3
      - host: 127.0.0.1
        port: 9002
        weight: 1

  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Render the site configuration
      ansible.builtin.template:
        src: templates/app.conf.j2
        dest: "/etc/nginx/conf.d/{{ app_name }}.conf"
        owner: root
        group: root
        mode: '0644'
        backup: true
      notify: nginx config changed

    - name: Make sure nginx is enabled and running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Test the nginx configuration
      ansible.builtin.command:
        cmd: /usr/sbin/nginx -t
      changed_when: false
      listen: nginx config changed

    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded
      listen: nginx config changed

mode: '0644' được đặt trong dấu ngoặc kép một cách có chủ ý. Tài liệu về tùy chọn file yêu cầu đặt các số bát phân trong dấu ngoặc kép “để Ansible nhận được một chuỗi và tự chuyển đổi từ chuỗi sang số”. Nếu không đặt trong dấu ngoặc kép, YAML parser sẽ đọc 0644 là một số thông thường và bạn có thể nhận được quyền truy cập khác với yêu cầu.

notify: nginx config changed đặt tên cho một topic, không phải một handler. Cả hai handler đều có listen: nginx config changed, nên một lần notify sẽ gọi cả hai. Sau này, nếu thêm handler thứ ba có cùng dòng listen, task template không cần chỉnh sửa. cache_valid_time: 3600 ngăn lần chạy thứ hai trong cùng một giờ truy cập lại các package mirror.

Chạy một lần, rồi đọc nội dung được in ra

ansible-playbook site.yml

Nếu sudo yêu cầu mật khẩu, thêm -K để Ansible hỏi mật khẩu.

Trước tiên, đọc các dòng của từng task, sau đó đọc PLAY RECAP ở cuối. Mỗi task in changed: khi Ansible phải thực hiện thay đổi, hoặc ok: khi host đã ở trạng thái mong muốn. Phần tổng kết cộng các bộ đếm đó theo từng host. Chỉ sau khi mọi task trong play hoàn tất, không sớm hơn, bạn mới thấy RUNNING HANDLER [Test the nginx configuration] theo sau là RUNNING HANDLER [Reload nginx].

Bây giờ hãy kiểm tra trực tiếp trên máy thay vì chỉ tin vào output:

sudo cat /etc/nginx/conf.d/learn.conf
sudo /usr/sbin/nginx -t
curl -sI http://127.0.0.1:8080/

nginx -t in nginx: configuration file /etc/nginx/nginx.conf test is successful khi cấu hình đã ghép parse thành công. curl trả về một dòng trạng thái từ nginx, còn 502 Bad Gateway là kết quả đúng trong trường hợp này, vì server block đang hoạt động và không có tiến trình nào listening trên các cổng 9001 hoặc 9002. sudo tail /var/log/nginx/error.log nêu rõ lý do: connect() failed (111: Connection refused) while connecting to upstream.

Chạy lần thứ hai để chứng minh tính idempotent

ansible-playbook site.yml

Đây là lần chạy cần kiểm tra, vì vậy hãy so sánh output của lần này với lần đầu tiên, từng dòng một. Task template lúc này phải in ok: thay vì changed:, và không handler nào được xuất hiện ở bất kỳ đâu trong output.

Cơ chế này đơn giản và đáng ghi nhớ, vì bạn sẽ dựa vào nó để debug. template render file trên controller rồi so sánh checksum của kết quả với checksum của file hiện có tại dest. Khi nội dung, owner và mode khớp nhau thì không có gì cần thực hiện. Vì vậy task báo ok, notify không bao giờ được kích hoạt và handler không chạy. Handler chỉ được kích hoạt khi changed, không phải trong trường hợp nào khác.

Hãy chứng minh cả chiều ngược lại. Đổi weight: 3 thành weight: 1 trong vars, chạy lại play. Task template sẽ báo changed, cả hai handler sẽ chạy và sudo cat /etc/nginx/conf.d/learn.conf sẽ hiển thị giá trị mới.

Nếu lần chạy thứ hai, với cùng một input, vẫn báo có thay đổi thì kết quả render không ổn định. Trước tiên hãy tìm giá trị phụ thuộc thời gian trong output, vì đây là nguyên nhân phổ biến và ansible_managed đã tùy chỉnh thường là thủ phạm. Sau đó kiểm tra modeowner trong task có khớp với giá trị thực tế trên disk hay không, vì việc không khớp cũng được xem là một thay đổi ngay cả khi các byte giống hệt nhau.

Xem thay đổi trước khi áp dụng

ansible-playbook site.yml --check --diff

--check chạy play mà không thay đổi host. --diff in ra nội dung mà mỗi task có thể đã thay đổi. Với template, đó là phần khác nhau từng dòng giữa nội dung render và file trên disk. Kết hợp lại, chúng trả lời câu hỏi "lần chạy này sẽ làm gì" mà không thực sự áp dụng thay đổi. Check mode cũng có những giới hạn riêng, chủ yếu ở các task có kết quả phụ thuộc vào task trước đó mà check mode không thực sự thực thi.

Vì sao handler chờ đến cuối play

Tài liệu về handler nêu rõ: "Theo mặc định, handler chạy sau khi tất cả task trong một play cụ thể đã hoàn tất. Các handler được notify sẽ tự động chạy sau từng section sau đây, theo thứ tự: pre_tasks, roles/taskspost_tasks."

Lý do là cơ chế gom nhóm. Một play render 4 file cấu hình cho một service nên chỉ restart service đó 1 lần ở cuối, khi cả 4 file đã sẵn sàng. Nếu restart sau từng file, service sẽ bị restart 4 lần, và 3 lần trong số đó sẽ nạp cấu hình chưa hoàn chỉnh. Cùng trang tài liệu cũng nêu rõ điều kiện đảm bảo: "Notify cùng một handler nhiều lần chỉ khiến handler đó chạy 1 lần, bất kể có bao nhiêu task notify nó."

Thứ tự cũng được cố định: "Handler chạy theo thứ tự được định nghĩa trong section handlers, không theo thứ tự được liệt kê trong statement notify." Vì vậy Test the nginx configuration nằm phía trên Reload nginx trong playbook. Test chạy trước vì nó được viết trước; nội dung trong dòng notify không ảnh hưởng đến điều này.

Cách chạy handler sớm và cách chạy handler sau khi có lỗi

Đôi khi một task ở phần sau của cùng play cần service đã chạy với cấu hình mới. Hãy flush các handler đã được notify tại thời điểm đó bằng module meta. Tài liệu mô tả module này là khiến “Ansible chạy mọi handler task đã được notify cho đến thời điểm hiện tại”.

    - name: Run the notified handlers now instead of at the end of the play
      ansible.builtin.meta: flush_handlers

    - name: Wait for the new listener to accept connections
      ansible.builtin.wait_for:
        host: 127.0.0.1
        port: 8080
        timeout: 10

Nếu bỏ dòng meta, task wait_for sẽ chạy khi nginx vẫn đang phục vụ cấu hình cũ. Trong lần chạy đầu tiên, chưa có listener nào trên port 8080, nên task chờ đủ mười giây rồi fail.

Trường hợp thứ hai là khi có lỗi. “Nếu một task notify một handler nhưng một task khác fail ở phần sau của play, theo mặc định handler sẽ không chạy trên host đó. Điều này có thể khiến host rơi vào trạng thái không mong muốn.” Vì vậy, một play render cấu hình rồi gặp lỗi ở một task không liên quan sẽ để file mới trên disk, trong khi service đang chạy vẫn nạp cấu hình cũ. Có thể thay đổi hành vi này bằng --force-handlers trên command line hoặc bằng force_handlers: true trong play. Tùy chọn tương tự cũng tồn tại dưới dạng force_handlers = True trong [defaults] của ansible.cfg và dưới dạng biến môi trường ANSIBLE_FORCE_HANDLERS. Giá trị mặc định là False.

Tên handler bị trùng và handler bị bỏ qua mà không báo lỗi

Tài liệu nêu rõ quy tắc: “Mỗi handler phải có một tên duy nhất trên toàn cục. Nếu nhiều handler được định nghĩa với cùng một tên, chỉ handler cuối cùng được load vào play mới có thể được notify và thực thi.” Handler được định nghĩa bên trong role cũng không nằm trong phạm vi riêng của role đó. Chúng được đưa vào một danh sách handler chung cho toàn bộ play. Vì vậy, nếu hai role cùng định nghĩa Restart nginx, tên đó chỉ trỏ đến đúng một trong hai handler. Thứ tự load quyết định handler nào được chọn, không phải role mà bạn đã notify.

Hãy kiểm tra quy tắc này trước khi dựa vào nó. Lưu nội dung sau vào handlers-dup.yml:

- name: Two handlers, one name
  hosts: local
  gather_facts: false

  tasks:
    - name: Notify the duplicated name
      ansible.builtin.command:
        cmd: /bin/true
      changed_when: true
      notify: Duplicated handler

  handlers:
    - name: Duplicated handler
      ansible.builtin.file:
        path: /tmp/dup-first
        state: touch
        mode: '0644'

    - name: Duplicated handler
      ansible.builtin.file:
        path: /tmp/dup-second
        state: touch
        mode: '0644'
rm -f /tmp/dup-first /tmp/dup-second
ansible-playbook handlers-dup.yml
ls -l /tmp/dup-first /tmp/dup-second

Play chạy thành công, RUNNING HANDLER [Duplicated handler] xuất hiện một lần, còn ls in một dòng cho /tmp/dup-firstls: cannot access '/tmp/dup-second': No such file or directory cho handler còn lại. Handler được chạy là handler được viết trước, không phải handler được load sau cùng như câu mô tả trên dự đoán.

Cần hiểu rõ sự khác biệt này, vì quy tắc trong tài liệu áp dụng cho các block handler, không phải cho từng dòng trong một file. Các handler đến từ những nơi khác nhau, chẳng hạn một role rồi đến một role khác, là các block riêng biệt. Block đến sau sẽ ghi đè block đến trước. Một danh sách handlers: thông thường trong play là một block duy nhất. Khi tìm trong một block, Ansible duyệt từ trên xuống dưới và dừng tại tên đầu tiên khớp. Vì vậy, trong cùng một file, định nghĩa đầu tiên được chọn và định nghĩa thứ hai không thể được truy cập. Giữa các role, cơ chế ghi đè lại hoạt động như tài liệu mô tả. Dù theo cách nào, bạn cũng không thể truy cập cả hai handler, và không nên xây dựng cấu hình dựa trên một trong hai hành vi này.

Có hai cách xử lý rõ ràng. Đặt tiền tố riêng của role vào tên mỗi handler, hoặc notify dạng có phạm vi là role_name : handler_name. Tài liệu nêu đây là cách “đảm bảo handler từ một role được notify thay vì handler bên ngoài role có cùng tên”. Dấu cách quanh dấu hai chấm là một phần của cú pháp. Vấn đề này sẽ xuất hiện ngay khi bạn bắt đầu dùng các role do người khác viết.

Trang tài liệu đó còn có một quy tắc khác: “Tránh đặt biến trong tên handler. Vì tên handler được template từ sớm, Ansible có thể chưa có giá trị cho một tên handler như vậy.” Handler có tên Restart {{ service_name }} sẽ làm toàn bộ play fail nếu biến đó chưa được định nghĩa tại thời điểm tên được template. Giữ tên handler dưới dạng chuỗi cố định và nhóm chúng bằng listen sẽ tránh được vấn đề này.

kiểm tra: từ chối cài đặt file render bị lỗi

validate chạy một command trên file đã render trước khi Ansible đưa file đó vào vị trí đích. Tài liệu ghi: “Command validation cần chạy trước khi copy file đã cập nhật vào destination cuối cùng. Một path tạm được dùng để kiểm tra và truyền qua %s; tham số này phải xuất hiện như trong các ví dụ bên dưới. Command cũng được truyền một cách an toàn, nên các tính năng của shell như expansion và pipe sẽ không hoạt động.”

Từ nội dung trên có 2 quy tắc. %s là bắt buộc. Chuỗi validate không có nó sẽ làm task fail với validate must contain %s. Ngoài ra không có shell, nên pipe, redirect, globbing và && sẽ không hoạt động. Mỗi lần chỉ chạy một command với một file argument.

Các ví dụ chính thức của module là 2 trường hợp hoạt động hoàn toàn đúng:

- name: Copy a new sudoers file into place, after passing validation with visudo
  ansible.builtin.template:
    src: /mine/sudoers
    dest: /etc/sudoers
    validate: /usr/sbin/visudo -cf %s

- name: Update sshd configuration safely, avoid locking yourself out
  ansible.builtin.template:
    src: etc/ssh/sshd_config.j2
    dest: /etc/ssh/sshd_config
    owner: root
    group: root
    mode: '0600'
    validate: /usr/sbin/sshd -t -f %s
    backup: yes

Cả hai đều hoạt động vì mỗi checker nhận một file và tự kiểm tra file đó theo quy tắc riêng. visudo -cf đọc một file sudoers. sshd -t -f đọc một sshd_config hoàn chỉnh.

Vì sao validate không thể kiểm tra file nginx trong hướng dẫn này

Thêm validate: /usr/sbin/nginx -t -c %s vào task template ở trên sẽ làm task thất bại. Thông báo đã nêu rõ nguyên nhân:

nginx: [emerg] "upstream" directive is not allowed here in <ansible temporary path>:2

nginx -t -c cần toàn bộ configuration, bắt đầu ở top level với các block eventshttp. File mà play này render chỉ là một fragment, được đưa vào block http bởi include /etc/nginx/conf.d/*.conf; bên trong /etc/nginx/nginx.conf. Khi đứng riêng và không có context đó, upstream đúng là một directive ở sai vị trí, nên nginx từ chối một file vốn hoàn toàn đúng ở nơi nó thực sự được dùng. Checker nhận một fragment nhưng lại được yêu cầu xem nó như toàn bộ configuration.

Cách làm đúng là cách đã có trong playbook. Cài fragment trước, sau đó kiểm tra configuration đã được ghép trong một handler được định nghĩa phía trên handler reload. Vì handlers chạy theo thứ tự định nghĩa, nginx -t sẽ thấy /etc/nginx/nginx.conf thực tế với fragment của bạn đã được include. Nếu kiểm tra thất bại, play sẽ thất bại trước khi systemctl reload được gọi. Cần hiểu rõ chi phí của cách này: file lỗi đã nằm trên disk khi bước kiểm tra thất bại, còn nginx vẫn tiếp tục phục vụ bằng configuration cuối cùng mà nó đã load cho đến khi có người restart nó.

Đó là lý do backup: true cần được dùng. Nó tạo một bản copy của file trước đó ngay cạnh file gốc trước khi ghi đè, với tên basename.PID.YYYY-MM-DD@HH:MM:SS~. Vì vậy directory sẽ có các entry như learn.conf.4127.2026-08-20@11:42:09~. Chạy sudo ls -l /etc/nginx/conf.d/ sau một lần thay đổi, bạn sẽ thấy một file như vậy.

Chi tiết về cách đặt tên này quan trọng hơn bạn nghĩ. Backup không gây hại trong /etc/nginx/conf.d/ vì main config chỉ include conf.d/*.conf và tên backup kết thúc bằng dấu tilde. Nhưng nó sẽ gây hại trong một directory được include bằng * không có pattern cụ thể. Trên Debian và Ubuntu, /etc/nginx/nginx.conf include /etc/nginx/sites-enabled/* đúng theo cách đó. Nếu template vào sites-enabled bằng backup: true, nginx sẽ load backup như một server block thứ hai đang hoạt động. Vì vậy play này ghi file vào conf.d thay vì directory đó.

Chạy cùng play trên các host thật trong inventory

Thay hosts: local bằng tên group bạn sử dụng. Không có phần nào khác trong play thay đổi. Template được render một lần cho mỗi host, vì vậy app_listen_portapp_backends có thể lấy giá trị từ group_varshost_vars, trong khi file template vẫn chỉ cần một bản. Đó là lợi ích của việc đặt giá trị trong các biến thay vì ghi trực tiếp vào file.

Có 2 điểm thay đổi. become: true giờ cần sudo password trên từng target, trừ khi các target đó đã cấu hình sudo không cần password, vì vậy hãy thêm -K. Mọi secret trong template, chẳng hạn database password hoặc API token, cũng không được để ở dạng plain vars: trong file mà bạn commit. Mã hóa các giá trị đó bằng Ansible Vault và tham chiếu chúng theo tên như hiện tại, vì template không phụ thuộc vào nguồn của biến.

Khi play phát triển để quản lý nhiều hơn 1 service, vars:, templates/handlers: đều đã có vị trí chuẩn tương ứng. Chuyển chúng vào đó chính là mục đích của việc tách playbook và role.

FAQ

Vì sao Ansible handler của tôi không chạy?

Gần như luôn là vì task gửi thông báo cho handler báo ok thay vì changed. Handler chỉ chạy khi có thay đổi, không vì lý do nào khác. Vì vậy, một task template có nội dung render giống hệt file hiện có trên disk sẽ không gửi thông báo. Sau đó, hãy kiểm tra 4 điểm. Chuỗi trong notify phải khớp chính xác với handler name hoặc topic listen, kể cả chữ hoa, chữ thường và khoảng trắng. Một task chạy sau đó bị lỗi trên host sẽ ngăn các handler đã được thông báo chạy, trừ khi bạn truyền --force-handlers. Handler được định nghĩa trong play khác sẽ không hiển thị trong play này. Task gửi thông báo nhưng bị bỏ qua do điều kiện when cũng không bao giờ gửi thông báo.

Vì sao playbook của tôi báo changed trong mọi lần chạy?

Nội dung được render không ổn định giữa các lần chạy. Nguyên nhân phổ biến nhất là output có timestamp. Chuỗi ansible_managed được tùy chỉnh và có chứa ngày tháng cũng gây ra đúng vấn đề này. Tiếp theo, hãy kiểm tra modeowner của task. Nếu các giá trị này không khớp với file hiện có trên disk, Ansible sẽ sửa chúng và báo có thay đổi dù nội dung giống hệt nhau. Chạy ansible-playbook site.yml --check --diff để biết vấn đề nằm ở phần nào, vì --diff cho bạn thấy thay đổi mà task dự định thực hiện.

Sự khác nhau giữa template và copy trong Ansible là gì?

ansible.builtin.copy gửi file không thay đổi. ansible.builtin.template render file bằng Jinja2 trên controller trước, rồi gửi kết quả. Vì vậy, các biến và vòng lặp được xử lý trước khi file đến target host. Dùng copy cho file phải giống nhau từng byte trên mọi host. Dùng template cho nội dung thay đổi theo từng host. Hai module dùng chung các tùy chọn file, nên mode, owner, backupvalidate hoạt động giống nhau trong cả hai.

Làm cách nào để handler chạy ở giữa một play?

Thêm ansible.builtin.meta: flush_handlers làm một task tại vị trí bạn muốn các handler chạy. Nó kích hoạt mọi handler đã được thông báo cho đến thời điểm đó, sau đó play tiếp tục bình thường. Dùng cách này khi một task tiếp theo trong cùng play phụ thuộc vào việc service đã chạy với cấu hình mới, chẳng hạn một wait_for trên cổng chỉ tồn tại sau khi reload. Đây là cách được hỗ trợ để chạy handler trước khi play kết thúc.

Tôi có thể dùng validate với một fragment cấu hình nginx không?

Không với nginx -t -c %s. Command đó yêu cầu một cấu hình hoàn chỉnh, bắt đầu bằng các block cấp cao nhất eventshttp. Vì vậy, nó từ chối fragment conf.d với thông báo như "upstream" directive is not allowed here. Fragment này hợp lệ bên trong block http nhưng không hợp lệ khi đứng riêng. Hãy cài file trước, sau đó chạy nginx -t trên cấu hình đã ghép trong một handler được định nghĩa trước handler reload. Handler chạy theo thứ tự định nghĩa, nên cấu hình lỗi sẽ làm play thất bại trước khi reload được thực hiện. Đặt backup: true trên task template để file trước đó vẫn còn và có thể khôi phục.

#ansible#jinja2#handlers#idempotence#automation