Ansible Error Handling: จัดการ Errors ด้วย block/rescue/always

Error handling ใน Ansible ช่วยให้ Playbook รับมือกับความล้มเหลวได้อย่างชาญฉลาด แทนที่จะหยุดทันทีเมื่อ task ใดล้มเหลว สามารถกำหนดได้ว่าจะ retry, ข้ามไป, rollback, หรือส่ง notification ก่อนหยุด

บทความนี้อธิบาย block/rescue/always pattern, การใช้ ignore_errors, failed_when, any_errors_fatal, และ max_fail_percentage สำหรับควบคุม error behavior ใน Playbook

block / rescue / always

Pattern block/rescue/always คล้ายกับ try/catch/finally ในภาษา programming ทั่วไป ใช้จัดการ error อย่างมีโครงสร้าง

---
- name: Deploy with error handling
  hosts: webservers
  tasks:
    - name: Deploy application
      block:
        - name: Pull latest code
          git:
            repo: https://github.com/myorg/myapp.git
            dest: /opt/myapp
            version: main

        - name: Install dependencies
          pip:
            requirements: /opt/myapp/requirements.txt

        - name: Run migrations
          shell:
            cmd: python manage.py migrate
            chdir: /opt/myapp

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

      rescue:
        - name: Notify team on failure
          debug:
            msg: "Deployment failed! Rolling back..."

        - name: Rollback to previous version
          shell:
            cmd: /opt/scripts/rollback.sh
            chdir: /opt/myapp

      always:
        - name: Log deployment result
          lineinfile:
            path: /var/log/deploy.log
            line: "Deploy attempt at {{ ansible_date_time.iso8601 }}"
            create: yes

Tasks ใน rescue รันเมื่อ task ใดใน block ล้มเหลว ส่วน always รันเสมอไม่ว่า block จะสำเร็จหรือล้มเหลว เหมาะสำหรับ cleanup และ logging

ignore_errors: ข้ามผ่าน Error

ignore_errors: yes บอกให้ Playbook ดำเนินต่อแม้ task นั้นจะล้มเหลว ใช้เมื่อ task นั้นไม่ใช่ requirement บังคับ

---
- name: Cleanup old files
  hosts: all
  tasks:
    - name: Remove old log files (may not exist)
      file:
        path: /var/log/oldapp.log
        state: absent
      ignore_errors: yes

    - name: Stop old service (may not be running)
      service:
        name: oldservice
        state: stopped
      ignore_errors: yes

    - name: Continue with new installation
      package:
        name: newapp
        state: present

failed_when: กำหนดเงื่อนไข Failure

failed_when กำหนด condition เองว่า task ถือว่า fail เมื่อไร ใช้เมื่อ command return exit code 0 แต่ output บอกว่ามีปัญหา หรือกลับกัน

---
- name: Check and validate
  hosts: all
  tasks:
    - name: Run health check
      shell:
        cmd: curl -s http://localhost/health
      register: health_check
      failed_when:
        - health_check.rc != 0
        - "'OK' not in health_check.stdout"

    - name: Run database backup
      shell:
        cmd: mysqldump mydb > /backup/mydb.sql
      register: backup_result
      failed_when: backup_result.rc != 0 and 'No tables' not in backup_result.stderr

    - name: Check disk space
      shell:
        cmd: df -h / | awk 'NR==2{print $5}' | tr -d '%'
      register: disk_usage
      failed_when: disk_usage.stdout | int > 90

changed_when: ควบคุม Changed Status

changed_when กำหนด condition ว่า task ถือว่า “changed” เมื่อไร ช่วยให้ handler ทำงานอย่างถูกต้องและ idempotency report แม่นยำขึ้น

---
- name: Manage configuration
  hosts: all
  tasks:
    - name: Apply config changes
      shell:
        cmd: /opt/scripts/apply-config.sh
      register: config_result
      changed_when: "'Applied' in config_result.stdout"
      # task ถือว่า changed เฉพาะเมื่อ output มีคำว่า 'Applied'

    - name: Run database vacuum
      shell:
        cmd: psql -c "VACUUM ANALYZE;"
      changed_when: false
      # task นี้ไม่เคย "changed" — เป็น read/maintenance operation

any_errors_fatal: หยุดทันทีเมื่อมี Error

โดย default Ansible รัน task บน host อื่นต่อไปแม้บาง host จะล้มเหลว ใช้ any_errors_fatal: true เพื่อหยุดทุก host ทันทีเมื่อ host ใดล้มเหลว

---
- name: Critical deployment
  hosts: all
  any_errors_fatal: true   # หยุดทุก host ทันทีถ้า host ใดล้มเหลว
  tasks:
    - name: Run pre-deployment check
      shell:
        cmd: /opt/scripts/preflight-check.sh
      register: preflight

    - name: Fail if preflight fails
      fail:
        msg: "Pre-flight check failed on {{ inventory_hostname }}"
      when: preflight.rc != 0

    - name: Deploy application
      shell:
        cmd: /opt/scripts/deploy.sh

max_fail_percentage: ยอมรับ Failure บางส่วน

max_fail_percentage กำหนดเปอร์เซ็นต์ host ที่ยอมให้ล้มเหลวได้ก่อนที่ Playbook จะหยุด เหมาะสำหรับ rolling deployment

---
- name: Rolling deployment
  hosts: webservers
  serial: 2                    # deploy ทีละ 2 เครื่อง
  max_fail_percentage: 20      # หยุดถ้ามากกว่า 20% ของ host ล้มเหลว
  tasks:
    - name: Remove from load balancer
      shell:
        cmd: /opt/lb/remove.sh {{ inventory_hostname }}

    - name: Deploy new version
      shell:
        cmd: /opt/scripts/deploy.sh v2.0

    - name: Run smoke tests
      shell:
        cmd: /opt/scripts/smoke-test.sh
      register: smoke_test
      failed_when: smoke_test.rc != 0

    - name: Add back to load balancer
      shell:
        cmd: /opt/lb/add.sh {{ inventory_hostname }}
      when: smoke_test.rc == 0

สรุป

Error handling ทำให้ Playbook มีความ robust และเชื่อถือได้ใน production Pattern ที่ควรจำ: ใช้ block/rescue/always สำหรับ deployment ที่ต้องการ rollback, ใช้ failed_when เมื่อ exit code ไม่สะท้อนความสำเร็จจริง, และใช้ any_errors_fatal สำหรับ task ที่ต้องการให้ทุก host พร้อมกัน

ignore_errors ควรใช้อย่างระมัดระวัง เพราะอาจซ่อนปัญหาจริงที่ต้องแก้ไข ให้ใช้เฉพาะกรณีที่ failure นั้นคาดได้และไม่กระทบต่อ task ถัดไปจริง ·