Ansible — Nginx Playbook
An idempotent Ansible playbook that installs Nginx on Ubuntu hosts, creates the document root with correct permissions, deploys a site configuration from a Jinja2 template, enables the site via symlink, and ensures the service is running — using fully-qualified module names and the notify/handler pattern.
Overview
An Ansible playbook is a YAML file that describes one or more plays. Each
play maps a set of tasks to a group of hosts from the inventory. This playbook contains a single play
targeting the webservers host group. Tasks run sequentially on all matching hosts in
parallel (by default), and Ansible reports each task's outcome: ok (no change needed),
changed (action was taken), or failed. Because every task uses Ansible
modules rather than raw shell commands, the playbook is idempotent — running it
multiple times produces the same result as running it once.
Run this playbook with:
ansible-playbook -i inventory.yml nginx.yml. To preview what changes would be made
without applying them, add --check (dry-run mode). To see exactly which values are
used for each variable, add -v or -vvv for verbose output.
ansible.builtin.apt
tasks with ansible.builtin.dnf and adjust the Nginx site-available path accordingly
(RHEL uses /etc/nginx/conf.d/ rather than sites-available/sites-enabled).
Full YAML Copy-paste ready
---
- name: Install and configure Nginx
hosts: webservers
become: true
vars:
nginx_port: 80
server_name: example.com
document_root: /var/www/html
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install Nginx
ansible.builtin.apt:
name: nginx
state: present
- name: Create document root
ansible.builtin.file:
path: "{{ document_root }}"
state: directory
owner: www-data
group: www-data
mode: '0755'
- name: Deploy Nginx config from template
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/{{ server_name }}
owner: root
group: root
mode: '0644'
notify: Reload Nginx
- name: Enable site
ansible.builtin.file:
src: /etc/nginx/sites-available/{{ server_name }}
dest: /etc/nginx/sites-enabled/{{ server_name }}
state: link
notify: Reload Nginx
- name: Ensure Nginx is started and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Reload Nginx
ansible.builtin.service:
name: nginx
state: reloaded
Key sections explained
become: true for privilege escalation
Most server administration tasks require root privileges — installing packages, writing to
/etc/, managing services. become: true tells Ansible to escalate privileges
using sudo before running each task in the play. By default it uses sudo
with the root user, but this is configurable: become_method can be set to
su, doas, or others, and become_user can target a specific
non-root user. become can also be applied at the task level rather than the play level,
letting you mix privileged and unprivileged tasks in the same play.
FQCN module names (ansible.builtin.apt)
Ansible 2.10 introduced Fully Qualified Collection Names (FQCNs) for all built-in
modules, in the format namespace.collection.module. ansible.builtin.apt is
the FQCN for the apt module included with Ansible core. Using FQCNs rather than short names like
apt is now the recommended practice because it is unambiguous — there is no risk of a
custom collection's module shadowing the built-in one — and it makes it clear at a glance which
collection each module comes from. Linters like ansible-lint will flag non-FQCN module
references as warnings in modern Ansible projects.
vars and Jinja2 {{ }} templating
The vars block at the play level defines variables scoped to this play. Any task or
template in the play can reference these variables using Jinja2's double-curly syntax:
{{ document_root }} resolves to /var/www/html at runtime. Variables make
the playbook reusable — changing server_name from example.com to
mysite.io in one place updates every task that references it. Variables can also be
defined in inventory host/group vars files, in group_vars/ directories, or passed on
the command line with -e nginx_port=8080, giving you multiple layers of override.
The ansible.builtin.template task goes further: it renders a Jinja2 template file
(nginx.conf.j2) on the Ansible controller, substituting all {{ }}
variables, and uploads the resulting file to the remote host. This means your Nginx configuration
can dynamically include the server name, port, and document root from the playbook variables —
no manual editing of config files on each server.
notify + handlers: idempotent service reload
The notify: Reload Nginx directive on both the template deployment task and the site
enable task tells Ansible to trigger the handler named Reload Nginx — but only if the
task made a change. Handlers run once at the end of the play, regardless of how many tasks notified
them. This means even if both the config and the symlink changed, Nginx is reloaded only once, not
twice. If neither task made any change (because the config is already correct and the symlink already
exists), the handler never runs at all. This is the idiomatic Ansible pattern for restarting or
reloading services only when necessary.
cache_valid_time and state: link
The cache_valid_time: 3600 parameter in the apt cache update task tells Ansible to skip
the apt-get update call if the cache was updated less than 3600 seconds (1 hour) ago.
Without this, every playbook run would update the apt cache even if it was refreshed moments ago —
wasting time and causing unnecessary network traffic. With it, the cache update is only performed
when truly needed.
The state: link in the "Enable site" task instructs ansible.builtin.file
to create a symbolic link at dest pointing to src. This mirrors the
Nginx convention of enabling sites by symlinking from sites-available/ to
sites-enabled/. If the symlink already exists and points to the correct target,
Ansible reports ok and makes no change — preserving idempotency.
Tips & variations
Check mode (dry run)
Run ansible-playbook -i inventory.yml nginx.yml --check to preview what changes would
be made without actually applying them. Tasks that would make changes report changed
in check mode; tasks that are already in the correct state report ok. Note that some
tasks (especially those using shell or command modules) may not behave correctly in check mode.
Limit to specific hosts
To run the playbook against only one host from the webservers group for testing:
ansible-playbook -i inventory.yml nginx.yml --limit web1. The --limit
flag accepts hostnames, group names, patterns, or comma-separated lists.
Use tags to run a subset of tasks
Add tags to individual tasks to run only specific parts of a playbook:
- name: Deploy Nginx config from template
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/{{ server_name }}
mode: '0644'
notify: Reload Nginx
tags:
- config
- nginx
Run only tasks tagged config:
ansible-playbook -i inventory.yml nginx.yml --tags config