Skip to main content
RunBook Academy

AnsibleXXV · Check Mode, Diff and Static ValidationCheck mode, diff and static validation

--diff and what it prints

Advanced⏱ ~22 minansible-playbookansible-config

What you'll learn

  • Read a --diff output and say precisely what it establishes
  • Choose between diff: false and no_log: true for a task that renders a secret
  • Name every destination a diff reaches beyond the operator terminal
  • Configure diff behaviour deliberately rather than inheriting it

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

--check tells you three files would change. --diff tells you which three lines, in which files.

That is a much larger difference than it sounds, and it is the single most useful artefact this whole part produces. A reviewer cannot audit a change count. They can audit a diff, in the same way they audit a pull request — and unlike a pull request, this diff is rendered against the actual current content of the actual target host, with the actual variables that host will get.

It also prints file contents to standard output, which is where the rest of this lesson comes from.

What a diff establishes

Read-only / Safea predicted change, in full
$ ansible-playbook -i inv.ini render.yml --check --diff --limit web01.example.com
TASK [Render the application config] *******************************************
--- before
+++ after: /etc/app/app.conf
@@ -0,0 +1,4 @@
+[database]
+host = db01.example.com
+user = appuser
+password = REPLACE_ME

changed: [web01.example.com]

The --- before with no path and @@ -0,0 mean the file does not exist yet; an existing file shows both paths and the changed hunks only.

What that output proves, precisely: the content this task would write differs from the content that is there, in these ways. It is a statement about the file. Whether the resulting configuration is correct, whether the service parses it, whether the value is the one you meant — none of that is in scope, and the previous lessons cover why.

What makes it worth running anyway is that most configuration mistakes are visible in the text. A port that should be 8443 and reads 8080, a hostname pointing at the staging database, an option removed by a template refactor that nobody noticed: all of them are in the diff, and none of them are in a change count.

The disclosure

Look at the last line of that output again.

--diff renders the file. Rendering a template resolves every variable in it, including the ones that came from a vault, an external secret store, or a lookup — because the whole point of rendering is to produce the final content. The final content contains the credential.

The two suppressions, and how they differ

Both exist. They are not interchangeable.

Read-only / Safeplain, diff: false, and no_log: true
$ ansible-playbook -i localhost, secrets.yml --check --diff -v
TASK [Plain, diff shown] *******************************************************
--- before
+++ after: /etc/app/a.conf
@@ -0,0 +1 @@
+password = REPLACE_ME

changed: [localhost] => {"changed": true}

TASK [diff false] **************************************************************
changed: [localhost] => {"changed": true}

TASK [no_log true] *************************************************************
changed: [localhost] => {"censored": "the output has been hidden due to the fact that 'no_log: true' was specified for this result", "changed": true}
diff: falseno_log: true
Suppresses the diffYesYes
Suppresses the rest of the task resultNoYes, replaced with a censored message
Suppresses the task arguments at -vvvNoYes
You can still see whether the task changed somethingYesYes — changed: true survives
Suppresses the reason a failure occurredNoYes, which is its cost
- name: Render the application configuration
  ansible.builtin.template:
    src: app.conf.j2
    dest: /etc/app/app.conf
    mode: '0640'
  diff: false
  no_log: true

Use diff: false when the file contains a secret and you still want ordinary diagnostics. The task reports success, failure, and its error message normally; only the content is withheld.

Use no_log: true when the task arguments also carry the secret. A copy with content: "{{ db_password }}", a uri with a bearer token in headers, a command with a password in argv — all of those put the secret in the module invocation, where -vvv and a failure traceback will print it regardless of diff: false.

Most template tasks that render a credential want both: diff: false because the rendered file has it, no_log: true because a failure dumps module arguments.

One host at a time

The documentation is explicit and the reason is practical:

Diff mode produces a large amount of output, so it is best used when checking a single host at a time.

ansible-playbook foo.yml --check --diff --limit foo.example.com

Two hundred hosts producing a forty-line diff each is eight thousand lines, and the one host whose diff is different from the other hundred-and-ninety-nine is invisible in it. One host is readable, and if the fleet is homogeneous it is also representative — which is what makes the canary the natural next step after it.

Where the fleet is not homogeneous, diff one host per meaningful class rather than one host in total. The classes are usually obvious from the inventory: one per environment, one per distribution version, one per role.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A template task renders a config containing a vaulted database password, and the play is run with --check --diff in CI. What has happened?

  2. Q2. A copy task uses content: "{{ db_password }}". Which statements are true? Select all that apply.

  3. Q3. Setting always = true in the [diff] section of ansible.cfg is a safe default for a shared controller.

  4. Q4. Why does the documentation recommend --diff with --limit to a single host?

Passing score: 75%. Answers are checked in this browser.