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.

🛠 Paste this YAML into the validator to check it instantly.

Open Validator →

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.

This playbook targets Ubuntu hosts. For RHEL/CentOS, replace the 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

nginx.yml
---
- 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:

task with tags
    - 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