اثنا عشر بناءً

مشغّل CI يبني صور الحاويات بدون صلاحيات الجذر

مشغّل إجراءات مستضاف ذاتيًا على مثيل Ryzen، يبني صور OCI بدون صلاحيات الجذر باستخدام Buildah ويدفعها إلى سجل خاص على نفس الجهاز.

ما الذي يبنيه هذا الدليل

مشغّل CI (تكامل مستمر) مرتبط بمنصة السجع الخاصة بك، والذي يقوم بسحب مستودع، وبناء صورة حاوية بدون daemon وبدون صلاحيات root، ويدفع النتيجة إلى سجل خاص يعمل على نفس الخادم. لا يتم تركيب أي socket متميز في أي مكان، ولا يعمل أي شيء في خط الأنابيب بمعرف مستخدم صفر.

معظم إعدادات CI المستضافة ذاتيًا تحل مشكلة بناء الصور عن طريق تركيب daemon حاويات المضيف في المهمة. هذا يعمل، وهو يمنح كل خط أنابيب، بما في ذلك ذلك الذي فتح شخص ما طلب سحب ضده، تحكمًا كاملاً في الجهاز. Buildah في الوضع غير المتميز (rootless) يقوم بنفس المهمة بدون أي من ذلك، وعلى النوى المخصصة ليس أبطأ.

قبل أن تبدأ

  • خادم R-8. تعد عمليات بناء الصور أكثر الأشياء ارتباطًا بنواة واحدة التي تشغلها معظم الفرق، و Zen 5 هو الأسرع لكل نواة نبيعه. أربعمائة غيغابايت من NVMe تتسع لعدد كبير جدًا من الطبقات.
  • منصة سجع (forge) تديرها بالفعل وتدعم بروتوكول Actions، ولديك الإذن لإنشاء رمز تسجيل مشغّل فيها.
  • أسماء المضيفين: ci.example.com للمشغّل و registry.example.com للسجل.

1. مستخدم غير متميز مع نطاق 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

هذان النطاقان هما ما يجعل الحاويات غير المتميزة (rootless) ممكنة: يمتلك حساب runner خمسة وستين ألف معرف تابع، لذا فإن العملية التي تعتقد أنها root داخل حاوية يتم تعيينها إلى معرف غير متميز خارجها. يبقي الإعداد lingering جلسة المستخدم نشطة بحيث تنجو وحدات systemd تحت هذا الحساب من تسجيل الخروج.

تم تثبيت Node لأن معظم الإجراءات القابلة لإعادة الاستخدام مكتوبة بلغة JavaScript وينفذها المشغّل على المضيف في هذا التكوين. اكتشاف ذلك في خطوة checkout هو تحويلة شائعة تستغرق عشر دقائق.

2. تخزين غير متميز على 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}}"

يجب أن يكون رد الأمر الأخير overlay true. إن كان برنامج التشغيل vfs بدلاً من ذلك، فهذا يعني أن fuse-overlayfs مفقود، و vfs ينسخ كل طبقة بالكامل في كل بناء، مما يحول خط أنابيب من تسعين ثانية إلى ست دقائق.

3. سجل خاص

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

اربط السجل بـ loopback ودع nginx يمتلك كل شيء يواجه الخارج، بما في ذلك المصادقة. في /etc/docker/registry/config.yml:

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

السجل نفسه لا يحمل أي بيانات اعتماد لأنه لا يستقبل أبدًا طلبًا لم يمر عبر الوكيل. مكان واحد للتحقق من كلمة المرور أفضل من مكانين قد يختلفان.

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 حد الرفع. اترك الإعداد الافتراضي لـ nginx في مكانه وكل طبقة أكبر من ميغابايت واحد ستفشل مع 413 في حوالي ثلثي الطريق خلال الدفع، وهي بعد ظهر لا يُنسى.

systemctl enable --now docker-registry nginx

4. المشغّل

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

عدّل الملف المُنشأ بحيث تعمل المهام على المضيف بدلاً من داخل حاوية، لأن Buildah يوفر بالفعل العزل والتضمين لا يضيفان شيئًا سوى التعقيد:

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

السعة اثنان على ثمانية أنوية أمر متعمد: فالبنيات متسلسلة إلى حد كبير، ومهمتان متزامنتان بأربعة أنوية لكل منهما تنتهيان أسرع من أربع مهام تتنافس على نفس ذاكرة التخزين المؤقت. سجّل ضد منصة السجع الخاصة بك بالرمز الذي ولدته:

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

ثم وحدة، تعمل كحساب غير متميز:

[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

استبدل المعرّف الفعلي لحساب runner في XDG_RUNTIME_DIR؛ يطبع id -u runner ذلك. يحتاج Podman غير المتميز إلى وجود هذا الدليل، وهو ما يضمنه إعداد linger في الخطوة الأولى.

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

5. سير عمل يبني ويدفع

في المستودع، في .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 بتشغيل التخزين المؤقت للطبقات، وهو الفرق بين إعادة بناء اعتمادياتك في كل commit وإعادة بنائها عندما تتغير.

تحقق منه

يجب أن يظهر المشغّل كمتصل في قائمة المشغّلين في المنصة خلال ثوانٍ من بدء الوحدة. ثم ادفع commit وشاهد المهمة من الجهاز:

journalctl -fu act-runner

عندما تنتهي، تأكد من وصول الصورة بالفعل بدلاً من مجرد الإبلاغ عن النجاح:

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

تريد الـ digest وقائمة الطبقات والوسمين. ثم أثبت أنها تعمل، على جهاز آخر إذا كان لديك واحد في متناول اليد:

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

أخيرًا، الادعاء الذي يقوم عليه هذا البناء بأكمله. بينما تعمل مهمة، انظر من يملك العمليات:

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

كل عملية تنتمي إلى runner، وفحص الأمان يجيب true. لا شيء في خط الأنابيب يحمل صلاحيات root، مما يعني أن سكربت بناء مخترق يحصل على حساب غير متميز و namespace، وليس مفاتيح السجل والمشرف الخاص بك.

بعد ذلك

ينمو تخزين السجل بدون حدود ما لم يقم شيء ما بإزالة الوسوم القديمة، لذا قم بتشغيل registry garbage-collect على مؤقت أسبوعي بمجرد أن يكون لديك قاعدة احتفاظ تؤمن بها. إذا أصبحت البنيات هي عنق الزجاجة بدلاً من الاختبارات، فإن صفحة المقارنة تُظهر ما يبدو عليه الحجم التالي؛ النوى الأكثر تساعد أقل بكثير مما يتوقع معظم الناس، والأسرع تساعد أكثر بكثير.

جاهز عندما تكون

اختر مدينة. اختر الحجم. ادفع بالعملة الرقمية.

لا نماذج عن هويتك، ولا انتظار لموافقة بشرية، ولا مكالمة للتحقق من أي شيء. بمجرد تصفية الفاتورة، تصل بيانات الدخول إلى بريدك الإلكتروني.