GitOps для GitHub Actions: ARC и Argo CD ApplicationSet

Иллюстрация с чёрным котом-астронавтом — обложка статьи о GitHub Actions и Kubernetes GitOps

Перестаньте управлять раннерами вручную — управляйте кодом: подход на основе 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.

Диаграмма развёртывания наборов раннеров GitHub через Argo CD ApplicationSet в кластере Kubernetes

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/
Примечание
  • common-values.yaml содержит общую конфигурацию для всех наборов раннеров: метки по умолчанию, образы раннеров, лимиты масштабирования.

  • Каждый config.yaml в директории конкретного раннера определяет настройки развёртывания: количество реплик, префиксы имён раннеров и т.д.

  • ApplicationSet автоматически подхватит все файлы config.yaml по пути environments///config.yaml и создаст Application для каждого из них.

Пример 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 гарантирует оплату только за фактически использованные вычислительные мощности — при пустой очереди раннеры масштабируются до нуля.

Что дальше?

Описанная архитектура — лишь фундамент. Чтобы развить её дальше, можно рассмотреть следующее:

  1. Спот-инстансы (Spot Instances): используйте taints и tolerations в config.yaml, чтобы запускать нагрузки на более дешёвых спот-инстансах и существенно сократить расходы.

  2. Тонкая настройка безопасности: внедрите External Secrets Operator (ESO) для безопасного управления учётными данными GitHub App без записи чувствительных токенов в Git.

  3. Кастомные образы раннеров: создавайте специализированные образы с предустановленными инструментами, чтобы сократить время «установки» (Setup) в CI-пайплайнах.

Масштабное управление CI/CD не должно быть ручным бременем. С GitOps ваши раннеры становятся такими же динамичными и масштабируемыми, как и приложения, которые они собирают.

© 2026 meganuke