Red Hat Certified Engineer (RHCE) #8 Error handling: block/rescue/always, failed_when, ignore_errors

9 분 소요

#7 Jinja2 템플릿에서 호스트별 설정 파일을 동적으로 생성하는 법을 익혔습니다. 그런데 플레이북은 한 task가 실패하면 그 호스트의 나머지 task를 건너뛰고 멈춥니다. 시험에서 “특정 단계가 실패해도 정리 작업은 반드시 수행하라” 또는 “command 결과를 보고 실패 여부를 판정하라” 같은 요구가 나오면, 기본 동작만으로는 점수를 받기 어렵습니다. 이번 글에서는 플레이북의 실패 흐름을 직접 통제하는 오류 처리(error handling)를 다루겠습니다.

오류 처리는 두 갈래입니다. 하나는 실패가 났을 때 어떻게 반응할지를 정하는 block/rescue/always와 ignore_errors이고, 다른 하나는 무엇을 실패로 볼지를 재정의하는 failed_when과 changed_when입니다. 특히 command,shell처럼 멱등성이 없는 모듈을 다룰 때 changed_when을 다룰 줄 아는지가 시험의 단골 포인트입니다.

기본 동작: 실패하면 멈춘다 #

오류 처리를 이해하려면 먼저 Ansible의 기본 실패 동작을 알아야 합니다. 플레이북을 돌리면 Ansible은 호스트마다 task를 위에서 아래로 실행합니다. 어떤 호스트에서 한 task가 실패하면, 그 호스트는 즉시 멈추고 남은 task를 실행하지 않습니다. 다른 호스트는 자기 차례까지 진행하다가 같은 지점에서 멈춥니다.

이 기본 동작이 합리적인 이유는, 앞선 단계가 실패한 상태에서 뒷단계를 강행하면 더 큰 손상을 부를 수 있기 때문입니다. 그래서 오류 처리란 이 기본 동작을 의도적으로 바꾸는 일입니다. 실패를 무시할지, 실패해도 정리 작업은 돌릴지, 무엇을 실패로 셀지를 명시적으로 정합니다.

ignore_errors: 실패를 흘려보낸다 #

ignore_errors: yes를 붙이면, 그 task가 실패해도 Ansible은 실패로 표시만 하고 다음 task로 넘어가며 호스트를 멈추지 않습니다.

ignore_errors 예시
- name: ignore_errors 예시
  hosts: webservers
  become: true
  tasks:
    - name: 있을 수도 없을 수도 있는 패키지 제거 시도
      ansible.builtin.dnf:
        name: legacy-tool
        state: absent
      ignore_errors: yes

    - name: 이 task는 위가 실패해도 실행된다
      ansible.builtin.debug:
        msg: "정리 단계로 넘어왔습니다"

주의할 점이 두 가지 있습니다. 첫째, ignore_errors는 task가 실패로 끝난 뒤 그 실패를 무시하는 것이지, 실패 자체를 없애지 않습니다. 실행 결과 요약에는 여전히 실패한 task로 집계됩니다. 둘째, ignore_errors는 호스트에 연결조차 못 한 unreachable 상황은 무시하지 못합니다. unreachable은 별도 범주이며, 이것까지 무시하려면 ignore_unreachable: yes를 씁니다.

ignore_errors는 “실패해도 상관없는 한두 task"에 국지적으로 쓰는 도구입니다. 여러 task를 묶어 실패 흐름을 구조적으로 다루려면 block을 씁니다.

block/rescue/always: 예외를 구조화한다 #

block은 여러 task를 하나의 단위로 묶습니다. 여기에 rescue와 always를 더하면, 다른 언어의 try,catch,finally와 같은 구조가 됩니다.

  • block. 정상 흐름에서 실행할 task들을 묶습니다.
  • rescue. block 안에서 task가 실패했을 때만 실행됩니다.
  • always. 성공,실패와 무관하게 항상 마지막에 실행됩니다.
block/rescue/always 예시
- name: block/rescue/always 예시
  hosts: webservers
  become: true
  tasks:
    - name: 애플리케이션 배포 시도와 복구
      block:
        - name: 새 설정 파일 배치
          ansible.builtin.template:
            src: app.conf.j2
            dest: /etc/myapp/app.conf

        - name: 서비스 재시작
          ansible.builtin.service:
            name: myapp
            state: restarted
      rescue:
        - name: 실패 시 백업 설정으로 롤백
          ansible.builtin.copy:
            src: /etc/myapp/app.conf.bak
            dest: /etc/myapp/app.conf
            remote_src: true

        - name: 롤백 후 서비스 재시작
          ansible.builtin.service:
            name: myapp
            state: restarted
      always:
        - name: 처리 결과 기록
          ansible.builtin.debug:
            msg: "배포 시도가 끝났습니다. 로그를 확인하겠습니다"

동작의 핵심을 정리하겠습니다. block 안의 어느 task가 실패하면, 그 시점에서 block의 나머지 task는 건너뛰고 rescue로 넘어갑니다. rescue의 task들이 모두 성공하면, 그 호스트는 실패가 아니라 성공으로 간주됩니다. block이 실패 없이 끝났다면 rescue는 실행되지 않습니다. always는 두 경우 모두 마지막에 실행됩니다.

rescue가 성공하면 호스트가 성공으로 처리된다는 점은 시험에서 특히 유용합니다. “X를 시도하되 실패하면 Y로 복구하고 플레이북은 계속 진행하라” 같은 요구를 정확히 이 구조로 표현합니다.

rescue 안에서 원인을 확인하는 magic 변수 #

rescue 블록에서는 실패 원인을 알아내는 몇 가지 변수를 쓸 수 있습니다.

  • ansible_failed_task. 실패한 task의 정보를 담습니다. ansible_failed_task.name으로 이름을 얻습니다.
  • ansible_failed_result. 실패한 task의 결과(반환값)를 담습니다.
실패한 task 확인
      rescue:
        - name: 어느 task가 실패했는지 출력
          ansible.builtin.debug:
            msg: "실패한 task: {{ ansible_failed_task.name }}"

failed_when: 실패 조건을 직접 정의한다 #

기본적으로 Ansible은 모듈이 반환하는 상태로 성공,실패를 판정합니다. 그런데 command나 shell 모듈은 명령의 종료 코드만 보기 때문에, “종료 코드는 0이지만 출력 안에 오류 문구가 있는” 경우를 실패로 잡지 못합니다. 이럴 때 failed_when으로 무엇을 실패로 볼지 직접 정합니다.

먼저 command 결과를 register로 변수에 담고, 그 변수를 조건으로 판정하는 패턴이 표준입니다.

failed_when 예시
- name: failed_when 예시
  hosts: appservers
  tasks:
    - name: 헬스 체크 명령 실행
      ansible.builtin.command: /usr/local/bin/healthcheck
      register: health
      failed_when: "'ERROR' in health.stdout"

    - name: 종료 코드와 출력 내용을 함께 판정
      ansible.builtin.command: /usr/local/bin/deploy
      register: deploy
      failed_when: deploy.rc != 0 or 'FAILED' in deploy.stdout

failed_when은 조건이 참이면 그 task를 실패로 표시합니다. 반대로 failed_when: false로 두면 무슨 일이 있어도 실패로 보지 않습니다. 이것은 ignore_errors와 비슷해 보이지만 의미가 다릅니다. ignore_errors는 “실패했지만 무시"이고, failed_when: false는 “애초에 실패로 치지 않음"입니다. 후자는 실행 요약에서도 실패로 집계되지 않습니다.

register 결과의 구조 #

failed_when과 changed_when을 제대로 쓰려면 register로 담은 결과의 구조를 알아야 합니다. command,shell 계열에서 자주 쓰는 키는 다음과 같습니다.

  • rc. 명령의 종료 코드(return code)입니다.
  • stdout. 표준 출력 전체를 하나의 문자열로 담습니다.
  • stdout_lines. 표준 출력을 줄 단위 리스트로 담습니다.
  • stderr. 표준 에러 출력입니다.

조건에서 특정 문구를 찾을 때는 '문구' in result.stdout 형태를 쓰고, 줄 단위로 다룰 때는 stdout_lines를 씁니다.

changed_when: 변경 조건을 직접 정의한다 #

changed_when무엇을 변경으로 볼지를 정합니다. 실패 판정이 아니라 changed 상태 판정을 다룬다는 점에서 failed_when과 짝을 이룹니다. 이 키가 RHCE에서 중요한 이유는 command,shell 모듈의 멱등성 문제 때문입니다.

command나 shell 모듈은 실행만 하면 무조건 changed로 보고합니다. 실제로 시스템을 바꾸지 않는 조회성 명령이라도 changed로 집계되어, 두 번째 실행에서 changed가 0이 되지 않습니다. 멱등성이 채점의 핵심인 시험에서 이는 감점 요인입니다.

그래서 조회만 하는 command에는 changed_when: false를 붙여 변경 없음으로 명시합니다.

changed_when으로 멱등성 보완
- name: changed_when으로 멱등성 보완
  hosts: appservers
  tasks:
    - name: 현재 버전 확인 (조회만, 변경 아님)
      ansible.builtin.command: /usr/local/bin/myapp --version
      register: appver
      changed_when: false

    - name: 결과에 따라 변경 여부 판정
      ansible.builtin.command: /usr/local/bin/sync-config
      register: sync
      changed_when: "'updated' in sync.stdout"
      failed_when: sync.rc != 0

두 번째 task처럼 failed_when과 changed_when을 한 task에 함께 쓰는 경우가 많습니다. “종료 코드가 0이 아니면 실패로, 출력에 updated가 있으면 변경으로” 판정하는 식입니다. command 모듈을 쓸 수밖에 없는 상황에서 이 두 키를 더하면, 전용 모듈에 가까운 멱등성을 흉내 낼 수 있습니다.

command 대신 모듈을 먼저 검토한다 #

changed_when으로 멱등성을 보완하기에 앞서, 원칙은 command,shell을 전용 모듈로 대체하는 것입니다. 패키지 설치는 dnf로, 파일 배치는 copy,template로, 서비스 제어는 service로 처리하면 멱등성이 모듈 안에 들어 있습니다. command,shell은 적당한 모듈이 없을 때만 쓰고, 그때 changed_when,failed_when으로 멱등성과 실패 판정을 보완하는 순서를 권합니다.

플레이 전체의 실패를 다루는 키 #

지금까지는 task,block 단위의 처리였습니다. 여러 호스트를 다루는 플레이 전체의 실패 전략을 정하는 키도 있으니 한 줄씩 정리하겠습니다.

  • any_errors_fatal: true. 한 호스트라도 task에서 실패하면, 그 batch의 모든 호스트를 즉시 중단합니다. 한 대라도 어긋나면 전체를 멈춰야 하는 작업에 씁니다.
  • max_fail_percentage: 30. 실패한 호스트 비율이 지정한 퍼센트를 넘으면 플레이를 중단합니다. “30% 넘게 실패하면 멈춰라” 같은 허용 한계를 표현합니다.
  • force_handlers: true. 플레이가 중간에 실패해도, 이미 notify로 예약된 handler를 끝나기 전에 강제로 실행합니다. 보통 handler는 실패하면 실행되지 않으므로, 반드시 돌려야 하는 정리성 handler가 있을 때 씁니다.

이 키들은 task가 아니라 play 레벨에 둡니다. 자주 출제되지는 않지만, 요구사항에 “한 대라도 실패하면 전체 중단” 같은 문구가 보이면 any_errors_fatal을 떠올리겠습니다.

시험 포인트 #

오류 처리에서 점수가 갈리는 지점을 정리하겠습니다.

  • command 결과로 실패 판정. command,shell을 register로 담은 뒤 failed_when으로 출력 내용이나 종료 코드를 보고 실패를 판정하는 패턴이 단골입니다. '문구' in result.stdout 표현을 손에 익혀 두겠습니다.
  • command 멱등성 보완. 조회성 command에는 changed_when: false를 붙여 두 번째 실행에서 changed가 0이 되도록 만듭니다. 멱등성 검증에서 자주 감점되는 부분입니다.
  • block/rescue로 복구 흐름 작성. “시도하되 실패하면 복구하고 계속 진행"은 block/rescue로 표현합니다. rescue가 성공하면 호스트가 성공으로 처리된다는 점을 기억하겠습니다.
  • always로 정리 작업 보장. 성공,실패와 무관하게 돌려야 하는 정리 작업은 always에 둡니다.
  • failed_when: false와 ignore_errors의 차이. 전자는 실패로 치지 않고, 후자는 실패를 무시할 뿐 집계에는 남습니다. 요구 문구를 보고 둘을 구분해 쓰겠습니다.
  • 두 번 돌려 검증. 작성한 플레이북을 두 번 실행해 두 번째에서 changed,failed가 의도대로 나오는지 확인하는 습관을 들이겠습니다.

정리 #

이번 글에서 잡은 것:

  • 기본 동작. task가 실패하면 그 호스트는 즉시 멈추고 남은 task를 건너뜁니다.
  • ignore_errors. 실패를 무시하고 다음 task로 넘어가되, 실행 요약에는 실패로 남습니다. unreachable은 무시하지 못합니다.
  • block/rescue/always. try,catch,finally 구조. rescue가 성공하면 호스트는 성공으로 처리되고, always는 항상 마지막에 실행됩니다.
  • failed_when. 무엇을 실패로 볼지 직접 정합니다. command 결과를 register로 담아 출력,종료 코드로 판정합니다.
  • changed_when. 무엇을 변경으로 볼지 정합니다. 조회성 command에 changed_when: false로 멱등성을 보완합니다.
  • play 레벨 키. any_errors_fatal, max_fail_percentage, force_handlers로 플레이 전체의 실패 전략을 정합니다.

다음: Tag와 조건부 #

오류 흐름을 통제하는 법을 잡았습니다. 이제 task를 언제 실행할지 정하는 조건과, 같은 task를 반복 실행하는 흐름을 다룰 차례입니다.

#9 Tag와 조건부: when, loop, until에서는 when으로 조건부 실행을 거는 법, loop로 리스트를 반복 처리하는 법, until로 조건이 충족될 때까지 재시도하는 법, 그리고 tag로 플레이북의 일부만 골라 실행하는 시험 단골 패턴까지 직접 작성하며 정리하겠습니다.

X