مختبر: تشغيل Runners ذاتية الاستضافة مؤقتة لـ GitHub Actions باستخدام Actions Runner Controller

نظرة عامة

تُعد GitHub-hosted runners مشتركة ومؤقتة بشكل افتراضي — حيث يحصل كل مهمة (job) على آلة افتراضية جديدة يتم تدميرها بعد اكتمال المهمة. أما Self-hosted runners، فهي دائمة ومشتركة عبر عمليات تشغيل سير العمل المختلفة. يُشكّل هذا خطراً أمنياً كبيراً: حيث يمكن أن تتسرب الأسرار (secrets) والرموز (tokens) ومخرجات البناء (build artifacts) من مهمة إلى أخرى. يمكن لسير عمل مخترق أن يُلوّث بيئة التشغيل لجميع المهام المستقبلية.

يحل Actions Runner Controller (ARC) هذه المشكلة. ARC هو مشغّل أصلي لـ Kubernetes يمنحك runners مؤقتة، قابلة للتوسع التلقائي، ومبنية على الحاويات. تحصل كل مهمة على pod جديد يتم تدميره عند اكتمال المهمة — تماماً مثل GitHub-hosted runners، ولكنها تعمل على بنيتك التحتية الخاصة مع أدواتك وسياسات الشبكة الخاصة بك.

في هذا المختبر العملي، ستقوم بـ:

  • نشر ARC على مجموعة Kubernetes محلية
  • تكوين مجموعات runners مؤقتة قابلة للتوسع
  • إثبات العزل بين المهام (الميزة الأمنية الأساسية)
  • بناء صور runners مخصصة
  • تطبيق عزل مجموعات runners لفصل المهام
  • تكوين التوسع التلقائي
  • تطبيق سياسات الشبكة لتقييد وصول runners إلى الشبكة

المتطلبات الأساسية

قبل بدء هذا المختبر، تأكد من توفر ما يلي:

  • مجموعة Kuberneteskind أو minikube أو مجموعة سحابية مُدارة (EKS أو GKE أو AKS)
  • Helm 3 — التثبيت من helm.sh
  • kubectl — مُهيأ للتواصل مع مجموعتك
  • حساب GitHub — مع صلاحيات المسؤول على مستودع أو منظمة
  • GitHub App أو Personal Access Token (PAT) — بصلاحيات repo و admin:org (PAT) أو صلاحيات GitHub App المناسبة
  • Docker — لبناء صور runners المخصصة (التمرين 4)

إعداد البيئة

سنستخدم kind (Kubernetes in Docker) لإنشاء مجموعة محلية. هذا يجعل المختبر مستقلاً وسهل التنظيف.

إنشاء مجموعة kind

kind create cluster --name arc-lab

تحقق من أن المجموعة تعمل:

kubectl cluster-info --context kind-arc-lab

إنشاء مستودع GitHub للاختبار

أنشئ مستودعاً جديداً (مثل arc-lab-test) في حسابك على GitHub. أضف ملف سير عمل بسيط في .github/workflows/test.yml:

name: ARC Test Workflow
on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Hello from GitHub-hosted runner
        run: echo "This runs on a GitHub-hosted runner"

ادفع هذا إلى مستودعك. سنعدّله لاحقاً ليستهدف runners الخاصة بـ ARC.

التمرين 1: تثبيت ARC باستخدام Helm

يستخدم Actions Runner Controller v2 مخططات Helm لنشر مكونين: وحدة تحكم (controller) تدير دورة حياة pods الـ runner، ومجموعة واحدة أو أكثر من runner scale sets التي تسجل مع GitHub وتقبل المهام.

الخطوة 1: إضافة مستودع Helm

helm repo add actions-runner-controller \
  https://actions-runner-controller.github.io/actions-runner-controller
helm repo update

الخطوة 2: تكوين المصادقة

يحتاج ARC إلى المصادقة مع GitHub API. لديك خياران:

الخيار أ: GitHub App (مُوصى به للإنتاج)

أنشئ GitHub App في إعدادات منظمتك أو حسابك:

  1. انتقل إلى Settings → Developer settings → GitHub Apps → New GitHub App
  2. عيّن الصلاحيات التالية:
    • Repository: Actions (قراءة)، Administration (قراءة/كتابة)، Metadata (قراءة)
    • Organization: Self-hosted runners (قراءة/كتابة)
  3. أنشئ مفتاحاً خاصاً وقم بتنزيله
  4. ثبّت التطبيق على منظمتك أو مستودعك
  5. سجّل App ID و Installation ID

الخيار ب: Personal Access Token (أبسط للمختبرات)

أنشئ PAT (كلاسيكي) بصلاحيات repo و admin:org، أو PAT دقيق الصلاحيات مع أذونات Actions و Administration. لهذا المختبر، سنستخدم PAT للبساطة.

الخطوة 3: تثبيت وحدة تحكم ARC

helm install arc \
  actions-runner-controller/gha-runner-scale-set-controller \
  --namespace arc-systems \
  --create-namespace

تحقق من أن وحدة التحكم تعمل:

kubectl get pods -n arc-systems

يجب أن ترى مخرجات مشابهة لـ:

NAME                                     READY   STATUS    RESTARTS   AGE
arc-gha-runner-scale-set-controller-xxx  1/1     Running   0          30s

الخطوة 4: تثبيت Runner Scale Set

الآن انشر runner scale set التي تسجل مع مستودعك على GitHub:

helm install arc-runner-set \
  actions-runner-controller/gha-runner-scale-set \
  --namespace arc-runners \
  --create-namespace \
  --set githubConfigUrl="https://github.com/<org>/<repo>" \
  --set githubConfigSecret.github_token="<PAT>"

استبدل <org>/<repo> بمسار مستودعك و <PAT> برمز الوصول الشخصي الخاص بك.

تحقق من runner scale set:

kubectl get pods -n arc-runners

في هذه المرحلة، قد لا توجد pods للـ runner بعد — يستخدم ARC نموذج التوسع إلى الصفر. يتم إنشاء Pods فقط عند وضع المهام في قائمة الانتظار.

الخطوة 5: التحقق في GitHub

انتقل إلى مستودعك على GitHub: Settings → Actions → Runners. يجب أن ترى runner scale set مدرجة باسم arc-runner-set. تُظهر الحالة أنها جاهزة لقبول المهام.

التمرين 2: تشغيل سير عمل على ARC Runners

الآن حدّث سير العمل التجريبي ليستهدف runner scale set الخاصة بـ ARC بدلاً من GitHub-hosted runners.

الخطوة 1: تحديث سير العمل

عدّل .github/workflows/test.yml لاستخدام تسمية ARC runner:

name: ARC Test Workflow
on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  test:
    runs-on: arc-runner-set
    steps:
      - name: Hello from ARC runner
        run: |
          echo "This runs on an ephemeral ARC runner!"
          echo "Hostname: $(hostname)"
          echo "Runner OS: $(uname -a)"
      - name: Show environment
        run: env | sort

التغيير الرئيسي هو runs-on: arc-runner-set — والذي يتطابق مع اسم إصدار Helm لـ runner scale set.

الخطوة 2: تشغيل سير العمل

ادفع ملف سير العمل المحدّث أو استخدم زر “Run workflow” (workflow_dispatch) في واجهة GitHub Actions.

الخطوة 3: مراقبة Pod الـ Runner

راقب مساحة الأسماء arc-runners أثناء تشغيل سير العمل:

kubectl get pods -n arc-runners -w

سترى pod يتم إنشاؤه للمهمة:

NAME                          READY   STATUS    RESTARTS   AGE
arc-runner-set-xxxxx-runner   1/1     Running   0          5s

بعد اكتمال المهمة، يتم إنهاء Pod وإزالته:

NAME                          READY   STATUS      RESTARTS   AGE
arc-runner-set-xxxxx-runner   0/1     Completed   0          45s

شغّل kubectl get pods -n arc-runners مرة أخرى — اختفى Pod. هذا هو النموذج المؤقت: كل مهمة تحصل على حاوية جديدة، ويتم تدمير الحاوية عند انتهاء المهمة. لا يوجد استمرارية للحالة بين المهام.

التمرين 3: إثبات الأمان المؤقت

يوضح هذا التمرين الميزة الأمنية الأساسية للـ runners المؤقتة: عدم وجود تلوث بين المهام.

الخطوة 1: إنشاء سير عمل يكتب بيانات حساسة

أنشئ .github/workflows/ephemeral-test.yml:

name: Ephemeral Security Test
on: workflow_dispatch

jobs:
  write-secret:
    runs-on: arc-runner-set
    steps:
      - name: Write sensitive data
        run: |
          echo "SECRET_API_KEY=sk-prod-abc123xyz" > /tmp/secret-data
          echo "DB_PASSWORD=super-secret-password" >> /tmp/secret-data
          echo "Written sensitive data to /tmp/secret-data"
          cat /tmp/secret-data

  read-secret:
    runs-on: arc-runner-set
    needs: write-secret
    steps:
      - name: Attempt to read previous job data
        run: |
          echo "Checking if /tmp/secret-data exists from previous job..."
          if [ -f /tmp/secret-data ]; then
            echo "SECURITY RISK: Found data from previous job!"
            cat /tmp/secret-data
          else
            echo "SECURE: /tmp/secret-data does not exist."
            echo "Each job gets a fresh container — no cross-job contamination."
          fi

الخطوة 2: تشغيل سير العمل

شغّل سير العمل عبر workflow_dispatch. تكتب المهمة الأولى (write-secret) بيانات حساسة إلى /tmp/secret-data. تعمل المهمة الثانية (read-secret) في pod جديد وتحاول قراءة ذلك الملف.

الخطوة 3: التحقق من النتائج

في سجلات GitHub Actions، سترى:

  • مهمة write-secret: تكتب الملف بنجاح وتطبع المحتويات
  • مهمة read-secret: الملف غير موجود — المخرجات تُظهر SECURE: /tmp/secret-data does not exist.

كل مهمة عملت في pod منفصل تم إنشاؤه حديثاً. عندما تم تدمير pod write-secret، تم تدمير جميع البيانات — بما في ذلك الملف الحساس — معه.

لماذا هذا مهم

على runner ذاتي الاستضافة دائم، سيبقى ملف /tmp/secret-data على القرص عند تشغيل المهمة الثانية. يمكن لسير عمل خبيث في طلب سحب (pull request) قراءة الأسرار والرموز وبيانات الاعتماد التي تركتها المهام السابقة. مع runners المؤقتة، يتم القضاء على هذا المتجه الهجومي.

التمرين 4: صور Runner المخصصة

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

الخطوة 1: إنشاء Dockerfile مخصص

أنشئ Dockerfile لـ runner المخصص الخاص بك:

FROM ghcr.io/actions/actions-runner:latest

USER root

# Install build tools
RUN apt-get update && apt-get install -y \
    curl \
    wget \
    git \
    jq \
    unzip \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Install Go
RUN wget -q https://go.dev/dl/go1.22.4.linux-amd64.tar.gz \
    && tar -C /usr/local -xzf go1.22.4.linux-amd64.tar.gz \
    && rm go1.22.4.linux-amd64.tar.gz
ENV PATH="$PATH:/usr/local/go/bin"

# Install cosign
RUN curl -sSL -o /usr/local/bin/cosign \
    https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \
    && chmod +x /usr/local/bin/cosign

# Install Docker CLI (for Docker-in-Docker workflows)
RUN curl -fsSL https://get.docker.com | sh

USER runner

الخطوة 2: بناء الصورة ودفعها

# Build the image
docker build -t ghcr.io/<org>/custom-runner:latest .

# Authenticate to GitHub Container Registry
echo "<PAT>" | docker login ghcr.io -u <username> --password-stdin

# Push the image
docker push ghcr.io/<org>/custom-runner:latest

الخطوة 3: تكوين ARC لاستخدام الصورة المخصصة

أنشئ ملف قيم custom-runner-values.yaml:

githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
  github_token: "<PAT>"

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/<org>/custom-runner:latest
        command: ["/home/runner/run.sh"]
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "2"
            memory: "2Gi"

قم بترقية runner scale set بالصورة المخصصة:

helm upgrade arc-runner-set \
  actions-runner-controller/gha-runner-scale-set \
  --namespace arc-runners \
  -f custom-runner-values.yaml

الخطوة 4: التحقق من الأدوات المخصصة

أنشئ سير عمل يستخدم الأدوات المخصصة:

name: Custom Runner Tools Test
on: workflow_dispatch

jobs:
  verify-tools:
    runs-on: arc-runner-set
    steps:
      - name: Verify Go
        run: go version
      - name: Verify cosign
        run: cosign version
      - name: Verify Docker CLI
        run: docker --version

الفائدة الأمنية: من خلال بناء صورة runner الخاصة بك، تتحكم بالضبط في الأدوات والاعتماديات الموجودة في بيئة البناء. لا توجد ملفات ثنائية غير متوقعة، ولا برامج مثبتة مسبقاً لم توافق عليها، ويمكنك تثبيت كل أداة على إصدار محدد. يمكنك أيضاً فحص الصورة بحثاً عن الثغرات الأمنية قبل نشرها.

التمرين 5: عزل مجموعات Runner

تتمتع سير العمل المختلفة بمستويات ثقة مختلفة. لا ينبغي أن يكون لتحقق طلبات السحب (pull request) وصول إلى أسرار الإنتاج. تحتاج سير عمل النشر إلى الأسرار ولكن يجب أن تعمل فقط من الفرع الرئيسي. يتيح لك ARC تنفيذ هذا الفصل من خلال إنشاء مجموعات runner scale sets متميزة بتسميات وتكوينات مختلفة.

الخطوة 1: إنشاء Runner Scale Set للتحقق من طلبات السحب

أنشئ pr-runner-values.yaml:

githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
  github_token: "<PAT>"

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/<org>/custom-runner:latest
        command: ["/home/runner/run.sh"]
        env:
          - name: RUNNER_GROUP
            value: "pr-validation"
        resources:
          requests:
            cpu: "250m"
            memory: "256Mi"
          limits:
            cpu: "1"
            memory: "1Gi"
helm install arc-runner-pr \
  actions-runner-controller/gha-runner-scale-set \
  --namespace arc-runners \
  -f pr-runner-values.yaml

الخطوة 2: إنشاء Runner Scale Set للنشر

أنشئ deploy-runner-values.yaml:

githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
  github_token: "<PAT>"

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/<org>/custom-runner:latest
        command: ["/home/runner/run.sh"]
        env:
          - name: RUNNER_GROUP
            value: "deployment"
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "2"
            memory: "2Gi"
    serviceAccountName: deploy-runner-sa
    nodeSelector:
      runner-type: deployment
helm install arc-runner-deploy \
  actions-runner-controller/gha-runner-scale-set \
  --namespace arc-runners \
  -f deploy-runner-values.yaml

الخطوة 3: تكوين سير العمل للعزل

استخدم تسميات runner مختلفة بناءً على محفّز سير العمل:

name: CI/CD Pipeline
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  validate:
    if: github.event_name == 'pull_request'
    runs-on: arc-runner-pr
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: make test
      - name: Run linter
        run: make lint

  deploy:
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: arc-runner-deploy
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: make deploy
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

ينفذ هذا فصل المهام على مستوى الـ runner. تعمل مهام التحقق من طلبات السحب على runners ليس لها وصول إلى أسرار النشر أو شرائح الشبكة المتميزة. تعمل مهام النشر على مجموعة منفصلة من runners التي تمتلك بيانات الاعتماد اللازمة والوصول إلى الشبكة، ولكنها تعمل فقط عند الدفع إلى main.

التمرين 6: التوسع التلقائي

يدعم ARC التوسع التلقائي بشكل أصلي. يتم إنشاء pods الـ runner عند الطلب وتدميرها عند عدم النشاط. يمكنك تكوين الحد الأدنى والأقصى للنسخ للتحكم في التكلفة والاستجابة.

الخطوة 1: تكوين معلمات التوسع التلقائي

حدّث ملف قيم runner scale set الخاص بك ليشمل معلمات التوسع:

githubConfigUrl: "https://github.com/<org>/<repo>"
githubConfigSecret:
  github_token: "<PAT>"

minRunners: 0
maxRunners: 10

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        command: ["/home/runner/run.sh"]
helm upgrade arc-runner-set \
  actions-runner-controller/gha-runner-scale-set \
  --namespace arc-runners \
  -f autoscale-values.yaml

الخطوة 2: توليد الحمل

أنشئ سير عمل يُطلق عدة مهام متوازية:

name: Autoscale Test
on: workflow_dispatch

jobs:
  parallel-job:
    runs-on: arc-runner-set
    strategy:
      matrix:
        id: [1, 2, 3, 4, 5]
    steps:
      - name: Simulate work
        run: |
          echo "Job ${{ matrix.id }} running on $(hostname)"
          sleep 60

شغّل سير العمل هذا وراقب توسع الـ pods:

kubectl get pods -n arc-runners -w

سترى خمس pods يتم إنشاؤها — واحدة لكل مهمة matrix:

NAME                              READY   STATUS    RESTARTS   AGE
arc-runner-set-abcde-runner       1/1     Running   0          5s
arc-runner-set-fghij-runner       1/1     Running   0          5s
arc-runner-set-klmno-runner       1/1     Running   0          5s
arc-runner-set-pqrst-runner       1/1     Running   0          5s
arc-runner-set-uvwxy-runner       1/1     Running   0          5s

بعد اكتمال المهام (60 ثانية)، يتم إنهاء جميع الـ pods. تعود مساحة الأسماء إلى صفر pods.

الخطوة 3: تكوين تأخير تقليص الحجم

لتحسين التكلفة، قد ترغب في بقاء الـ pods جاهزة لفترة قصيرة بعد اكتمال المهمة. هذا يتجنب تأخر البدء البارد لأحمال العمل المتقطعة. سلوك التوسع إلى الصفر في ARC هو الخيار الافتراضي والأكثر أماناً. إذا كنت تحتاج runners جاهزة، اجعل النافذة قصيرة (أقل من 5 دقائق) وتأكد من أن الوضع المؤقت لا يزال مفروضاً.

التمرين 7: سياسات الشبكة لـ Runners

تتيح لك Kubernetes NetworkPolicies تقييد وصول pods الـ runner إلى الشبكة. هذا دفاع حاسم ضد تسريب البيانات من عمليات البناء المخترقة.

الخطوة 1: إنشاء NetworkPolicy

طبّق NetworkPolicy التالية على مساحة الأسماء arc-runners:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: runner-egress-policy
  namespace: arc-runners
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    # Allow DNS resolution
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Allow GitHub API and Actions services
    - to:
        - ipBlock:
            cidr: 140.82.112.0/20
        - ipBlock:
            cidr: 143.55.64.0/20
        - ipBlock:
            cidr: 185.199.108.0/22
        - ipBlock:
            cidr: 4.0.0.0/8
      ports:
        - protocol: TCP
          port: 443
    # Allow your container registry (example: ghcr.io)
    - to:
        - ipBlock:
            cidr: 140.82.112.0/20
      ports:
        - protocol: TCP
          port: 443
    # Allow your artifact storage (replace with your CIDR)
    # - to:
    #     - ipBlock:
    #         cidr: 10.0.0.0/8
    #   ports:
    #     - protocol: TCP
    #       port: 443
kubectl apply -f runner-network-policy.yaml

ملاحظة: تنشر GitHub نطاقات IP الخاصة بها على https://api.github.com/meta. استخدم نطاقات actions و api. النطاقات CIDR أعلاه هي أمثلة — تحقق من النطاقات الحالية وحدّثها وفقاً لذلك.

الخطوة 2: اختبار NetworkPolicy

أنشئ سير عمل يحاول الوصول إلى عنوان URL خارجي:

name: Network Policy Test
on: workflow_dispatch

jobs:
  test-network:
    runs-on: arc-runner-set
    steps:
      - name: Test GitHub API (should work)
        run: curl -s -o /dev/null -w "%{http_code}" https://api.github.com

      - name: Test external URL (should be blocked)
        run: |
          if curl -s --connect-timeout 5 https://evil-exfiltration-server.example.com; then
            echo "FAIL: External access was allowed"
            exit 1
          else
            echo "PASS: External access was blocked by NetworkPolicy"
          fi

عند تشغيل سير العمل هذا:

  • ينجح طلب GitHub API (HTTP 200) لأن NetworkPolicy تسمح بحركة المرور إلى نطاقات IP الخاصة بـ GitHub.
  • تنتهي مهلة طلب URL الخارجي ويفشل لأنه ليس في قائمة الخروج المسموح بها.

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

التنظيف

أزل جميع الموارد التي تم إنشاؤها خلال هذا المختبر:

# Delete Helm releases
helm uninstall arc-runner-set -n arc-runners
helm uninstall arc-runner-pr -n arc-runners
helm uninstall arc-runner-deploy -n arc-runners
helm uninstall arc -n arc-systems

# Delete namespaces
kubectl delete namespace arc-runners
kubectl delete namespace arc-systems

# Delete the kind cluster
kind delete cluster --name arc-lab

إذا أنشأت GitHub App لهذا المختبر، يمكنك حذفه من Settings → Developer settings → GitHub Apps. قم بإلغاء أي PATs أنشأتها.

النقاط الرئيسية

  • تقضي Runners المؤقتة على التلوث بين المهام. تحصل كل مهمة على حاوية جديدة — يتم تدمير الأسرار والرموز ومخرجات البناء عند اكتمال المهمة.
  • يوفر ARC فوائد runners ذاتية الاستضافة دون المخاطر الأمنية. تحصل على أدوات مخصصة ووصول إلى شبكة خاصة والتحكم في التكاليف مع الحفاظ على نموذج الأمان المؤقت.
  • تمنحك صور runner المخصصة تحكماً كاملاً في بيئة البناء. ثبّت إصدارات الأدوات، وافحص الثغرات الأمنية، وتخلص من مخاطر سلسلة التوريد من البرامج المثبتة مسبقاً.
  • يطبّق عزل مجموعات runner فصل المهام. تعمل سير عمل التحقق من طلبات السحب والنشر على مجموعات runner منفصلة بصلاحيات ووصول شبكي مختلف.
  • سياسات الشبكة هي طبقة دفاع حاسمة. يمنع تقييد خروج runner تسريب البيانات حتى لو تم اختراق خطوة بناء.
  • يقلل التوسع التلقائي إلى الصفر من التكلفة وسطح الهجوم. توجد pods الـ runner فقط لمدة المهمة — لا توجد بنية تحتية دائمة للصيانة أو التأمين.

الخطوات التالية

واصل تعزيز وضعك الأمني لـ CI/CD مع هذه الأدلة ذات الصلة: