SSD Nodes Learn Hosting plans →
가이드 Matt Connor작성자 Matt Connor · 업데이트됨 2026-08-21

Ansible check mode와 --diff 사용법 및 주의사항

Ansible의 --check와 --diff 플래그를 사용하여 실제 변경 사항을 예측하는 방법을 설명합니다. 특히 check mode를 지원하지 않는 모듈에서 발생할 수 있는 오작동과 모의 실행 시 주의해야 할 핵심 사항을 정리했습니다.

Ansible check mode의 역할

Ansible check mode는 모의 실행(dry run)입니다. ansible-playbook --check은 플레이에 포함된 모든 호스트에 연결하여, 각 모듈에 현재 상태가 요청한 상태와 일치하는지 확인하고, 실제로 아무것도 변경하지 않은 채 무엇이 바뀔지 보고합니다. --diff을 추가하면 변경될 파일의 수정 전후 내용도 출력합니다. 이 두 가지를 함께 사용하면 실제 실행 전 반드시 확인해야 할 질문, 즉 "이 서버들에서 무엇이 변경될 것인가?"에 대한 답을 얻을 수 있습니다.

check mode는 플레이북을 시뮬레이션하는 것이 아닙니다. 서버에 대한 모델은 어디에도 존재하지 않습니다. 각 모듈은 단순히 쓰기 대신 읽기만 수행하도록 요청받습니다. 읽기 전용으로 응답할 수 있는 모듈은 changed를 보고하고 다음 단계로 넘어갑니다. 응답할 수 없는 모듈은 아무것도 하지 않으며 아무것도 보고하지 않습니다. Ansible 문서에서는 이를 한 줄로 요약합니다. "check mode를 지원하지 않는 모듈은 아무것도 보고하지 않으며 아무것도 하지 않는다." 이 간극이 바로 모의 실행이 잘못된 결과를 줄 수 있는 지점이며, 이 가이드의 대부분은 바로 이 간극을 다룹니다.

드라이 런 실행: --check 및 --diff

ansible-playbook -i inventory.ini site.yml --check --diff --limit web1

-C-D은 두 플래그의 단축형입니다. --limit는 의도된 것입니다. 호스트 한 대의 diff는 읽을 수 있지만, 호스트 스무 대의 diff는 그냥 지나치게 됩니다.

네 가지 결과 단어가 전체 보고서의 핵심을 전달합니다.

  • ok: [web1]은 모듈이 상태를 확인했으며 이미 일치함을 의미합니다. 아무것도 변경되지 않습니다.
  • changed: [web1]는 모듈이 무언가를 기록했을 것임을 의미합니다. --diff와 함께 사용하면 그 위의 줄들이 무엇이 변경될지 보여줍니다.
  • skipping: [web1]은 작업이 평가되지 않았음을 의미합니다. when이 거짓이거나, 모듈이 체크 모드에서 실행될 수 없는 경우입니다.
  • fatal: [web1]은 확인 도중 작업이 실패했음을 의미합니다. 플레이북이 고장 났다고 단정하기 전에 메시지를 먼저 읽으십시오.

--diff는 파일 모듈에 대해 통합 diff를 출력하며, 제거된 줄은 -으로, 추가된 줄은 +로 표시합니다. 헤더의 줄은 --- before+++ after으로 시작하며 대상 경로를 명시합니다. 파일을 기록하지 않는 모듈은 자체적인 이전/이후 상태를 출력하므로, ansible.builtin.user는 파일 내용 대신 변경될 속성을 보여줍니다.

플래그를 잊지 않도록 ansible.cfg에서 diff를 영구적으로 활성화하십시오:

[diff]
always = true
context = 5

체크 모드 이전에 수행할 수 있는 더 가벼운 두 가지 확인 방법이 있습니다. ansible-playbook site.yml --syntax-check은 호스트에 접속하지 않고 YAML과 플레이 구조를 구문 분석합니다. ansible-playbook site.yml --list-tasks은 실행될 작업을 출력하며, 이를 통해 태그가 지정되었다고 생각한 역할이 실제로는 그렇지 않음을 확인할 수 있습니다. 두 방법 모두 연결을 시도하지 않으므로 즉시 완료됩니다.

체크 모드 자체는 실제로 연결을 수행합니다. 패턴에 포함된 모든 호스트에 SSH로 접속하여 팩트를 수집하므로, 다운된 호스트가 있으면 드라이 런은 실패합니다. 이는 그 자체로 유용한 신호이며, CI에 드라이 런을 도입하기 전에 연결할 수 없는 호스트에 대해 플레이북이 어떻게 동작할지 결정하는 것이 중요한 이유이기도 합니다.

새 서버에서 체크 모드가 실패하는 이유

이 플레이북은 올바르게 작성되었습니다. 하지만 nginx가 아직 설치되지 않은 서버에 --check을 실행하면 대부분의 작업이 실패합니다.

- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present

- name: Write the site config
  ansible.builtin.template:
    src: site.conf.j2
    dest: /etc/nginx/conf.d/site.conf

- name: Start and enable nginx
  ansible.builtin.service:
    name: nginx
    state: started
    enabled: true

apt 작업은 changed을 보고하는데, 이는 정상입니다. 패키지가 없으므로 실제 실행 시에는 설치가 진행되어야 하기 때문입니다. 체크 모드에서는 설치가 수행되지 않습니다. 그 결과 template 작업은 실패합니다. 해당 호스트에 /etc/nginx/conf.d/가 존재하지 않고, 이를 생성할 작업도 수행되지 않았기 때문입니다. service 작업 역시 쿼리할 nginx 유닛이 없으므로 실패합니다. 이러한 실패는 플레이북의 버그가 아닙니다. 드라이 런(dry run)은 필요한 상태를 확보하지 못한 채 실행되었으며, 이는 체크 모드가 이전 작업의 변경 사항에 의존하는 작업에 대해서는 유용한 결과를 생성할 수 없다고 경고하는 문서의 의미와 같습니다.

따라서 이 규칙을 솔직하게 표현하자면 다음과 같습니다. 체크 모드는 이미 컨버전스(converged)가 완료된 호스트에서는 정확하지만, 새로운 호스트에서는 불필요한 오류 메시지를 많이 출력합니다. 모든 작업이 ok을 보고하는 --check 실행 결과는 컨버전스가 완료된 호스트에 대해서는 유효한 상태를 의미합니다. 즉, 아무것도 변경되지 않을 것임을 뜻합니다. 완전히 새로운 호스트에서 --check은 대부분 호스트가 새로 설치되었다는 사실만을 알려줄 뿐입니다. VPS를 대상으로 첫 Ansible 플레이북을 작성할 때는 첫 번째 드라이 런에서 수많은 오류가 발생할 것을 예상하고, 플레이북의 완성도는 두 번째 실행 결과를 기준으로 판단하십시오.

체크 모드에서 command 및 shell 작업이 건너뛰어지는 이유

ansible.builtin.commandansible.builtin.shell는 사용자가 실행하는 명령의 내용을 알 수 없습니다. 임의의 바이너리를 실행할 때 읽기 전용으로 동작하게 할 방법이 없으므로, 체크 모드에서는 해당 모듈이 실행을 거부합니다. 작업 결과에는 skipped: true이 포함되며 메시지는 Command would have run if not in check mode로 표시되고, 출력 결과에는 skipping: [web1]가 나타납니다.

모듈 문서에서는 이러한 체크 모드 지원을 "부분적(partial)"이라고 부르며, 해결책으로 createsremoves를 제시합니다. 작업에 creates 경로를 지정하면 체크 모드에서도 최소한 파일 테스트를 평가할 수 있습니다.

- name: Extract the release bundle
  ansible.builtin.command: /usr/bin/tar xf /tmp/app.tar.gz -C /opt/app
  args:
    creates: /opt/app/bin/app

/opt/app/bin/app이 이미 존재하면 체크 모드는 Would not run command since '/opt/app/bin/app' exists을 보고하는데, 이는 올바른 결과입니다. 경로가 없으면 Command would have run if not in check mode을 반환하며, 이 역시 올바른 결과입니다. creates가 없으면 해당 작업은 드라이 런(dry run)에서 아무런 정보도 제공하지 않는 빈 공간이 됩니다.

이로 인한 연쇄 효과는 단순히 빈 공간이 생기는 것보다 더 심각합니다. 건너뛴 작업도 결과는 기록되지만, 이는 건너뛰었다는 결과일 뿐이며 stdout 키를 포함하지 않습니다. 따라서 다음 작업의 조건을 평가할 때 'dict object' has no attribute 'stdout'과 유사한 오류가 발생하며 실패하게 됩니다. 실제 실행 시에는 잘 작동하던 플레이북이 드라이 런에서만 실패하는 상황이 발생하며, 이는 이 기능에서 가장 혼란스러운 오류 유형입니다.

check_mode: false 설정과 그 적절한 사용처

check_mode: false를 작업에 사용하면 --check 모드에서도 해당 작업을 실제로 실행합니다. 이는 명령이 건너뛰어지는 문제를 해결하는 방법이며, 읽기 전용 작업에서만 안전하게 사용할 수 있습니다.

- name: Read the installed app version
  ansible.builtin.command: /usr/local/bin/app --version
  register: app_version
  check_mode: false
  changed_when: false

해당 작업은 두 모드 모두에서 정직하게 동작합니다. 버전 정보를 읽기만 할 뿐 쓰기 작업을 수행하지 않으므로, changed_when: false를 사용하면 수행하지 않은 변경 사항이 보고되는 것을 방지할 수 있습니다. 또한 check_mode: false를 사용하면 드라이 런(dry run) 중에도 app_version.stdout이 존재하게 되어, 이를 기반으로 하는 조건문이 정상적으로 평가됩니다.

이 키워드를 다른 곳에 붙여넣기 전에 그 의미를 문자 그대로 이해해야 합니다. check_mode: false이 포함된 작업은 ansible-playbook --check 중에도 서버에 쓰기 작업을 수행합니다. 이를 apt 작업이나 template 작업에 사용하면 드라이 런 결과는 깔끔해 보일 수 있으나, 더 이상 드라이 런이 아니게 됩니다. 쓰기 작업을 안전하게 만들 수 없다면 대신 가드(guard)를 사용하십시오.

- name: Apply the database migration
  ansible.builtin.command: /usr/local/bin/app migrate --apply
  when: not ansible_check_mode

ansible_check_mode은 Ansible이 체크 모드 실행 중에 true로 설정하는 매직 변수입니다. 반대되는 키워드도 존재합니다. check_mode: true은 실제 실행 중에도 작업을 항상 체크 모드로 고정하며, 이를 통해 드리프트 탐지기(drift probe)로 활용할 수 있습니다. 결과를 등록하고 changed가 보고된다면, 해당 호스트가 작업에서 정의한 상태와 더 이상 일치하지 않음을 의미합니다.

작업이 매번 변경됨(changed)으로 보고되는 이유

플레이북을 아무런 변경 없이 연속으로 두 번 실행하십시오. 두 번째 실행 시 모든 작업은 ok 상태여야 합니다. 여전히 changed으로 보고되는 작업이 있다면, 모듈이 관리하는 상태를 파악할 수 없거나 입력값이 불안정하다는 의미입니다. 두 경우 모두 수정이 가능하며, 무시해야 할 단순한 소음이 아닙니다.

  • creates, removes 또는 changed_when 없이 commandshell을 사용하면 모듈이 변경 여부를 확인할 방법이 없으므로 매번 changed로 보고합니다. creates을 추가하거나 출력 결과의 문자열을 기준으로 changed_when를 설정하십시오.
  • state: touch을 포함한 ansible.builtin.file는 설계상 매번 changed을 보고합니다. 파일을 터치(touch)하면 타임스탬프가 갱신되기 때문입니다. 소유자나 권한만 설정하려는 경우라면 state: file을 사용하십시오.
  • 렌더링된 출력값이 계속 변하는 template는 매번 파일을 다시 씁니다. ansible_date_time의 타임스탬프, now() 호출, 또는 매번 새로 생성되는 비밀번호는 모두 서로 다른 바이트를 생성하므로 모듈은 변경 사항이 있다고 올바르게 보고합니다. 변하는 값을 템플릿에서 제거하십시오.
  • password: "{{ pw | password_hash('sha512') }}"을 사용한 ansible.builtin.user는 매번 변경됩니다. password_hash는 호출될 때마다 무작위 솔트(salt)를 선택하므로, 결과 해시값이 /etc/shadow에 이미 저장된 값과 일치하지 않기 때문입니다. 안정적인 값에서 파생된 명시적인 솔트를 전달하십시오.
  • 패키지 모듈의 state: latest은 업그레이드가 가능할 때마다 changed을 보고합니다. 이는 정직한 동작입니다. 또한 state: latest을 사용하면 결과가 예측 불가능한 플레이북이 되는 이유이기도 합니다. state: present를 사용하고 의도적으로 업그레이드를 수행하십시오.
  • creates 없이 URL을 지정한 ansible.builtin.unarchive은 매번 다시 가져와서 압축을 풉니다. creates 경로를 제공하십시오.

--diff은 이러한 차이를 구분하는 가장 빠른 방법입니다. 작업 결과가 changed이고 diff에서 바이트 차이가 보인다면 입력값이 불안정한 것입니다. 반면 changed라고 표시되는데 diff에 아무것도 나타나지 않는다면, 모듈이 변경 내용을 표현할 수 없는 상태이며, 이는 보통 command 작업이거나 타임스탬프와 같은 메타데이터 전용 쓰기 작업일 가능성이 높습니다.

시끄러운 작업을 조용하게 만들려고 changed_when: false을 사용하지 마십시오. 이 옵션은 보고를 억제하므로 notify이 발생하지 않으며, 결과적으로 서비스를 재시작하는 핸들러도 실행되지 않습니다. 대신 작업을 수정하십시오.

영향 범위 축소: --limit, --tags 및 --step

Check mode는 어떤 변경이 발생할지 알려줍니다. 다음 플래그들은 한 번에 얼마나 많은 머신에 작업을 적용할지 결정합니다.

--limit는 인벤토리의 일부로 플레이 범위를 좁힙니다. hosts:과 동일한 패턴을 사용하므로 --limit web1--limit 'webservers:!web3' 모두 작동합니다. 패턴은 따옴표로 묶으십시오. 대화형 bash 세션에서 따옴표를 사용하지 않은 !느낌표에 대한 히스토리 확장을 유발하며, Ansible이 명령을 전달받기 전에 셸이 명령을 다시 작성해 버립니다.

패턴을 신뢰하기 전에 확인하십시오. ansible-playbook site.yml --limit 'webservers:!web3' --list-hosts는 일치하는 호스트를 출력하고 어떤 호스트에도 연결하지 않은 채 종료합니다. 아무것도 일치하지 않는 패턴은 안전합니다. Ansible은 전체 인벤토리로 대체하지 않기 때문입니다. 호스트 패턴과 일치하는 항목을 찾을 수 없다는 경고를 출력한 뒤, 호스트와 --limit가 어떤 호스트와도 일치하지 않는다는 오류와 함께 종료됩니다. 인벤토리 파일이 그룹을 정의하는 방식을 이해하는 것이 패턴을 예측 가능하게 만드는 첫걸음입니다.

--tags deploy은 태그가 지정된 작업만 실행하며, --skip-tags packages은 그 외의 모든 작업을 실행합니다. --list-tags은 사용 가능한 태그를 출력합니다. 플레이가 전체를 실행하기 부담스러울 정도로 커지면 태그의 진가가 드러나며, 이는 긴 플레이북을 역할(role) 단위로 분리해야 하는 이유 중 하나이기도 합니다.

--start-at-task "Write the site config"는 실패한 실행을 특정 작업부터 재개합니다. 복구 목적으로 사용하되 그 비용을 이해해야 합니다. 해당 작업 이전의 모든 과정은 건너뛰며, 여기에는 팩트를 설정하거나 이후 작업이 참조할 변수를 등록하는 작업도 포함됩니다.

--step은 각 작업마다 프롬프트를 띄우고 yes, no, continue 중 하나를 입력할 때까지 대기합니다. 속도는 느리지만, 파괴적인 작업을 처음 수행할 때는 올바른 도구입니다. 20개의 작업이 끝난 뒤가 아니라 작업과 작업 사이에서 멈출 수 있기 때문입니다.

serial을 사용한 변경 사항 배포

기본적으로 Ansible은 다음 태스크를 시작하기 전에 플레이에 포함된 모든 호스트를 대상으로 하나의 태스크를 실행합니다. 이는 속도는 빠르지만, 잘못된 태스크가 순식간에 전체 서버군으로 확산된다는 의미이기도 합니다. 오류를 확인하고 Ctrl-C를 누를 때쯤이면 이미 모든 곳에 변경 사항이 적용된 상태입니다.

serial은 플레이를 여러 배치로 나눕니다. 첫 번째 배치에 대해 전체 플레이가 실행된 후, 다음 배치로 넘어갑니다.

- name: Roll out the web tier
  hosts: webservers
  serial: [1, 5, "30%"]
  max_fail_percentage: 0
  tasks:
    - name: Deploy the release
      ansible.builtin.include_role:
        name: webapp

첫 번째 배치는 1대의 호스트로 구성됩니다. 이 배치가 성공하면 두 번째 배치는 5대가 되며, 그 이후의 모든 배치는 전체 호스트의 30퍼센트씩으로 구성됩니다. max_fail_percentage: 0는 배치 내의 호스트 중 하나라도 실패하면 즉시 플레이를 종료하므로, 잘못된 릴리스가 단 한 대의 장비에서 멈추게 됩니다. any_errors_fatal: true은 더 강력한 방식으로, 첫 번째 호스트에서 실패가 발생하면 즉시 전체 플레이를 중단합니다.

먼저 1대의 호스트에 대해 실행하는 것은 편집증적인 행동이 아니며, 그 이유는 명확합니다. 인벤토리 그룹은 시간이 지나면서 구성이 달라질 수 있습니다. 다른 서버들보다 6개월 늦게 추가된 서버는 다른 배포판 릴리스를 실행 중이거나, 누군가 수동으로 설치한 서비스가 있거나, 디스크 구성이 다를 수 있습니다. 플레이북은 해당 그룹에 대해서는 올바르지만 그 특정 호스트에는 맞지 않을 수 있으며, 이미 구성이 완료된(converged) 호스트를 대상으로 하는 dry run으로는 이를 발견할 수 없습니다. Linux 서버군 관리는 결국 변경 사항이 문제를 일으키기 전에 예외적인 호스트를 찾아내는 과정입니다.

실행 순서

  1. ansible-playbook site.yml --syntax-check는 네트워크 연결 없이도 YAML 및 구조적 오류를 잡아냅니다.
  2. ansible-playbook site.yml --limit web1 --list-hosts는 패턴이 의도한 대로 일치하는지 확인합니다.
  3. ansible-playbook site.yml --limit web1 --check --diff은 모의 실행(dry run)입니다. 변경 사항(diff)을 확인하십시오.
  4. ansible-playbook site.yml --limit web1 --diff은 해당 호스트에 설정을 적용합니다.
  5. 4단계를 다시 실행하십시오. 모든 항목이 ok 상태여야 합니다. 여전히 changed인 항목은 전체 인프라에 적용하기 전에 수정해야 할 작업입니다.
  6. 이제 전체 인벤토리에 대해 ansible-playbook site.yml --check --diff을 실행하면 의미 있는 결과를 얻을 수 있습니다. 수렴된 호스트는 조용해지고 남은 항목이 실제 변경분(delta)이기 때문입니다.

3단계와 관련하여 한 가지 주의할 점이 있습니다. --diff은 파일 내용을 터미널과 CI 작업 로그에 출력하므로, 데이터베이스 비밀번호를 렌더링하는 템플릿은 해당 비밀번호를 로그에 그대로 노출합니다. 해당 작업에 diff: false를 설정하여 출력을 억제하거나, no_log: true을 사용하여 결과 전체를 숨기십시오. 또한 값 자체는 저장소가 아닌 암호화된 Ansible Vault 파일에 보관하십시오.

FAQ

Does ansible-playbook --check change anything on the server?

No, with one exception that you control. In check mode every module is asked to report instead of write, and modules that cannot do that report nothing and do nothing. The exception is the check_mode: false task keyword, which forces that single task to execute for real even during a --check run. Search your playbooks and roles for check_mode: false before you trust a dry run, and confirm every match is a task that only reads state.

What is the difference between --check and --diff?

--check decides whether anything runs for real. --diff decides how much detail you see. --check on its own tells you that a file would change. --diff on its own applies the change and shows you the lines it changed. Use them together for a dry run you can actually read, and leave --diff on for real runs too by setting always = true under [diff] in ansible.cfg.

Why does my Ansible task report changed on every run?

Because the module cannot see the state it manages, or the value you hand it is different each time. command and shell report changed always unless you add creates or changed_when. file with state: touch changes by design. A template that renders a timestamp or a freshly generated password produces different bytes each run, so the file really is being rewritten. Run the playbook twice in a row: anything still changed on the second pass is the task to fix.

Why are my command and shell tasks skipped during a dry run?

Because there is no read-only way to run an arbitrary command. In check mode the command module sets skipped: true with the message Command would have run if not in check mode. Add creates or removes so check mode can evaluate the file test instead. For a task that only reads state, set check_mode: false together with changed_when: false, so the registered result still exists during the dry run and the conditions built on it keep working.

Why does check mode fail on a new server but pass on an existing one?

Because check mode does not create the state that later tasks depend on. A dry run against a host without nginx reports the install as changed, then fails on the task that writes into /etc/nginx/conf.d/, because that directory was never created. This is expected behaviour. Check mode is a drift detector for hosts the playbook has already converged. It cannot validate a first run. On a new host, apply the playbook to one machine and read the second run instead.

#ansible#check-mode#idempotency#automation#safety