Files

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_status with until: 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 - RETRYING messages are normal — they just mean the until condition 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.