За последние шесть лет испробовал множество аспектов работы с API-контрактами, не только с точки зрения проектирования, но и остальных этапов жизненного цикла этих артефактов.

Так-то API-контракт может быть не просто набором методов, которые «по-бырику» выставили, пока дальше код пишется, а что-то более зрелое и, соответственно, требующее более зрелого и продуманного подхода.

На что стоит обратить внимание при работе с API-контрактами внутри дружественной корпоративной среды множества разрабатываемых ИТ-систем?


Посмотрим не на сами API-контракты или их виды, а про то, как с ними можно работать.

Что это?

Базово контракт – это соглашение, которое стороны заключают, чтобы зафиксировать достигнутые договоренности.

API-контракт – это тоже соглашение, соглашение о структуре и поведении интерфейсов вашей системы или систем, ожидаемое всеми участниками такого соглашения.

Видов таких участников не так много, из основных:

  • Те, кто будут разрабатывать реализацию обработчиков, принимающих вызовы от других систем и сервисов, разработчики server side.

  • Те, кто будут разрабатывать реализацию обработчиков, выполняющих вызовы, разработчики client side.

  • Те, кто это всё будут тестировать ?

Разумно предположить, что на этом список не заканчивается. Но это, если позволите, ключевые участники соглашения, без которых в нём нет особой необходимости.

Немного про границы публичности

Контракты могут использоваться, как внутри ИТ-системы, чтобы упорядочить взаимодействие компонент, из которых она состоит. Так и вне ИТ-системы, чтобы упорядочить взаимодействие других ИТ-систем с ней или друг с другом. То есть из всего множества API-методов, которым владеет и реализует ИТ-система, часть этого множества доступна для вызова снаружи системы (публичное API), а остальная часть внутри (приватное API).

Если сталкивались с разработкой на Java/JVM, то возможно вспомните, что у класса не все методы доступны для вызова из другого класса, но все доступны для вызова изнутри, первые шли с модификатором public, вторые private (немного упрощаю, опуская другие модификаторы доступа).

Место API-контрактов в SDLC

Посмотрим на API-контракты, как на один из артефактов SDLC (процесса разработки программного обеспечения).

Взглянем на процесс примитивно:

Требования -> Архитектура -> Код -> Тестирование -> Развертывание

Далее рассмотрим два варианта, когда момент появления контрактов находится правее (после) кода и когда и когда API-контракты появляются в процессе левее (раньше) кода.

После кода или вместе с кодом (code first):

  • Внутри команды: группа тестирования сможет взяться за работы, зависящие от API-контрактов, только после окончания разработчиками своей работы.

  • Вне команды та же история. Разработчики других ИТ-систем вынуждены ожидать поставки API-контрактов, чтобы начать свои работы (проектирование, код, тесты).

Раньше кода (API first):

  • Внутри команды: возможность распараллелить работы по написанию кода и по подготовке тестов (в первую очередь тестов, связанных с вызовом API).

  • Вне команды: для других команд планирующих использовать публичное API другой ИТ-системы, появляется возможность разрабатывать свою часть параллельно с этой другой системой.

API first дает возможность распараллелить работы, как внутри команды (между разработчиками и тестировщиками), так и между командами. Code first, напротив затрудняет распараллеливание работ.

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

Работа с API контрактами

Базовые этапы жизненного цикла: проектирование и публикация -> хранение -> использование в коде

Коснемся трех основных аспектов, возникающих при работе с API-контрактами, и немного погрузимся в каждый из них.

Первый аспект: проектирование и публикация

Создали, проревьюили, провалидировали, назначили версию, опубликовали.

Речь про регулярную работу по описанию инкремента, ревью изменений, контроль обратной совместимости и проверка соблюдения внутренних правил и ограничений.

Что влияет на проектирование:

  •   Требования: бизнес-требования, требования стейхолдеров, требования к решению (эти больше всех).

  • Дополнительные правила и ограничения.

Требования будут основой для вносимых изменений. А дополнительные правила и ограничения идут как результат соблюдения процесса/регламента/правила, цель которых сделать работу с контрактами стабильной и консистентной в долгосрочной перспективе.

Разберем подробнее.

Проектирование

Коллаборативная работа по описанию и ревью результатов коллегами.

API-контракты представляют из себя текстовые файлы. А когда в разработке есть текстовые файлы, над которыми ведется совместная работа по модификации и ревью, git-платформы напрашиваются сами собой. Там и возможность ветвления состояний (ветки), и удобная работа с изменениями (коммитами), и дополнительные функции вокруг этого (сравнение веток, перенос коммитов, теги и т.п.). Правила работы с мердж-реквестами и возможности построения пайплайнов позволяют выстроить выпуск контрактов со всеми необходимыми приседаниями и проверками.

В целом, подход с API-контрактами, будет похож на подход с кодом, грубо:

  1. Создали ветку от основного стрима.

  2. Выполнили работу по описанию.

  3. Проревьюили.

  4. Залили ветку в основной стрим.

Gitlab, github, bitbucket – одни из самых распространенных решений среди git-систем, но выбор только ими, разумеется, не ограничен.

Валидация

Набор автоматических проверок на соблюдение различных правил и ограничений.

Проверку требований оставим на откуп этапу тестирования, а здесь пройдемся по немного другому аспекту.

Какие дополнительные правила могут быть предъявлены к контрактам:

  • Соответствие схеме (типу и версии). Контроль соблюдения схемы лучше автоматизировать, во-первых, это легко, во-вторых, вы избавляетесь от человеческого фактора.

  • Соответствие соглашению о версионировании.

  • Соответствие внутренним правилам и ограничениям заполнения полей. Опциональные поля из базовой схемы могут требовать заполнения по внутренним правилам (например: комментарии или описания полей, или примеры значений полей – это распространенный подход). Дополнительные поля, расширяющие базовую схему (например extensions OpenAPI).

  • Проверка работы генераторов кода. Так как распространенные схемы описания АПИ-контрактов имеют множество библиотек генерации кода для разнообразных языков программирования, возникает потребность проверки качества генерации, чтобы узнать о проблемах раньше, чем контракт будет передан в разработку. Всё и вся проверить нельзя (слишком затратно), но хотя бы для внутренней разработки имеет смысл.

Всё это (варианты валидации) отлично заворачивается в пайплайны git-систем, чтобы отработать между моментом попадания коммита в ветку и выполнением публикации готовых контрактов. Не забудьте позаботиться о формировании отчетов об ошибках для каждого типа валидации, так будет сильно проще устранять проблемные места в API-контрактах, обнаруженные автоматическими проверками.

Версионирование

Порядок управления версиями (сквозная или для каждого контракта индивидуальная).

Есть пара распространённых вариантов:

  1. Одна версия для всех API-контрактов в ветке (напомню, что там может лежать набор API-контрактов).

  2. Своя собственная версия для каждого API-контракта (как например в OpenAPI через свойство version в блоке info).

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

Далее под этой версией выполняется публикация всех контрактов. Будут определенные накладные расходы по хранению лишних артефактов, так как различие версий не гарантирует наличие изменений.

В дальнейшем можно перейти на раздельное версионирование каждого API-контракта по отдельности.

Раздельное версионирование, кроме того, что дает возможность управлять версиями выпускаемых контрактов раздельно для каждого, приносит сложности связанные с контролем наличия изменений. Каждый контракт, в котором были внесены правки, должен получить новую версию в соответствии с правилами, и только после этого опубликован. API-контракты, у которых изменений не было, остаются с прежней версией и не публикуются.

Публикация

Подготовка дистрибутива под формат, требуемый выбранным хранилищем артефактов, и его публикация туда.

Во время публикации выполняется отправка в место хранения и распространения.

При публикации, как и при управлении версиями, есть два граничных подхода:

  1. Всё упаковывать в единый дистрибутив. Это может быть удобным при использовании подхода с единой версией для всех публикуемых API-контрактов. На первых парах даже выглядит приемлемым уровнем простоты и скорости внедрения подхода управления контрактами.

  2. Заворачивать каждый API-контракт по отдельности. Это точно необходимо для подхода с индивидуальными версиями, но можно использоваться и для любого другого. Обычно при разработке сервиса вам в нём необходимы вполне конкретные API-контракты, и тащить всё остальное выглядит излишним.

А вот если необходимо ещё и разграничивать доступ (например, на чтение) к опубликованным API-контрактам по отдельности, то и публиковать их будет необходимо тоже по отдельности.

Второй аспект: выбор места хранения

Разместили, запретили изменения, сделали доступным для использования.

Что и где мы храним

Хранить необходимо опубликованные API-контракты, то есть те, что получились на этапе проектирования и были успешно пропущены через весь пайплайн выпуска.

Место хранения может предъявлять требования к не только формату файлов, но и к особенностям их идентификации. Например, maven repo от вас попросит, в дополнение к предоставленному zip-файлу, версию и пару идентификаторов, через которые он однозначно будет определять каждый уникальный экземпляр загруженного артефакта.

Использование своего приватного менеджера артефактов (например nexus от sonatype или artifactory от jfrog) распространенная практика в компаниях. Там и сборки кодика лежат, и контейнеров.

В качестве альтернативы хранилищ контрактов могут выступать различные реестры схем.

Ну и на крайний случай, всегда можно посмотреть в сторону своей кастомной реализации. Ведь в первом приближении вам просто надо хранить набор текстовых файлов ?

Кому это должно быть доступно

Давайте прикинем, какие могут быть стороны потребителей API-контрактов:

  • Команда ИТ-системы, чьи контракты хранятся. Внутри отдела или департамента. Самые близкие и доверенные пользователи API-контрактов, обычно у них есть доступ даже до исходников.

  • Команды других ИТ-систем, заинтересованных в использовании ваших API-контрактов. Внутри компании или набора ассоциированных друг с другом юр.лиц (крупные компании редко обходятся одним юридическим лицом). Политика безопасности не всегда разрешает открывать исходники между различными кадровыми объединениями (отделами, департаментами и т.п.).

  • Команды условно-дружественных компаний (поставщики, подрядчики, аутсорс). Почти посторонние люди, но они будут осуществлять поставки артефактов для вас. Это сторонняя компания, но поставляемые артефакты могут быть загружены в хранилища исходников или менеджер артефактов. А также прочитаны оттуда.

  • Команды третьих ИТ-систем (совершенно вам неизвестные). Все остальные компании (неопределенный круг сторонних ИТ-систем). То есть те о которых вы можете толком и не знать, но они заинтересованы в интеграции с вашим ИТ-решением и использовании API-контрактов.

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

Так же, часто внутренние ресурсы вообще скрыты за КСПД (корпоративной сетью передачи данных)/VPN.

Типы потребителей + правила безопасности = основные факторы влияющие на требования к порядку разграничения доступа.

Как хотим управлять доступом на изменения

Такс, к этому моменту мы уже вписали в контекст понимание разграничения по доступности чтения. Теперь давайте пройдемся по модификации: загрузка нового, изменение/удаление старого.

Разумно предположить, что никогда нельзя исключать вероятность появления ошибки, которая потребует от вас изменение уже загруженного API-контракта. Но до того, как вы кинетесь всё исправлять, вам необходимо определиться с подходом и круг потребителей влияет на это самым непосредственным образом. Чем он шире и неизвестнее для вас, те более строгие правила должны использоваться.

Хороший тон – это отсутствие модификаций уже загруженных версий API-контрактов, то есть гарантия иммутабельности хранимых даннх. Даже если возникает острое желание перезаписать существующий артефакт той же версией, что по сути подмена, стоит загрузить именно новую версию, а для совсем публичных сопроводить её внятным чендж-логом и причинами обновления.

Ваше хранилище API-контрактов должно поддерживать ограничение прав доступа на запись (вставку) не только новых контрактов, но и на перезапись (обновление) уже существующих.

Как будем получать API-контракт в код

Наибольшая полезность от API-контрактов будет если они не просто существуют, но и могут использоваться в исходниках кода для кодогенерации (моделей и методов). При выборе места хранения необходимо принимать во внимание, как именно API-контракт будет скачиваться к исходникам кода, чтобы его можно было использовать для кодогенерации. И желательно чтобы это было удобно.

Системы сборки приложений (билд фреймворки) «из коробки» умеют работать с целым рядом различных популярных репозиториев (например, с maven repo прекрасно работают сборщики JVM приложений типа gradle или maven). Если выбранное хранилище неизвестно вашему билд-фреймворку, то будет необходимо дополнительно реализовывать такую возможность, вероятно прямой работой через API-запросы к этому хранилищу. Это точно лучше, чем скачивать контракт каждый раз вручную.

Какие дополнительные функции хранилища контрактов необходимы

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

  • Различные виды поиска. Для случаев если потребителю API-контрактов необходимо предоставить возможность искать целевой артефакт среди опубликованных.

  • Возможность визуального рендеринга (например, Swagger UI). Хорошо закрывает потребность, когда необходимо предоставить визуализацию API-контракта. Не встречал блокирующей необходимости в этом, но Swagger UI для OpenAPI схем очень популярен.

Может получиться так, что какое-то одно решение все пожелания поддержать не сможет. В таком случае эти хотелки необходимо приоритезировать (например по MoSCoW), а ещё разделить между разными решениями или от каких-то вообще отказаться.

Третий аспект: использование в исходниках кода.

Скачали к сорцам, сгенерировали по нему модели и интерфейсы.

Что толку от этих APIшек, если ими нельзя воспользоваться в коде. API-контракт уже содержит описание методов и моделей, этим необходимо пользоваться, чтобы сократить объем ручного труда разработчиков по описанию этих же в коде. Для большинства популярных схем описания API-контрактов есть библиотеки генерации кода, это позволяет сэкономить время и делегировать скучную работу библиотеке-кодогенератору.

Зачем писать если уже написали?

API-контракт это описание моделей, методов и остальных метаданных интерфейса взаимодействия, это можно и нужно использовать для кодогенерации.

Да, для некоторых схем это само собой разумеющееся (для grpc/proto, например). Но при использовании HTTP огромное количество людей вручную описывает модели и методы для контроллеров и клиентов. Может им нравится ручной труд, а может у них редкие языки программирования (ЯП) и фреймворки, под которые нет нормальных генераторов кода.

Тем не менее, API-контракт уже описан, если под ваш ЯП и фреймворк есть соответствующий кодогенератор (или частично есть, например только под модели), то этим точно надо пользоваться.

 Переписывая вручную модели и методы, даже глядя на контракт, вы закладываете проблемы человеческого фактора. Разработчик легко может просто ошибиться или опечататься. Да и это довольно примитивная работа, зачем ею утомлять разработчика, если можно делегировать кодогенератору.

Контракт качать каждый раз или положить к сорцам?

Как разместить API-контракт по отношению к исходникам кода? Есть смысл относиться к API-контракту как к внешней библиотеке или зависимости, а мы их в исходный код обычно не складываем.

Но тут важно учесть один момент, бывает необходимо локально проверить правки в API-контракте на предмет совместимости с вашим проектом, причем прямо в коде. То есть открыть IDE, модифицировать API-контракт, собрать или запустить проект, может тесты прогнать. С высокой долей вероятности вы столкнетесь с подобной необходимостью. Поэтому стоит предусмотреть такую возможность. Как вариант, разделив обновление файла API-контракта и его скачивание из репозитория по разным задачам для билд-фреймворка (программы сборщика вашего приложения, например gradle) Будет отлично иметь возможность запускать локальную сборку проекта так, чтобы задействовался контракт с локальными изменениями. Но важно чтобы это не конфликтовало со сборкой на ci-пайплайне.

Немного удобства или работаем с контрактами как с зависимостями

На одном из проектов мы выбрали nexus maven repo для размещения опубликованных контрактов (это были OpenAPI схемы), для публикации упаковывали в zip-архив. С этим репозиторием нативно работал билд-фреймворк gradle (мы разрабатывали с использованием Java). Это давало нам возможность внутри приложения работать с API-контрактами как с зависимостями, то есть с помощью типового dsl от gradle API-контракт указывался в списке зависимостей, а билд-фреймворк обеспечивал его скачивание к сорцам. Оставалось лишь распаковать его и использовать при кодогенерации. Немного расширив типовое поведение кастомными скриптами (распаковка + кодогенерация + привязка к этапу компиляции) получили простой и удобный инструмент. Ты просто указываешь API-контракт как зависимость и практически сразу получаешь по нему код, который можно использовать для дальнейшей логики.

Удобство использования в исходниках будет зависеть от:

  1. Места хранения. Ваш билд-фреймворк уже умеет с ним работать или будет необходимо это реализовывать дополнительно.

  2. Формата файла опубликованного API-контракта. У вас на выбор архив с кучей файлов API-контрактов или публикация по одному.

  3. Выбранного языка программирования и core-frameworks.Если grpc/proto вас приятно удивит, то OpenAPI может порадовать не так сильно, а где-то вообще удивит отсутствием необходимого кодогенератора.

Итоги

Когда это всё может быть особенно полезно:

  • Ваша ИТ-система успешно прошла пару этапов MVP (минимально жизнеспособный продукт) и имеет все шансы дальше разрабатываться долго и счастливо.

  • Ваша ИТ-система часть крупного корпоративного ИТ-ландшафта.

  • Большинство ваших потребителей находится во внутреннем контуре компании. Просто если большинство ваших потребителей API это внешние клиенты, то есть смысл предоставлять им API-контракты в составе документации удобных для клиентов способом.

Чем раньше вы выстроите работу с API-контрактами, тем меньше это будет болеть в будущем и негативно влиять на управляемость изменениями на более поздних этапах развития вашей ИТ-системы.

****

Удачного дня, мои дорогие коллеги по ИТ-цеху.

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