Upgrade role
Upgrades all packages using the native package manager (apk or apt).
Problem
Tailscale may be updated during the upgrade. When its service restarts, the SSH tunnel drops and Ansible loses connectivity, causing a fatal UNREACHABLE.
Solution — async + poll
Instead of a blocking upgrade, we fire the job in the background and poll for completion, tolerating temporary unreachability.
sequenceDiagram
participant A as Ansible
participant T as Target
A->>T: apk upgrade (async, poll: 0)
T-->>A: job ID: 42
Note over T: 🔄 upgrade en cours...
A->>T: async_status jid=42
T-->>A: finished: false
Note over A: FAILED-RETRYING (normal)
A-->>A: sleep 10s
A->>T: async_status jid=42
T-->>A: finished: false
Note over A: FAILED-RETRYING (normal)
A-->>A: sleep 10s
A->>T: async_status jid=42
T-->>A: finished: true ✅
Note over A: ok
async: 3600+poll: 0— fire and forget. Ansible gets a job ID and moves on.async_statuswithuntil: finished— polls every 10s for up to 60 retries (~10 min).ignore_unreachable: true— if Tailscale dropped SSH, Ansible doesn't crash. It retries until the host is back.- The
FAILED - RETRYINGmessages are normal — they just mean theuntilcondition isn't met yet.
Idempotency
A task with async + poll: 0 always reports changed: true by default, even when nothing happened. To fix this:
graph LR
A["Fire: changed_when: false"] --> B["Package manager runs in background"]
B --> C["Wait: polls every 10s"]
C -->|finished| D["async_status reads real changed value"]
D -->|"packages updated"| E["changed ✅"]
D -->|"nothing to do"| F["ok"]
- The fire task has
changed_when: false— it never claims change. - The wait task reads
changed_when: async_result.changed— it propagates the real status from the package manager once the job finishes.
Second run with nothing to upgrade → changed=0 on all hosts.