왜 멱등성이 문제가 되는가
Ansible의 핵심 가치는 "여러 번 실행해도 같은 결과"라는 멱등성이다. 그러나 실무 플레이북은 command, shell, template을 남발하면서 이 원칙이 쉽게 깨진다. 실행할 때마다 changed가 뜨는 태스크가 하나둘 쌓이면, 정작 진짜 변경이 발생했을 때 그것을 구분할 수 없다. 배포 후 "changed=3"이 나왔을 때 그것이 의도된 변경인지, 매번 발생하는 노이즈인지 판단이 불가능해지는 것이다.
특히 shell로 감싼 명령은 Ansible이 상태 변화를 알 수 없으므로 항상 changed로 보고된다. 이 노이즈는 CI/CD 파이프라인에서 드리프트(drift) 탐지를 무력화한다.
체크 모드(--check)의 동작 원리
--check는 실제 변경 없이 "이번에 실행하면 무엇이 바뀔지"만 예측하는 드라이런 모드다. --diff와 함께 쓰면 파일 단위 변경 내용까지 미리 확인할 수 있다.
ansible-playbook site.yml --check --diff
# 특정 태스크만 확인하고 싶을 때
ansible-playbook site.yml --check --diff --tags nginx
여기서 나오는 changed 개수가 0이어야 정상적으로 수렴(converged)된 상태다. 만약 아무것도 바꾸지 않은 서버에 체크 모드를 돌렸는데 changed가 나온다면, 그 태스크는 멱등하지 않다는 신호다.
check_mode를 지원하지 않는 태스크 다루기
command/shell은 기본적으로 체크 모드에서 스킵되므로, 뒤따르는 태스크가 그 결과에 의존하면 파이프라인이 깨진다. 정보 조회성 명령은 check_mode: false로 명시해 체크 모드에서도 실행되게 하고, changed_when으로 changed 판정을 직접 통제한다.
- name: 현재 버전 조회 (조회는 변경이 아님)
ansible.builtin.command: /usr/local/bin/app --version
register: app_version
check_mode: false
changed_when: false
- name: 설정 반영 여부 판단
ansible.builtin.command: /usr/local/bin/app reload
register: reload_result
changed_when: "'reloaded' in reload_result.stdout"
failed_when: reload_result.rc not in [0, 2]
changed_when: false는 조회성 태스크의 노이즈를 없애는 가장 흔한 처방이고, 조건부 changed_when은 명령 출력에 근거해 진짜 변경만 changed로 보고하게 만든다.
멱등성 자동 검증: 두 번째 실행은 항상 0이어야 한다
가장 확실한 검증은 "같은 플레이북을 두 번 연속 실행"하는 것이다. 두 번째 실행에서 changed 또는 failed가 0이 아니면 멱등성이 깨진 것이다. Molecule의 idempotence 시나리오가 이 원리를 그대로 쓴다. CI에서 간단히 구현하려면 아래처럼 JSON 콜백으로 판정할 수 있다.
#!/usr/bin/env bash
set -euo pipefail
ansible-playbook site.yml # 1차: 수렴
export ANSIBLE_STDOUT_CALLBACK=json
out=$(ansible-playbook site.yml) # 2차: 검증
changed=$(echo "$out" | python3 -c '
import sys, json
d = json.load(sys.stdin)
c = sum(h["changed"] for p in d["plays"]
for h in [s for t in p["tasks"]
for s in t["hosts"].values()])
print(c)')
if [ "$changed" -ne 0 ]; then
echo "멱등성 위반: 2차 실행에서 changed=$changed"
exit 1
fi
체크 모드 vs 실제 실행 비교
| 항목 | --check | 실제 실행 |
|---|---|---|
| 변경 발생 | 없음(예측만) | 있음 |
| command/shell | 기본 스킵 | 실행 |
| register 값 | 비어있을 수 있음 | 정상 채워짐 |
| 주 용도 | 드리프트 탐지, 사전 검토 | 실제 배포 |
주의점
첫째, 체크 모드는 만능이 아니다. 이전 태스크가 실제로 실행돼야 존재하는 파일이나 패키지에 의존하는 후속 태스크는, 체크 모드에서 그 선행 변경이 적용되지 않아 오탐(false changed)이나 실패를 낸다. 이런 경우 오탐을 멱등성 위반과 혼동하지 말아야 한다.
둘째, check_mode: false는 신중히 써야 한다. 조회성 명령에만 한정하고, 실제 상태를 바꾸는 명령에 붙이면 체크 모드의 안전성이 무너진다. 셋째, template의 줄바꿈이나 권한 차이처럼 사소한 요소도 매번 changed를 유발하니, --diff로 원인을 정확히 짚고 mode·lstrip_blocks 등을 고정하는 것이 좋다.
결국 멱등성은 선언형 모듈을 우선 사용하고, 불가피한 명령형 태스크는 changed_when/failed_when으로 상태 판정을 명시하며, 2차 실행 검증을 CI에 못박는 세 가지로 지켜진다.