Modeling an existing nginx site safely with Ansible

ArticleCreated 2026-07-12Updated 2026-07-12reviewed

Ansiblenginxconfiguration-managementoperations

A staged workflow for bringing an already-running nginx site under Ansible without overwriting unreviewed drift or reloading an invalid configuration.

Provisioning a new nginx site and adopting an existing one are different operations. During adoption, the running host is the source of truth until its configuration has been captured and reviewed. The first Ansible run should reproduce that state, not redesign it.

A safe takeover has four properties:

The paths and service names below are examples. Use only values observed on the target host. In particular, do not assume a Debian-style sites-available layout, /usr/sbin/nginx, or a service named nginx.

Discover the effective configuration

Start with read-only inspection. Record the nginx binary, main configuration file, include tree, service name, site file, ownership, mode, and any enablement symlink.

console sudo /path/to/nginx -t umask 077 sudo /path/to/nginx -T > ./nginx-effective.conf sudo stat -c '%U %G %a %n' /path/to/site.conf sudo sha256sum /path/to/site.conf

If the site is enabled through a symbolic link, record both the link and its resolved target:

console sudo readlink /path/to/enabled-site.conf sudo readlink -f /path/to/enabled-site.conf

nginx -T tests the configuration and writes the complete configuration, including included files, to standard output. Review that output before storing or sharing it because configuration files can contain sensitive values.

Discovery must also cover referenced objects that are not part of the site file itself, such as TLS certificate and key paths, authentication files, log directories, upstream Unix sockets, and additional includes. The first takeover should not rename or relocate any of them.

Capture an exact baseline

Copy the reviewed site file into the role without introducing Jinja expressions or formatting changes:

text roles/nginx_site/ ├── files/ │ └── example.conf └── tasks/ └── main.yml

Use ansible.builtin.copy for this first stage. A static file makes the intended baseline explicit and keeps the first run easy to compare with the host. Do not parameterize hostnames, ports, paths, or TLS settings yet. First establish that Ansible can manage the existing file without changing behavior.

Define takeover variables from the discovered state. The following values are examples and must be replaced:

yaml nginx_binary: /usr/sbin/nginx nginx_service_name: nginx nginx_site_path: /etc/nginx/sites-available/example.conf nginx_site_owner: root nginx_site_group: root nginx_site_mode: "0644" nginx_site_expected_sha256: "<sha256-from-the-reviewed-host>" nginx_site_takeover_guard: true

The expected checksum is an optimistic lock. The takeover tasks compare it with both the installed file and the captured repository baseline, preventing an old or incorrectly copied snapshot from replacing a site that was edited after discovery.

Refuse an unsafe takeover

Validate the currently installed configuration before changing anything, then verify that the target is the reviewed regular file:

`yaml

`

Keep the takeover guard enabled through the first live run and the following idempotence run. Disable it only in a reviewed commit before making intentional later changes, or replace it with a versioned precondition maintained for every rollout.

If the existing layout uses an enablement symlink, assert its current target before managing it:

`yaml

`

This check is preferable to creating or retargeting the link during a no-change takeover. Manage the link with ansible.builtin.file only after its intended state is explicit.

Install, validate, and roll back as one operation

An nginx site file is usually an included fragment, so it often cannot be validated in isolation. Ansible's file modules can validate a temporary file before installation, but that mechanism is insufficient when the validator needs the complete include tree or the candidate at its final path.

For that case, create a backup while installing the candidate, run nginx -t against the complete on-disk configuration, reload only after success, and restore the backup on failure:

`yaml

rescue:

`

nginx itself checks a new configuration during reload and continues with the old configuration if it cannot apply the new one. That protects the running workers, but it does not restore an invalid file on disk. Explicit file rollback is still required so a later reload or restart does not encounter the rejected configuration again.

Retain or remove Ansible's timestamped backup according to the host's retention policy after the rollout is verified.

Prove the first run is a no-op

Start with one reviewed host:

console ansible-playbook -i inventory playbooks/nginx-site.yml \ --check --diff --limit web-01

Check mode is a simulation, not proof of a successful deployment. Tasks that depend on registered results may not execute exactly as they do in a live run. Diff output can also expose sensitive configuration values, so handle it as operational data.

If the diff contains any unplanned content or metadata change, stop and reconcile the repository baseline with the host. Then run the play normally on the same host and immediately run it a second time:

console ansible-playbook -i inventory playbooks/nginx-site.yml --limit web-01 ansible-playbook -i inventory playbooks/nginx-site.yml --limit web-01

For an exact takeover, the first live run should report no change to the site file. The second run must be idempotent. After each run, perform the environment-specific HTTP, TLS, and upstream smoke tests used for that service.

For multiple equivalent hosts, roll out in controlled batches rather than changing the whole group concurrently:

`yaml

roles:

`

A checksum mismatch on any host is a discovery result, not an error to suppress. Review that host separately before adding it to the same model.

Introduce templating only after adoption

Once the static baseline has completed a no-change rollout, convert files/example.conf to templates/example.conf.j2 and replace only values that truly vary between managed hosts or environments.

Make one class of change at a time. For each step:

  1. compare the rendered file with the accepted baseline;
  2. run the play in check and diff mode on one host;
  3. validate the complete configuration with nginx -t;
  4. apply to one host and run service-specific smoke tests;
  5. confirm a second run reports no changes.

Keep the same backup, full-configuration validation, reload, and rescue structure when changing from copy to template. The deployment mechanism changes; the safety properties do not.

Acceptance criteria

The site is safely modeled when all of the following are true:

The important boundary is simple: adoption records existing behavior; improvement changes it. Keep those as separate, reviewable rollouts.

References