Ce que cela construit
Un runner CI attaché à votre propre forge, qui récupère un dépôt, construit une image conteneur sans démon et sans root, et pousse le résultat vers un registre privé tournant sur la même instance. Aucun socket privilégié n'est monté nulle part, et rien dans le pipeline ne s'exécute avec l'uid zéro.
La plupart des configurations CI auto-hébergées résolvent la construction d'images en montant le démon conteneur de l'hôte dans le job. Cela fonctionne et cela donne à chaque pipeline, y compris celui qu'une personne a ouvert via une pull request, le contrôle complet de la machine. Buildah en mode rootless fait le même travail sans aucun de ces problèmes, et sur des cœurs dédiés, ce n'est pas plus lent.
Avant de commencer
- Un R-8. Les builds d'images sont la chose la plus liée au cœur unique que la plupart des équipes exécutent, et Zen 5 est le plus rapide par cœur que nous vendons. Quatre cents gigaoctets de NVMe contiennent un très grand nombre de couches.
- Une forge que vous exécutez déjà et qui parle le protocole Actions, avec la permission de créer un jeton d'enregistrement de runner.
- Noms d'hôtes :
ci.example.compour le runner etregistry.example.compour le registre.
1. Un utilisateur non privilégié avec une plage d'espaces de noms
apt update && apt install -y podman buildah skopeo fuse-overlayfs uidmap slirp4netns git nodejs nginx apache2-utils
useradd -m -s /bin/bash runner
echo "runner:200000:65536" >> /etc/subuid
echo "runner:200000:65536" >> /etc/subgid
loginctl enable-linger runnerCes deux plages sont ce qui rend les conteneurs rootless possibles : le compte runner possède soixante-cinq mille identifiants subordonnés, donc un processus qui croit être root dans un conteneur est mappé vers un identifiant non privilégié à l'extérieur. Linger maintient la session utilisateur active pour que les unités systemd de ce compte survivent à la déconnexion.
Node est installé parce que la plupart des actions réutilisables sont en JavaScript et que le runner les exécute sur l'hôte dans cette configuration. Découvrir cela à l'étape du checkout est un détour courant de dix minutes.
2. Stockage rootless sur le NVMe
sudo -u runner mkdir -p /home/runner/.config/containers
sudo -u runner tee /home/runner/.config/containers/storage.conf <<EOF
[storage]
driver = "overlay"
graphroot = "/home/runner/.local/share/containers/storage"
[storage.options.overlay]
mount_program = "/usr/bin/fuse-overlayfs"
EOF
sudo -u runner podman info --format "{{.Store.GraphDriverName}} {{.Host.Security.Rootless}}"Cette dernière commande devrait répondre overlay true. Un pilote vfs à la place signifie que fuse-overlayfs est manquant, et que vfs copie chaque couche en entier à chaque build, ce qui transforme un pipeline de quatre-vingt-dix secondes en un pipeline de six minutes.
3. Un registre privé
apt install -y docker-registry
htpasswd -c /etc/docker/registry/htpasswd ci
certbot certonly --standalone -d registry.example.comLiez le registre à loopback et laissez nginx posséder tout ce qui est tourné vers l'extérieur, y compris l'authentification. Dans /etc/docker/registry/config.yml :
version: 0.1
storage:
filesystem:
rootdirectory: /srv/registry
delete:
enabled: true
http:
addr: 127.0.0.1:5000Le registre lui-même ne porte aucune information d'identification car il ne reçoit jamais de requête qui n'est pas déjà passée par le proxy. Un seul endroit pour vérifier un mot de passe vaut mieux que deux endroits qui peuvent ne pas être d'accord.
server {
listen 443 ssl;
server_name registry.example.com;
ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem;
client_max_body_size 0;
chunked_transfer_encoding on;
location /v2/ {
auth_basic "restricted";
auth_basic_user_file /etc/docker/registry/htpasswd;
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900s;
}
}client_max_body_size 0 supprime la limite de téléversement. Laissez la valeur par défaut de nginx en place et chaque couche de plus d'un mégaoctet échoue avec un 413 environ aux deux tiers d'un push, ce qui est un après-midi mémorable.
systemctl enable --now docker-registry nginx4. Le runner
cd /usr/local/bin
wget -O act_runner https://code.forgejo.org/forgejo/runner/releases/download/v6.3.1/forgejo-runner-6.3.1-linux-amd64
chmod +x act_runner
sudo -u runner mkdir -p /home/runner/.runner-cfg
cd /home/runner/.runner-cfg && sudo -u runner /usr/local/bin/act_runner generate-config > config.yamlModifiez le fichier généré pour que les jobs s'exécutent sur l'hôte plutôt que dans un conteneur, car Buildah fournit déjà l'isolation et l'imbrication n'ajoute que de la complexité :
runner:
capacity: 2
timeout: 1h
labels:
- "debian-13:host"
host:
workdir_parent: /home/runner/work
cache:
enabled: true
dir: /home/runner/cacheLa capacité de deux sur huit cœurs est délibérée : les builds sont largement sérialisés, et deux jobs concurrents avec quatre cœurs chacun se terminent plus tôt que quatre jobs se battant pour le même cache. Enregistrez-vous auprès de votre forge avec le jeton qu'elle a généré :
cd /home/runner/.runner-cfg
sudo -u runner /usr/local/bin/act_runner register --no-interactive \
--instance https://forge.example.com --token <registration token> \
--name ci-ams --labels debian-13:hostPuis une unité, s'exécutant sous le compte non privilégié :
[Unit]
Description=Actions runner
After=network-online.target
[Service]
User=runner
WorkingDirectory=/home/runner/.runner-cfg
ExecStart=/usr/local/bin/act_runner daemon --config /home/runner/.runner-cfg/config.yaml
Restart=always
Environment=HOME=/home/runner
Environment=XDG_RUNTIME_DIR=/run/user/3001
NoNewPrivileges=yes
[Install]
WantedBy=multi-user.targetRemplacez l'uid réel du compte runner dans XDG_RUNTIME_DIR ; id -u runner l'affiche. Podman rootless a besoin que ce répertoire existe, ce que le paramètre linger de l'étape un garantit.
systemctl daemon-reload && systemctl enable --now act-runner5. Un workflow qui construit et pousse
Dans le dépôt, à .forgejo/workflows/image.yaml :
on:
push:
branches: [main]
jobs:
image:
runs-on: debian-13
steps:
- uses: actions/checkout@v4
- name: Build
run: |
buildah bud --layers --format oci -t app:${{ github.sha }} .
- name: Push
run: |
buildah login -u ci -p ${{ secrets.REGISTRY_PASSWORD }} registry.example.com
buildah push app:${{ github.sha }} docker://registry.example.com/app:${{ github.sha }}
buildah push app:${{ github.sha }} docker://registry.example.com/app:latest--layers active la mise en cache des couches, ce qui fait la différence entre reconstruire vos dépendances à chaque commit et les reconstruire lorsqu'elles changent.
Vérifiez-le
Le runner devrait apparaître en ligne dans la liste des runners de la forge quelques secondes après le démarrage de l'unité. Ensuite, poussez un commit et observez le job depuis la machine :
journalctl -fu act-runnerLorsqu'il se termine, confirmez que l'image est réellement arrivée plutôt que de simplement signaler un succès :
skopeo inspect --creds ci:<password> docker://registry.example.com/app:latest | head -20
skopeo list-tags --creds ci:<password> docker://registry.example.com/appVous voulez le digest, la liste des couches et les deux tags. Maintenant, prouvez qu'elle s'exécute, sur une autre machine si vous en avez une sous la main :
podman run --rm registry.example.com/app:latest --versionEnfin, la revendication sur laquelle repose tout ce build. Pendant qu'un job est en cours, regardez à qui appartiennent les processus :
ps -eo user,pid,comm | grep -E "buildah|podman" | head
sudo -u runner podman info --format "{{.Host.Security.Rootless}}"Chaque processus appartient à runner, et la vérification de sécurité répond true. Rien dans le pipeline ne détient root, ce qui signifie qu'un script de build compromis obtient un compte non privilégié et un espace de noms, et non vos clés de registre et votre hyperviseur.
Ensuite
Le stockage du registre croît sans limite à moins que quelque chose ne supprime les anciens tags, donc exécutez registry garbage-collect sur une minuterie hebdomadaire une fois que vous avez une règle de conservation en laquelle vous croyez. Si les builds deviennent le goulot d'étranglement plutôt que les tests, la page de comparaison montre à quoi ressemble la taille supérieure ; plus de cœurs aide beaucoup moins que ce que la plupart des gens attendent, et des cœurs plus rapides aident beaucoup plus.