Chapter 3: Modules & Ad-hoc Commands
Topic 15

Essential Modules

Modules

A handful of modules cover most of what configuring a server actually requires, and every one of them is idempotent against live state: install a package, run a service, place a file, set ownership, create a user, edit one line in a config. Learning this toolbox against the Larkspur stack is most of what you need before roles and templates enter the picture.

The mapping is concrete — apt for nginx, service for gunicorn and PostgreSQL, copy and template for config, lineinfile for a single tuning setting. Each module names a category of work, and knowing which one owns which job is the difference between a clean play and a pile of shell commands pretending to be one.

The everyday module families — each idempotent against live state
package
apt on Ubuntu, dnf on RHEL — installs, removes, and pins packages.
service
Starts, stops, and enables units idempotently (a restart belongs in a handler); systemd for a daemon_reload.
file & copy
Places files and sets owner, group, mode; template renders first.
user
Creates and manages accounts and their group membership.

package and apt

The apt module installs, removes, and pins packages on the Ubuntu 22.04 nodes: name=nginx state=present, with update_cache=yes to refresh the index first. The package module is the OS-agnostic wrapper that dispatches to apt or dnf by fact, useful when one role targets mixed distros.

On a known-Ubuntu fleet, apt is the explicit choice — you gain nothing by hiding behind the generic wrapper when every host runs the same package manager. Reach for package only when a single role genuinely has to span distributions.

service and systemd

The service module starts, stops, and enables units idempotently: name=gunicorn state=started enabled=yes reports changed only when it actually had to act. state=restarted is the exception — it bounces the service on every run and always reports changed, which is why a restart belongs in a handler fired by a real change, not in a routine task. The systemd module is the systemd-specific variant for when you need daemon_reload after dropping a new unit file — something service alone will not do.

Set both state and enabled on every long-running service. state=started means running now; enabled=yes means surviving a reboot. Setting one without the other gives you gunicorn that is enabled but not running, or running but gone after the next restart.

copy and template

The copy module places a static file with declared owner, group, and mode. The template module renders a Jinja2 file first — the nginx larkspur.conf, the app's app.env — then places it the same way. Both report changed only when the destination content or attributes actually differ, which is the idempotency contract from the last topic applied to files.

The task below installs and configures the web tier in one pass: apt for the package, template for the rendered config, service to run it. Every module is idempotent, so a second run reports all ok unless a fact behind the template changed.

Three idempotent modules configuring the Larkspur web tier
- hosts: web
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present
        update_cache: true

    - name: Render the site config
      ansible.builtin.template:
        src: larkspur.conf.j2
        dest: /etc/nginx/sites-available/larkspur.conf
        owner: root
        group: root
        mode: "0644"

    - name: Ensure nginx is running and enabled
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: true

Nothing in those tasks describes how to install a package or render a file — they declare the destination and let each module reconcile against whatever the host currently looks like. That is the toolbox doing its job.

file

The file module manages a path's existence and attributes without touching content: create a directory (/opt/larkspur, state=directory), set mode and owner on an existing path, make a symlink, or remove a path with state=absent. It is the module for "this directory must exist with these permissions."

Because it carries no content, file pairs naturally with copy and template: file ensures the directory and ownership are right, then template drops the rendered config into it. Each owns one concern and stays idempotent.

user and group

The user module creates the service account the app runs as — name=larkspur system=yes shell=/usr/sbin/nologin — sets its groups, and manages SSH keys via authorized_key. It is idempotent, so it is safe in every run and converges a drifted account back to the declared state rather than erroring because the user already exists.

A service account created this way is reproducible: rebuild web2.larkspur.io from the same play and the larkspur user comes back identical, no login shell, same groups. That is the difference between a managed account and one someone useradd-ed by hand during an incident.

lineinfile and blockinfile

For surgical edits to a file Ansible does not own wholesale, lineinfile ensures one line matches a regex — a single sysctl entry or a pg_hba.conf line — and blockinfile manages a delimited block. Both are the fallback for files you cannot template in full, and both are easy to overuse where template would be cleaner.

Reach for full-file management first. When Ansible controls the file end to end, template or copy rewrites it to a declared state in one pass; reserve lineinfile for a single setting inside a vendor-managed config you genuinely manage only part of. Building a whole file line by line across many lineinfile tasks produces a brittle, ordering-dependent mess.

Common Mistakes
  • Using apt without update_cache=yes on a fresh node and installing a stale package version, or failing outright because the package index was never refreshed.
  • Dropping a new systemd unit with copy and starting it with service but skipping daemon_reload, so systemd runs the old unit definition until something forces a reload — use the systemd module's daemon_reload=yes.
  • Setting enabled=yes but not state=started, or the reverse, and being surprised that gunicorn is enabled-but-not-running now, or running-but-won't-survive-a-reboot.
  • Reaching for lineinfile to build up a whole config line by line across many tasks, producing a brittle, ordering-dependent file that template would render correctly in one pass.
  • Omitting owner, group, or mode on copy/template for app.env and leaving a secret-bearing file world-readable, because the module placed it with whatever default the umask gave.
Best Practices
  • Use template for any config with a variable in it — the nginx larkspur.conf, app.env — and copy only for genuinely static files, so the content is declared, not hand-assembled.
  • Pair service with explicit state=started enabled=yes on every long-running service so it is both running now and persistent across a reboot.
  • Set owner, group, and mode explicitly on every copy/template/file, especially for secret-bearing files like app.env, rather than relying on the node's umask.
  • Prefer full-file management with template or copy over lineinfile, and reserve lineinfile/blockinfile for single settings in files Ansible does not own end to end.
  • Use the systemd module with daemon_reload=yes whenever you drop a new unit file, since service alone will not reload systemd's view of it.
Comparable tools Puppet package / service / file resources, the direct analog Chef package · service · template · cookbook_file Shell script apt-get install + systemctl + cp, the imperative version

Knowledge Check

When does template beat copy, and when does lineinfile beat both?

  • template for any file with a variable, copy for static files, and lineinfile for a single setting in a file Ansible does not own whole
  • copy for any file with variables in it, template for the genuinely static files, and lineinfile whenever a managed file just happens to be large
  • lineinfile for every single config file so that changes always stay minimal, with template reserved purely for binaries
  • template and copy are fully interchangeable in practice; only lineinfile is actually idempotent of the three

Why does a long-running service need both state and enabled set?

  • state=started makes it run now and enabled=yes makes it survive a reboot; set one without the other and it is half-configured
  • state and enabled are really just two aliases for the one thing, so you set both of them purely for backward compatibility
  • enabled is the one that actually starts the service running right now, and state only ever affects its later boot-time behaviour
  • Omitting either one of them causes the service module to report failed almost immediately on the run

What does daemon_reload solve, and which module provides it?

  • It makes systemd re-read a newly dropped unit file before the service starts, and the systemd module provides it, not service
  • It forcibly restarts every single service currently running on the host all at once, and the generic service module is what provides it
  • It refreshes the local apt package cache before installs, and the apt package module is the one that provides it
  • It reloads the underlying SSH connection to the target node, and the file-writing copy module is what provides it

What is the consequence of omitting mode on a template task that writes app.env?

  • The file lands with whatever the node's umask gives, which can leave a secret-bearing file world-readable
  • The whole task simply fails outright, because mode is a strictly required argument for the template module
  • The file is created with no read permission at all for anyone on the box, including the app that needs it
  • Ansible flatly refuses to even render out the template at all until an explicit mode is declared on it

You got correct