Tự động co giãn GitHub Actions Runner trên Kubernetes với Actions Runner Controller (ARC)

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Bối cảnh & Lý do

Vào lúc 2:15 sáng thứ Năm, PagerDuty đánh thức tôi vì một bản hotfix production bị kẹt trong hàng đợi. Ba mươi bộ integration test suite đã làm nghẽn toàn bộ pipeline GitHub Actions của chúng tôi.

Chúng tôi chạm ngưỡng giới hạn 20 tiến trình đồng thời của GitHub-hosted runner, trong khi 4 runner tĩnh chạy trên EC2 c5.2xlarge chạm đỉnh 100% CPU. Trong khi đó, 64 core CPU nhàn rỗi trong cụm staging EKS ngay bên cạnh lại không hề được đụng tới. Vấn đề quá rõ ràng: cụm runner tĩnh đốt hàng nghìn USD mỗi dịp cuối tuần khi chẳng ai commit code, nhưng lại nghẽn cứng trong những đợt release cao điểm.

GitHub-hosted runner mặc định rất tiện lợi, nhưng hóa đơn thanh toán của đội ngũ tăng lên rất nhanh. Với mức giá 0,008 USD mỗi phút cho tài nguyên Linux tiêu chuẩn, việc tiêu tốn 50.000 phút build mỗi tháng sẽ nhanh chóng làm thâm hụt ngân sách.

Nghiêm trọng hơn, các hosted runner không thể truy cập vào database nội bộ, private registry hay cache container đã được làm ấm sẵn bên trong VPC. Dùng VM tĩnh có thể vượt qua các rào cản mạng này, nhưng chi phí vận hành 24/7 lại quá lãng phí. Giải pháp tối ưu hơn là tách biệt workload build khỏi các VM cố định và ủy quyền toàn bộ vòng đời runner cho một Kubernetes operator.

Actions Runner Controller (ARC) là một Kubernetes operator chính thức do GitHub hỗ trợ. ARC theo dõi hàng đợi workflow thông qua kết nối HTTPS long-polling và cấp phát các pod runner tạm thời (ephemeral) bên trong cụm của bạn theo nhu cầu. Pod sẽ bị hủy ngay khi job kết thúc. Không còn tình trạng dirty state, không lo xung đột dữ liệu đĩa giữa các job, và hoàn toàn không tốn chi phí tài nguyên tính toán khi lập trình viên đã hết giờ làm việc.

Cài đặt

Để triển khai kiến trúc tự động co giãn của ARC, bạn cần một cụm Kubernetes (v1.25+) và Helm 3.8+. ARC chia tách nhiệm vụ giữa hai Helm chart: một controller manager quản lý các Custom Resource, và các scale set đại diện cho từng worker pool riêng biệt.

1. Cấu hình xác thực GitHub

ARC xác thực với organization hoặc repository của bạn bằng GitHub App. Cách này giúp tránh được giới hạn nghiêm ngặt 5.000 request/giờ áp dụng cho Personal Access Token. Hãy đăng ký một GitHub App cho organization với các quyền tối thiểu sau:

  • Organization Permissions > Self-hosted runners: Read & write
  • Repository Permissions > Actions: Read-only
  • Repository Permissions > Metadata: Read-only
  • Repository Permissions > Checks: Read-only

Ghi lại App ID và Installation ID, sau đó tải về private key (.pem). Lưu các giá trị này vào một Kubernetes secret trong namespace của controller:

# Tạo namespace cho ARC
kubectl create namespace arc-systems

# Lưu thông tin xác thực vào Secret
kubectl create secret generic arc-github-app \
  --namespace=arc-systems \
  --from-literal=github_app_id="348210" \
  --from-literal=github_app_installation_id="42981045" \
  --from-file=github_app_private_key=/path/to/arc-app.private-key.pem

2. Triển khai ARC Operator

Cài đặt controller manager từ OCI registry của GitHub. Chart này cung cấp các Custom Resource Definition và logic điều khiển:

helm install arc \
  --namespace arc-systems \
  --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

Kiểm tra đảm bảo pod controller hoạt động ổn định trước khi tiếp tục:

kubectl get pods -n arc-systems -l app.kubernetes.io/name=gha-runner-scale-set-controller

Cấu hình

Sau khi operator đã chạy, hãy cấu hình một runner scale set tự động co giãn. Scale set chart này quản lý một listener pod gọn nhẹ có nhiệm vụ theo dõi hàng đợi job của GitHub và khởi tạo các runner tạm thời.

Triển khai Runner Set tự động co giãn

Trước tiên, tạo file runner-values.yaml với các giới hạn co giãn rõ ràng và mức tài nguyên chỉ định cho container:

githubConfigUrl: "https://github.com/your-org"
githubConfigSecret: "arc-github-app"

## Giới hạn tự động co giãn
minRunners: 0
maxRunners: 30

runnerGroup: "k8s-infra-runners"

template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        command: ["/home/runner/run.sh"]
        resources:
          requests:
            cpu: "1000m"
            memory: "2Gi"
          limits:
            cpu: "2000m"
            memory: "4Gi"

Cài đặt runner scale set chart bằng các giá trị cấu hình vừa tạo:

helm install k8s-runner-set \
  --namespace arc-runners \
  --create-namespace \
  -f runner-values.yaml \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set

Việc thiết lập minRunners: 0 đảm bảo số lượng pod giảm về 0 khi hàng đợi trống. Khi một job chỉ định runs-on: k8s-runner-set, listener sẽ phát hiện sự kiện hàng đợi và yêu cầu Kubernetes scheduler cấp phát một pod.

Chỉ định Runner trong Workflow

Điều hướng các job trong pipeline tới tên của scale set trong .github/workflows/ci.yml:

name: Kiểm thử tích hợp
on:
  push:
    branches: [ main ]
  pull_request:

jobs:
  test:
    runs-on: k8s-runner-set
    steps:
      - name: Checkout mã nguồn
        uses: actions/checkout@v4

      - name: Kiểm tra môi trường runner
        run: |
          echo "Đang chạy trên host: $(hostname)"
          free -m
          nproc

Kiểm tra & Giám sát

Kích hoạt workflow dispatch hoặc push một nhánh thử nghiệm để quan sát cách ARC phản hồi.

1. Quan sát hành vi co giãn của Pod

Theo dõi quá trình tạo pod trực tiếp trong runner namespace:

kubectl get pods -n arc-runners -w

Listener sẽ chạy liên tục. Khi có job được kích hoạt, runner pod chuyển trạng thái từ ContainerCreating sang Running, hoàn thành tác vụ và tự động kết thúc:

NAME                                           READY   STATUS        RESTARTS   AGE
k8s-runner-set-754b578d-listener               1/1     Running       0          12m
k8s-runner-set-runner-8f2jc                    1/1     Running       0          18s
k8s-runner-set-runner-8f2jc                    0/1     Completed     0          52s
k8s-runner-set-runner-8f2jc                    0/1     Terminating   0          54s

2. Kiểm tra log của controller khi gặp sự cố

Nếu workflow bị treo trong hàng đợi mà runner không thể lập lịch, hãy kiểm tra log của listener và controller để tìm lỗi phân quyền hoặc kết nối mạng:

# Kiểm tra log của operator manager
kubectl logs -n arc-systems -l app.kubernetes.io/name=gha-runner-scale-set-controller --tail=100

# Kiểm tra log của scale set listener
kubectl logs -n arc-runners -l app.kubernetes.io/component=runner-scale-set-listener

Phần lớn lỗi kết nối bắt nguồn từ RSA key bị sai định dạng bên trong Secret hoặc chưa cấp quyền Self-hosted runners: Read & write trong GitHub App.

3. Xử lý áp lực tài nguyên node với Karpenter

Kết hợp ARC với các công cụ tự động co giãn node như Karpenter hoặc Cluster Autoscaler. Khi 20 runner đồng loạt khởi chạy, các pod ở trạng thái pending sẽ kích hoạt Karpenter cấp phát các instance EC2 Spot trong chưa đầy 45 giây. Khi job hoàn tất, Karpenter sẽ drain và tắt các instance đó. Bạn vừa đạt được thông lượng CI đồng thời rất cao vào các giờ cao điểm làm việc, vừa không phải trả tiền cho các node nhàn rỗi qua đêm.

Share: