Перестаньте управлять раннерами вручную — управляйте кодом: подход на основе GitOps для построения эластичной CI/CD-инфраструктуры с помощью ARC и Argo CD.
Если вы когда-либо использовали GitHub Actions в промышленных масштабах, вы знаете, что опора исключительно на раннеры, предоставляемые GitHub, быстро начинает ограничивать. Производительность, стоимость и гибкость становятся реальными проблемами. Именно здесь на помощь приходят self-hosted runners (самостоятельно размещаемые раннеры) — они дают полный контроль над окружением, вычислительными ресурсами и масштабированием.
Однако управлять несколькими self-hosted раннерами вручную утомительно, особенно в среде Kubernetes. В этой статье я покажу, как автоматизировал этот процесс с помощью Argo CD ApplicationSet, сделав раннеры динамическими, декларативными и масштабируемыми.
|
Примечание
|
TL;DR: Раннеры от GitHub могут быть дорогими и негибкими. Объединив GitHub Actions Runner Controller (ARC) с Argo CD ApplicationSets, можно построить «фабрику раннеров», которая автоматически создаёт и масштабирует их на основе структуры Git-репозитория. Добавьте конфигурационный файл в Git → Argo CD создаст набор раннеров → GitHub получит дополнительные вычислительные мощности. |
Зачем нужны self-hosted раннеры?
Раннеры GitHub отлично подходят для небольших нагрузок: они запускаются мгновенно и поставляются с уже настроенным набором инструментов. Но когда рабочие процессы требуют большего, возникают ограничения:
-
Производительность и гибкость настройки: нельзя выбрать тип инстанса или контролировать предустановленные зависимости.
-
Масштабирование: лимиты на параллельное выполнение тормозят крупные рабочие процессы.
-
Безопасность и доступ к сети: раннеры GitHub не имеют прямого доступа к приватным сетям и внутренним ресурсам, что неприемлемо для чувствительных нагрузок и корпоративных окружений.
-
Стоимость: модель «плати за минуту» при работе больших команд, длинных сборках или простоях быстро приводит к огромным счетам.
Self-hosted раннеры решают эти проблемы, предоставляя полный контроль над окружением. С ними можно:
-
выбирать тип инстанса и ОС для ускорения сборок;
-
предустанавливать зависимости, сокращая время сборки;
-
безопасно подключаться к приватным сетям и внутренним ресурсам.
Тем не менее управлять ими вручную в нескольких окружениях или кластерах крайне неудобно. Именно поэтому я обратился к Argo CD.
Argo CD и ApplicationSet
Argo CD — GitOps-инструмент для Kubernetes, управляющий ресурсами декларативно. ApplicationSet — функция Argo CD, позволяющая генерировать несколько приложений Argo CD из единого шаблона на основе источников данных: Git-репозиториев, кластеров или пользовательских генераторов.
Объединив ApplicationSet с self-hosted раннерами, я получил возможность:
-
автоматически разворачивать раннеры для разных окружений и сценариев использования;
-
динамически масштабировать раннеры;
-
хранить всю конфигурацию в полностью декларативном виде.
Общая схема работы
Чтобы лучше понять процесс, рассмотрим диаграмму, которая показывает, как GitHub Runner ApplicationSet взаимодействует с вашим репозиторием, Helm-чартами и Kubernetes-кластером. Каждый config.yaml порождает набор раннеров (runner scale set) в своём пространстве имён, которое затем регистрируется в вашей организации GitHub.
ApplicationSet использует конфигурационные файлы из репозитория для развёртывания наборов раннеров в выделенных пространствах имён, которые регистрируются в вашей организации GitHub.
Детали реализации
1. Подготовка кластера Kubernetes и Argo CD
Для декларативного управления self-hosted раннерами будем использовать Argo CD. Если Argo CD уже установлен — этот шаг можно пропустить. В противном случае вот минимальная установка:
# Создать пространство имён для Argo CD
kubectl create namespace argocd
# Установить Argo CD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Опционально: открыть доступ к UI Argo CD
kubectl port-forward svc/argocd-server -n argocd 8080:443
В результате получаем работающий экземпляр Argo CD, готовый управлять self-hosted раннерами GitHub.
2. Установка CRD контроллера Runner Scale Set
GitHub Runner Scale Set Controller использует Custom Resource Definitions (CRD) для описания RunnerPool, RunnerSet и ScaleSet в Kubernetes. Если CRD уже установлены — этот шаг можно пропустить. В противном случае следуйте официальной документации GitHub. Не забудьте также настроить секреты для аутентификации в вашей организации GitHub, как описано по той же ссылке.
3. Создание ApplicationSet для self-hosted раннеров
ApplicationSet в Argo CD позволяет динамически генерировать несколько Applications из единого шаблона. Это идеально подходит для self-hosted раннеров: можно автоматически разворачивать наборы раннеров в нескольких окружениях.
Пример ApplicationSet:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: github-runners
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: "https://github.com/example-org/github-actions-runner-controller.git"
revision: main
files:
- path: 'environments/**/**/config.yaml'
template:
metadata:
name: 'runners-{{.path.basename}}'
spec:
project: github-runners
sources:
- repoURL: "ghcr.io/actions/actions-runner-controller-charts"
chart: gha-runner-scale-set
targetRevision: 0.12.1
helm:
valueFiles:
- "$$values/environments/{{index .path.segments 2}}/common-values.yaml"
- "$$values/environments/{{index .path.segments 2}}/{{index .path.segments 2}}/{{.path.basename}}/config.yaml"
ignoreMissingValueFiles: true
parameters:
- name: "ghaRunnerScaleSet.githubConfigSecret"
value: "{{.path.basename}}-runners-github-app-credentials"
- repoURL: "https://github.com/example-org/github-actions-runner-controller.git"
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: arc-{{index .path.segments 2}}
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: true
syncOptions:
- CreateNamespace=true
Разбор ApplicationSet
а. Настройки Go-шаблонов
goTemplate: true
goTemplateOptions: ["missingkey=error"]
-
Включает Go-шаблонизацию в ApplicationSet.
-
missingkey=errorозначает: если переменная шаблона отсутствует, синхронизация завершится ошибкой — это защищает от неверных конфигураций.
б. Генераторы
generators:
- git:
repoURL: "https://github.com/example-org/github-actions-runner-controller.git"
revision: main
files:
- path: 'environments/**/**/config.yaml'
-
Git-генератор сканирует репозиторий в поисках файлов, соответствующих шаблону пути.
-
Каждый найденный файл порождает новое Application.
-
Использование
**обеспечивает рекурсивный обход, что позволяет автоматически поддерживать несколько окружений и наборов раннеров.
в. Шаблон Application
template:
metadata:
name: 'runners-{{.path.basename}}'
-
Каждое создаваемое Application получает имя на основе имени конфигурационного файла (
.path.basename). -
Это позволяет разворачивать множество раннеров без написания отдельных манифестов для каждого.
г. Источники
sources:
- repoURL: "ghcr.io/actions/actions-runner-controller-charts"
chart: gha-runner-scale-set
targetRevision: 0.12.1
helm:
valueFiles:
- "$$values/environments/{{index .path.segments 1}}/common-values.yaml"
- "$$values/environments/{{index .path.segments 1}}/{{index .path.segments 2}}/{{.path.basename}}/config.yaml"
ignoreMissingValueFiles: true
parameters:
- name: "ghaRunnerScaleSet.githubConfigSecret"
value: "{{.path.basename}}-runners-github-app-credentials"
- repoURL: "https://github.com/example-org/github-actions-runner-controller.git"
targetRevision: main
ref: values
Первый источник (Helm-чарт):
-
Указывает официальный Helm-чарт GitHub Actions Runner (
gha-runner-scale-set) для развёртывания набора раннеров. -
valueFilesвыбираются динамически на основе пути к конфигурационному файлу в Git, что позволяет задавать индивидуальные настройки для каждого окружения или набора раннеров.
Например, если путь — environments/runners-ci/amd/ci-amd-l/config.yaml:
-
индекс 0:
environments -
индекс 1:
runners-ci -
индекс 2:
amd -
parametersиспользуются для передачи чувствительных значений — например, токена GitHub App — через Kubernetes-секреты. -
ignoreMissingValueFiles: trueгарантирует, что чарт будет развёрнут даже при отсутствии некоторых необязательных файлов с параметрами.
|
Внимание
|
Вы также можете создать собственный чарт на основе официального, если вам нужны дополнительные ресурсы — например, секреты для подключения к организации GitHub. Для простоты в данном примере мы используем официальный чарт. |
Второй источник (репозиторий со значениями):
-
Указывает на репозиторий с файлами параметров для раннеров.
-
ref: valuesпозволяет ссылаться на эти файлы из первого Helm-чарта через$$values, разделяя логику чарта и конфигурацию конкретного окружения.
д. Назначение
destination:
server: https://kubernetes.default.svc
namespace: arc-{{index .path.segments 1}}
-
Определяет, куда в Kubernetes будет развёрнуто Application.
-
Использует структуру пути в Git для автоматического выбора пространства имён для каждого окружения.
-
Обеспечивает изоляцию раннеров по окружениям.
е. Политика синхронизации
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: true
syncOptions:
- CreateNamespace=true
-
CreateNamespace=trueгарантирует создание пространства имён, если оно ещё не существует.
4. Начальное развёртывание наборов раннеров
Теперь, когда ApplicationSet настроен, можно приступать к созданию конфигурационных файлов, которые будут автоматически генерировать Applications для раннеров. Каждый набор раннеров описывается в файле config.yaml, который ApplicationSet обнаружит по пути в Git.
Структура каталогов
Пример организации репозитория:
environments/
├── runners-ci/
│ ├── amd/
│ │ ├── ci-amd-l/
│ │ │ └── config.yaml
│ │ ├── ci-amd-m/
│ │ │ └── config.yaml
│ │ └── ci-amd-s/
│ │ └── config.yaml
│ ├── arm/
│ └── common-values.yaml
└── runners-cd/
|
Примечание
|
|
Пример config.yaml
Конкретный пример для набора раннеров ci-amd-m:
ghaRunnerScaleSet:
runnerScaleSetName: "ci-amd-m"
minRunners: 1
maxRunners: 20
template:
spec:
containers:
- name: runner
image: my-awesome-image:latest
env:
- name: RUNNER_FEATURE_FLAG_EPHEMERAL
value: "true"
command: ["/home/runner/run.sh"]
resources:
requests:
cpu: "2000m"
memory: "8Gi"
ephemeral-storage: 6Gi
limits:
cpu: "2000m"
nodeSelector:
instance-type: spot
tolerations:
- key: "dedicated"
operator: "Equal"
value: "runners"
effect: "NoSchedule"
Этот config.yaml создаст пространство имён arc-runners-ci (согласно настройке destination: arc-{{index .path.segments 1}} в ApplicationSet).
Внутри этого пространства имён Argo CD развернёт набор раннеров ci-amd-m в соответствии с конфигурацией:
-
поды раннеров будут следовать спецификации контейнера, запросам и лимитам ресурсов, селекторам узлов и допускам (tolerations), заданным в шаблоне;
-
набор раннеров будет автоматически управлять от 1 до 20 раннеров, масштабируясь вверх и вниз в зависимости от очереди задач GitHub Actions;
-
общие настройки из
common-values.yamlприменяются, если они не переопределены вconfig.yamlконкретного раннера.
Таким образом, одного этого конфигурационного файла достаточно для получения полностью декларативного, изолированного по пространству имён и автоматически масштабируемого развёртывания self-hosted раннеров GitHub.
Заключение
Объединив Actions Runner Controller (ARC) с Argo CD ApplicationSets, мы ушли от ручного управления уникальными конфигурациями раннеров к по-настоящему эластичной и декларативной инфраструктуре.
Такая архитектура даёт инженерным командам ряд ключевых преимуществ:
-
Самообслуживание инфраструктуры: платформенная команда задаёт стандарт, а разработчики могут запросить новый тип раннера, просто открыв Pull Request с новым
config.yaml. -
Операционная эффективность: вместо управления десятками отдельных Argo Applications, ApplicationSet работает как фабрика, автоматически синхронизируя состояние кластера с репозиторием Git.
-
Оптимизация ресурсов: встроенное автомасштабирование ARC совместно с управлением ресурсами Kubernetes гарантирует оплату только за фактически использованные вычислительные мощности — при пустой очереди раннеры масштабируются до нуля.
Что дальше?
Описанная архитектура — лишь фундамент. Чтобы развить её дальше, можно рассмотреть следующее:
-
Спот-инстансы (Spot Instances): используйте taints и tolerations в
config.yaml, чтобы запускать нагрузки на более дешёвых спот-инстансах и существенно сократить расходы. -
Тонкая настройка безопасности: внедрите External Secrets Operator (ESO) для безопасного управления учётными данными GitHub App без записи чувствительных токенов в Git.
-
Кастомные образы раннеров: создавайте специализированные образы с предустановленными инструментами, чтобы сократить время «установки» (Setup) в CI-пайплайнах.
Масштабное управление CI/CD не должно быть ручным бременем. С GitOps ваши раннеры становятся такими же динамичными и масштабируемыми, как и приложения, которые они собирают.