
이전 챕터까지 task 결과를 저장하고 조건 분기하는 방법을 배웠습니다. 이번에는 **패키지 버전을 고정(hold)**하여 실수로 업그레이드되는 것을 방지하고, failed_when으로 커스텀 실패 조건을 정의하며, block/rescue/always로 에러를 구조적으로 처리하는 방법을 다룹니다.
apt upgrade를 실행하면 시스템의 모든 패키지가 최신 버전으로 올라갑니다. 하지만 Kubernetes 환경에서 kubeadm, kubelet, kubectl은 의도적으로 특정 버전을 유지해야 합니다. 실수로 버전이 올라가면 클러스터 호환성 문제가 발생할 수 있기 때문입니다.
dpkg_selections 모듈의 hold 옵션을 사용하면 특정 패키지의 업그레이드를 차단할 수 있습니다.
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를 해제할 때는 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가지뿐입니다.
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(=실패 조건 충족)라는 뜻입니다.
처음 작성할 때 따옴표 중첩을 잘못하면 다음 에러가 발생합니다:
"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'
프로그래밍의 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로 복원
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
흐름을 따라가면:
curl install (hold 해제)curl hold)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을 구현합니다.