Essential 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.
packageapt on Ubuntu, dnf on RHEL — installs, removes, and pins packages.servicesystemd for a daemon_reload.file & copyowner, group, mode; template renders first.userpackage 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.
- 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.
- Using
aptwithoutupdate_cache=yeson 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
copyand starting it withservicebut skippingdaemon_reload, so systemd runs the old unit definition until something forces a reload — use thesystemdmodule'sdaemon_reload=yes. - Setting
enabled=yesbut notstate=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
lineinfileto build up a whole config line by line across many tasks, producing a brittle, ordering-dependent file thattemplatewould render correctly in one pass. - Omitting
owner,group, ormodeoncopy/templateforapp.envand leaving a secret-bearing file world-readable, because the module placed it with whatever default the umask gave.
- Use
templatefor any config with a variable in it — the nginxlarkspur.conf,app.env— andcopyonly for genuinely static files, so the content is declared, not hand-assembled. - Pair
servicewith explicitstate=started enabled=yeson every long-running service so it is both running now and persistent across a reboot. - Set
owner,group, andmodeexplicitly on everycopy/template/file, especially for secret-bearing files likeapp.env, rather than relying on the node's umask. - Prefer full-file management with
templateorcopyoverlineinfile, and reservelineinfile/blockinfilefor single settings in files Ansible does not own end to end. - Use the
systemdmodule withdaemon_reload=yeswhenever you drop a new unit file, sinceservicealone will not reload systemd's view of it.
Knowledge Check
When does template beat copy, and when does lineinfile beat both?
templatefor any file with a variable,copyfor static files, andlineinfilefor a single setting in a file Ansible does not own wholecopyfor any file with variables in it,templatefor the genuinely static files, andlineinfilewhenever a managed file just happens to be largelineinfilefor every single config file so that changes always stay minimal, withtemplatereserved purely for binariestemplateandcopyare fully interchangeable in practice; onlylineinfileis actually idempotent of the three
Why does a long-running service need both state and enabled set?
state=startedmakes it run now andenabled=yesmakes it survive a reboot; set one without the other and it is half-configuredstateandenabledare really just two aliases for the one thing, so you set both of them purely for backward compatibilityenabledis the one that actually starts the service running right now, andstateonly ever affects its later boot-time behaviour- Omitting either one of them causes the
servicemodule to reportfailedalmost 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
systemdmodule provides it, notservice - It forcibly restarts every single service currently running on the host all at once, and the generic
servicemodule is what provides it - It refreshes the local apt package cache before installs, and the
aptpackage module is the one that provides it - It reloads the underlying SSH connection to the target node, and the file-writing
copymodule 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
modeis a strictly required argument for thetemplatemodule - 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
modeis declared on it
You got correct