0

Lab 7 — Ansible Handlers & Idempotency

Mục tiêu: Sau Lab này, người học hiểu được Idempotency, biết sử dụng Handlers để chỉ reload/restart service khi configuration thực sự thay đổi, và bắt đầu hình thành tư duy automation theo mô hình Desired State trong môi trường production.


1. Bối cảnh

Ở Lab 6, chúng ta đã biết sử dụng:

Variables
     ↓
Jinja2 Template
     ↓
Configuration file

Ví dụ:

server_name: todo.example.com
app_port: 8080

Template:

server {
    listen 80;

    server_name {{ server_name }};

    location / {
        proxy_pass http://127.0.0.1:{{ app_port }};
    }
}

Ansible tạo ra:

/etc/nginx/sites-available/todo.conf

Nhưng có một vấn đề.

Sau khi configuration thay đổi, Nginx cần được reload:

systemctl reload nginx

Một người mới có thể viết:

- name: Deploy nginx configuration
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/todo.conf

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

Nhìn qua có vẻ đúng.

Nhưng hãy chạy:

ansible-playbook playbook.yml

Lần 1:

Template changed
      ↓
Restart nginx

Lần 2:

Template không thay đổi
      ↓
Restart nginx

Lần 3:

Template không thay đổi
      ↓
Restart nginx

Vấn đề là:

Nginx bị restart dù configuration không thay đổi.

Trong production, đây là điều không mong muốn.


2. Idempotency là gì?

Idempotency có thể hiểu đơn giản:

Chạy automation nhiều lần với cùng một input thì trạng thái cuối cùng vẫn giống nhau và không tạo ra những thay đổi không cần thiết.

Ví dụ:

Desired State:

Nginx
running
port 80
config đúng

Ansible chạy lần đầu:

Current State
     ↓
Sai
     ↓
Change
     ↓
Desired State

Chạy lần hai:

Current State
     ↓
Đã đúng
     ↓
Không cần change

Chạy lần ba:

Current State
     ↓
Đã đúng
     ↓
Không cần change

Đây chính là tư duy:

Current State
      +
Desired State
      ↓
Ansible
      ↓
Required Changes

3. Idempotency không có nghĩa là "chạy lại không làm gì"

Đây là một hiểu nhầm phổ biến.

Idempotency không có nghĩa:

Playbook chạy lần 2
→ Không thực hiện task nào

Mà là:

Playbook chạy lần 2
→ Không thực hiện những thay đổi không cần thiết

Ví dụ:

- name: Ensure nginx is running
  ansible.builtin.service:
    name: nginx
    state: started

Nếu Nginx đã chạy:

changed=0

Nếu Nginx đang stopped:

changed=1

Ansible chỉ thay đổi khi cần.


4. changed trong Ansible

Một trong những khái niệm quan trọng nhất của Lab này là:

changed

Ví dụ:

ansible-playbook playbook.yml

Kết quả:

ok=3
changed=1
failed=0

Có nghĩa:

3 tasks thành công
1 task thực sự thay đổi hệ thống
0 task thất bại

Nếu chạy lại:

ok=3
changed=0
failed=0

thì automation đã đạt trạng thái ổn định.


5. Vì sao changed quan trọng?

Nhiều tính năng của Ansible dựa vào trạng thái:

changed = true

Ví dụ:

Template changed
       ↓
notify
       ↓
Handler
       ↓
Reload service

Nếu:

Template unchanged
       ↓
notify không xảy ra
       ↓
Handler không chạy

Do đó:

changed chính là cầu nối giữa Task và Handler.


6. Handler là gì?

Handler là một task đặc biệt chỉ chạy khi được một task khác notify.

Ví dụ:

tasks:
  - name: Deploy nginx configuration
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify: Reload nginx

handlers:
  - name: Reload nginx
    ansible.builtin.service:
      name: nginx
      state: reloaded

Flow:

Template
   │
   │ changed?
   │
   ├── No ──────────────→ Handler không chạy
   │
   └── Yes
        │
        ▼
      notify
        │
        ▼
   Reload nginx

7. Tạo Lab

Tạo project:

mkdir ansible-lab-07
cd ansible-lab-07

Cấu trúc:

ansible-lab-07/
├── ansible.cfg
├── inventory.ini
├── group_vars/
│   └── webservers.yml
├── templates/
│   └── nginx.conf.j2
└── playbook.yml

8. ansible.cfg

[defaults]
inventory = ./inventory.ini

9. Inventory

[webservers]
web-01 ansible_host=192.168.56.101
web-02 ansible_host=192.168.56.102

Kiểm tra:

ansible-inventory --graph

10. Variables

Tạo:

group_vars/webservers.yml
app_name: todo
app_host: 127.0.0.1
app_port: 8080

server_name: todo.example.com
nginx_port: 80

11. Template

Tạo:

templates/nginx.conf.j2
server {
    listen {{ nginx_port }};

    server_name {{ server_name }};

    location / {
        proxy_pass http://{{ app_host }}:{{ app_port }};

        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

12. Playbook đầu tiên

Tạo:

playbook.yml
---
- name: Configure nginx
  hosts: webservers
  become: true

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

    - name: Deploy nginx configuration
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/todo.conf
        owner: root
        group: root
        mode: "0644"

    - name: Ensure nginx is running
      ansible.builtin.service:
        name: nginx
        state: started

Chạy:

ansible-playbook playbook.yml

13. Chạy lần đầu

Bạn có thể thấy:

TASK [Install nginx]
changed: [web-01]

TASK [Deploy nginx configuration]
changed: [web-01]

TASK [Ensure nginx is running]
ok: [web-01]

Ví dụ:

changed=2

Bởi vì:

Nginx chưa tồn tại
        ↓
Install

Configuration chưa tồn tại
        ↓
Deploy

14. Chạy lần thứ hai

ansible-playbook playbook.yml

Bạn kỳ vọng:

TASK [Install nginx]
ok

TASK [Deploy nginx configuration]
ok

TASK [Ensure nginx is running]
ok

Cuối cùng:

changed=0

Đây là một Playbook idempotent.


15. Nhưng configuration thay đổi thì sao?

Thay:

app_port: 8080

thành:

app_port: 8081

Chạy:

ansible-playbook playbook.yml

Template thay đổi:

proxy_pass http://127.0.0.1:8080;

thành:

proxy_pass http://127.0.0.1:8081;

Nhưng Nginx vẫn đang chạy với configuration cũ.

Chúng ta cần:

systemctl reload nginx

Đây chính là lúc Handler xuất hiện.


16. Thêm Handler

Sửa:

- name: Deploy nginx configuration
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/todo.conf
    owner: root
    group: root
    mode: "0644"
  notify: Reload nginx

Thêm:

handlers:
  - name: Reload nginx
    ansible.builtin.service:
      name: nginx
      state: reloaded

Full Playbook:

---
- name: Configure nginx
  hosts: webservers
  become: true

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

    - name: Deploy nginx configuration
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/todo.conf
        owner: root
        group: root
        mode: "0644"
      notify: Reload nginx

    - name: Ensure nginx is running
      ansible.builtin.service:
        name: nginx
        state: started

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

17. Flow mới

Bây giờ:

              Template
                  │
                  ▼
           Configuration
                  │
           ┌──────┴──────┐
           │             │
        Changed       Unchanged
           │             │
           ▼             ▼
        notify        Nothing
           │
           ▼
    Handler: Reload
           │
           ▼
         Nginx

Đây là pattern bạn sẽ sử dụng cực kỳ nhiều trong DevOps.


18. Test Handler

Chạy:

ansible-playbook playbook.yml

Lần đầu:

TASK [Deploy nginx configuration]
changed

RUNNING HANDLER [Reload nginx]
changed

Chạy lại:

ansible-playbook playbook.yml

Lần hai:

TASK [Deploy nginx configuration]
ok

Không có:

RUNNING HANDLER [Reload nginx]

Đây chính là điều chúng ta muốn.


19. Thay đổi Configuration

Sửa:

app_port: 8081

Chạy:

ansible-playbook playbook.yml

Bạn sẽ thấy:

TASK [Deploy nginx configuration]
changed

RUNNING HANDLER [Reload nginx]
changed

Vì:

Template
   ↓
Rendered output thay đổi
   ↓
changed = true
   ↓
notify
   ↓
Reload nginx

20. Handler chỉ chạy một lần

Giả sử Playbook có:

tasks:

  - name: Deploy nginx config
    ansible.builtin.template:
      src: nginx.conf.j2
      dest: /etc/nginx/nginx.conf
    notify: Reload nginx

  - name: Deploy another nginx config
    ansible.builtin.template:
      src: app.conf.j2
      dest: /etc/nginx/conf.d/app.conf
    notify: Reload nginx

Nếu cả hai thay đổi:

Task 1
  ↓
notify Reload nginx

Task 2
  ↓
notify Reload nginx

Handler không reload 2 lần.

Ansible sẽ gom notification:

Task 1 ──┐
         │
Task 2 ──┼──→ Reload nginx
         │
Task 3 ──┘

Handler chạy một lần ở cuối play.

Đây là một ưu điểm rất quan trọng.


21. Vì sao không dùng systemctl restart trực tiếp?

Cách không tốt:

- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf

- name: Restart nginx
  ansible.builtin.command:
    cmd: systemctl restart nginx

Vấn đề:

Playbook chạy
      ↓
Restart nginx
      ↓
Dù config có thay đổi hay không

Nếu chạy mỗi 5 phút:

00:00 → restart
00:05 → restart
00:10 → restart
00:15 → restart

Không tốt cho production.


22. Handler giải quyết vấn đề gì?

Với Handler:

Configuration changed?
       │
   ┌───┴───┐
  Yes      No
   │        │
   ▼        ▼
notify    nothing
   │
   ▼
reload

Do đó:

Handler giúp automation phản ứng với thay đổi thay vì thực hiện hành động một cách mù quáng.


23. state: startedstate: restarted

Hai trạng thái này rất khác nhau.

started

state: started

Ý nghĩa:

Service phải đang chạy.

Nếu đang chạy:

Không làm gì

Nếu stopped:

Start

restarted

state: restarted

Ý nghĩa:

Restart service.

Do đó không nên dùng:

state: restarted

một cách tùy tiện.


24. reload vs restart

Restart

Stop
 ↓
Start

Có thể gây gián đoạn.

Reload

Process đang chạy
       ↓
Đọc configuration mới
       ↓
Tiếp tục phục vụ

Với những service hỗ trợ reload, thường nên ưu tiên:

reload

khi chỉ thay đổi configuration.

Ví dụ:

state: reloaded

25. Khi nào cần Restart?

Không phải mọi thay đổi đều có thể reload.

Ví dụ:

Configuration
     ↓
Service hỗ trợ reload?
     │
 ┌───┴────┐
 Yes      No
 │         │
 ▼         ▼
Reload   Restart

Do đó DevOps Engineer cần biết lifecycle của service đang quản lý.


26. Handler với nhiều service

Ví dụ application:

- name: Deploy application config
  ansible.builtin.template:
    src: application.yml.j2
    dest: /opt/todo/application.yml
  notify: Restart todo application

Handler:

handlers:
  - name: Restart todo application
    ansible.builtin.service:
      name: todo
      state: restarted

27. Handler cho Docker Compose

Ví dụ:

- name: Deploy compose file
  ansible.builtin.template:
    src: docker-compose.yml.j2
    dest: /opt/todo/docker-compose.yml
  notify: Restart todo application

Handler:

handlers:
  - name: Restart todo application
    community.docker.docker_compose_v2:
      project_src: /opt/todo
      state: present
      recreate: auto

Pattern:

docker-compose.yml
       ↓
changed
       ↓
notify
       ↓
Docker Compose
       ↓
Recreate required containers

28. Handler Naming

Tên Handler nên mô tả hành động:

Tốt:

notify: Reload nginx
notify: Restart todo application
notify: Reload systemd

Không nên:

notify: Handler 1

hoặc:

notify: Something

Tên rõ ràng giúp debug dễ hơn.


29. Handler và listen

Khi project lớn, nhiều task có thể cần cùng một hành động.

Ví dụ:

notify: Restart application

Có thể dùng:

handlers:
  - name: Restart application
    ansible.builtin.service:
      name: todo
      state: restarted

Ngoài ra Ansible có cơ chế listen:

handlers:

  - name: Restart application service
    ansible.builtin.service:
      name: todo
      state: restarted
    listen: Restart application

  - name: Clear application cache
    ansible.builtin.command:
      cmd: /opt/todo/clear-cache.sh
    listen: Restart application

Task:

notify: Restart application

sẽ kích hoạt các Handler cùng listen:

notify
   │
   ▼
Restart application
   │
   ├── Restart service
   │
   └── Clear cache

Phần này khá hữu ích khi project lớn.


30. Handler chạy khi nào?

Mặc định Handler chạy:

Sau khi tất cả Tasks trong Play hoàn thành.

Ví dụ:

Task 1
Task 2
Task 3
Task 4
Task 5
   │
   ▼
Handlers

Điều này rất quan trọng.

Không phải:

Task 1
 ↓
Handler
 ↓
Task 2
 ↓
Handler

31. meta: flush_handlers

Đôi khi chúng ta muốn Handler chạy ngay.

Có thể sử dụng:

- name: Flush handlers
  ansible.builtin.meta: flush_handlers

Ví dụ:

tasks:

  - name: Deploy configuration
    ansible.builtin.template:
      src: app.conf.j2
      dest: /etc/myapp/app.conf
    notify: Restart application

  - name: Flush handlers
    ansible.builtin.meta: flush_handlers

  - name: Run application health check
    ansible.builtin.uri:
      url: http://localhost:8080/health

Flow:

Deploy config
     ↓
notify
     ↓
flush_handlers
     ↓
Restart
     ↓
Health check

Nếu không flush_handlers, Handler thường sẽ đợi đến cuối Play.


32. Tại sao flush_handlers hữu ích?

Scenario:

Deploy configuration
       ↓
Restart application
       ↓
Health check

Nếu Handler đợi đến cuối:

Deploy config
       ↓
Health check
       ↓
Restart application

Health check có thể kiểm tra configuration cũ.

Do đó:

meta: flush_handlers

cho phép chúng ta kiểm soát thời điểm Handler chạy.


33. Exercise 1 — Basic Handler

Tạo Playbook:

---
- name: Handler exercise
  hosts: webservers
  become: true

  tasks:
    - name: Deploy configuration
      ansible.builtin.template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/todo.conf
      notify: Reload nginx

  handlers:
    - name: Reload nginx
      ansible.builtin.service:
        name: nginx
        state: reloaded

Yêu cầu:

Lần 1:
changed → handler chạy

Lần 2:
ok → handler không chạy

34. Exercise 2 — Trigger Handler

Thay đổi:

app_port: 8080

thành:

app_port: 9090

Chạy:

ansible-playbook playbook.yml

Xác nhận:

Template changed
      ↓
Handler chạy

35. Exercise 3 — Không Trigger Handler

Không thay đổi bất kỳ Variable nào.

Chạy:

ansible-playbook playbook.yml

Yêu cầu:

changed=0

và:

Handler không chạy

36. Exercise 4 — Hai Template, Một Handler

Tạo:

templates/
├── nginx.conf.j2
└── security.conf.j2

Tasks:

- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Reload nginx

- name: Deploy security config
  ansible.builtin.template:
    src: security.conf.j2
    dest: /etc/nginx/conf.d/security.conf
  notify: Reload nginx

Chỉ cần một:

handlers:
  - name: Reload nginx
    ansible.builtin.service:
      name: nginx
      state: reloaded

Thử thay đổi cả hai file.

Kiểm tra Handler chỉ chạy một lần.


37. Exercise 5 — Detect Idempotency

Chạy Playbook liên tiếp 3 lần:

ansible-playbook playbook.yml
ansible-playbook playbook.yml
ansible-playbook playbook.yml

Ghi lại:

Run 1:
changed = ?

Run 2:
changed = ?

Run 3:
changed = ?

Mục tiêu:

Run 1 → changed > 0
Run 2 → changed = 0
Run 3 → changed = 0

38. Exercise 6 — Intentional Failure

Thử sửa Template thành:

server {
    listen {{ nginx_port }}

    server_name {{ server_name }};
}

Bạn đã cố tình bỏ:

;

Chạy:

ansible-playbook playbook.yml

Sau đó kiểm tra:

nginx -t

Mục tiêu của bài này là hiểu:

Template render thành công không đồng nghĩa configuration hợp lệ.

Đây là một insight rất quan trọng trong production.


39. Exercise 7 — Validate Template

template module hỗ trợ validate.

Ví dụ với Nginx:

- name: Deploy nginx configuration
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/todo.conf
    validate: "nginx -t -c %s"
  notify: Reload nginx

Flow:

Template
   ↓
Render temporary file
   ↓
nginx -t
   │
   ├── FAIL → Không deploy
   │
   └── OK
        ↓
     Deploy
        ↓
      notify
        ↓
      Reload

Đây là một pattern rất đáng học.


40. Tại sao validate quan trọng?

Không có validate:

Template
   ↓
Configuration lỗi
   ↓
Deploy
   ↓
Reload
   ↓
Nginx fail

validate:

Template
   ↓
Validate
   │
   ├── FAIL → Stop
   │
   └── PASS
        ↓
      Deploy
        ↓
      Reload

Tư duy production:

Validate trước khi đưa configuration vào service.


41. Exercise 8 — Production Nginx Deployment

Xây dựng Playbook:

Template
   ↓
Validate nginx configuration
   ↓
Deploy
   ↓
Notify
   ↓
Reload nginx

Yêu cầu:

Nếu configuration đúng:

Template → changed
Validate → success
Reload → success

Nếu configuration sai:

Template
   ↓
Validate FAILED
   ↓
Không reload nginx

42. Idempotency với Package

Ví dụ:

- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present

Nếu chưa có:

changed=1

Nếu đã có:

changed=0

Không nên:

- name: Install nginx
  ansible.builtin.command:
    cmd: apt install -y nginx

Vì bạn đang nói với Ansible:

"Hãy chạy command này."

thay vì:

"Hãy đảm bảo nginx tồn tại."


43. Imperative vs Declarative

Đây là một khái niệm rất quan trọng.

Imperative

Run this command
Run this command
Run this command

Ví dụ:

apt install nginx
systemctl restart nginx

Bạn mô tả cách làm.


Declarative

Nginx phải được cài đặt
Nginx phải đang chạy
Configuration phải như thế này

Ansible tự quyết định cần thay đổi gì.

Bạn mô tả trạng thái mong muốn.


44. Ansible và Desired State

Có thể hình dung:

             Desired State
                  │
                  ▼
           ┌──────────────┐
           │   Ansible    │
           └──────┬───────┘
                  │
                  ▼
            Current State
                  │
           ┌──────┴──────┐
           │             │
         Same          Different
           │             │
           ▼             ▼
         Nothing       Change

Đây chính là tư duy nền tảng của Infrastructure as Code.


45. Những module thường có tính Idempotent

Các module Ansible chính thức thường được thiết kế để quản lý trạng thái.

Ví dụ:

apt
yum
dnf
package
service
systemd
file
copy
template
user
group
lineinfile
blockinfile

Ví dụ:

- name: Create deploy user
  ansible.builtin.user:
    name: deploy
    state: present

Chạy nhiều lần:

Lần 1 → tạo user
Lần 2 → không tạo lại
Lần 3 → không tạo lại

46. Những module cần cẩn thận

Ví dụ:

ansible.builtin.command
ansible.builtin.shell

Ansible không phải lúc nào cũng biết command đó đã được thực hiện hay chưa.

Ví dụ:

- name: Run migration
  ansible.builtin.command:
    cmd: ./migrate.sh

Mỗi lần Playbook chạy:

migrate.sh
migrate.sh
migrate.sh

Có thể gây vấn đề.


47. Làm command idempotent hơn

Ví dụ sử dụng:

creates:
- name: Create initialization file
  ansible.builtin.command:
    cmd: touch /opt/app/initialized
    creates: /opt/app/initialized

Nếu file chưa tồn tại:

Run command

Nếu đã tồn tại:

Skip

Flow:

File exists?
   │
 ┌─┴─┐
Yes  No
 │    │
Skip Run

48. changed_when

Một kỹ thuật nâng cao hơn:

- name: Check application
  ansible.builtin.command:
    cmd: /opt/todo/check.sh
  changed_when: false

Nói với Ansible:

Task này chỉ kiểm tra, không làm thay đổi hệ thống.

Do đó:

changed=0

49. failed_when

Tương tự:

- name: Run health check
  ansible.builtin.command:
    cmd: /opt/todo/health-check.sh
  register: health_check
  failed_when: health_check.rc != 0

Điều này cho phép kiểm soát chính xác:

Task result
    ↓
failed_when
    ↓
Success / Failure

Đây là kỹ thuật sẽ rất hữu ích khi làm automation phức tạp.


50. Tư duy Senior #1 — Đừng restart service nếu không cần

Một nguyên tắc rất quan trọng:

Configuration changed
        ↓
Service needs reload/restart

Không phải:

Playbook chạy
        ↓
Restart service

Đây là khác biệt giữa:

Script automation

và:

Infrastructure automation

51. Tư duy Senior #2 — Handler biểu diễn Dependency

Ví dụ:

nginx.conf
    │
    │ changed
    ▼
nginx reload

Ta có dependency:

Configuration
      ↓
Service

Hoặc:

application.yml
      ↓
Application restart

Hoặc:

sshd_config
      ↓
SSH reload

Handler giúp mô hình hóa dependency này.


52. Tư duy Senior #3 — Idempotency giúp chạy automation an toàn

Trong production, automation có thể chạy:

Deployment
Configuration management
CI/CD
Scheduled job
Disaster recovery
Server rebuild

Nếu Playbook không idempotent:

Run
 ↓
Unexpected change
 ↓
Service restart
 ↓
Downtime

Nếu idempotent:

Run
 ↓
Compare state
 ↓
Only required changes

Automation trở nên đáng tin cậy hơn rất nhiều.


53. Tư duy Senior #4 — "No change" là một kết quả tốt

Người mới thường nghĩ:

changed=0

là:

"Ansible không làm gì."

Nhưng trong Infrastructure as Code:

changed=0

có thể là kết quả hoàn toàn chính xác.

Nó có nghĩa:

Infrastructure đã ở đúng trạng thái mong muốn.


54. Tư duy Senior #5 — Validate trước khi Reload

Một deployment tốt:

Generate
   ↓
Validate
   ↓
Deploy
   ↓
Reload
   ↓
Health Check

Không nên:

Generate
   ↓
Deploy
   ↓
Reload
   ↓
Oops!

Pattern này sẽ trở thành nền tảng cho các bài:

Application Deployment
Production Provisioning
CI/CD

sau này.


55. Mini Production Project

Bây giờ hãy kết hợp toàn bộ kiến thức Lab 1 → Lab 7.

Yêu cầu:

Ansible
   │
   ▼
Install Nginx
   │
   ▼
Deploy Template
   │
   ▼
Validate
   │
   ▼
Notify Handler
   │
   ▼
Reload Nginx

Variables:

app_name: todo
app_host: 127.0.0.1
app_port: 8080
server_name: todo.example.com
nginx_port: 80

Template:

server {
    listen {{ nginx_port }};

    server_name {{ server_name }};

    location / {
        proxy_pass http://{{ app_host }}:{{ app_port }};
    }
}

Task:

- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/todo.conf
    validate: "nginx -t -c %s"
  notify: Reload nginx

Handler:

handlers:
  - name: Reload nginx
    ansible.builtin.service:
      name: nginx
      state: reloaded

56. Test Matrix

Hãy test ít nhất 5 scenario.

Scenario Template Handler
Server mới Changed Chạy
Chạy lại Playbook OK Không chạy
Đổi app_port Changed Chạy
Đổi server_name Changed Chạy
Template lỗi Fail validation Không reload

Nếu toàn bộ đều hoạt động đúng, bạn đã thực sự hiểu Lab 7.


57. Debug Handler

Khi Handler không chạy, kiểm tra:

1. Task có changed không?

changed: [web-01]

Nếu:

ok: [web-01]

thì Handler sẽ không được trigger.

2. Có notify không?

notify: Reload nginx

3. Tên có khớp không?

notify: Reload nginx

và:

- name: Reload nginx

phải giống nhau.

4. Handler có nằm trong Play không?

handlers:

phải cùng cấp với:

tasks:

58. Debug bằng Verbose

Có thể chạy:

ansible-playbook playbook.yml -v

hoặc:

ansible-playbook playbook.yml -vv

Nếu cần sâu hơn:

ansible-playbook playbook.yml -vvv

Nhưng không nên mặc định dùng -vvv mọi lúc vì output rất dài.


59. Kiểm tra Check Mode

Chạy:

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

Bạn có thể thấy:

- proxy_pass http://127.0.0.1:8080;
+ proxy_pass http://127.0.0.1:8081;

Đây là một workflow rất tốt trước khi thay đổi Production.


60. Checklist hoàn thành Lab 7

Người học cần hiểu:

  • [ ] Idempotency là gì.
  • [ ] Desired State là gì.
  • [ ] changed nghĩa là gì.
  • [ ] ok nghĩa là gì.
  • [ ] Handler là gì.
  • [ ] notify hoạt động thế nào.
  • [ ] Handler chỉ chạy khi task thay đổi.
  • [ ] Handler thường chạy cuối Play.
  • [ ] Một Handler có thể được notify bởi nhiều Task.
  • [ ] Nhiều notify có thể được gom thành một lần Handler.
  • [ ] meta: flush_handlers.
  • [ ] state: started.
  • [ ] state: restarted.
  • [ ] state: reloaded.
  • [ ] validate.
  • [ ] changed_when.
  • [ ] failed_when.
  • [ ] creates.
  • [ ] Vì sao shell/command cần cẩn thận với idempotency.
  • [ ] Phân biệt imperative và declarative.
  • [ ] Biết kiểm tra --check --diff.
  • [ ] Biết debug khi Handler không chạy.

61. Kiến thức sau Lab 7

Đến đây người học đã có một nền tảng Ansible khá tốt:

Lab 1
Ansible Fundamentals
        │
        ▼
Lab 2
Inventory & SSH
        │
        ▼
Lab 3
Ad-Hoc Commands
        │
        ▼
Lab 4
Playbook
        │
        ▼
Lab 5
Variables & Facts
        │
        ▼
Lab 6
Jinja2 Templates
        │
        ▼
Lab 7
Handlers & Idempotency

Quan trọng hơn, họ đã bắt đầu chuyển tư duy từ:

"Chạy một loạt command trên server"

sang:

"Đảm bảo server luôn đạt Desired State"

Đây chính là tư duy cốt lõi của Infrastructure as Code.


62. Bài tập tổng hợp cuối Lab

🎯 Production Nginx Automation

Hãy xây dựng một Playbook có khả năng:

                    Ansible
                       │
                       ▼
               Install Nginx
                       │
                       ▼
               Create directories
                       │
                       ▼
                Deploy Template
                       │
                       ▼
                  Validate
                       │
                ┌──────┴──────┐
                │             │
              FAIL           PASS
                │             │
                ▼             ▼
              STOP          Deploy
                              │
                              ▼
                            Notify
                              │
                              ▼
                        Reload Nginx

Yêu cầu:

  1. Nginx phải được cài đặt.

  2. Nginx phải luôn ở trạng thái started.

  3. Configuration được tạo bằng Jinja2.

  4. Configuration phải được validate bằng nginx -t.

  5. Configuration thay đổi → reload Nginx.

  6. Configuration không thay đổi → không reload.

  7. Chạy Playbook 3 lần liên tiếp phải đạt:

    Run 1 → changed > 0
    Run 2 → changed = 0
    Run 3 → changed = 0
    
  8. Nếu cố tình làm configuration sai:

    validation fail
    ↓
    không reload nginx
    

Kết quả mong muốn

Sau Lab 7, người học không chỉ biết viết Handler, mà phải hiểu được nguyên tắc:

Ansible không nên chỉ "thực thi lệnh". Ansible phải xác định trạng thái hiện tại, so sánh với trạng thái mong muốn và chỉ thực hiện thay đổi khi thực sự cần thiết.

Đây là nền tảng rất quan trọng trước khi bước sang Lab 8 — Ansible Roles, nơi toàn bộ các Task, Template, Handler và Variables sẽ được tổ chức thành cấu trúc có thể tái sử dụng ở quy mô lớn.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí