lenec ru

← все посты

Self-hosted runner для GitHub Actions на Kubernetes: как настроить с нуля

14K

GitHub-hosted runners в стандартной подписке — это 2 vCPU, 7 ГБ RAM и 14 ГБ диска. Этого хватает на маленькие репозитории, но как только у тебя крупный монорепо с тестами, докер-сборками и e2e — начинается боль. То раннер кончился по диску посреди сборки, то время ожидания свободного слота больше, чем сама сборка. Я переехал на self-hosted в k8s в трёх компаниях и расскажу, как делать это правильно в 2026.

Версия — k8s 1.30, ARC (Actions Runner Controller) 0.10, GitHub Enterprise Cloud / GitHub.com.

Зачем вообще self-hosted

Конкретные причины, по которым стоит съезжать с GitHub-hosted:

  • Нужны жирные машины — 16 vCPU, 64 ГБ RAM для линковки больших проектов или интеграционных тестов с БД.
  • Нужен GPU для ML CI.
  • Сборки требуют доступа в private VPC (тестовая БД, internal API).
  • Хочется кешировать большие docker-образы и npm/cargo/go-mod без выкачивания каждый раз.
  • Объём минут больше, чем платишь за GitHub-hosted.

Раньше self-hosted ставили на VMки, что плохо: один runner = одна VM, нагрузка скачет, ресурсы простаивают. На k8s раннеры запускаются эфемерно: пришёл job — поднялся pod, отработал — pod удалился. Это эластично и дёшево.

Какой контроллер выбрать

В 2026 фактически два варианта:

actions-runner-controller (ARC) от GitHub

Официальный контроллер, активно развивается, поддержка от GitHub. Использует архитектуру с двумя CRD: AutoscalingRunnerSet и RunnerSet. Всё через scale set — это новый API GitHub.

summerwind/actions-runner-controller (legacy)

Старый комьюнити-проект, который GitHub форкнул и продолжает. На него ещё много туториалов, но он уже legacy. Новые проекты — только официальный ARC.

Я ставлю ARC. Дальше в статье — про него.

Подготовка

1. Создаём GitHub App

Раньше можно было использовать PAT (personal access token), но ARC уже не рекомендует. Идёшь в Settings → Developer settings → GitHub Apps → New GitHub App:

  • Name: self-hosted-runners-arc
  • Homepage URL: что угодно валидное.
  • Webhook: отключаешь.
  • Repository permissions: Actions (Read and write), Administration (Read and write), Metadata (Read-only).
  • Organization permissions (если на уровне org): Self-hosted runners (Read and write).

После создания: запоминаешь App ID, генерируешь private key (.pem-файл скачается). Затем ставишь приложение на нужную организацию или конкретные репозитории.

2. Создаём Installation

В Install App кликаешь, выбираешь репозитории, и в URL увидишь installations/<number> — это Installation ID. Запомни.

3. Кладём секреты в k8s

kubectl create namespace arc-systems
kubectl create namespace arc-runners

kubectl create secret generic github-app-secret \
  --namespace=arc-runners \
  --from-literal=github_app_id=<APP_ID> \
  --from-literal=github_app_installation_id=<INSTALLATION_ID> \
  --from-file=github_app_private_key=./private-key.pem

private-key.pem — тот файл, который GitHub дал при создании App.

Установка ARC

Через Helm-чарт официальный, два релиза — controller и scale set.

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

Проверяем, что controller поднялся:

kubectl get pods -n arc-systems

Должен быть arc-gha-rs-controller-xxx в Running.

Создаём scale set

Scale set — это группа раннеров с общими настройками. Можно сделать несколько — разных размеров, для разных команд.

helm install arc-runner-set \
  --namespace arc-runners \
  --set githubConfigUrl="https://github.com/my-org" \
  --set githubConfigSecret=github-app-secret \
  --set minRunners=2 \
  --set maxRunners=20 \
  --set 'containerMode.type=kubernetes' \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --version 0.10.1

githubConfigUrl — на org или на конкретный репо. minRunners=2 держит warm-pool из 2 готовых раннеров, чтобы не было cold start. maxRunners=20 — потолок.

Используем в workflow

name: build
on: [push]
jobs:
  build:
    runs-on: arc-runner-set
    steps:
      - uses: actions/checkout@v4
      - run: make test

Имя arc-runner-set — это название Helm-релиза. Просто пишешь его в runs-on.

Тонкости и грабли

Docker внутри раннера

Если у тебя в job-ах docker build — нужен docker socket или dind. ARC поддерживает оба режима.

# values.yaml для chart
template:
  spec:
    containers:
      - name: runner
        image: ghcr.io/actions/actions-runner:latest
        command: ["/home/runner/run.sh"]
      - name: dind
        image: docker:dind
        args: ["dockerd", "--host=unix:///var/run/docker.sock"]
        securityContext:
          privileged: true
        volumeMounts:
          - name: dind-sock
            mountPath: /var/run
    volumes:
      - name: dind-sock
        emptyDir: {}

privileged: true — да, без этого dind не работает. На прод-кластере с другими нагрузками это страшновато. Я вырезаю dind в отдельный namespace и отдельный пул нод, помеченный taint-ом, чтобы туда не поехали приложения.

Альтернатива — kaniko или buildkit без privileged. Это правильнее, но требует переписывания pipeline под их CLI.

Кеширование

actions/cache хранит кеш в GitHub-managed storage. На GitHub-hosted это бесплатно, на self-hosted — тоже работает, но медленно: каждое скачивание идёт через интернет.

Локальный кеш — через actions/cache с альтернативным backend. Например, actions/cache@v4 с настройкой S3-backend или MinIO в кластере. Быстрее в десятки раз.

- uses: actions/cache@v4
  with:
    path: ~/.cargo
    key: cargo-${{ hashFiles('Cargo.lock') }}

Для больших монорепо я ставлю tespkg/actions-cache или everpcpc/actions-cache-s3 с MinIO в самом кластере — кеш качается за секунды.

Container mode kubernetes vs dind

В values есть containerMode.type=kubernetes или type=dind.

Kubernetes mode — это когда сам job запускается как отдельный pod рядом с раннером. Чище, безопаснее, но нужен hook-extension в раннере и поддержка от Actions.

Dind — старая схема, всё в одном pod-е. Проще, но privileged.

Для большинства случаев dind проще, для compliance-ориентированных — kubernetes mode.

Мониторинг

ARC отдаёт Prometheus-метрики. Что я смотрю:

  • arc_runners_count — сколько runners сейчас активно.
  • arc_runners_pending — сколько jobs ждут раннера. Если постоянно >0, увеличивай minRunners.
  • CPU/memory usage самих runner-pods через kubelet metrics.

Алёрты в Prometheus:

- alert: ARCRunnersStuck
  expr: arc_runners_pending > 5
  for: 10m
  annotations:
    summary: "5+ jobs ждут runner больше 10 минут"

Стоимость и насколько окупается

Считал по своему e-commerce-кейсу: средне 8000 jobs в месяц, средний job — 5 минут. На GitHub-hosted это ~80K минут, что выходит в $640/месяц на business-тарифе.

На self-hosted у меня 5 spot-нод m6a.2xlarge в AWS, постоянно 2 ноды и спайки до 5 — стоит около $200/месяц. Плюс время инженера на поддержку — пару часов в месяц.

Окупается, если у тебя минут больше пары тысяч в месяц. Меньше — оставайся на GitHub-hosted, не плоди технический долг.

Что запомнить

Self-hosted в k8s — это эластично и дёшево, если объём оправдывает. ARC от GitHub — текущий стандарт, не легаси-форки. GitHub App вместо PAT. Warm-pool через minRunners. Кеш — локальный S3, не GitHub-managed. Dind — privileged, выноси на отдельные ноды.

Куда копать: официальная документация ARC и репо actions/actions-runner-controller. Если нужны GPU — есть отдельный гайд по runtime-class и device plugins, я как-нибудь напишу.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.