Skip to main content
RunBook Academy

AnsibleXVII · Templates and Jinja2Templates and Jinja2

The template module in production

Intermediate⏱ ~19 minansible-playbookansible-doc

What you'll learn

  • Treat mode, owner and group as part of the desired state rather than an afterthought
  • Read the template module attributes to know what check mode and diff mode support
  • Choose when backup and force are worth setting
  • Explain why the replacement is atomic and what that protects against

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.

ansible-doc ansible.builtin.template lists around two dozen parameters. About six of them decide whether the task is safe to run in production. This lesson is those six, plus the attributes block, which tells you what the module promises about check mode and diff mode and is the part almost nobody reads.

The production shape

Configuration changeevery parameter here is doing work
- name: Deploy the application configuration
ansible.builtin.template:
  src: billing.conf.j2
  dest: /etc/billing/billing.conf
  owner: root
  group: billing
  mode: '0640'
  backup: true
  validate: /usr/local/bin/billing-check --config %s
notify: billing config changed

mode, owner, group are the desired state, not decoration

A configuration file has permissions, and those permissions are part of what the file is. Leaving them off does not mean “keep whatever is there” — it means something more subtle and worse.

From the module documentation, verified on 2.21.3:

If mode is not specified and the destination filesystem object does not exist, the default umask on the system will be used when setting the mode for the newly created filesystem object.

If mode is not specified and the destination filesystem object does exist, the mode of the existing filesystem object will be used.

Read those two sentences together and the failure mode falls out. On a host where the file already exists, the existing mode is preserved — including a wrong one. On a host where it does not, the mode comes from the remote user’s umask, which is an environment property you do not control and which can differ between hosts in the same fleet.

So a task with no mode produces a file whose permissions depend on whether the host had been configured before, and the fleet ends up with two populations. That is not a hypothetical: the documentation points at CVE-2020-1736 for the case where this created world-readable files containing secrets.

The attributes block: what --check and --diff actually promise

Every module documents what it supports. For template on 2.21.3:

Read-only / Safethe attributes that matter
$ ansible-doc ansible.builtin.template
ATTRIBUTES:

      action:
      description: Indicates this has a corresponding action plugin so some parts of the
        options can be executed on the controller
      support: full

      check_mode:
      description: Can run in check_mode and return changed status prediction without modifying
        target, if not supported the action will be skipped.
      support: full

      diff_mode:
      description: Will return details on what has changed (or possibly needs changing in
        check_mode), when in diff mode
      support: full

      safe_file_operations:
      description: Uses Ansible's strict file operation functions to ensure proper permissions
        and avoid data corruption
      support: full

      vault:
      description: Can automatically decrypt Ansible vaulted files
      support: full

Four full supports, each with an operational consequence:

check_mode: full--check genuinely predicts whether the file would change, rather than skipping the task. This is the difference between a dry run that tells you something and one that tells you nothing. Contrast command and shell, which have no check-mode support and are simply skipped, making a --check of a shell-based play worthless.

diff_mode: full--diff shows the line-by-line difference between what is on the host and what would be written. Combined with --check, that is a review of the actual change, per host, before anything is touched. It is the single most useful habit in this part.

safe_file_operations: full — the replacement is atomic. A reader of the file sees either the old content or the new content, never a partial write, and never a zero-length file because the transfer was interrupted.

vault: full — a template source that is itself vault-encrypted is decrypted transparently.

backup, force, newline_sequence

backup: true writes a timestamped copy of the previous file next to the original before replacing it. Verified defaults on 2.21.3: backup is false.

It costs a file per change and buys a fast local rollback. Worth it on files where a bad change is expensive and the previous content is not trivially reproducible. Two caveats: the backups accumulate and nothing cleans them up, and — a genuine gotcha covered in the validate lesson — the backup is taken before validation, so a validation failure can leave a backup behind despite the destination being unchanged.

force: false (the default is true) changes the module from “maintain this file” to “create this file if it does not exist”. Every subsequent run leaves an existing file alone regardless of content.

That is the right choice for a file the machine or an operator is expected to own after first boot, and it is a trap everywhere else: with force: false the task reports ok forever, drift is invisible, and the configuration in Git has no relationship to the configuration on the host. If you find yourself reaching for it to stop Ansible fighting someone else over a file, the answer is usually a drop-in file instead — which is lesson 7 of this part.

newline_sequence defaults to \n, with \r and \r\n as the other choices. Relevant when generating a file consumed by something that requires CRLF. Rarely needed on Linux, and worth knowing exists before you spend an hour on a parser that rejects your file.

Read-only / Safereview the change before making it
ansible-playbook -i inventories/prod site.yml \
--limit webservers \
--check --diff

Knowledge check

Knowledge check · 4 questions

  1. Q1. A template task omits mode. Half the fleet already had the file; half did not. What are the resulting permissions?

  2. Q2. What does check_mode: full on the template module guarantee, and what does it not?

  3. Q3. Which statements about the template module are accurate? Select all that apply.

  4. Q4. Running a play with --diff is safe regardless of what the templates contain, because check mode makes no changes.

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