Handlers, Loops và Conditionals
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ùngloop. 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ưngloopqua 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 playRUNNING HANDLER [Restart nginx] → changed. - Lần 2 (config không đổi): task copy
ok→ không notify → handler không chạy (PLAY RECAPchanged=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 chocommand/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.