Co to buduje
Runner CI podpięty do własnego forge, który pobiera repozytorium, buduje obraz kontenera bez demona i bez roota, i wypycha wynik do prywatnego rejestru działającego na tej samej instancji. Nigdzie nie jest zamontowany uprzywilejowany socket i nic w pipeline nie działa jako uid zero.
Większość samodzielnie hostowanych konfiguracji CI rozwiązuje budowanie obrazów przez montowanie demona kontenerów hosta do zadania. To działa i daje każdemu pipeline, w tym temu otwartemu przez pull request, pełną kontrolę nad maszyną. Buildah w trybie rootless robi to samo bez tego wszystkiego, a na dedykowanych rdzeniach nie jest wolniejszy.
Zanim zaczniesz
- R-8. Budowanie obrazów jest najbardziej ograniczonym pojedynczym rdzeniem zadaniem, jakie większość zespołów uruchamia, a Zen 5 jest najszybszy na rdzeń, jaki sprzedajemy. Czterysta gigabajtów NVMe pomieści bardzo wiele warstw.
- Forge, które już prowadzisz i które mówi protokołem Actions, oraz uprawnienie do utworzenia tokena rejestracji runnera w nim.
- Nazwy hostów:
ci.example.comdla runnera iregistry.example.comdla rejestru.
1. Nieuprzywilejowany użytkownik z zakresem przestrzeni nazw
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 runnerTe dwa zakresy umożliwiają kontenery rootless: konto runner posiada sześćdziesiąt pięć tysięcy podrzędnych identyfikatorów, więc proces, który wierzy, że jest rootem wewnątrz kontenera, jest mapowany na nieuprzywilejowany identyfikator na zewnątrz. Lingering utrzymuje sesję użytkownika przy życiu, więc jednostki systemd pod tym kontem przetrwają wylogowanie.
Node jest zainstalowany, ponieważ większość wielokrotnego użytku akcji to JavaScript, a runner wykonuje je na hoście w tej konfiguracji. Odkrycie tego na etapie checkout to częsta dziesięciominutowa objazdka.
2. Pamięć rootless na 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}}"To ostatnie polecenie powinno odpowiedzieć overlay true. Sterownik vfs zamiast tego oznacza brak fuse-overlayfs, a vfs kopiuje każdą warstwę w całości przy każdym budowaniu, co zamienia dziewięćdziesięciosekundowy pipeline w sześciominutowy.
3. Prywatny rejestr
apt install -y docker-registry
htpasswd -c /etc/docker/registry/htpasswd ci
certbot certonly --standalone -d registry.example.comPrzypnij rejestr do loopback i pozwól nginxowi obsługiwać wszystko skierowane na zewnątrz, w tym uwierzytelnianie. W /etc/docker/registry/config.yml:
version: 0.1
storage:
filesystem:
rootdirectory: /srv/registry
delete:
enabled: true
http:
addr: 127.0.0.1:5000Sam rejestr nie przenosi żadnych poświadczeń, ponieważ nigdy nie otrzymuje żądania, które nie przeszło już przez proxy. Jedno miejsce do sprawdzenia hasła jest lepsze niż dwa miejsca, które mogą się nie zgadzać.
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 usuwa limit przesyłania. Pozostaw domyślne ustawienie nginx na miejscu, a każda warstwa powyżej jednego megabajta zakończy się błędem 413 w około dwóch trzecich pusha, co jest pamiętnym popołudniem.
systemctl enable --now docker-registry nginx4. 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.yamlEdytuj wygenerowany plik, aby zadania działały na hoście, a nie wewnątrz kontenera, ponieważ Buildah już zapewnia izolację, a zagnieżdżanie dodaje tylko złożoność:
runner:
capacity: 2
timeout: 1h
labels:
- "debian-13:host"
host:
workdir_parent: /home/runner/work
cache:
enabled: true
dir: /home/runner/cachePojemność dwa na ośmiu rdzeniach jest celowa: budowanie jest w dużej mierze szeregowe, a dwa równoczesne zadania z czterema rdzeniami każde kończą się szybciej niż cztery zadania walczące o ten sam cache. Zarejestruj się w swoim forge z wygenerowanym tokenem:
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:hostNastępnie jednostka, działająca jako nieuprzywilejowane konto:
[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.targetPodstaw rzeczywiste uid konta runner w XDG_RUNTIME_DIR; id -u runner wypisze je. Rootless podman potrzebuje, aby ten katalog istniał, co gwarantuje ustawienie linger z kroku pierwszego.
systemctl daemon-reload && systemctl enable --now act-runner5. Workflow, który buduje i wypycha
W repozytorium, w .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 włącza cache warstw, co jest różnicą między przebudowywaniem zależności przy każdym commitcie a przebudowywaniem ich, gdy się zmienią.
Zweryfikuj to
Runner powinien pojawić się jako online na liście runnerów forge w ciągu kilku sekund od uruchomienia jednostki. Następnie wypchnij commit i obserwuj zadanie z maszyny:
journalctl -fu act-runnerGdy się zakończy, potwierdź, że obraz naprawdę dotarł, a nie tylko zgłosił sukces:
skopeo inspect --creds ci:<password> docker://registry.example.com/app:latest | head -20
skopeo list-tags --creds ci:<password> docker://registry.example.com/appChcesz digest, listę warstw i oba tagi. Teraz udowodnij, że działa, na innej maszynie, jeśli masz pod ręką:
podman run --rm registry.example.com/app:latest --versionNa koniec twierdzenie, na którym opiera się cała ta budowa. Podczas gdy zadanie działa, sprawdź, do kogo należą procesy:
ps -eo user,pid,comm | grep -E "buildah|podman" | head
sudo -u runner podman info --format "{{.Host.Security.Rootless}}"Każdy proces należy do runner, a kontrola bezpieczeństwa odpowiada true. Nic w pipeline nie ma roota, co oznacza, że skompromitowany skrypt budowania otrzymuje nieuprzywilejowane konto i przestrzeń nazw, a nie twoje klucze rejestru i twój hiperwizor.
Potem
Pamięć rejestru rośnie bez ograniczeń, chyba że coś usuwa stare tagi, więc uruchamiaj registry garbage-collect na cotygodniowym timerze, gdy już ustalisz regułę przechowywania, w którą wierzysz. Jeśli budowanie stanie się wąskim gardłem, a nie testy, strona porównawcza pokazuje, jak wygląda następny rozmiar; więcej rdzeni pomaga znacznie mniej, niż większość ludzi oczekuje, a szybsze pomagają znacznie więcej.