Skip to main content
RunBook Academy

AnsibleXLI · Service and Application DeploymentService and Application Deployment

The limits of Ansible as a deployment tool

Expert⏱ ~25 minansible-playbook

What you'll learn

  • Name the four capabilities a push-based convergence tool structurally cannot provide
  • Explain why a rolling deploy is always a period of mixed versions and what that requires of the application
  • Identify the concrete signals that a fleet has outgrown a push-based deployer
  • Place the hand-off to an image pipeline, an orchestrator or a traffic layer correctly

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Everything in this part has been about making a push-based deployment as safe as it can be. This lesson is about where that ceiling is.

The four limits below are not missing features waiting for a release. They follow from what Ansible is: a tool that connects to hosts, applies a described state, and exits. A tool that exits cannot maintain anything, and a tool with no data path cannot route anything.

Knowing the ceiling is what stops you building an increasingly elaborate playbook to reach something the shape of the tool does not reach.

Limit one: no atomic cut-over

A rolling deploy is, by construction, an interval during which some hosts run version N and some run N+1. There is no serial value that removes the interval; serial: 1 maximises it, and a single batch covering the fleet is a simultaneous outage rather than a cut-over.

The consequence is a requirement on the application, not on the playbook: version N and N+1 must be able to coexist behind the same load balancer, sharing the same database, consuming the same queues, for the duration of the rollout.

Coexistence requirementWhat breaks it
Shared database schemaa rename or a drop in the same release
Shared cachea serialisation format change with the same key namespace
Shared queuea message format the old consumer cannot parse
Sticky sessionsa session structure the other version cannot read
Internal APIs between servicesa breaking change deployed in one direction only

None of these are visible in the playbook, and all of them produce errors during the rollout window that disappear once it completes — which is the worst diagnostic signature available, because by the time anyone investigates, the evidence has resolved itself.

Limit two: no traffic control

Ansible can call a load balancer’s API. It has no data path, so it cannot do anything a load balancer will not do on instruction.

The practical boundary: canary by host is available; canary by traffic is not.

Service impact possiblewhat a push-based canary can express
- name: Canary the new release on one host
hosts: appservers
serial: 1
max_fail_percentage: 0
tasks:
  - name: Deploy and validate
    ansible.builtin.include_tasks: deploy-one-host.yml

On a twenty-host pool with round-robin balancing, that canary receives about 5% of traffic — not because you specified 5%, but because 1/20 is what it happens to be. On a five-host pool the same play sends 20% of production traffic to an untested build.

What a traffic-aware system offers instead: shift 1% of requests, hold, observe error rate against a baseline, shift 5%, hold, shift 25%, revert automatically on a regression. Weight is a dial rather than a consequence of arithmetic, and it can be turned back in seconds without touching a host.

That capability lives in the load balancer, the service mesh or the ingress layer. Ansible’s role is to configure those systems, which it does well. It cannot be one.

Limit three: no automatic revert

A failed health gate stops the play. It does not undo anything.

This is worth stating precisely because “rollback” is often described as if the tool provides it. What Ansible provides is:

  • a block/rescue/always structure, which runs tasks you wrote on failure;
  • a stop-the-run failure policy, which prevents further batches;
  • an idempotent role that can be rerun with a previous version pinned.

What it does not provide is a stored previous state to return to. There is no transaction, no snapshot, no automatic inverse of a converged change. A rescue block that “rolls back” is a set of forward tasks that happen to install an older version — which works, and is a rollback only to the extent that the older version can be installed and the configuration and schema still fit it.

Service impact possiblewhat a rescue block can and cannot do
- name: Deploy with a scripted fallback
block:
  - name: Deploy the new version
    ansible.builtin.include_tasks: deploy-one-host.yml

rescue:
  - name: Reinstall the previously recorded version
    ansible.builtin.apt:
      name: 'myapp={{ myapp_previous_release }}'
      state: present
      allow_downgrade: true
    notify: Restart myapp

  - name: Announce that this host was reverted, and fail the batch anyway
    ansible.builtin.fail:
      msg: >-
        {{ inventory_hostname }} failed its health gate and was reverted
        to {{ myapp_previous_release }}. The run is stopping; the fleet
        is in a mixed state and needs a decision.

The deliberate fail at the end matters. A rescue that succeeds marks the host as recovered and the play continues to the next batch — which is almost never what you want after a canary failed. Recovering the host and stopping the run are two separate decisions, and the second one has to be written.

Limit four: no continuous reconciliation

A run is a point in time. After ansible-playbook exits, nothing maintains the state it applied.

A host that drifts an hour later stays drifted until the next run. A host that was unreachable during the deploy is simply not deployed, and nothing remembers to come back for it. An instance created by autoscaling ten minutes after the play finished comes up running whatever its image carries — usually the previous release — and does not appear in any recap.

The signals you have outgrown it

These are concrete, and any two of them together are a strong case for changing tools rather than improving the playbook.

Deploy frequency approaches run duration. A rolling play across 300 hosts takes ninety minutes. If you deploy hourly, runs overlap, and two concurrent ansible-playbook processes converging the same fleet on different versions is a state nothing in this course can make safe.

You need traffic-percentage canaries. Not “one host first” but “1% of requests, then 5%”. This is a routing capability and no playbook provides it.

You need automatic revert on an SLO regression. A control loop, as above.

Your fleet scales in and out during deployments. The autoscaling gap becomes structural rather than occasional.

You are rebuilding a scheduler. If the playbook has grown logic for where a workload should run, how many replicas there should be, and what to do when a host disappears — that is an orchestrator, and there are good ones.

Hosts are cattle with no state. If every host is identical, disposable and rebuilt rather than repaired, the convergence machinery is overhead. Bake an image and replace instances.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Why can no choice of serial value give a rolling Ansible deployment an atomic cut-over?

  2. Q2. A team wants deployments to revert automatically if the error rate degrades in the ten minutes after a release. Why is this a poor fit for Ansible?

  3. Q3. Which of these are genuine signals that a fleet has outgrown a push-based deployer? Select all that apply.

  4. Q4. A rescue block that reinstalls the previous version constitutes a rollback, because Ansible restores the state the host was in before the play ran.

Passing score: 75%. Answers are checked in this browser.