핸들러 — 변경이 있을 때만 실행되는 태스크

핸들러 — 변경이 있을 때만 실행되는 태스크

가끔은 태스크가 머신에서 변경을 만들 때만 실행되길 원할 때가 있어요. 예를 들어 어떤 태스크가 서비스의 설정 파일을 갱신하면 그 서비스를 재시작하고 싶지만, 설정이 바뀌지 않았다면 재시작하고 싶지 않죠. Ansible은 이 상황을 핸들러(handler)로 해결해요. 핸들러는 통지(notify)를 받았을 때만 실행되는 태스크예요.

이 글에서는 핸들러의 기본 사용법, 이름 짓는 법, 실행 시점을 제어하는 방법을 살펴볼게요.

출처: 공식문서

핸들러 예시

verify-apache.yml 플레이북은 핸들러 하나를 담은 단일 플레이로 구성돼요.

---
- name: Verify apache installation
  hosts: webservers
  vars:
    http_port: 80
    max_clients: 200
  remote_user: root
  tasks:
  - name: Ensure apache is at the latest version
    ansible.builtin.yum:
      name: httpd
      state: latest

  - name: Write the apache config file
    ansible.builtin.template:
      src: /srv/httpd.j2
      dest: /etc/httpd.conf
    notify:
    - Restart apache

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

  handlers:
    - name: Restart apache
      ansible.builtin.service:
        name: httpd
        state: restarted

이 예시에서 Apache 서버는 플레이의 모든 태스크가 끝난 뒤 핸들러에 의해 재시작돼요.

핸들러 이름 짓기

태스크가 notify 키워드로 통지할 수 있으려면 핸들러는 이름이 있어야 해요.

대신 핸들러는 listen 키워드도 사용할 수 있어요. listen을 쓰면 여러 핸들러를 묶어주는 주제(topic)에 귀를 기울일 수 있어요.

tasks:
  - name: Restart everything
    command: echo "this task will restart the web services"
    notify: "restart web services"

handlers:
  - name: Restart memcached
    service:
      name: memcached
      state: restarted
    listen: "restart web services"

  - name: Restart apache
    service:
      name: apache
      state: restarted
    listen: "restart web services"

restart web services 주제를 통지하면 이름과 상관없이 그 주제를 듣는 모든 핸들러가 실행돼요. 이 방식은 여러 핸들러를 훨씬 쉽게 트리거하게 해주고, 핸들러를 이름에서 분리해 플레이북과 역할(role) 사이에서 공유하기 쉽게 만들어요. 특히 Ansible Galaxy 같은 공유 소스의 서드파티 역할을 쓸 때 유용하죠.

각 핸들러는 전역적으로 유일한 이름을 가져야 해요. 같은 이름의 핸들러가 여러 개 정의되면, 플레이에 마지막으로 로드된 핸들러만 통지·실행될 수 있고 이전 핸들러들은 사실상 가려져요.

핸들러가 플레이에 들어가는 순서

핸들러는 어디에 정의돼 있든(handlers: 섹션이든 역할이든) 전역적인 단일 플레이 수준 스코프만 있어요. 핸들러가 플레이에 추가되는 순서는 다음과 같아요.

  1. roles: 섹션의 역할에서 온 핸들러
  2. handlers: 섹션의 핸들러
  3. import_role 태스크로 정적으로 가져온 역할의 핸들러
  4. include_role 태스크로 동적으로 포함된 역할의 핸들러(런타임에 include_role 태스크가 실행된 뒤에만 사용 가능)

위 순서에 따라 같은 이름의 핸들러가 있으면 마지막에 로드된 핸들러가 통지·실행될 수 있어요.

핸들러가 실행되는 시점 제어하기

기본적으로 핸들러는 특정 플레이의 모든 태스크가 끝난 뒤 실행돼요. 통지된 핸들러는 각 섹션(pre_tasks, roles/tasks, post_tasks)이 끝난 뒤 이 순서대로 자동 실행돼요. 이 방식은 효율적이에요. 몇 개의 태스크가 통지하든 핸들러는 한 번만 실행되거든요. 예를 들어 여러 태스크가 설정 파일을 갱신하고 Apache 재시작 핸들러를 통지해도, Ansible은 불필요한 재시작을 피하려고 Apache를 한 번만 재시작해요.

플레이가 끝나기 전에 핸들러를 실행해야 한다면, 메타(meta) 모듈로 flush하는 태스크를 추가해요.

tasks:
  - name: Some tasks go here
    ansible.builtin.shell: ...
  - name: Flush handlers
    meta: flush_handlers
  - name: Some other tasks
    ansible.builtin.shell: ...

meta: flush_handlers 태스크는 그 시점까지 플레이에서 통지된 모든 핸들러를 트리거해요.

핸들러는 각 섹션 뒤에 자동으로, 또는 flush_handlers 메타 태스크로 수동으로 실행된 뒤에는, 플레이의 이후 섹션에서 다시 통지되어 다시 실행될 수 있어요.

태스크가 언제 변경되는지 정의하기

changed_when 키워드로 핸들러가 태스크 변경에 대해 언제 통지받을지 제어할 수 있어요.

다음 예시에서 핸들러는 설정 파일을 복사할 때마다 서비스를 재시작해요.

tasks:
  - name: Copy httpd configuration
    ansible.builtin.copy:
      src: ./new_httpd.conf
      dest: /etc/httpd/conf/httpd.conf
    # The task is always reported as changed
    changed_when: True
    notify: Restart apache

changed_when에 대한 자세한 내용은 "Defining changed" 문서를 참고해요.

핸들러에서 변수 사용하기

Ansible 핸들러가 변수를 사용하길 원할 수 있어요. 예를 들어 분포에 따라 서비스 이름이 조금씩 다르다면, 대상 머신마다 재시작한 서비스의 정확한 이름을 출력에 보여주고 싶을 거예요. 핸들러의 이름에는 변수를 두지 않는 게 좋아요. 핸들러 이름은 일찍 템플릿화되기 때문에, 이런 이름에 쓸 값이 아직 없을 수 있어요.

handlers:
# This handler name may cause your play to fail!
- name: Restart "{{ web_service_name }}"

핸들러 이름에 쓴 변수를 쓸 수 없으면 플레이 전체가 실패해요. 플레이 도중 그 변수를 바꿔도 새 핸들러가 생기지 않아요.

대신 변수는 핸들러의 태스크 매개변수에 두세요. include_vars로 값을 불러올 수 있어요.

tasks:
  - name: Set host variables based on distribution
    include_vars: "{{ ansible_facts.distribution }}.yml"

handlers:
  - name: Restart web service
    ansible.builtin.service:
      name: "{{ web_service_name | default('httpd') }}"
      state: restarted

핸들러 이름은 템플릿을 담을 수 있지만, listen 주제는 템플릿을 담을 수 없어요.

역할에서의 핸들러

역할의 핸들러는 역할 안에만 갇혀 있지 않고 플레이의 다른 모든 핸들러와 함께 전역 스코프에 삽입돼요. 그래서 정의된 역할 밖에서도 사용할 수 있어요. 동시에 역할 밖의 핸들러와 이름이 충돌할 수도 있다는 뜻이죠. 역할의 핸들러가 역할 밖의 같은 이름의 핸들러 대신 통지되도록 하려면 role_name:handler_name 형태로 이름을 사용해 통지해요.

roles 섹션 안에서 통지된 핸들러는 tasks 섹션 끝에 자동으로 flush돼요.

핸들러에서의 include와 import

include_task 같은 동적 include를 핸들러로 통지하면 그 include 안의 모든 태스크가 실행돼요. 동적 include 안에 정의된 핸들러는 통지할 수 없어요.

import_task 같은 정적 include를 핸들러로 쓰면, 플레이 실행 전에 그 import 안의 핸들러가 실질적으로 그 핸들러를 다시 작성해요. 정적 include 자체는 통지할 수 없지만, 그 include 안의 태스크는 개별적으로 통지할 수 있어요.

메타 태스크를 핸들러로

Ansible 2.14부터 메타 태스크를 핸들러로 사용·통지할 수 있어요. 다만 flush_handlers는 예상치 못한 동작을 막기 위해 핸들러로 쓸 수 없어요.

제한 사항

핸들러는 import_roleinclude_role도 실행할 수 없어요. 핸들러는 태그를 무시해요(tags on handlers).

더 알아보기

  • 플레이북 기초: 플레이와 태스크의 기본 구조
  • 조건문(Conditionals): when을 사용한 조건부 실행
  • changed_when: 태스크가 '변경됨'으로 보고되는 시점 정의
  • 역할(Roles): roles: 섹션과 include_role/import_role 사용법