Платформенная инженерия Конвейер как платформенный сервис: общее против «у каждого своё»
0%

Конвейер как платформенный сервис: общее против «у каждого своё»

Конвейер как платформенный сервис: общее против «у каждого своё»

Первая сцена. Компания на тридцать продуктовых команд, около двухсот пятидесяти инженеров. Приходит требование: все образы, попадающие в прод, должны быть подписаны, к каждому нужен SBOM, базовый образ — только из внутреннего реестра. Платформенная команда садится считать объём работ и обнаруживает пятнадцать разных конвейеров: четыре поколения GitLab CI, два Jenkins (один без владельца), самописный скрипт на Python, который запускается с ноутбука тимлида, и три репозитория, где сборка живёт в Makefile, а деплой — в чужом аккаунте облака. Одиннадцать недель ходьбы по командам. Четыре команды к дедлайну не успевают, им выписывают исключение «до конца квартала», исключение живёт два года.

Вторая сцена, та же компания через год. Платформа сделала вывод и построила единый конвейер: один шаблон, все на нём. Шаблон — тысяча двести строк YAML, сорок входных параметров, среди них skip_tests, legacy_mode, custom_dockerfile_path и extra_kaniko_args. Флаг skip_tests: true стоит у семи команд, причём у трёх — со времён «мы временно, до конца спринта». Прогон на пул-реквесте занимает сорок семь минут. Три команды тихо завели свои workflow, потому что «в общий не влезаем», и на вопрос «почему не пришли» отвечают: приходили, ответ был «через два спринта».

Обе сцены — про одну и ту же ошибку в постановке вопроса. Вопрос «общий конвейер или у каждого свой» неразрешим, потому что он неправильный. Правильный: что именно в конвейере общее по своей природе, а что принадлежит команде и никогда не станет общим. Первая сцена — цена того, что общими не сделали инварианты. Вторая — цена того, что общими попытались сделать шаги.

Эта глава — про конвейер как платформенный сервис: у него есть пользователи, SLO, стоимость и метрики принятия. Про технику CI как таковую — конвейеры и их основы, современные CI-платформы, стратегии выката и тесты в CI в соседних треках; здесь мы не пересказываем, как настроить кэш, а разбираем, кто им владеет, во сколько он обходится и почему команды уходят.

Почему конвейер — самый заметный сервис платформы

У платформы много поверхностей, но конвейер бьёт по нервам сильнее всех по трём причинам.

Частота. С каталогом инженер сталкивается раз в квартал, с шаблоном сервиса — раз в полгода, с конвейером — по десять раз в день. Любой дефект здесь умножается на частоту. Тридцать секунд лишнего ожидания в шаге, который выполняется шестьсот раз в день, — это пять часов в сутки по компании.

Позиция в потоке. Конвейер стоит ровно между «я закончил» и «это работает у пользователей». Когда он сломан, останавливается не одна задача, а вся поставка: невозможно ни выкатить фичу, ни выкатить исправление того, что уже сломалось. Отсюда правило, которое стоит проговорить до первого инцидента: недоступность конвейера — это инцидент того же класса, что и недоступность прода, потому что она блокирует восстановление прода. Механика инцидентов и дежурств — в треке SRE.

Видимость авторства. Тесты пишет команда, а красный крестик показывает платформа. Из этого рождается вечный спор «вы всё время падаете» против «это ваши флаки», и без механизма атрибуции этот спор не разрешается никогда — к нему вернёмся отдельно.

Есть и четвёртое, менее очевидное: конвейер — единственное место, через которое гарантированно проходит каждое изменение. Это делает его соблазнительным местом для всего: проверок безопасности, лицензионного аудита, сбора метрик, согласований, отчётности перед регулятором. Соблазн понятен, и он же — главный механизм превращения золотого пути в забор, о котором шла речь в главе про золотой путь. Каждая новая обязательная проверка кажется бесплатной для того, кто её добавляет, и стоит минут для всех, кто через неё проходит.

Разделение владения: субстрат, инварианты, содержание, маршрут

Полезно разложить конвейер на четыре разных по природе слоя. Они по-разному масштабируются, по-разному ломаются и требуют разных владельцев.

  1. Субстрат — раннеры, очередь, сеть, кэш, реестр артефактов, выдача секретов и доступов. Это инфраструктура с эффектом масштаба: содержать её тридцатью командами по отдельности бессмысленно и дорого.
  2. Инварианты — что должно быть истинно про любой артефакт, попадающий в прод: подписан, есть SBOM, базовый образ из разрешённых, известен владелец, есть запись в каталоге. Это про гарантии компании, не про удобство.
  3. Содержание — что именно собирается и как тестируется. Это предметная область команды. Платформа, которая решает, какие юнит-тесты запускать у сервиса платежей, взяла на себя ответственность, которую не сможет нести.
  4. Маршрут — через какие окружения и в каком порядке едет изменение, кто и когда одобряет, как откатывается. Здесь общее и частное перемешаны: сама механика выката общая, а политика «кто нажимает» — командная, потому что дежурит команда.

Разделение владения конвейером и место проверки гарантий

Из этой раскладки следует главный технический вывод главы, который спасает от обеих сцен во вступлении:

Инварианты нужно проверять на приёмке, а не шагом конвейера.

Если «образ подписан» — это шаг sign в общем шаблоне, то любая команда, ушедшая со своего пути, автоматически теряет гарантию, и вам приходится делать конвейер обязательным, то есть забором. Если «образ подписан» — это правило на входе в реестр и в кластер (admission-контроль), то команде можно разрешить собственный конвейер: гарантия проверяется там, где артефакт применяется, а не там, где он произведён. Свобода на входе, строгость на выходе.

# Инвариант, живущий на приёмке, а не в чужом конвейере.
# Kyverno: в прод-namespace попадают только подписанные образы из внутреннего реестра.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce   # блокируем, а не только предупреждаем
  rules:
    - name: verify-signature
      match:
        any:
          - resources:
              kinds: ["Pod"]
              namespaces: ["prod-*"]
      verifyImages:
        - imageReferences: ["registry.internal/*"]
          attestors:
            - entries:
                - keyless:
                    # доверяем только identity нашего CI, а не «кому угодно из sigstore»
                    subject: "https://gitlab.internal/platform/ci/*"
                    issuer: "https://gitlab.internal"

Такое правило работает одинаково для образа, собранного общим конвейером, и для образа, собранного командой самостоятельно. Именно это превращает «у каждого своё» из угрозы в допустимый режим. Документация: Kyverno verifyImages, Sigstore keyless signing, уровни цепочки поставки — SLSA.

Таблица владения, которую полезно согласовать письменно и повесить рядом с документацией конвейера:

Слой Кто владеет Кто чинит в 3 часа ночи Что происходит при неправильном владельце
Раннеры, очередь, кэш платформа платформа у каждой команды свой пул: пятикратная переплата, никто не обновляет образы раннеров
Реестр, подпись, SBOM платформа платформа пятнадцать конвейеров и одиннадцать недель на внедрение подписи
Выдача доступов в облако платформа платформа долгоживущие ключи в переменных CI, компрометация без срока давности
Шаги сборки команда команда платформа отлаживает чужой Dockerfile и становится узким местом
Набор и пороги тестов команда команда платформа отвечает за чужой флаки-тест, при этом не может его исправить
Инварианты артефакта платформа платформа инвариант держится на дисциплине, то есть не держится
Маршрут выката общий механизм, командная политика команда «одобряет платформа» превращает её в отдел согласований
Классификация падений платформа платформа спор «ваши флаки против вашего CI» не заканчивается никогда

Отдельно проговорите границы поддержки. Формулировка, которая работает: «платформа отвечает за то, что ваш конвейер запустился, получил ресурсы, кэш, секреты и опубликовал артефакт; платформа не отвечает за то, что ваш код собирается и тесты зелёные». Без этой строчки платформенная команда за полгода превращается в общий отдел отладки чужих сборок — самый частый способ израсходовать её и не получить ничего. Это прямое следствие разговора о когнитивной нагрузке и границах в Team Topologies.

Три способа сделать общее — и почему выигрывает композиция

Общее можно раздавать четырьмя разными способами, и они отличаются на порядок по цене владения и обратимости.

Форма Что связывает вас с пользователем Цена владения Обратимость Где ломается
Скаффолд, генерация при создании сервиса момент генерации низкая высокая: файлы у команды дрейф — через год тридцать разных копий
Библиотека шагов, компоненты, составные действия версия, закреплённая командой средняя средняя: можно отцепиться требует настоящего версионирования и changelog
Единый монолитный шаблон с параметрами каждый прогон высокая низкая растёт флагами, ломает всех сразу
Генерация конвейера из контракта сервиса каждый прогон очень высокая очень низкая отлаживать приходится сгенерированный YAML
Свой CI-движок всё запредельная никакой оправдано в единицах компаний мира

Практическое правило: композиция вместо наследования. Монолитный шаблон — это наследование: команда наследует всё поведение и настраивает его флагами. Библиотека компонентов — композиция: команда берёт нужные кубики и складывает свой конвейер. Наследование даёт быстрый старт и медленную смерть от флагов; композиция требует дисциплины в проектировании контрактов и живёт годами.

Плохо — монолит, у которого растёт число входов:

# Шаблон платформы, версия «мы уже не помним»
# Признаки болезни видны без чтения кода: имена команд внутри общего шаблона
.build-and-deploy:
  variables:
    LANGUAGE: ""
    BUILD_TOOL: ""
    SKIP_TESTS: "false"
    RUN_E2E: "false"
    CUSTOM_DOCKERFILE: ""
    EXTRA_KANIKO_ARGS: ""
    LEGACY_DEPLOY: "false"
    # ... ещё 33 переменные
  script:
    - if [ "$LANGUAGE" = "go" ]; then make build; fi
    - if [ "$LANGUAGE" = "java" ] && [ "$BUILD_TOOL" = "gradle" ]; then ./gradlew build; fi
    - if [ "$CI_PROJECT_NAME" = "checkout" ]; then ./hacks/checkout-only.sh; fi   # приговор

Хорошо — узкие версионируемые компоненты, которые команда складывает сама:

# .gitlab-ci.yml продуктовой команды: конвейер собран из кубиков платформы
include:
  - component: gitlab.internal/platform/ci/go-build@2.4.1
    inputs: { go_version: "1.23", test_command: "make test" }
  - component: gitlab.internal/platform/ci/container-image@3.1.0
    inputs: { context: ".", base: "registry.internal/base/distroless:2024-11" }
  - component: gitlab.internal/platform/ci/deploy@1.9.2
    inputs: { service: "checkout", strategy: "canary" }

# Свой шаг команды — рядом, без разрешения платформы и без форка компонента
contract-tests:
  stage: test
  needs: ["go-build"]
  script: ["make pact-verify"]

Версии закреплены явно, обновления приезжают ботом (Renovate умеет это и для CI-компонентов), ломающее изменение обязано сопровождаться кодмодом от того, кто ломает. Аналогично устроены переиспользуемые workflow в GitHub Actions, компоненты в GitLab и shared libraries в Jenkins. Важная деталь, которую забывают: компонент без закрепления версии — это не компонент, а глобальная переменная; ссылка на @main означает, что вы ломаете тридцать команд одним мержем.

Как распознать, что общий шаблон уже не общий

Диагноз ставится по данным, а не по ощущениям. Соберите фактически заданные входы по всем репозиториям и посчитайте:

"""Диагностика общего шаблона конвейера: живы ли его входы и есть ли вообще общий случай.
На вход — по одному словарю фактически заданных входов на репозиторий."""

from collections import Counter
from typing import Iterable, Mapping, Any


def flag_report(repos: Iterable[Mapping[str, Any]], declared: list[str]) -> dict:
    used = Counter()          # сколько репозиториев задаёт каждый вход
    combos = Counter()        # какие комбинации входов встречаются
    n = 0
    for cfg in repos:
        n += 1
        keys = tuple(sorted(k for k in cfg if k in declared))
        combos[keys] += 1
        used.update(keys)

    dead = [k for k in declared if used[k] == 0]              # никто не задаёт — удалять
    single = [k for k, c in used.items() if c == 1]           # ровно один пользователь — это хук, а не флаг
    top_combo, top_count = combos.most_common(1)[0] if combos else ((), 0)
    return {
        "репозиториев": n,
        "объявлено входов": len(declared),
        "мёртвых входов": dead,
        "входов ради одной команды": single,
        "уникальных комбинаций": len(combos),
        "доля самой частой комбинации": round(top_count / n, 2) if n else 0.0,
    }

Время O(n·k) по числу репозиториев и объявленных входов, память O(k + c), где c — число уникальных комбинаций. Читается результат так:

{'репозиториев': 96, 'объявлено входов': 40, 'мёртвых входов': ['legacy_mode', 'extra_kaniko_args', ...],
 'входов ради одной команды': ['custom_dockerfile', 'skip_lint', 'jvm_agent'],
 'уникальных комбинаций': 71, 'доля самой частой комбинации': 0.09}

Семьдесят одна уникальная комбинация на девяносто шесть репозиториев и самая популярная — девять процентов. Общего случая нет: под одним шаблоном живут три-четыре разные задачи. Это ровно тот механизм объединения полей, который разобран в главе про абстракции: одна абстракция поверх трёх разных потребностей всегда вырождается в объединение всех полей. Здоровая картина выглядит иначе — пять-восемь комбинаций, самая частая покрывает 40–60 %.

Что делать с диагнозом. Не «переписать шаблон», а расщепить по типам нагрузки: HTTP-сервис, батч по расписанию, консьюмер очереди, библиотека, фронтенд-приложение, мобильная сборка. У каждого свой узкий контракт и своя честная документация; общая реализация — внизу, в компонентах субстрата (кэш, публикация, подпись). Общая реализация дешевле общего интерфейса. Мёртвые входы удаляются сразу, входы «ради одной команды» превращаются в объявленную точку расширения, доступную всем.

Конвейер — это прод: SLO, атрибуция, дежурство

Платформенная команда, которая не измеряет свой конвейер, узнаёт о его состоянии из мемов в общем чате. Конвейер — сервис, у него должны быть SLI и SLO по тем же правилам, что описаны в главе про SLI и SLO, и бюджет ошибок, который останавливает разработку новых фич платформы.

SLI Определение Ориентир для старта Почему именно так
Ожидание раннера p95 времени от постановки в очередь до старта job < 30 с в рабочие часы ожидание — чистая потеря, оно не производит ничего
Длительность PR-прогона p50 и p95 успешных прогонов на пул-реквесте p50 < 10 мин, p95 < 20 мин 10 минут — граница, после которой человек уходит из контекста
Доступность выката доля рабочего времени, когда команда может выкатить в прод 99,5 % за 30 дней считается по возможности выкатиться, а не по аптайму сервера CI
Падения по вине платформы доля прогонов, упавших не из-за кода команды < 1 % это и есть тот самый спорный показатель, см. ниже
Флаки доля прогонов, зелёных при перезапуске без изменений кода < 1,5 % владелец — команда, но измерять обязана платформа
Время восстановления конвейера MTTR инцидентов самого CI < 30 мин в рабочие часы остановка конвейера блокирует и починку прода

Обратите внимание на строчку про доступность. Считать её как аптайм сервера — самый распространённый способ обмануть себя: сервер жив, а выкатиться нельзя, потому что реестр не отдаёт слои, кончились раннеры или протух токен. Правильный знаменатель — рабочие минуты, в которые команда, захотевшая выкатиться, смогла бы это сделать. Проверяется синтетическим прогоном: раз в пять минут платформа сама собирает и выкатывает игрушечный сервис по полному маршруту и записывает результат.

Атрибуция падений — обязательный механизм, а не приятное дополнение

Пока падения не классифицированы, любой разговор о качестве конвейера — это обмен впечатлениями. Классификатор должен быть автоматическим, встроенным в конвейер и работать по одному жёсткому правилу: неопознанное падение по умолчанию относится на платформу. Правило кажется несправедливым и именно поэтому работает: оно создаёт у платформы стимул улучшать распознавание, а не прятать неудобное в корзину «прочее».

"""Классификация причины падения прогона. Порядок правил важен: первое совпадение выигрывает."""

PLATFORM = "platform"     # виновата платформа: инфраструктура, кэш, реестр, доступы
USER = "user"             # виноват код или конфигурация команды
FLAKY = "flaky"           # тот же коммит зелёный при перезапуске
EXTERNAL = "external"     # внешняя зависимость: чужой реестр, чужой API

RULES = [
    (PLATFORM, ("runner system failure", "no space left on device", "context deadline exceeded",
                "error dialing backend", "failed to pull image from registry.internal",
                "oidc token exchange failed", "cache upload failed")),
    (EXTERNAL, ("dial tcp: lookup registry-1.docker.io", "429 too many requests",
                "npm ERR! network", "proxy.golang.org: 502")),
    (USER,     ("compilation error", "test failed", "lint:", "policy violation:",
                "manifest validation failed")),
]


def classify(log_tail: str, rerun_same_sha_passed: bool | None) -> str:
    """log_tail — последние строки лога; rerun_same_sha_passed — известен ли исход перезапуска."""
    if rerun_same_sha_passed is True:
        return FLAKY                                  # тот же код, другой результат
    low = log_tail.lower()
    for label, needles in RULES:
        if any(n.lower() in low for n in needles):
            return label
    return PLATFORM                                   # неопознанное — на себя, это дисциплинирует

Событие с результатом отправляйте в общий поток телеметрии по семантическим соглашениям OpenTelemetry для CI/CD — чужая схема дешевле своей, и она сразу совместима с расчётом DORA-метрик и с инструментами вроде Four Keys. Минимальный набор атрибутов: идентификатор конвейера и задания, сервис, тип нагрузки, версия компонента платформы, длительность по шагам, метка причины падения.

Жизненный цикл прогона с точки зрения атрибуции удобно держать перед глазами:

Карантин флаки-тестов — отдельная тема, где платформа и команда должны разойтись правильно: платформа даёт механизм (детект, статистика, автоматический карантин, отчёт владельцу), команда чинит. Если карантин будет чинить платформа, у команд исчезнет стимул. Google описывал свою механику и порядок цифр в «Flaky Tests at Google»; практическая часть — в главе про тесты в CI.

Очередь, мощность и деньги

Раннеры — общий ограниченный ресурс, а значит, в системе есть очередь, и у очереди есть свойства, о которых надо думать заранее.

Шумный сосед. Одна команда с матрицей в двести job на каждый коммит съедает пул, остальные двадцать девять ждут. Каждая команда при этом действует разумно со своей колокольни — ускоряет собственную обратную связь; сумма разумных локальных решений даёт общую деградацию. Это учебный случай локальной оптимизации: пока метрика есть только у команды, оптимизировать общий ресурс некому. Лечится квотами по группам и отдельными классами обслуживания, а не просьбами в чате. Разумный минимум — три класса: hotfix (приоритетный, всегда есть свободная ёмкость), main (сборки основной ветки, деплой), pr (пул-реквесты, вытесняемые). Прогон на пул-реквесте, вытесненный ради хотфикса, — это правильное поведение системы, и это надо объявить, а не делать молча.

Пиковость. Нагрузка на CI повторяет рабочий день и особенно — вечер вторника и четверга перед релизом. Ёмкость под пик, оплачиваемая круглосуточно, — типичная переплата; автоскейлинг раннеров (Actions Runner Controller, autoscaling executors в GitLab) решает это, но добавляет холодный старт, который надо мерить и держать в пределах десятков секунд.

Экономика ожидания. Это тот самый счёт, без которого платформа превращается в налог. Посчитаем честно на нашей компании: шестьсот прогонов в день, сокращение p50 с 47 до 16 минут.

Куда уходит время от коммита до продакшна

Триста десять часов календарного ожидания в сутки — цифра, которую нельзя предъявлять как экономию, и это принципиальный момент честного счёта. Инженер, ждущий сборку, не сидит без дела: он переключается на ревью, читает задачу, отвечает в чате. Но переключение контекста не бесплатно, а самое дорогое ожидание — то, что стоит между «нашёл причину инцидента» и «исправление в проде». Рабочая практика: берите консервативный коэффициент блокирующего ожидания 0,3–0,5 и обосновывайте его вслух. Триста десять часов × 0,4 ≈ 124 часа в сутки, примерно 2700 часов в месяц, порядка шестнадцати человеко-месяцев. Против этого ставится честная стоимость: рост счёта за раннеры и кэш, скажем, с 6000 до 11 000 USD в месяц, плюс полтора инженера платформы на поддержку. Сделка очевидно хорошая — но она стала очевидной только после того, как обе стороны названы. Тот же расчёт при трёх продуктовых командах и шестидесяти прогонах в день даёт экономию, не покрывающую и половины ставки одного инженера, — и это правильный повод не строить платформенный конвейер, а обойтись общим шаблоном (подробно — в главе про небольшую компанию).

Приоритизировать улучшения по ощущениям бессмысленно; полезнее разложить их по частоте и боли:

Первым всегда идёт ожидание: пул раннеров, кэш, параллельность. Это дёшево и мгновенно заметно. Отбор тестов по изменённым файлам и удалённое кэширование сборки (Bazel remote caching и аналоги) дают больший выигрыш, но требуют дисциплины в описании зависимостей — это квартальная работа, а не спринт. Собственный портал запусков и собственный язык описания конвейеров стоят дорого и почти не влияют на поток — про них дальше.

CD как сервис: кто нажимает кнопку и где живут права

Continuous delivery добавляет к конвейеру то, чего нет в сборке: необратимость. Отсюда три вещи, которые платформа обязана дать как сервис, и одна, которую обязана не забрать.

Даёт: единый механизм выката (одна и та же механика для канареек, поэтапного расширения и отката), артефакт как единицу поставки (собрали один раз — двигаем по окружениям, а не пересобираем на каждом), автоматическую проверку после выката (сравнение показателей канарейки с базой). Не забирает: решение о выкате. Кнопку нажимает команда, потому что дежурит команда. Платформа, которая согласовывает релизы, становится отделом согласований и первым узким местом; подробности про стратегии — devops про CD и SRE про безопасность релиза.

Отдельный вопрос — где живут прод-права. Классическая ошибка: у CI есть постоянные ключи с правами администратора в облаке, потому что «иначе неудобно деплоить». Это делает конвейер самой привлекательной мишенью в компании: тот, кто может изменить workflow в любом репозитории, может всё. Здоровая схема: у CI нет постоянных прав вообще, он получает короткоживущий токен через федерацию OIDC под конкретный сервис и конкретное окружение (про OIDC в GitHub Actions), а лучше — вообще не ходит в прод: он публикует артефакт и обновляет декларацию, а выкат делает контроллер внутри кластера, который сам тянет изменения (GitOps-подход). Тогда компрометация CI не даёт прямого доступа в прод. Смежное — управление секретами и безопасность цепочки поставки.

Золотой путь в CI и процедура для тех, кому он не подходит

Есть команды, которым общий конвейер не подходит не из вредности, а по существу. Три честных примера, которые встречаются почти в каждой компании:

  • Мобильная сборка. Нужны macOS-раннеры, подпись сертификатами Apple, выкладка в TestFlight, прогоны на устройствах. Пересечение с бэкендовым конвейером — примерно ноль, кроме реестра артефактов и секретов.
  • ML-обучение. Прогон на GPU идёт четыре часа, артефакт — модель весом в десятки гигабайт, «тесты» — метрики качества на валидационной выборке. Общий шаблон с таймаутом в час не применим в принципе.
  • Крупный монорепозиторий фронтенда. Смысл конвейера — в отборе затронутых пакетов и удалённом кэше; общий шаблон, собирающий всё целиком, ухудшает время прогона в разы.

Попытка затащить эти три случая в общий шаблон и есть тот самый механизм появления сорока флагов. Правильная реакция — объявленная процедура съезда, устроенная так же, как в главе про золотой путь, но с конкретикой конвейера:

Уровень Что делает команда Что остаётся обязательным Что даёт платформа
0. На пути компоненты платформы + свои шаги в объявленных точках всё по умолчанию полная поддержка, обновления PR-ами
1. Своя сборка свои шаги сборки и тестов, публикация через компонент платформы подпись, SBOM, базовый образ, каталог поддержка публикации и выката, консультации
2. Свой конвейер всё своё, включая раннеры инварианты на приёмке: подпись, SBOM, владелец, запись в каталоге, события в телеметрию доступ к реестру, секретам, OIDC, шаблоны политик
3. Полный выход своя инфраструктура доставки только периметр безопасности и учёт расходов ничего, кроме консультаций; регистрируется в реестре исключений со сроком пересмотра

Ключевое свойство этой лестницы: спуск на уровень ниже не отключает платформу. Команда со своим конвейером всё равно получает секреты, попадает в каталог, шлёт события в общую телеметрию и видна в метриках DORA. Как только съезд означает потерю всего сразу, вы получаете теневую платформу: свой аккаунт в облаке, свои алерты, никакого аудита. Именно это описано во второй сцене вступления.

Каждый съезд регистрируется: причина, срок пересмотра, владелец со стороны команды. Реестр исключений с нулём записей при тридцати командах означает не порядок, а слепоту. И самое важное для платформы: реестр исключений — это её бэклог. Три команды с одинаковой причиной съезда — это заявка на новый компонент, а не три нарушителя.

Типовые провалы

Обёртка над облачным CI ради обёртки. Платформа делает ci build поверх GitHub Actions с собственным форматом описания. Проверка та же, что для любой абстракции (глава 05): сколько решений пользователь перестал принимать? Если обёртка транслирует те же поля с другими именами — она не убрала ни одного решения и стоит дорого: свой транслятор ошибок (без него пользователь видит чужие сообщения о вашем YAML), отставание от фич апстрима на месяцы, невозможность найти ответ в интернете, потому что термины ваши. Обёртка оправдана ровно тогда, когда за одним полем стоит несколько решений: strategy: canary вместо двухсот строк описания шагов выката — оправдана, image_name вместо image — нет.

Портал запусков, которым никто не пользуется. Инженер живёт в редакторе, терминале и пул-реквесте. Веб-интерфейс, дублирующий кнопку «перезапустить job», проигрывает по всем параметрам той кнопке, что уже есть в пул-реквесте. Портал начинает быть нужен там, где в PR ничего нет: сквозная история артефакта («этот образ собран из какого коммита, кем подписан, в каких окружениях крутится»), кросс-сервисный релиз, откат с одной кнопки, поиск владельца в три часа ночи. Мерьте не просмотры, а действия: если 95 % трафика — «посмотреть статус», вам нужна интеграция в мессенджер и в PR, а не портал. Backstage здесь — фреймворк, который надо собирать, обновлять и сопровождать, а не готовый продукт (что такое Backstage); заводить его ради витрины конвейера обычно не окупается.

Одна абстракция поверх трёх разных потребностей. Разобрано выше через число комбинаций входов. Признак-индикатор, который видно без анализа: в общем шаблоне встречается имя конкретного репозитория или команды.

Конвейер как склад политик. За два года в обязательную часть добавлено четырнадцать проверок: лицензии, SAST, DAST, секреты, зависимости, качество кода, соглашение об именах, отчёт для аудита. Суммарно двадцать минут на каждом пул-реквесте, половина отчётов не читается никем. Лечение: у каждой обязательной проверки должен быть владелец, сформулированный риск и дата пересмотра; тяжёлые проверки уезжают из PR-прогона в ночной или в предрелизный; блокирует только то, что действительно блокирующе. Подробнее — безопасность в конвейере и глава 10.

Платформа чинит чужие тесты. Начинается с доброты («давайте поможем»), заканчивается тем, что двое из шести платформенных инженеров заняты отладкой чужих сборок, а бэклог платформы стоит. Границы поддержки объявляются письменно, флаки уходят в карантин автоматически, задача на исправление заводится владельцу сервиса.

Миграция всех на новый CI без причины и без кодмода. «Мы переезжаем с Jenkins на новый CI» — это отъём двух-трёх недель у каждой из тридцати команд, то есть порядка года чистого инженерного времени. Такая миграция оправдана только с посчитанной выгодой, и правило то же, что в главе про миграции: кто ломает, тот и пишет автоматический перенос. Ручная миграция, разосланная письмом, растягивается на год и оставляет вечный хвост.

Свой CI-движок. Соблазн возникает, когда чужой инструмент не даёт нужного. Оценка честной цены: оркестратор задач, изоляция, кэш, артефакты, UI, права, интеграции, отказоустойчивость — это годы работы отдельной команды, и всё это время вы конкурируете с инструментами, в которые вложены сотни человеко-лет. Компаний, где это оправдано, в мире единицы.

Цена владения инструментами

Никакой инструмент не делает платформу; каждый добавляет обязательства. Полезно называть их вслух до внедрения — фильтром здесь работает эссе Дэна Маккинли «Choose Boring Technology».

Инструмент Что реально даёт Чем платите Когда оправдан
Управляемый CI (GitHub Actions, GitLab CI) не содержите control plane, экосистема готовых шагов лимиты, стоимость минут, синтаксис как форма привязки, сторонние действия как риск цепочки поставки почти всегда — стартовая точка по умолчанию
Jenkins зрелость, любая интеграция, полный контроль плагины и их совместимость, обновления, безопасность, дефицит желающих это поддерживать есть исторический парк и люди, которые его знают
Tekton, Argo Workflows конвейер как объекты Kubernetes, единая модель с остальной платформой вы становитесь оператором CI: раннеры, хранилище, UI, отладка в терминах CRD уже есть зрелая экспертиза по Kubernetes и нужен свой control plane
Dagger и подобные одинаковый прогон локально и в CI, конвейер на языке программирования ещё один слой и язык между инженером и CI, своя отладка сборка сложная и «работает в CI, не работает локально» — регулярная боль
Bazel и удалённый кэш серьёзное сокращение времени в монорепозитории описание зависимостей, миграция сборки, выделенные люди большой монорепозиторий, счёт времени сборки идёт на часы
Backstage каркас портала и каталога приложение на TypeScript, которое вы собираете и обновляете; плагины разного качества каталог уже наполняется автоматически, нужна витрина

Общее правило приёмки инструмента то же, что и для абстракции: назовите, какое пользовательское решение он убирает и во сколько человеко-часов в год обойдётся его сопровождение. Если ответ на первый вопрос — «он современный», сделки нет.

Метрики принятия конвейера

Полнота функций не измеряет ничего. Измеряет принятие — и знаменатель здесь важнее числителя. Требования к метрике здесь ровно продуктовые (метрики в продуктовом треке): она должна меняться от ваших действий, не считаться по собственным данным и не улучшаться от оттока недовольных. Общий подход к метрикам платформы разобран в главе про опыт разработчика; ниже — специфика конвейера.

Метрика Как считать Ловушка знаменателя
Доля деплоев через платформенный конвейер события деплоя из кластера и реестра, а не из вашего же CI если считать по своим прогонам, метрика равна единице по построению
Репозиториев со своим конвейером скан всех репозиториев организации на файлы CI, не ссылающиеся на компоненты «мы про них не знали» — самый частый источник расхождения
Доля прогонов на актуальной версии компонента телеметрия прогонов по версии форк компонента выглядит как использование, если смотреть по имени
Время до принятия новой версии медиана дней от релиза компонента до его появления у 80 % команд среднее прячет хвост из десяти команд, застрявших год назад
Тикеты «почини мой пайплайн» доля от всех обращений в платформу рост означает, что диагностика непонятна, а не что пользователи глупые
Флаг-обходы число активных skip_* и их возраст флаг, включённый «на спринт» год назад, — это ваш долг, а не их

Скан репозиториев стоит автоматизировать и запускать еженедельно — обход не приходит к вам сам:

# Ищем конвейеры, которые не используют компоненты платформы, и датируем их
for repo in $(gh repo list my-org --limit 1000 --json name -q '.[].name'); do
  gh api "repos/my-org/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null |
  while read -r wf; do
    if ! gh api "repos/my-org/$repo/contents/.github/workflows/$wf" --jq '.content' |
         base64 -d | grep -q 'my-org/platform-ci/'; then
      echo "$repo/$wf — свой конвейер, последнее изменение: $(gh api "repos/my-org/$repo/commits?path=.github/workflows/$wf&per_page=1" --jq '.[0].commit.committer.date')"
    fi
  done
done

Каждая найденная строка — не нарушение, а заявка на функциональность, которую вам не подали. Разговор начинается с вопроса «что в общем конвейере вам не подошло», а не «почему вы не на платформе».

И главный счёт, который платформенная команда обязана вести сама: сколько чужого времени сэкономлено. Три инженера платформы — это примерно 450 часов в месяц, не пошедших в продукт. Сокращение p50 прогона, снижение доли падений по вине платформы и исчезновение тикетов «выдайте раннер» переводятся в часы других команд по формуле из раздела про экономику. Если такой счёт не ведётся и не показывается, платформа незаметно превращается в налог — и первым это заметит не руководство, а инженер, который заведёт себе свой workflow.

Мини-итог

  • Вопрос «общий конвейер или свой» неразрешим. Разрешимый вопрос: что общее по природе — субстрат и инварианты; что своё — содержание шагов и решение о выкате.
  • Инварианты проверяются на приёмке (реестр, admission), а не шагом конвейера. Только так съезд с общего пути перестаёт означать выход из гарантий, а путь не превращается в забор.
  • Композиция вместо наследования: версионируемые компоненты с закреплёнными версиями и автообновлением, а не один шаблон с флагами. Число входов, растущее с каждой новой командой, — диагноз, а не особенность.
  • Общее у трёх разных нагрузок — это общая реализация, а не общий интерфейс. Расщепляйте по типу нагрузки, оставляя общими кэш, публикацию и подпись.
  • Конвейер — прод платформы: свои SLI и SLO, синтетический прогон, дежурство, атрибуция падений с правилом «неопознанное на себя».
  • Ожидание — самая большая статья расходов конвейера, но считать его в экономию нужно с консервативным коэффициентом и рядом со счётом за раннеры. Обе стороны обязаны быть названы.
  • Мобильные, ML- и монорепозиторные сборки не влезают в общий конвейер по существу. Для них нужна процедура съезда с сохранением платформенных гарантий, а не сорок первый флаг.
  • Инструменты — Jenkins, Tekton, Bazel, Backstage — имеют цену владения. Называйте, какое решение инструмент убирает и сколько стоит его сопровождение в год.
  • Принятие измеряется по знаменателю из внешних источников: события деплоя, скан репозиториев, возраст флагов-обходов. Свой конвейер в чужом репозитории — это заявка на функциональность.

Источники

Что дальше

Мы разобрали конвейер как платформенный сервис: где проходит граница владения, почему инварианты живут на приёмке, во что обходится ожидание и по каким признакам общий шаблон перестаёт быть общим. Один сквозной мотив здесь остался недоговорённым — телеметрия. Атрибуция падений, синтетический прогон, метрики принятия и события выката бесполезны, если каждая команда собирает их по-своему: сравнить нельзя, инцидент разбирать нечем, а платформа не видит собственных пользователей. Следующая глава — про то, как сделать наблюдаемость общим сервисом и не построить при этом ещё один забор.

Наблюдаемость как платформа: единые метрики, логи и трейсы

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов