state: latest in a deployment role means “whatever the repository has
right now”. It is a statement about a moment, and every host that runs
the role at a different moment gets a different answer.
That is fine for a fleet that is deployed once. Real fleets are
deployed continuously — a host replaced on Tuesday, three added by
autoscaling on Thursday, one rebuilt after a disk failure in March — and
the role that “converges” them produces a version distribution rather
than a version.
What latest actually produces
Read-only / Safea fleet converged by a role that pins nothing— Read-only. An ad-hoc query of installed versions across a group. The command inspects the package database and changes nothing.
$ ansible appservers -i inventories/production -m package_facts -a 'manager=auto'
web01.example.com : myapp 4.2.1 # built in March
web02.example.com : myapp 4.2.1
web03.example.com : myapp 4.3.0 # replaced in May
web04.example.com : myapp 4.3.0
web05.example.com : myapp 4.4.2 # autoscaled in July
web06.example.com : myapp 4.4.2
web07.example.com : myapp 4.4.3 # rebuilt after a disk failure last week
Nothing failed. Every run of the role reported success. Each host is
running “the latest version” as of the day it was built, and the fleet
is running four different versions of the application behind one load
balancer.
The consequences are not evenly distributed:
Debugging becomes unreliable. “It works on web01 and not on web07”
now has a boring explanation that nobody checks first, because the
repository says one version.
A rollback has no target. Rolling back to “the previous version”
means seven different previous versions.
Reproduction is impossible. A bug reported against production cannot
be reproduced in staging, because staging was built last Tuesday and
production is a museum.
Version syntax, per manager
The version specifier is a package-manager property, and it differs.
Manager
Exact version
Range
apt
myapp=4.4.3-1ubuntu1
myapp>=4.4.0
dnf
myapp-4.4.3-1.el9
myapp >= 4.4.0 (spaces required)
The dnf documentation is explicit about the spacing: comparison
operators are valid in name, and “Spaces around the operator are
required”. The apt documentation is equally explicit in the other
direction: “Do not use single or double quotes around the version when
referring to the package name with a specific version”.
Configuration changea version pinned per family, from one variable— Installs the exact version named by the variable. Rerunning against a host already at that version reports ok and installs nothing.
state: present with an exact version is the correct combination.
state: latest with an exact version is contradictory and the manager
resolves it in ways that vary; do not write it.
The upgrade path is then a one-line change to group_vars, which is a
diff in a pull request with an author, a reviewer and a date. That is
the whole point: an upgrade should be a reviewable change to the
repository, not an emergent property of when a play happened to run.
Artifacts: get_url with a checksum
For applications shipped as a tarball or a binary rather than a package,
get_url plus a checksum gives you the same guarantee, and the checksum
is what makes it converged.
The checksum parameter format is documented as
`<algorithm>:<checksum|url>` — either a literal digest or a URL to
a checksum file:
Configuration changean artifact fetch that is both pinned and idempotent— Downloads the artifact and verifies its digest. On a rerun where the destination already matches the checksum, the download is skipped entirely.
The checksum parameter is doing two jobs at once, and the second is
the one people miss. From the module documentation: if a checksum is
supplied and the file already exists at dest, the destination’s
checksum is computed, and if they match the download is skipped; if
they do not match the destination is replaced.
So checksum converts get_url from “download if absent” into
“converge on this content”. Without it, force: false — the default —
only checks whether the path exists, so a truncated or corrupted
previous download is never noticed and never repaired.
unarchive needs creates for the same reason: without it the module
re-extracts on every run, which is slow, produces a permanently
changed task, and notifies the restart handler on every play.
Recording what was deployed
The run log records what the play tried to do. It is transient, it lives
on whoever ran the play, and it is not available to the person
debugging at 03:00 six weeks later.
Configuration changeleaving the evidence on the host— Writes a small file recording the deployed version and when. Idempotent: the content is derived from variables, so a rerun with the same version rewrites identical content and reports ok.
- name: Record what this host is running
ansible.builtin.copy:
dest: /etc/myapp/deployed-version
owner: root
group: root
mode: '0644'
content: |
version={{ myapp_version }}
artifact_sha256={{ myapp_sha256 }}
deployed_by=ansible
source_ref={{ deploy_source_ref | default('unknown') }}
Deliberately not in that file: a timestamp. A {{ ansible_date_time }} value changes on every run, so the task reports changed every
time, notifies handlers it should not, and destroys the role’s
idempotence to record something the file’s own mtime already carries.
The best version evidence is the one the application reports about
itself — a /healthz endpoint returning its version — because it
describes the running process rather than what was installed on disk.
The health gate from the previous lesson asserts on exactly that, which
is what makes it a deployment check rather than a liveness check.
Knowledge check
Knowledge check · 4 questions
Q1. A deployment role uses state: latest. Hosts have been built and replaced over eight months. What does the fleet look like?
Q2. What does supplying a checksum to get_url change about the module behaviour when the destination file already exists?
Q3. Which of these are genuine problems with a version-recording task that writes a file containing ansible_date_time? Select all that apply.
Q4. Pinning an exact version makes a rollback safe, because the previous version can always be reinstalled from the repository.
Passing score: 75%. Answers are checked in this browser.