Skip to main content
All Posts

The Ansible Play That Catches What Deployment Misses

4 min read
AnsibleDevOpsIaCAutomationAzureNginx
The Ansible Play That Catches What Deployment Misses

Three Ansible plays. The third makes no changes — it just asserts the deployment worked. Here's what it caught, and why automated verification is not optional.

The Ansible playbook completed without errors. Terraform had provisioned the Azure environment. The VM was reachable over SSH. By every visible measure, the deployment had succeeded.

The site returned 403.

This is what verification is for.

The three-play structure

The playbook for this project runs three plays in sequence:

Play 1 — Install    apt update → install nginx + git → start and enable nginx
Play 2 — Deploy     clone repo → sync to /var/www/html/ → set www-data ownership → reload nginx
Play 3 — Verify     uri module → GET http://<public_ip> → assert status_code == 200

Play 1 and Play 2 do the work. Play 3 does not change anything on the server — it runs on localhost and makes a single HTTP GET request to the public IP, then asserts the response code is 200. If it is not, the playbook fails with a clear message:

TASK [Assert HTTP 200 was returned]
fatal: [localhost]: FAILED! => {
    "assertion": "http_response.status == 200",
    "msg": "HTTP 200 expected but got 403"
}

The play that makes no changes is the only one that found the fault.

What the 403 was hiding

File ownership. The content had been synced to /var/www/html/ by the deploy task, but the file module setting owner: www-data, group: www-data, recurse: true had not applied correctly on the first run. Nginx was running, the files were present — but Nginx could not serve them because it did not own them.

From Nginx's perspective, this is a permissions error. From Ansible's perspective, Play 2 had reported ok or changed for every task with no failures. From Terraform's perspective, the infrastructure was exactly as specified.

The 403 existed in the gap between "deployment succeeded" and "the application works."

Without the verify play, finding it would have required opening a browser, noticing the 403, tracing it back through Nginx logs, and identifying the ownership issue. With it, the fault was surfaced immediately, the fault domain was clear (configuration, not infrastructure), and the fix was a single corrected task.

The uri module as a deployment gate

Ansible's uri module makes HTTP requests from the control node. Combined with assert, it becomes a deployment gate — a step the playbook cannot pass unless the application is actually reachable and returning the expected response:

- name: Check site is reachable
  uri:
    url: "http://{{ public_ip }}"
    method: GET
    return_content: false
  register: http_response

- name: Assert HTTP 200 was returned
  assert:
    that:
      - http_response.status == 200
    fail_msg: "Expected HTTP 200 but got {{ http_response.status }}"
    success_msg: "Site is reachable and returned HTTP 200"

This runs on localhost — the machine running the playbook — not on the remote server. It tests the deployment from the outside, the same way a user or a monitoring tool would. A running Nginx process that cannot serve files still returns 403. The verify play catches that. A process check on the remote server would not.

Why this matters

Most deployment pipelines end when the last task completes without error. The infrastructure is provisioned, the configuration is applied, the service is started, and the pipeline goes green.

None of that confirms the application is actually reachable and returning the expected response.

The gap between "the last task ran" and "the system is in its intended state" is where silent failures live — file permissions, firewall rules that did not apply, services that started and crashed thirty seconds later. A verify step collapses that gap. It does not care what each preceding task reported. It tests the end state directly and fails loudly if the result is wrong.

This is what closed-loop automation means: not just executing steps, but asserting the outcome.