iT邦幫忙

2025 iThome 鐵人賽

DAY 9
0
DevOps

不爆肝學習 Ansible 的短暫30天系列 第 9

Day09 - Handlers 有效率的重啟機制

  • 分享至 

  • xImage
  •  

今日目標

  • 理解 Handlers 的核心概念和使用場景
  • 掌握 notify 機制,實現條件式服務重啟
  • 避免無意義的服務重啟,提升部署效率

為什麼需要 Handlers?

想像一下這個情況:你管理著 50 台 web server,每次更新 nginx 設定檔後都要重啟服務。

如果用傳統方式,不管設定檔有沒有真的改變,你都會重啟所有的 nginx,這樣:

  1. 浪費時間 - 沒改變的服務也被重啟了
  2. 影響服務 - 不必要的服務中斷
  3. 效率低下 - 整個部署流程變慢

白話來說 Handlers 就像個煙霧偵測器,只有在偵測到煙霧時才會觸發警報,沒事就不會亂響。

基本語法與使用

最簡單的範例

---
- name: Update nginx config and restart if needed
  hosts: web
  become: yes
  tasks:
    - name: Copy new nginx config
      copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf
        backup: yes
      notify: Restart nginx

  handlers:
    - name: Restart nginx
      service:
        name: nginx
        state: restarted

執行邏輯:

  1. Ansible 檢查目標檔案是否與來源檔案不同
  2. 如果不同,複製檔案並觸發 handler
  3. 如果相同,跳過複製,handler 不會執行
  4. Handler 會在所有 tasks 執行完後才執行

💡Tips:這就是 Ansible 冪等性,只有真正被改變時才會執行任務。

Handler 的執行時機

---
- name: Multiple tasks with handlers demo
  hosts: web
  become: yes
  tasks:
    - name: Update nginx config
      copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf
      notify:
        - Restart nginx
        - Check nginx status

    - name: Update Rails app config
      template:
        src: application.yml.j2
        dest: /var/www/myapp/config/application.yml
      notify: Restart puma

    - name: Some other task
      debug:
        msg: "This runs before any handlers"

  handlers:
    - name: Restart nginx
      service:
        name: nginx
        state: restarted

    - name: Restart puma
      systemd:
        name: puma
        state: restarted

    - name: Check nginx status
      command: nginx -t
      listen: "Check nginx status"  # 可以用 listen 給 handler 別名

執行順序:

  1. 所有 tasks 按順序執行
  2. 所有 handlers 在 tasks 執行完後才執行
  3. 同一個 handler 被多個 task 觸發,也只會執行一次
  4. Handlers 按照定義順序執行(不是被通知的順序)

進階應用技巧

條件式 Handler 觸發

- name: Update config only for production
  template:
    src: app.conf.j2
    dest: /etc/app/app.conf
  notify: Restart application
  when: environment == "production"

- name: Update development config
  copy:
    src: app-dev.conf
    dest: /etc/app/app.conf
  notify: Reload application  # 開發環境只需 reload
  when: environment == "development"

Handler 鏈式執行

handlers:
  - name: Restart nginx
    service:
      name: nginx
      state: restarted
    notify: Verify nginx  # Handler 可以觸發其他 Handler

  - name: Verify nginx
    uri:
      url: http://localhost
      method: GET
      status_code: 200

flush_handlers 強制執行

tasks:
  - name: Update critical config
    copy:
      src: critical.conf
      dest: /etc/app/critical.conf
    notify: Restart application

  - name: Force handlers to run now
    meta: flush_handlers  # 立即執行所有 pending 的 handlers

  - name: Run health check after restart
    uri:
      url: http://localhost/health
      status_code: 200

實戰場景範例

場景一:完整 Web 服務更新

---
- name: Deploy web application with smart restart
  hosts: web
  become: yes
  vars:
    app_version: "1.2.3"

  tasks:
    - name: Update application code
      unarchive:
        src: "app-{{ app_version }}.tar.gz"
        dest: /var/www/html
        remote_src: no
      notify:
        - Restart puma
        - Precompile assets

    - name: Update nginx virtual host
      template:
        src: nginx-vhost.j2
        dest: /etc/nginx/sites-available/app
      notify:
        - Reload nginx
        - Test nginx config

    - name: Update database schema
      command: rails db:migrate
      args:
        chdir: /var/www/html
      environment:
        RAILS_ENV: production
      register: migration_result
      changed_when: "'migrated' in migration_result.stdout"
      notify: Clear rails cache

  handlers:
    - name: Test nginx config
      command: nginx -t

    - name: Reload nginx
      service:
        name: nginx
        state: reloaded  # 可以多多善用 nginx reload 特性,避免使用 restart

    - name: Restart puma
      systemd:
        name: puma
        state: restarted

    - name: Precompile assets
      command: rails assets:precompile
      args:
        chdir: /var/www/html
      environment:
        RAILS_ENV: production

    - name: Clear rails cache
      command: rails cache:clear
      args:
        chdir: /var/www/html
      environment:
        RAILS_ENV: production

場景二:容器化應用管理

---
- name: Docker application deployment
  hosts: app
  become: yes
  tasks:
    - name: Update docker-compose config
      template:
        src: docker-compose.yml.j2
        dest: /opt/myapp/docker-compose.yml
        backup: yes
      notify:
        - Validate compose config
        - Restart docker services

    - name: Update application environment
      template:
        src: app.env.j2
        dest: /opt/myapp/.env
      notify: Restart app container
      no_log: yes  # 不要在 log 中顯示敏感環境變數

    - name: Update nginx proxy config
      template:
        src: nginx.conf.j2
        dest: /opt/myapp/nginx/nginx.conf
      notify:
        - Test nginx config
        - Restart nginx container

  handlers:
    - name: Validate compose config
      command: docker-compose -f /opt/myapp/docker-compose.yml config
      register: compose_config_check
      failed_when: compose_config_check.rc != 0

    - name: Test nginx config
      command: docker exec myapp_nginx nginx -t
      register: nginx_config_check
      failed_when: nginx_config_check.rc != 0

    - name: Restart docker services
      command: docker-compose -f /opt/myapp/docker-compose.yml up -d --force-recreate
      args:
        chdir: /opt/myapp

    - name: Restart app container
      command: docker-compose -f /opt/myapp/docker-compose.yml restart app
      args:
        chdir: /opt/myapp

    - name: Restart nginx container
      command: docker-compose -f /opt/myapp/docker-compose.yml restart nginx
      args:
        chdir: /opt/myapp

驗證與除錯

檢查 Handler 是否被觸發

方法一:使用 debug handler

handlers:
  - name: Debug notification
    debug:
      msg: "Handler was triggered! Config changed."

  - name: Restart nginx
    service:
      name: nginx
      state: restarted

方法二:查看 Ansible 輸出

# 使用 -v 參數可以看到更多詳細資訊
ansible-playbook -i inventory deploy.yml -v

# 輸出範例:
# RUNNING HANDLER [Restart nginx] ****
# changed: [web01]

方法三:檢查服務狀態變化

- name: Get nginx process start time before
  shell: ps -o lstart= -p $(pgrep nginx | head -1)
  register: nginx_start_before

# ... 你的 tasks 和 handlers ...

- name: Verify nginx was restarted
  shell: ps -o lstart= -p $(pgrep nginx | head -1)
  register: nginx_start_after
  failed_when: nginx_start_before.stdout == nginx_start_after.stdout
  when: config_changed is defined

實用除錯技巧

# 1. 強制觸發 handler(測試用)
- name: Force handler trigger
  command: /bin/true
  notify: Restart nginx
  changed_when: true  # 強制標記為 changed

# 2. 條件式 handler 執行
- name: Conditional restart
  service:
    name: nginx
    state: restarted
  when: force_restart | default(false)
  listen: "Restart nginx"

# 3. Handler 執行狀態檢查
- name: Check if service is running after handler
  service:
    name: nginx
    state: started
  listen: "Verify nginx running"

常見的坑

1. Handler 名稱大小寫敏感

# 錯誤:大小寫不符
tasks:
  - copy:
      src: config.conf
      dest: /etc/config.conf
    notify: restart nginx  # 小寫

handlers:
  - name: Restart Nginx  # 大寫開頭
    service:
      name: nginx
      state: restarted
    # 這個 handler 永遠不會被觸發!

2. Failed Task 不會觸發 Handler

# 如果 copy 失敗,handler 不會執行
- name: Copy config
  copy:
    src: nonexistent.conf  # 檔案不存在會失敗
    dest: /etc/app.conf
  notify: Restart service
  # 解決方案:加上錯誤處理
  ignore_errors: yes
  # 或使用 block/rescue

3. Handler 在 Play 結束時才執行

# 問題:想要立即重啟服務再繼續
- name: Update config
  copy:
    src: app.conf
    dest: /etc/app.conf
  notify: Restart app

- name: Test application
  uri:
    url: http://localhost/health
  # 這時 app 還沒重啟!會失敗

# 解決方案:使用 flush_handlers
- name: Update config
  copy:
    src: app.conf
    dest: /etc/app.conf
  notify: Restart app

- meta: flush_handlers  # 強制執行 handlers

- name: Test application
  uri:
    url: http://localhost/health
  # 現在 app 已經重啟了

4. Handler 重複執行問題

# 錯誤認知:多個 task 觸發同一個 handler 會執行多次
tasks:
  - copy:
      src: file1.conf
      dest: /etc/file1.conf
    notify: Restart service

  - copy:
      src: file2.conf
      dest: /etc/file2.conf
    notify: Restart service

# 實際上:不管幾個 task 觸發,handler 只會執行一次

作業練習時間

練習 1:基礎 Handler 應用

建立一個 Playbook,實現以下功能:

  • 更新 nginx 主設定檔
  • 更新 nginx 虛擬主機設定
  • 只在設定檔真的有變化時才重啟 nginx
  • 重啟後驗證 nginx 正常運行

提示:

# 你的 handlers 應該包含:
handlers:
  - name: Restart nginx
    service:
      name: nginx
      state: restarted

  - name: Verify nginx
    uri:
      url: http://localhost
      status_code: 200

練習 2:多服務協調更新

建立一個各位常用的應用(ex: Rails、Laravel、Django)環境更新的 Playbook:

  • 更新 nginx 配置
  • 更新 Rails、Laravel、Django 配置
  • 更新應用程式的程式碼
  • 適當地重啟相關服務
  • 加入健康檢查的機制

挑戰要求:

  • 使用 flush_handlers 確保服務重啟順序
  • 只有真正需要時才重啟服務
  • 包含錯誤處理機制

練習 3:進階挑戰 - 零停機部署

設計一個零停機部署流程:

  • 先更新一台服務器的配置
  • 重啟該服務器的服務
  • 健康檢查通過後,再更新其他服務器
  • 使用 serial: 1 逐台更新
# 提示架構:
- name: Rolling deployment
  hosts: web
  serial: 1  # 一次只處理一台
  tasks:
    # 從負載均衡器移除
    - name: Remove from load balancer
      # ...

    # 更新配置
    - name: Update config
      # ...
      notify: Restart service

    # 等待 handler 執行
    - meta: flush_handlers

    # 健康檢查
    - name: Health check
      # ...

    # 加回負載均衡器
    - name: Add back to load balancer
      # ...

Production 上的一些實務範例

1. Rolling restart

# 不要一次重啟所有服務
- name: Rolling restart
  hosts: web
  serial: "30%"  # 一次重啟 30% 的機器
  tasks:
    - name: Update config
      template:
        src: app.conf.j2
        dest: /etc/app.conf
      notify: Restart app

2. Health Check 的整合

handlers:
  - name: Restart application
    service:
      name: myapp
      state: restarted
    notify: Wait for application

  - name: Wait for application
    wait_for:
      port: 8080
      host: "{{ inventory_hostname }}"
      delay: 5
      timeout: 60
    notify: Verify application health

  - name: Verify application health
    uri:
      url: "http://{{ inventory_hostname }}:8080/health"
      method: GET
      status_code: 200

明日預告

寫程式都會有 exception 了,自動化任務想必也會有,所以明天我們來學學怎麼做好錯誤處理吧!


上一篇
Day08 – 條件與迴圈讓 Playbook 更聰明
系列文
不爆肝學習 Ansible 的短暫30天9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言