Ansible Handlers: Trigger Actions เมื่อ Task มีการเปลี่ยนแปลง

Ansible Handlers คือ task พิเศษที่ถูก trigger โดย notify เมื่อ task อื่นมีการเปลี่ยนแปลง (changed) เท่านั้น — เหมาะสำหรับ action ที่ควรทำเฉพาะเมื่อจำเป็น เช่น restart service หลังแก้ไข config, reload Nginx หลังเพิ่ม virtual host หรือ clear cache หลัง deploy code

บทความนี้ครอบคลุมการประกาศ handlers, การใช้ notify, ลำดับการ execute, การ flush handlers, การใช้ listen เพื่อ group notifications และ pattern สำหรับ service management ใน deployment workflow

Handlers พื้นฐาน

ประกาศ handlers ใน handlers section ของ Play และ trigger ด้วย notify ใน task หลัก Handler จะถูกรันเพียงครั้งเดียวหลัง tasks ทั้งหมดของ Play เสร็จ แม้จะมี notify หลายครั้ง

---
- name: Basic handlers usage
  hosts: all
  become: true

  tasks:
    # เมื่อ task นี้ changed จะ notify handler
    - name: Deploy Nginx configuration
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: Reload Nginx

    # notify หลาย handler พร้อมกัน
    - name: Deploy application config
      template:
        src: app.conf.j2
        dest: /etc/myapp/app.conf
      notify:
        - Restart myapp
        - Clear app cache

  handlers:
    # handler ถูกรันเฉพาะเมื่อมี task notify มา
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

    - name: Restart myapp
      service:
        name: myapp
        state: restarted

    - name: Clear app cache
      command: /opt/myapp/bin/clear-cache

ลำดับการ Execute — Handlers รันเมื่อไร

Handlers ไม่รันทันทีหลัง notify — รันหลัง tasks ทั้งหมดของ Play เสร็จ และรันเพียงครั้งเดียวต่อ Play แม้จะ notify หลายครั้ง ลำดับการรันตาม order ที่ประกาศใน handlers section

---
- name: Handler execution order
  hosts: all
  become: true

  tasks:
    # task 1: แก้ไข nginx.conf → notify Reload Nginx
    - name: Update nginx main config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: Reload Nginx

    # task 2: เพิ่ม virtual host → notify Reload Nginx อีกครั้ง
    - name: Add virtual host config
      template:
        src: vhost.conf.j2
        dest: /etc/nginx/conf.d/mysite.conf
      notify: Reload Nginx

    # task 3: copy SSL cert → notify Reload Nginx
    - name: Copy SSL certificate
      copy:
        src: files/ssl/mysite.crt
        dest: /etc/ssl/certs/mysite.crt
      notify: Reload Nginx

    # Reload Nginx จะถูกรันแค่ 1 ครั้งหลัง tasks 1-3 เสร็จ
    # ไม่ใช่ 3 ครั้ง

  handlers:
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

Flush Handlers — บังคับรัน Handler ทันที

ใช้ meta: flush_handlers เพื่อบังคับให้ handlers ที่ pending ทั้งหมดรันทันที ณ จุดนั้น ก่อน tasks ที่เหลือจะดำเนินต่อ เหมาะเมื่อ task ถัดไปต้องการ service ที่ restart แล้ว

---
- name: Flush handlers example
  hosts: all
  become: true

  tasks:
    # deploy database config
    - name: Update database configuration
      template:
        src: postgresql.conf.j2
        dest: /etc/postgresql/14/main/postgresql.conf
      notify: Restart PostgreSQL

    # flush handlers — บังคับ restart PostgreSQL ก่อนทำ migration
    - name: Flush handlers before migration
      meta: flush_handlers

    # task นี้ต้องการ PostgreSQL ที่ restart แล้ว
    - name: Run database migrations
      command: /opt/myapp/bin/migrate
      become_user: appuser

  handlers:
    - name: Restart PostgreSQL
      service:
        name: postgresql
        state: restarted

listen — Group Notifications

ใช้ listen เพื่อให้หลาย handlers ตอบสนองต่อ notification เดียวกัน เหมาะสำหรับกรณีที่ event หนึ่งต้องการ trigger หลาย action โดยไม่ต้อง notify แต่ละ handler ทีละชื่อ

---
- name: listen for grouped handlers
  hosts: all
  become: true

  tasks:
    # notify ด้วยชื่อ event แทนชื่อ handler
    - name: Deploy application code
      git:
        repo: "{{ app_repo }}"
        dest: /opt/myapp/current
        version: "{{ app_version }}"
      notify: app deployed

    - name: Update application config
      template:
        src: app.conf.j2
        dest: /etc/myapp/app.conf
      notify: app deployed

  handlers:
    # handlers ที่ listen "app deployed" จะทำงานทั้งหมด
    - name: Restart application
      listen: app deployed
      service:
        name: myapp
        state: restarted

    - name: Clear application cache
      listen: app deployed
      command: /opt/myapp/bin/clear-cache
      become_user: appuser

    - name: Notify monitoring
      listen: app deployed
      uri:
        url: https://monitoring.example.com/webhook/deploy
        method: POST
        body_format: json
        body:
          app: myapp
          version: "{{ app_version }}"
          status: deployed

Handlers ใน Role

เมื่อใช้ Ansible Roles ให้วาง handlers ในไฟล์ handlers/main.yml ใน role directory โครงสร้างนี้ทำให้ handlers เป็น reusable และแยก concerns ออกจาก tasks หลักได้ชัดเจน

# roles/nginx/handlers/main.yml
---
- name: Reload Nginx
  service:
    name: nginx
    state: reloaded
  listen: nginx config changed

- name: Restart Nginx
  service:
    name: nginx
    state: restarted
  listen: nginx restart required

# roles/nginx/tasks/main.yml
---
- name: Install Nginx
  apt:
    name: nginx
    state: present

- name: Deploy main Nginx config
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: nginx config changed    # trigger handler จาก handlers/main.yml

- name: Deploy site config
  template:
    src: site.conf.j2
    dest: /etc/nginx/conf.d/mysite.conf
  notify: nginx config changed

- name: Deploy SSL config (requires full restart)
  template:
    src: ssl.conf.j2
    dest: /etc/nginx/conf.d/ssl.conf
  notify: nginx restart required

Pattern: Service Management ใน Deployment

ตัวอย่าง Playbook deploy application stack โดยใช้ handlers จัดการ service restart อย่างมีประสิทธิภาพ — service จะถูก restart เฉพาะเมื่อ config หรือ binary เปลี่ยนจริง ๆ เท่านั้น

---
- name: Deploy application with proper handler usage
  hosts: appservers
  become: true
  vars:
    app_name: myapp
    app_version: "{{ deploy_version }}"

  tasks:
    # deploy config files
    - name: Deploy main application config
      template:
        src: app.conf.j2
        dest: "/etc/{{ app_name }}/app.conf"
        owner: root
        group: appgroup
        mode: '0640'
      notify: Restart application

    - name: Deploy Nginx virtual host
      template:
        src: nginx-vhost.conf.j2
        dest: "/etc/nginx/conf.d/{{ app_name }}.conf"
      notify: Reload Nginx

    # deploy binary
    - name: Deploy application binary
      copy:
        src: "dist/{{ app_name }}-{{ app_version }}"
        dest: "/opt/{{ app_name }}/bin/{{ app_name }}"
        owner: appuser
        group: appgroup
        mode: '0755'
      notify: Restart application

    # flush handlers หลัง deploy ทุกอย่าง
    # เพื่อให้ restart เสร็จก่อนตรวจสอบ
    - name: Apply all pending service restarts
      meta: flush_handlers

    # ตรวจสอบ service หลัง restart
    - name: Verify application is running
      wait_for:
        host: localhost
        port: 8080
        timeout: 60

  handlers:
    - name: Restart application
      service:
        name: "{{ app_name }}"
        state: restarted

    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

สรุป

Handlers ทำให้ Playbook idempotent มากขึ้นโดย trigger service restart หรือ action อื่น เฉพาะเมื่อมีการเปลี่ยนแปลงจริง ๆ รันเพียงครั้งเดียวต่อ Play แม้มี notify หลายครั้ง และสามารถ group ด้วย listen ให้จัดการ multi-action events ได้สะดวก

Pattern ที่ควรจำ: ใช้ meta: flush_handlers เมื่อ task ถัดไปต้องการผลจาก handler ที่ pending, ใช้ listen แทนการ notify ชื่อ handler โดยตรงเมื่อต้องการ trigger หลาย handler จาก event เดียว, วาง handlers ใน handlers/main.yml ใน role เสมอ และตั้งชื่อ handler ให้อ่านเข้าใจง่ายเพราะชื่อคือ interface ที่ tasks ใช้ notify