🔧

Ansible - apt 심화와 에러 핸들링

최민석·2026-03-19

ansible

apt 심화와 에러 핸들링

🔧 Ansible 실습 시리즈

이전 챕터까지 task 결과를 저장하고 조건 분기하는 방법을 배웠습니다. 이번에는 **패키지 버전을 고정(hold)**하여 실수로 업그레이드되는 것을 방지하고, failed_when으로 커스텀 실패 조건을 정의하며, block/rescue/always로 에러를 구조적으로 처리하는 방법을 다룹니다.


dpkg_selections — 패키지 버전 고정 (hold)

왜 필요한가

apt upgrade를 실행하면 시스템의 모든 패키지가 최신 버전으로 올라갑니다. 하지만 Kubernetes 환경에서 kubeadm, kubelet, kubectl의도적으로 특정 버전을 유지해야 합니다. 실수로 버전이 올라가면 클러스터 호환성 문제가 발생할 수 있기 때문입니다.

dpkg_selections 모듈의 hold 옵션을 사용하면 특정 패키지의 업그레이드를 차단할 수 있습니다.

playbook 작성

playbook/apt_hold.yml:

---
- name: apt hold
  hosts: managed
  become: true
  tasks:
  - name: install curl
    ansible.builtin.apt:
      name: curl
      state: present

  - name: check curl ver
    shell: "apt-cache policy curl"
    register: result

  - name: print
    debug:
      msg: "{{ result.stdout }}"

  - name: hold curl
    dpkg_selections:
      name: curl
      selection: hold

  - name: check hold
    shell: "dpkg --get-selections | grep curl"
    register: result

  - name: print check
    debug:
      msg: "{{ result.stdout }}"

실행 결과

TASK [hold curl] *********************************************************************
changed: [node1]
changed: [node2]
changed: [node3]

TASK [check hold] ********************************************************************
changed: [node1]
changed: [node2]
changed: [node3]

TASK [print check] *******************************************************************
ok: [node1] => {
    "msg": "curl\t\t\t\t\t\thold\nlibcurl4t64:arm64\t\t\t\tinstall"
}

curl → hold로 표시되면 성공입니다. libcurl4t64는 의존성 패키지인데, dpkg_selections지정한 패키지만 hold를 걸기 때문에 install 상태 그대로입니다.

재실행하면 이미 hold 상태이므로 ok(멱등성)가 됩니다.

hold 해제

hold를 해제할 때는 selection: install을 사용합니다:

- name: unhold curl
  dpkg_selections:
    name: curl
    selection: install

주의: selection: unhold는 존재하지 않습니다. 다음 에러가 발생합니다:

"value of selection must be one of: install, hold, deinstall, purge, got: unhold"

dpkg_selections가 허용하는 값은 install, hold, deinstall, purge 4가지뿐입니다.


failed_when — 커스텀 실패 조건

기본 사용법

Ansible에서 task의 성공/실패는 기본적으로 return code로 판단합니다 (0이면 성공). 하지만 명령이 성공(rc=0)했더라도 출력 내용이 기대와 다를 때 실패로 처리하고 싶은 경우가 있습니다.

playbook/failed_when.yml:

---
- name: failed when test
  hosts: managed
  become: true
  tasks:
    - name: get selections
      shell: "dpkg --get-selections | grep curl"
      register: result
      failed_when: "'hold' not in result.stdout"

    - name: print
      debug:
        msg: "curl is held"

실행 결과 — 성공

curl이 hold 상태이므로 stdout에 "hold"가 포함되어 있어 정상 통과합니다:

TASK [get selections] ****************************************************************
changed: [node1]
changed: [node2]
changed: [node3]

TASK [print] *************************************************************************
ok: [node1] => {
    "msg": "curl is held"
}

실행 결과 — 의도적 실패

'hold''purge'로 변경하면 stdout에 "purge"가 없으므로 실패합니다:

fatal: [node1]: FAILED! => {"changed": true, ..., "failed_when_result": true, ...
    "stdout": "curl\t\t\t\t\t\thold\nlibcurl4t64:arm64\t\t\t\tinstall"}

failed_when_result: true — 표현식이 정상 평가된 결과 true(=실패 조건 충족)라는 뜻입니다.

에러: Jinja2 따옴표 충돌

처음 작성할 때 따옴표 중첩을 잘못하면 다음 에러가 발생합니다:

"failed_when_result": "Syntax error in expression: chunk after expression"

원인: YAML의 따옴표와 Jinja2의 문자열 따옴표가 충돌합니다.

해결: 바깥 따옴표와 안쪽 따옴표를 다르게 사용합니다:

# 바깥 ", 안쪽 '
failed_when: "'hold' not in result.stdout"

# 또는 바깥 ', 안쪽 "
failed_when: '"hold" not in result.stdout'

block/rescue/always — 구조적 에러 핸들링

개념

프로그래밍의 try/catch/finally와 동일한 구조입니다:

Ansible 프로그래밍 역할
block try 시도할 작업
rescue catch 실패 시 복구 작업
always finally 성공/실패 무관하게 항상 실행

기본 사용법

playbook/block_rescue.yml:

---
- name: block rescue
  hosts: managed
  become: true

  tasks:
  - name: command 404

    block:
    - name: block test
      shell: cat /temp/nonexistent_file

    rescue:
    - name: rescue test
      debug:
        msg: "에러 발생, 복구 작업 실행"
    - name: print recovery
      shell: "echo 'recovered' > /tmp/recovery.txt"

    always:
    - name: print always
      debug:
        msg: "항상 실행되는 정리 작업"

실행 결과

TASK [block test] ********************************************************************
fatal: [node1]: FAILED! => {"changed": true, ...,
    "stderr": "cat: /temp/nonexistent_file: No such file or directory"}

TASK [rescue test] *******************************************************************
ok: [node1] => {
    "msg": "에러 발생, 복구 작업 실행"
}

TASK [print recovery] ****************************************************************
changed: [node1]

TASK [print always] ******************************************************************
ok: [node1] => {
    "msg": "항상 실행되는 정리 작업"
}

PLAY RECAP ***************************************************************************
node1                      : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

PLAY RECAP에서 failed=0, rescued=1 — block에서 실패했지만 rescue가 처리했으므로 최종적으로는 실패가 아닙니다. rescue가 없었다면 failed=1로 playbook이 중단됐을 것입니다.


실전 조합 — hold/unhold 안전 패턴

배운 것들을 조합해서, 패키지 업그레이드 실패 시 자동으로 hold를 복원하는 패턴을 구현합니다.

시나리오: unhold → 업그레이드 시도 → 실패 시 다시 hold로 복원

playbook/safe_upgrade.yml:

---
- name: safe upgrade
  hosts: managed
  become: true

  tasks:
    - name: safe upgrade task
      block:
        - name: unhold curl
          dpkg_selections:
            name: curl
            selection: install

        - name: check unhold
          shell: "dpkg --get-selections | grep curl"
          register: result

        - name: print result
          debug:
            msg: "{{ result.stdout }}"

        - name: trigger error
          shell: "apt-get install nonexistent-package-xyzabc"

      rescue:
        - name: rescue sstart
          debug:
            msg: "업그레이드 실패, hold 복원"

        - name: restore hold
          dpkg_selections:
            name: curl
            selection: hold

      always:
        - name: always do this
          shell: "dpkg --get-selections | grep curl"
          register: result_always

        - name: print always
          debug:
            msg: "{{ result_always.stdout }}"

실행 결과

TASK [unhold curl] *******************************************************************
changed: [node1]

TASK [check unhold] ******************************************************************
changed: [node1]

TASK [print result] ******************************************************************
ok: [node1] => {
    "msg": "curl\t\t\t\t\t\tinstall\nlibcurl4t64:arm64\t\t\t\tinstall"
}

TASK [trigger error] *****************************************************************
fatal: [node1]: FAILED! => {"changed": true, ...,
    "stderr": "E: Unable to locate package nonexistent-package-xyzabc"}

TASK [rescue sstart] *****************************************************************
ok: [node1] => {
    "msg": "업그레이드 실패, hold 복원"
}

TASK [restore hold] ******************************************************************
changed: [node1]

TASK [always do this] ****************************************************************
changed: [node1]

TASK [print always] ******************************************************************
ok: [node1] => {
    "msg": "curl\t\t\t\t\t\thold\nlibcurl4t64:arm64\t\t\t\tinstall"
}

PLAY RECAP ***************************************************************************
node1                      : ok=8    changed=4    unreachable=0    failed=0    skipped=0    rescued=1    ignored=0

흐름을 따라가면:

  1. unholdcurl install (hold 해제)
  2. 업그레이드 실패 — 존재하지 않는 패키지로 에러 발생
  3. rescue — hold 복원 (curl hold)
  4. always — 최종 상태 확인 → curl hold (원상복구 확인)

block에서 뭘 하다 실패하든, rescue가 원래 상태로 되돌려줍니다. K8s 업그레이드에서 이 패턴이 그대로 적용됩니다:

block:   unhold → apt upgrade kubeadm → 버전 확인
rescue:  실패 시 다시 hold + uncordon
always:  최종 상태 로깅

정리

개념 역할 활용 예시
dpkg_selections (hold) 패키지 버전 고정/해제 K8s 컴포넌트 버전 고정
failed_when 커스텀 실패 조건 정의 명령 성공이지만 출력이 기대와 다를 때
block/rescue/always try/catch/finally 에러 핸들링 업그레이드 실패 시 자동 롤백

selection 값 참고: install(일반), hold(고정), deinstall(설정 유지 삭제), purge(완전 삭제) — unhold는 없으므로 install로 해제합니다.

다음 챕터에서는 이번까지 배운 모든 내용을 종합하여, 실제 K8s 클러스터 업그레이드를 자동화하는 playbook을 구현합니다.