Skip to main content
RunBook Academy

AnsibleXVII · Templates and Jinja2Templates and Jinja2

Validate before it becomes live

Advanced⏱ ~22 minansible-playbook

What you'll learn

  • Use validate: with the correct %s placeholder on template, copy, lineinfile and blockinfile
  • State exactly what happens to the destination when validation fails
  • Choose the right validator command for nginx, sshd, haproxy and sudoers
  • Explain why validation does not run under check mode

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.

Everything in this part so far can produce a file the service cannot parse: a whitespace mistake, a missing variable, a conditional that included the wrong block. Without validation, the sequence is

render → write over the live file → notify → reload → service fails to start

and the host is now down, with the configuration that broke it in place and the previous configuration gone.

validate: changes the order so the check happens before the replacement. It is one line of YAML, it is available on template, copy, lineinfile and blockinfile — verified on 2.21.3, the parameter and its documentation text are identical across all four — and it is the most valuable line in this part.

What it does, precisely

The documented behaviour, quoted from ansible-doc ansible.builtin.template on 2.21.3:

The validation command to run before copying the updated file into the final destination.

A temporary file path is used to validate, passed in through %s which must be present as in the examples below.

Also, the command is passed securely so shell features such as expansion and pipes will not work.

Three separate facts in three sentences, each with consequences.

%s is the temporary file, and it is mandatory. Ansible substitutes the path of the staged file for %s before running the command. Omit %s and the module fails outright with validate must contain %s: <your command> — read in the 2.21.3 copy module source, which checks for the literal %s before running anything. It does not silently validate the wrong file.

The validator runs on the managed node, against the staged file, not against the destination. That is what makes the check meaningful: whatever the validator says is about the content that is about to be installed.

No shell. The command is executed directly, not through /bin/sh. Pipes, redirections, &&, globs, $VAR expansion and backticks do not work. A validator that needs any of those needs a wrapper script on the managed node, invoked as the validate command.

The real validators

Configuration changenginx
- name: Deploy the nginx site configuration
ansible.builtin.template:
  src: site.conf.j2
  dest: /etc/nginx/conf.d/site.conf
  owner: root
  group: root
  mode: '0644'
  validate: nginx -t -c %s
notify: Reload nginx
Configuration changesshd - the one where this matters most
- name: Deploy the SSH daemon configuration
ansible.builtin.template:
  src: sshd_config.j2
  dest: /etc/ssh/sshd_config
  owner: root
  group: root
  mode: '0600'
  validate: /usr/sbin/sshd -t -f %s
notify: Reload sshd
Configuration changehaproxy and sudoers
- name: Deploy the load balancer configuration
ansible.builtin.template:
  src: haproxy.cfg.j2
  dest: /etc/haproxy/haproxy.cfg
  mode: '0644'
  validate: haproxy -c -f %s
notify: Reload haproxy

- name: Deploy a sudoers drop-in
ansible.builtin.template:
  src: deploy-sudoers.j2
  dest: /etc/sudoers.d/deploy
  owner: root
  group: root
  mode: '0440'
  validate: /usr/sbin/visudo -cf %s

What happens when validation fails

The task fails. The destination is not touched. The service keeps running the configuration it already had.

The failure result carries the validator’s own output, which is usually the most useful part:

Read-only / Safethe shape of a validation failure
$ ansible-playbook -i inventories/prod site.yml --limit web01.example.com
TASK [Deploy the nginx site configuration] *************************************
fatal: [web01.example.com]: FAILED! => {
  "changed": false,
  "exit_status": 1,
  "msg": "failed to validate",
  "stderr": "nginx: [emerg] unknown directive \"proxy_pas\" in /tmp/tmpk3n8f1:14\nnginx: configuration file /tmp/tmpk3n8f1 test failed",
  "stdout": ""
}

PLAY RECAP *********************************************************************
web01.example.com          : ok=3    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

Read the recap: changed=0. Nothing was written, so nothing was notified, so no handler ran. The whole downstream chain is correctly inert.

The stderr names the temporary path rather than the destination, which looks odd the first time. It is correct — that genuinely is the file the validator examined — and the line number in it maps to the rendered output, which is what you want when debugging a template.

validate does not run under --check

This is the limitation most likely to catch you out, and it follows directly from the implementation.

In the 2.21.3 copy module, the entire replacement sequence — backup, validate, atomic move — sits inside if not module.check_mode. A check run predicts the file difference and then stops. The validator is never invoked.

So this is true and worth stating plainly: --check --diff tells you what the file will become; it does not tell you the service will accept it. A dry run of a play that would deploy a broken configuration reports a clean diff and a changed prediction, and gives no hint that the real run will fail validation.

The corollary is the canary. Run the play for real against one host with --limit, and let validation actually execute there. That is the only cheap way to find out that the rendered file is unparseable before it reaches the fleet.

Knowledge check

Knowledge check · 4 questions

  1. Q1. In validate: nginx -t -c %s, what does %s refer to?

  2. Q2. A template renders an nginx config with a typo. The task has validate: nginx -t -c %s. What is the state of the host after the run?

  3. Q3. Which statements about validate: are accurate on ansible-core 2.21? Select all that apply.

  4. Q4. Adding validate: to a role slows down every run, because the validator executes on every host each time the play runs.

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