Dua belas build

Runner CI yang membangun image container tanpa root

Runner Actions yang di-host sendiri di instance Ryzen, membangun image OCI tanpa root dengan Buildah dan mendorongnya ke registry pribadi di mesin yang sama.

Yang akan dibangun

Sebuah runner CI yang terhubung ke forge Anda sendiri, yang melakukan checkout repositori, membangun image container tanpa daemon dan tanpa root, serta mengirim hasilnya ke registry privat yang berjalan di instance yang sama. Tidak ada socket privileged yang di-mount di mana pun, dan tidak ada apa pun dalam pipeline yang berjalan sebagai uid zero.

Sebagian besar pengaturan CI self-hosted menyelesaikan pembangunan image dengan me-mount daemon container host ke dalam job. Itu berfungsi dan menyerahkan setiap pipeline, termasuk yang seseorang buka pull request-nya, kendali penuh atas mesin. Buildah dalam mode rootless melakukan pekerjaan yang sama tanpa semua itu, dan pada core khusus tidak lebih lambat.

Sebelum memulai

  • Sebuah R-8. Pembangunan image adalah hal yang paling terikat single-core yang dijalankan sebagian besar tim, dan Zen 5 adalah yang tercepat per core yang kami jual. Empat ratus gigabyte NVMe menampung banyak layer.
  • Sebuah forge yang sudah Anda jalankan dan mendukung protokol Actions, serta izin untuk membuat token registrasi runner di dalamnya.
  • Nama host: ci.example.com untuk runner dan registry.example.com untuk registry.

1. Pengguna tanpa hak istimewa dengan rentang namespace

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 runner

Kedua rentang tersebut adalah yang memungkinkan container rootless: akun runner memiliki enam puluh lima ribu id bawahan, sehingga proses yang meyakini dirinya root di dalam container dipetakan ke id tanpa hak istimewa di luarnya. Lingering menjaga sesi pengguna tetap hidup sehingga unit systemd di bawah akun tersebut bertahan setelah logout.

Node diinstal karena sebagian besar action yang dapat digunakan kembali adalah JavaScript dan runner mengeksekusinya di host dalam konfigurasi ini. Menemukan itu pada langkah checkout adalah jalan memutar sepuluh menit yang umum.

2. Penyimpanan rootless di 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}}"

Perintah terakhir itu harus menjawab overlay true. Driver vfs sebagai gantinya berarti fuse-overlayfs hilang, dan vfs menyalin setiap layer secara penuh pada setiap build, yang mengubah pipeline sembilan puluh detik menjadi enam menit.

3. Registry privat

apt install -y docker-registry
htpasswd -c /etc/docker/registry/htpasswd ci
certbot certonly --standalone -d registry.example.com

Bind registry ke loopback dan biarkan nginx memiliki semua yang menghadap keluar, termasuk autentikasi. Di /etc/docker/registry/config.yml:

version: 0.1
storage:
  filesystem:
    rootdirectory: /srv/registry
  delete:
    enabled: true
http:
  addr: 127.0.0.1:5000

Registry itu sendiri tidak membawa kredensial karena tidak pernah menerima permintaan yang belum melewati proxy. Satu tempat untuk memeriksa kata sandi lebih baik daripada dua tempat yang bisa berselisih.

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 menghapus batas unggah. Biarkan default nginx tetap dan setiap layer di atas satu megabyte akan gagal dengan 413 sekitar dua pertiga jalan melalui push, yang merupakan sore yang tak terlupakan.

systemctl enable --now docker-registry nginx

4. 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.yaml

Edit file yang dihasilkan agar job berjalan di host daripada di dalam container, karena Buildah sudah menyediakan isolasi dan nesting keduanya tidak menambah apa pun selain kompleksitas:

runner:
  capacity: 2
  timeout: 1h
  labels:
    - "debian-13:host"
host:
  workdir_parent: /home/runner/work
cache:
  enabled: true
  dir: /home/runner/cache

Kapasitas dua pada delapan core adalah disengaja: build sebagian besar serial, dan dua job bersamaan dengan masing-masing empat core selesai lebih cepat daripada empat job yang berebut cache yang sama. Daftarkan ke forge Anda dengan token yang dihasilkannya:

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:host

Lalu sebuah unit, berjalan sebagai akun tanpa hak istimewa:

[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.target

Ganti dengan uid sebenarnya dari akun runner di XDG_RUNTIME_DIR; id -u runner mencetaknya. Podman rootless membutuhkan direktori itu ada, yang dijamin oleh pengaturan linger pada langkah pertama.

systemctl daemon-reload && systemctl enable --now act-runner

5. Workflow yang membangun dan mengirim

Di repositori, di .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 mengaktifkan caching layer, yang menjadi perbedaan antara membangun ulang dependensi pada setiap commit dan membangun ulang dependensi saat berubah.

Verifikasi

Runner akan muncul sebagai online dalam daftar runner forge dalam beberapa detik setelah unit dimulai. Lalu push sebuah commit dan tonton job dari mesin:

journalctl -fu act-runner

Setelah selesai, konfirmasikan bahwa image benar-benar tiba dan bukan hanya melaporkan keberhasilan:

skopeo inspect --creds ci:<password> docker://registry.example.com/app:latest | head -20
skopeo list-tags --creds ci:<password> docker://registry.example.com/app

Anda menginginkan digest, daftar layer, dan kedua tag. Sekarang buktikan berjalan, di mesin yang berbeda jika Anda memilikinya:

podman run --rm registry.example.com/app:latest --version

Terakhir, klaim yang menjadi dasar seluruh build ini. Selama job berjalan, lihat siapa pemilik proses:

ps -eo user,pid,comm | grep -E "buildah|podman" | head
sudo -u runner podman info --format "{{.Host.Security.Rootless}}"

Setiap proses milik runner, dan pemeriksaan keamanan menjawab true. Tidak ada apa pun dalam pipeline yang memegang root, yang berarti skrip build yang disusupi mendapatkan akun tanpa hak istimewa dan namespace, bukan kunci registry dan hypervisor Anda.

Setelahnya

Penyimpanan registry tumbuh tanpa batas kecuali sesuatu menghapus tag lama, jadi jalankan registry garbage-collect dengan timer mingguan setelah Anda memiliki aturan retensi yang Anda yakini. Jika build menjadi bottleneck daripada tes, halaman perbandingan menunjukkan seperti apa ukuran berikutnya; lebih banyak core membantu jauh lebih sedikit daripada yang orang harapkan, dan yang lebih cepat membantu jauh lebih banyak.

Siap saat Anda siap

Pilih kota. Pilih ukuran. Bayar dengan koin.

Tanpa formulir tentang siapa Anda, tanpa menunggu manusia untuk menyetujui, tanpa panggilan telepon untuk memverifikasi apa pun. Invoice dibersihkan dan kredensial masuk ke kotak masuk Anda.