Ansible 기초 · 멱등성과 핸들러 · 이론
멱등성 — 두 번 돌려도 아무 일이 없어야 한다
한 줄 요약
플레이북의 품질은 첫 실행이 성공하는 것이 아니라 두 번째 실행에서 changed=0 이 나오는 것으로 판정한다.
왜 이게 필요했나
구성 관리 도구의 진짜 가치는 "설치를 자동화한다"가 아니라 "현재 상태가 원하는 상태인지 언제든 확인하고 맞출 수 있다" 이다. 그러려면 아무 때나 돌려도 안전해야 한다. 그래야 크론으로 주기적으로 돌려 드리프트를 잡을 수 있고, 배포 파이프라인에서 앞 단계 실패 후 재실행해도 무섭지 않다.
반대로 멱등하지 않은 플레이북은 실행할 때마다 서비스를 재시작하거나 파일에 줄을 덧붙인다. 그러면 사람들은 그것을 무서워하게 되고, 무서운 자동화는 결국 쓰이지 않는다. 그 순간 인프라는 다시 수작업으로 돌아간다.
어떻게 동작하나
멱등성을 만드는 것은 세 가지 장치다.
첫째, 상태를 아는 모듈. file, copy, template, lineinfile 같은 모듈은 현재 상태를 읽고 다른 부분만 바꾼다. 바꿀 것이 없으면 ok 를 낸다.
둘째, 셸 명령의 가드. command/shell 은 스스로 멱등할 수 없으므로 조건을 붙여야 한다.
| 장치 | 의미 |
| --- | --- |
| creates: /path | 그 경로가 이미 있으면 실행하지 않음 |
| removes: /path | 그 경로가 없으면 실행하지 않음 |
| changed_when: <조건> | 언제 "바꿨다"고 보고할지 직접 정의 |
| failed_when: <조건> | 종료 코드 대신 실패 판정을 직접 정의 |
changed_when: false 는 특히 자주 쓰인다. 상태를 조회만 하는 명령(예: 버전 확인)은 결코 변경이 아니기 때문이다.
셋째, 핸들러. 핸들러는 "무언가 실제로 바뀌었을 때만" 도는 태스크다. 설정 파일이 바뀌었을 때만 서비스를 다시 읽게 하는 용도로 쓴다.
tasks: - name: 설정 배치 ansible.builtin.template: src: app.conf.j2 dest: /etc/app/app.conf notify: reload apphandlers: - name: reload app ansible.builtin.command: /usr/bin/app-reload핵심 규칙 세 가지. (1) notify 는 그 태스크가 changed 일 때만 발화한다. (2) 여러 태스크가 같은 핸들러를 부르더라도 핸들러는 한 번만 돈다. (3) 핸들러는 기본적으로 플레이의 모든 태스크가 끝난 뒤 실행된다. 중간에 꼭 실행해야 한다면 meta: flush_handlers 를 쓴다.
세 번째 규칙에서 함정이 나온다. 플레이가 중간에 실패하면 아직 실행되지 않은 핸들러는 그냥 사라진다. 설정은 바뀌었는데 서비스는 새 설정을 읽지 않은 상태가 되는 것이다. 이럴 때는 --force-handlers 로 실패해도 핸들러를 돌리게 할 수 있다.
현장에서 만나는 모습
첫째, "매번 재시작되는 배포". 원인은 거의 언제나 템플릿의 렌더링 결과가 매번 달라지기 때문이다. 타임스탬프나 랜덤 값, 순서가 보장되지 않는 딕셔너리 순회가 범인이다. 렌더링 결과가 같으면 template 모듈은 ok 를 내고 핸들러도 조용하다.
둘째, 진짜 검증 방법. CI 에서 플레이북을 두 번 연속 실행하고 두 번째 실행의 changed 와 failed 가 모두 0 인지 확인하는 것이 표준적인 멱등성 테스트다. 사람이 눈으로 보는 대신 종료 코드로 판정하게 만들어야 한다.
셋째, 멱등성과 드리프트. 멱등한 플레이북은 그 자체가 드리프트 감지기다. 손으로 고쳐 놓은 서버에 돌리면 changed 가 나오고, 그 숫자가 곧 "코드와 어긋난 정도"다. 이 성질은 Terraform 의 plan 과 완전히 같은 아이디어다.
다음 실습에서 할 것
같은 플레이북을 두 번 돌려 첫 실행에는 변경이 있고 두 번째에는 changed=0 이 나오게 만든다. 핸들러가 첫 실행에만 발화하는 것을 확인하고, 셸 명령에 가드를 붙여 두 번 실행해도 로그가 한 줄만 쌓이게 만든다. 마지막에는 멱등성 판정 보고서를 만든다.