Réponse à « qu'est-ce qui nous empêche d'upgrade des deux côtés ? » : rien. Le playbook déployait `gitea/act_runner:latest` avec `pull: missing`, c'est-à-dire la pire combinaison possible — un tag FLOTTANT qui n'est JAMAIS rafraîchi. Chaque hôte garde donc ce que « latest » voulait dire le jour de son premier pull : pi1 : sha256:7bdc8d31… → v0.3.1 pi3 : sha256:0f65fa10… → v0.2.13 Deux machines censées être équivalentes, deux versions à trois mineures d'écart. Effets mesurés : le MÊME job, sur la MÊME image de CI, met 511 s sur pi1 et 397 s sur pi3 (114 s d'écart imputables à la machine) ; et pi3 a mal lu la définition d'un job dont il dépendait (« 'runs-on' key not defined », puis « No steps found »). ⚠ POURQUOI PAS `latest` + `pull: always`. `latest` vaut aujourd'hui **0.6.1** (Docker Hub, 30/04/2026), soit 3 à 4 versions mineures devant tout ce qui est éprouvé ici. Le runner exécute TOUTE la CI de la forge : une montée subie, non datée et non choisie s'y paie cher. On épingle donc, et on monte délibérément. ⚠ POURQUOI 0.3.1 ET PAS 0.6.1. 0.3.1 est la version que pi1 exécute DÉJÀ avec succès sur cette forge. Ce changement aligne donc pi3 VERS LE HAUT, sur du prouvé, sans saut de quatre versions. Passer ensuite à 0.6.1 devient une modification d'UNE ligne, datée et reculable — c'est tout l'intérêt de la variable. ⚠ Et `pull: missing` redevient CORRECT avec un tag épinglé : changer la version change le tag, donc l'image est absente, donc elle est tirée. Aucun besoin de `pull: always`, qui interrogerait le registre à chaque passage pour rien. ⚠ NE PAS jouer ce playbook pendant qu'une CI tourne : il recrée les conteneurs de runner et TUE les jobs en vol (journaux perdus). Vérifier `list_runs` avant — et se rappeler qu'un merge est un déclencheur. Refs arcodange-org/factory#50 Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Use Ansible
Run locally (uv)
A project-local venv is defined in pyproject.toml at the repo root (ansible-core + the kubernetes, jmespath, dnspython libraries that kubernetes.core and friends need at runtime).
uv sync # creates .venv/ and installs ansible-core + python deps
uv run ansible-galaxy collection install -r ansible/requirements.yml
uv run ansible-playbook -i ansible/arcodange/factory/inventory ansible/arcodange/factory/playbooks/<playbook>.yml
The localhost entry in the inventory uses ansible_python_interpreter: "{{ ansible_playbook_python }}", so uv run is enough — Ansible picks up the venv's Python automatically without any hardcoded path.
Run with docker ssh agent side proxy
build docker images
git clone -q --depth 1 --branch master https://github.com/arcodange/ssh-agent.git /tmp/ssh-agent
(cd /tmp/ssh-agent ; docker build -t docker-ssh-agent:latest -f Dockerfile . ; rm -rf /tmp/ssh-agent)
(cd ansible; docker build -t arcodange-ansible:0.0.0 .)
run in container
# git clone -q --depth 1 --branch master https://github.com/arcodange/ssh-agent.git /tmp/ssh-agent
# (cd /tmp/ssh-agent ; docker build -t docker-ssh-agent:latest -f Dockerfile . ; rm -rf /tmp/ssh-agent)
# (cd ansible; docker build -t arcodange-ansible:0.0.0 .)
docker run -d --name=ssh-agent docker-ssh-agent:latest
docker run --rm --volumes-from=ssh-agent -v ~/.ssh:/.ssh -it docker-ssh-agent:latest ssh-add /root/.ssh/id_rsa
docker run --rm -u root --name test --volumes-from=ssh-agent -v $PWD:/home/arcodange/code \
-v "$HOME"/.kube/config:/home/arcodange/.kube/config \
-e ANSIBLE_VAULT_PASSWORD_FILE=$ANSIBLE_VAULT_PASSWORD_FILE -v $ANSIBLE_VAULT_PASSWORD_FILE:$ANSIBLE_VAULT_PASSWORD_FILE \
arcodange-ansible:0.0.0 \
ansible-playbook ansible/arcodange/factory/playbooks/03_cicd.yml -i ansible/arcodange/factory/inventory -vv
use vault with single password
Important
Required for gitea mailer
kubectl create secret generic arcodange-ansible-vault --from-literal="pass=<ansible_vault_password>" -n kube-system`to be set as a file variable for gitea runners
ANSIBLE_VAULT_PASSWORD_FILE=~/.local/bin/read-vault-key.sh;
mkdir -p `dirname $ANSIBLE_VAULT_PASSWORD_FILE`; set +o histexpand;
echo -e "#!/bin/bash\nkubectl get secret -n kube-system arcodange-ansible-vault --template='{{index .data.pass | base64decode}}'" > $ANSIBLE_VAULT_PASSWORD_FILE;
set -o histexpand; chmod +x $ANSIBLE_VAULT_PASSWORD_FILE; echo "export ANSIBLE_VAULT_PASSWORD_FILE=$ANSIBLE_VAULT_PASSWORD_FILE" >> `find ~ -maxdepth 1 -type f -name '\.*profile' -or -name '\.bashrc' -or -name '\.zshenv' | head -n1`
export ANSIBLE_VAULT_PASSWORD_FILE
a tool to reuse a ssh agent (not required)
FIND_SSH_AGENT=$HOME/.local/bin/ssh-find-agent
curl -s https://raw.githubusercontent.com/wwalker/ssh-find-agent/master/ssh-find-agent.sh > $FIND_SSH_AGENT
chmod +x $FIND_SSH_AGENT
echo 'ssh_find_agent "$@"' >> $FIND_SSH_AGENT
which brew && brew install coreutils # if on macos
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
dev
test an expression
ansible -i ,localhost -c local localhost -m raw -a "echo hello world {{ inventory_hostname }} : {{ hostvars | to_nice_json | regex_replace(\"['\n]\",' ') }}"
local python environment with uv
Install UV (one-time)
python3 -m pip install uv
python3 -m uv python install 3.12
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshenv
Bootstrap the project venv
uv sync # honors .python-version (3.12) and pyproject.toml
uv run ansible-galaxy collection install -r ansible/requirements.yml
# `--token <token>` is only needed if you hit galaxy.ansible.com rate limits
Run
uv run ansible-galaxy collection install ./ansible/arcodange/factory -f
uv run ansible-playbook -i ansible/arcodange/factory/inventory ansible/arcodange/factory/playbooks/02_setup.yml