Документация Эфемерные раннеры (CI / Kubernetes)

Эфемерные раннеры

Эфемерный раннер в Mockarty — это процесс раннера, жизненный цикл которого ограничен одной CI-задачей (или фиксированным окном простоя). Раннер регистрируется, берёт работу, потом завершается — и координатор сразу удаляет строку раннера из таблицы вместо того, чтобы оставлять её в статусе offline до следующей фоновой проверки.

Используйте эфемерные раннеры, когда:

  • Раннер живёт внутри CI-контейнера (GitLab job, GitHub Actions container job, Jenkins agent), который исчезает по окончании пайплайна.
  • Вы поднимаете раннеры по требованию в Kubernetes (HPA, Job, KEDA) и не хотите, чтобы в admin UI копились «мёртвые» строки после завершения подов.
  • Хотите, чтобы CI-шаг чисто завершался через N секунд простоя — пусть раннер сам уйдёт, когда у него нет работы.

Долгоживущие раннеры (поднял один раз и забыл) сохраняют поведение по умолчанию — никакой опции включать не нужно.

Быстрый старт

Передайте EPHEMERAL=true при запуске раннера. По желанию добавьте IDLE_TIMEOUT, чтобы процесс завершился, когда работы нет.

Перед примерами задайте shell- или CI-переменную MOCKARTY_RUNNER_IMAGE точной неизменяемой ссылкой repository@sha256:... на образ раннера из проверенного release set. Это переменная примера, а не настройка сервера Mockarty. В air-gapped установке сначала перенесите образ в приватный registry; не подставляйте изменяемый тег latest.

Docker run

docker run --rm \
  -e API_TOKEN=mki_your_integration_token \
  -e COORDINATOR_ADDR=mockarty.example.com:5773 \
  -e RUNNER_NAME=ci-runner-${CI_JOB_ID} \
  -e LABELS=ci,gitlab,linux \
  -e EPHEMERAL=true \
  -e IDLE_TIMEOUT=120s \
  "$MOCKARTY_RUNNER_IMAGE"

Когда раннер закончит свои задачи (нет активных задач в течение 120 секунд), он чисто разрегистрируется и контейнер завершится. Строка раннера исчезнет из списка при следующем обновлении.

Docker Compose (одноразовый CI-помощник)

services:
  runner:
    image: ${MOCKARTY_RUNNER_IMAGE:?задайте неизменяемый repository@sha256 digest}
    restart: "no"
    environment:
      API_TOKEN: ${MOCKARTY_INTEGRATION_TOKEN}
      COORDINATOR_ADDR: ${MOCKARTY_COORDINATOR_ADDR}
      RUNNER_NAME: ci-${CI_PIPELINE_ID}-${CI_JOB_ID}
      LABELS: ci,gitlab,${CI_RUNNER_TAGS}
      EPHEMERAL: "true"
      IDLE_TIMEOUT: 90s

GitLab .gitlab-ci.yml

Зеркалируйте и зафиксируйте Docker CLI и DinD service в том же проверенном CI release set; замените оба digest placeholder в примере ниже.

performance-test:
  image: <PRIVATE_REGISTRY>/docker-cli@sha256:<DOCKER_CLI_IMAGE_DIGEST>
  services:
    - name: <PRIVATE_REGISTRY>/docker-dind@sha256:<DOCKER_DIND_IMAGE_DIGEST>
  variables:
    MOCKARTY_INTEGRATION_TOKEN: $MOCKARTY_RUNNER_TOKEN
  script:
    - |
      docker run --rm \
        -e API_TOKEN=$MOCKARTY_INTEGRATION_TOKEN \
        -e COORDINATOR_ADDR=mockarty.example.com:5773 \
        -e RUNNER_NAME=gitlab-$CI_PROJECT_NAME-$CI_JOB_ID \
        -e LABELS=ci,gitlab \
        -e EPHEMERAL=true \
        -e IDLE_TIMEOUT=300s \
        "$MOCKARTY_RUNNER_IMAGE"

Kubernetes Job

apiVersion: batch/v1
kind: Job
metadata:
  generateName: mockarty-runner-
spec:
  ttlSecondsAfterFinished: 60
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: runner
        image: <PRIVATE_REGISTRY>/mockarty-runner@sha256:<RUNNER_IMAGE_DIGEST>
        env:
        - name: API_TOKEN
          valueFrom:
            secretKeyRef: { name: mockarty-runner, key: token }
        - name: COORDINATOR_ADDR
          value: mockarty.example.com:5773
        - name: EPHEMERAL
          value: "true"
        - name: IDLE_TIMEOUT
          value: "180s"
        - name: LABELS
          value: "ci,k8s,linux"

Режимы «одна задача» и «слив» (--once / --drain)

Помимо окна ожидания у общего раннера (mockarty-runner) есть два явных режима CI-жизненного цикла. Их передают флагами командной строки (флаги главнее переменных окружения) или задают переменными:

Режим Флаг Переменные (каноническая / устаревшая) Что делает раннер
Одна задача --once MOCKARTY_RUNNER_ONCE=true / RUNNER_ONCE=true Берёт ровно одну задачу, дожидается её завершения, разрегистрируется, выходит с кодом 0.
Слив --drain MOCKARTY_RUNNER_DRAIN=true / RUNNER_DRAIN=true Не берёт ни одной новой задачи, дожидается всего, что уже исполняется в этом процессе, разрегистрируется, выходит с кодом 0.
# CI-контейнер, который выполняет ровно одну задачу и исчезает:
mockarty-runner --once

# Мягкая остановка, при которой раннер не должен взять ещё одну задачу:
mockarty-runner --drain

Режимы взаимоисключающие: --once --drain (или обе переменные сразу) — это ошибка конфигурации при старте, а не молчаливый выбор одного из них.

Сочетайте --once с CLAIM_TOKENS, когда CI-триггер поднимает раннер под один конкретный запуск: раннер заберёт ровно задачу с этим токеном, выполнит её и завершится.

Security-раннер (mockarty-redteam-runner) поддерживает те же контракты: --once (эквивалент --max-tasks=1) и --drain (отказывает новым задачам по dispatch/lease и дожидается своих текущих).

Что происходит при разрегистрации

Когда EPHEMERAL=true:

  1. Раннер вызывает DeregisterRunner при graceful shutdown (SIGTERM, истечение IDLE_TIMEOUT или штатное завершение DRAIN_TIMEOUT).
  2. Координатор удаляет строку из runner_agents (вместо перевода в status=offline).
  3. Список раннеров в admin UI больше не показывает этот раннер — никаких ручных чисток, никаких «призраков».

Если разрегистрация не удалась (координатор недоступен, сетевая ошибка), раннер пытается ещё до трёх раз с задержкой в одну секунду, прежде чем сдаться. Оставшиеся «осиротевшие» строки подберёт leader-only sweeper, который работает каждые 60 секунд на админ-ноде и удаляет эфемерные строки, чей последний heartbeat старше трёх минут. То есть даже жёсткое падение (kill -9, OOM, отказ узла) рано или поздно приведёт к очистке.

Долгоживущий (не ephemeral) раннер списан, но всё ещё виден в списке.

  • Удалите его явно: DELETE /api/v1/runners/<id> переводит строку в offline (повторное
    подключение с тем же токеном оживит её), а DELETE /api/v1/runners/<id>?purge=true
    удаляет строку целиком. Нужно право integrations write. Живой раннер с активным
    heartbeat просто появится снова — purge предназначен для раннеров, которых больше нет.

Переменные окружения

Переменная По умолчанию Описание
EPHEMERAL false Если true, помечает раннер как эфемерный. Разрегистрация становится hard-delete; leader-only sweeper подбирает упущенные строки через три минуты.
IDLE_TIMEOUT 0s Если > 0 и EPHEMERAL=true, раннер чисто завершится после указанного времени простоя (нет активных задач). Удобно для одноразовых CI-задач. Принимает Go duration (30s, 2m, 1h) или просто число — секунды.
MOCKARTY_EPHEMERAL_SWEEP_INTERVAL 60s (на стороне админ-ноды) Как часто leader-only sweeper проверяет устаревшие эфемерные строки. Уменьшайте только в тестах; в проде менять не нужно.
MOCKARTY_EPHEMERAL_SWEEP_STALE_AFTER 3m (на стороне админ-ноды) Сколько должен «молчать» heartbeat, чтобы sweeper счёл строку устаревшей. Увеличивайте только если у вас реально большой джиттер сети.

Что нужно знать

  • Заголовок X-Mockarty-Runner-Instance обязателен на каждом запросе от эфемерного раннера. Клиент Mockarty-раннера ставит его автоматически (UUID на процесс) — оператору ничего делать не нужно.
  • Метки остаются полезными и для эфемерных раннеров — комбинируйте EPHEMERAL=true с LABELS=ci,gitlab, и ваши CI-пайплайны смогут целиться в нужные label-выражения (например, ci & gitlab & !staging) без загрязнения долгоживущего флота.

Pull-режим — раннеры на машинах команды (только исходящие)

Раннеру не нужен входящий сетевой доступ. По умолчанию он работает в pull-режиме: сам дозванивается до админ-ноды, длинным опросом забирает задачу, выполняет её и отправляет результат обратно — каждое соединение идёт раннер → админ. Это именно то, что нужно на ноутбуке разработчика или CI-воркере за NAT/файрволом: открыт только исходящий доступ к админу, ничего входящего. Член команды может поднять раннер и взять на себя браузерные тесты, шарды нагрузки, мобильные тесты или сканы безопасности, не открывая ни одного порта.

Единственные две обязательные настройки — URL админа и токен раннера:

docker run --rm \
  -e MOCKARTY_ADMIN_URL=https://mockarty.example.com \
  -e MOCKARTY_RUNNER_TOKEN=mki_ваш_интеграционный_токен \
  -e MOCKARTY_RUNNER_NAMESPACES=team-a \
  -e MOCKARTY_RUNNER_LABELS=region=eu,env=dev \
  "$MOCKARTY_RUNNER_IMAGE"

Этого достаточно — pull-режим стоит по умолчанию, флаг транспорта указывать не нужно. Токен выпускается в Админ → Интеграции: создайте интеграцию типа Тестовый раннер и скопируйте её токен.

Общие настройки (одни и те же имена работают для всех типов раннеров):

Переменная По умолчанию Описание
MOCKARTY_ADMIN_URL — Базовый URL админа, к которому дозванивается раннер (https). Только исходящие.
MOCKARTY_RUNNER_TOKEN — Интеграционный токен; одновременно идентичность раннера.
MOCKARTY_RUNNER_MODE pull pull (исходящий long-poll, по умолчанию) — указывайте, только если осознанно нужен другой транспорт.
MOCKARTY_RUNNER_INSTANCE на процесс Различает несколько раннеров с одним токеном.
MOCKARTY_RUNNER_NAMESPACES — Пространства имён через запятую, которые обслуживает раннер.
MOCKARTY_RUNNER_LABELS — Метки-селекторы ключ=значение через запятую (region=eu,env=dev).
MOCKARTY_RUNNER_MAX_CONCURRENT по раннеру Потолок параллелизма.

Каждый тип раннера — та же картина со своим образом и парой специфичных ручек:

  • Обычный раннер (API/функциональные/нагрузка) — неизменяемый образ из MOCKARTY_RUNNER_IMAGE в примере выше.
  • Браузерный UI-раннер — образ browser-runner; capability ui-test объявляется автоматически.
  • Мобильный раннер — образ mobile-runner; добавьте RUNNER_PLATFORM=android и подключите устройство/эмулятор. Живой стрим экрана использует MOCKARTY_STREAM_MODE=relay (по умолчанию) — кадры идут через админ, поэтому стрим виден даже без входящей доступности раннера (WebRTC/TURN не нужны). MOCKARTY_STREAM_MODE=webrtc ставьте только в сети, где p2p-медиа способно соединиться.
  • Раннеры безопасности (сканеры) — образы redteam/exploit-runner; pull по умолчанию, callback-URL не нужен.

Сетевое требование: разрешите раннеру исходящие к админу (HTTPS). Входящее правило к раннеру в pull-режиме не требуется никогда.

Что видно в admin UI

  • Раннер с бейджем ephemeral в списке, пока он жив.
  • Строка исчезает, как только раннер разрегистрируется (или sweeper подберёт её через три минуты после последнего heartbeat).
  • История задач этого раннера остаётся в истории прогонов (Test Plans, Performance Tests) — результаты задач не зависят от строки раннера.

Решение проблем

Строка раннера не исчезает после завершения контейнера.

  • Посмотрите логи координатора на предупреждения Ephemeral runner DELETE failed. Самая частая причина — рестарт координатора между последней попыткой раннера разрегистрироваться и следующим тиком sweeper’а.
  • Подождите три минуты. Sweeper работает каждые 60 секунд и убирает устаревшие эфемерные строки, чей heartbeat старше порога MOCKARTY_EPHEMERAL_SWEEP_STALE_AFTER (по умолчанию 3 минуты).
  • Убедитесь, что EPHEMERAL=true действительно применился. Стартовая запись лога раннера содержит ephemeral=true, когда флаг включён.

Раннер завершается раньше, чем закончил работу.

  • Уменьшайте IDLE_TIMEOUT только если знаете, что раннер получит хотя бы одну задачу за это время. Типичный паттерн: IDLE_TIMEOUT=300s (5 минут) — достаточно, чтобы предыдущий шаг CI-пайплайна положил задачу в очередь, и достаточно мало, чтобы зависшие контейнеры исчезали.
  • IDLE_TIMEOUT=0s (по умолчанию) полностью отключает idle-watcher — раннер работает, пока его не завершит SIGTERM или родительский процесс.

Разрегистрация прошла, но строка остаётся ещё 3 минуты.

  • Это путь «страховка» (ошибка разрегистрации провалилась к sweeper’у). Во время этого окна строка безвредна — в in-memory registry раннера уже нет, и задачи на него не назначаются. Окно можно сократить через MOCKARTY_EPHEMERAL_SWEEP_STALE_AFTER, если ваши дашборды требуют чистого состояния.

Раннер подключился, но не берёт UI- или мобильные задачи.

  • Запустите встроенную проверку готовности на этой машине — ей не нужно подключение к админ-ноде, и она ничего не меняет:

    mockarty-runner doctor
    

    Она печатает честный отчёт о том, что ИМЕННО эта машина умеет обслуживать: ✓ ready — работает сейчас, ◐ auto — доустановится при первом использовании (лёгкий движок браузера ставится автоматически), ✗ needs action — называет, чего не хватает (например, adb not found).

  • Capabilities включаются сами. Имея лишь токен и URL админ-ноды, раннер зондирует хост и рекламирует каждую capability, которую реально может обслужить — руками ничего поддерживать не нужно. В частности:

    • Мобильный (Android) авто-включается, когда подключено устройство по adb (считаются только авторизованные устройства). Переопределите через RUNNER_MOBILE=true, чтобы эфемерный раннер ждал устройство, или RUNNER_MOBILE=false, чтобы отключить на CI-машине без телефона.
    • RUNNER_BROWSERS=true запрашивает настоящие браузеры: раннер сканирует уже установленные браузеры Playwright и предлагает каждый из них рядом с всегда доступным лёгким движком (он скачивается при первом использовании). Точный контроль остаётся через RUNNER_UI_ENGINES.