Handlers, Loops và Conditionals

K
Kai··5 min read

Playbook tới giờ chạy task tuần tự, vô điều kiện. Thực tế bạn cần: lặp một task qua nhiều giá trị, chạy task chỉ khi điều kiện đúng, phản ứng khi có thay đổi (restart dịch vụ khi đổi config), và xử lý lỗi. Đây là bốn công cụ điều khiển luồng: loop, when, handlers, block/rescue.

Loop: lặp một task qua nhiều giá trị

Thay vì viết ba task cài ba gói, dùng loop:

- name: Cài nhiều gói
  ansible.builtin.dnf:
    name: "{{ item }}"
    state: present
  loop:
    - git
    - vim
    - htop

{{ item }} lần lượt nhận từng giá trị trong loop. Log chạy cho thấy mỗi vòng một dòng, và idempotency vẫn áp cho từng item:

TASK [Cài nhiều gói] ******************
changed: [lab] => (item=git)
ok: [lab]      => (item=vim)      # vim đã cài → ok, không changed
changed: [lab] => (item=htop)

Loop cũng duyệt được danh sách dict (cài gói kèm phiên bản, tạo nhiều user kèm thuộc tính...):

- name: Tạo nhiều user
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.group }}"
  loop:
    - { name: alice, group: dev }
    - { name: bob, group: ops }

Tài liệu cũ dùng with_items; bản hiện đại dùng loop. Khi cần lặp phức tạp (qua dict, kết hợp danh sách...), có các filter như dict2items, product — nhưng loop qua một list là đủ cho phần lớn.

when: chạy có điều kiện

when quyết định task có chạy hay không, dựa trên biến/facts (Bài 6):

- name: Chỉ chạy trên Amazon Linux
  ansible.builtin.debug:
    msg: "Chạy trên {{ ansible_distribution }}"
  when: ansible_distribution == "Amazon"

Nếu điều kiện sai, task bị skipped (Bài 4). when cực hữu ích để viết playbook đa nền tảng (cài dnf khi RedHat, apt khi Debian — dựa trên ansible_distribution), hoặc chạy task dựa trên kết quả task trước (register ở Bài 6):

- name: Kiểm tra file tồn tại
  ansible.builtin.stat:
    path: /opt/app/installed
  register: app_check

- name: Chỉ cài nếu chưa có
  ansible.builtin.command: /opt/install.sh
  when: not app_check.stat.exists

Kết hợp điều kiện bằng and/or, hoặc một list when (mọi điều kiện phải đúng):

  when:
    - ansible_distribution == "Amazon"
    - ansible_distribution_version == "2023"

Handlers: phản ứng với thay đổi

Đây là một pattern đặc trưng của Ansible. Vấn đề: khi bạn đổi file cấu hình của một dịch vụ, cần restart dịch vụ — nhưng chỉ khi config thực sự đổi, không phải mỗi lần chạy playbook. Handler giải quyết: một task đặc biệt chỉ chạy khi được notify.

  tasks:
    - name: Sửa config nginx
      ansible.builtin.copy:
        content: "server_tokens off;\n"
        dest: /etc/nginx/conf.d/hardening.conf
      notify: Restart nginx        # báo handler nếu task này CHANGED

  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

Cơ chế:

   Task "Sửa config nginx"
        ├─ config ĐỔI (changed) ──► notify "Restart nginx"
        └─ config KHÔNG đổi (ok) ──► không notify
   ... các task khác chạy tiếp ...
   ── CUỐI play ──► handler được notify chạy MỘT lần

Hai tính chất quan trọng: handler chỉ chạy nếu task notify nó báo changed, và chạy một lần ở cuối play (dù nhiều task cùng notify nó). Log chạy thật:

  • Lần 1 (config mới): task copy changed → notify → cuối play RUNNING HANDLER [Restart nginx] → changed.
  • Lần 2 (config không đổi): task copy ok → không notify → handler không chạy (PLAY RECAP changed=0).

Đây chính là idempotency áp cho việc restart: nginx chỉ restart khi config thật sự thay đổi. Không có handler, bạn sẽ restart mù mỗi lần chạy — gây gián đoạn vô ích.

block / rescue / always: xử lý lỗi

Mặc định, một task lỗi làm play dừng trên host đó. Khi cần xử lý lỗi gọn gàng (như try/catch), dùng block + rescue + always:

- name: Có thể lỗi
  block:
    - name: Lệnh có thể thất bại
      ansible.builtin.command: /opt/risky-migration.sh
  rescue:
    - name: Khắc phục khi lỗi
      ansible.builtin.debug:
        msg: "Migration lỗi — đã rollback"
  always:
    - name: Luôn chạy (dọn dẹp)
      ansible.builtin.debug:
        msg: "Dọn dẹp dù thành công hay lỗi"
  • block — nhóm task chạy bình thường.
  • rescue — chạy nếu có task trong block lỗi (như catch). Sau rescue, play tiếp tục thay vì dừng.
  • always — luôn chạy dù block thành công hay lỗi (như finally) — hợp để dọn dẹp.

Log chạy (block cố tình lỗi rồi rescue bắt được) cho thấy ở PLAY RECAP: failed=0 rescued=1 — lỗi đã được "cứu", play không dừng.

Ngoài block/rescue, còn cách kiểm soát lỗi ở mức task: ignore_errors: true (bỏ qua lỗi, không nên lạm dụng), failed_when: (tự định nghĩa khi nào coi là lỗi), changed_when: (tự định nghĩa khi nào coi là changed — hữu ích cho command/shell ở Bài 5).

Tags: chạy chọn lọc (xem trước, đào sâu ở Bài 13)

Bạn gắn tags cho task/play để chạy một phần playbook:

    - name: Cài gói
      ansible.builtin.dnf: { name: nginx, state: present }
      tags: [install]

Rồi ansible-playbook site.yml --tags install chỉ chạy task gắn tag install. Tiện khi playbook dài mà bạn chỉ muốn chạy phần config. Bài 13 nói kỹ tags cùng các kỹ thuật vận hành.

🧹 Dọn dẹp

Code mẫu ở repo nghiadaulau/ansible-series, thư mục 07-flow-control.

Tổng kết

Bốn công cụ điều khiển luồng: loop lặp một task qua danh sách (idempotency vẫn áp từng item); when chạy task có điều kiện theo biến/facts/kết quả (register); handlers phản ứng với thay đổi qua notify — chỉ chạy khi task báo changed, một lần ở cuối play (pattern "restart khi đổi config"); block/rescue/always xử lý lỗi như try/catch/finally. Cộng tags để chạy chọn lọc.

Playbook giờ đã đầy đủ công cụ, nhưng để gấp một lần với hệ thống lớn thì cần tổ chức code tái sử dụng. Bài 8: roles — cách đóng gói playbook, biến, template, handler thành đơn vị dùng lại được.