Организация API-first подхода, используя OpenAPI generator и Gitlab CI

от автора

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

Прежде, чем продолжить, немного добавлю про свой стек: Java, Spring Framework и все-все-все из этой «истории».

Возвращаясь к первому приседания с API-first…

Старый подход

Раньше я использовал распространенный подход, в котором в проекте было 2 модуля — API контракты и само приложение. Примерно так:

crud-service-api (API контракты), crud-service-app (приложение)

crud-service-api (API контракты), crud-service-app (приложение)

Где-то В API контракте (crud-service-api в данном случае) лежал файл со спецификацией (openapi.yaml) и в Gradle (у вас может быть Maven) был подключен OpenAPI generator, который генерировал классы для приложения и jar, затем этот jar загружался в maven-репозиторий.

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

И в чем тут же неудобство?

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

Возможно минусов этого подхода больше, пока писал статью, вспомнил только эти.

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

Новый подход

Идея сводилась к простому: есть моно-репозиторий, в нем по папкам разложены API контракты и общие модели, сборка и публикация артефактов производится оттуда же. Так и вышло.

Структура моно-репозитория
Структура папки common

Структура папки common

В папке common могут находиться любые общие и переиспользуемые модели и спецификации. В моем простом примере, это файл models.yaml с описанием Page и Pageable объектов

Содержимое файла models.yaml:

Page:   type: object   description: 'Description'   properties:     size:       type: integer     page:       type: integer     totalElements:       type: integer     totalPages:       type: integer  Pageable:   type: object   description: 'Description'   properties:     size:       type: integer     page:       type: integer     sort:       type: array       items:         type: string 
Структура папки crud-service-api

Структура папки crud-service-api

Вот примерно такая простая структура у меня получилась. Приводить пример структуры notification-service-api не стал, потому что она идентична структуре crud-service-api.

Давайте немного пробежимся по CI файлам.

1) Корневой .gitlab-ci.yml:

stages:   - trigger  .trigger:   stage: trigger   trigger:     strategy: depend  crud-service-api:   extends: .trigger   trigger:     include: crud-service-api/.gitlab-ci.yml   rules:     - changes:         - crud-service-api/*  notification-service-api:   extends: .trigger   trigger:     include: notification-service-api/.gitlab-ci.yml   rules:     - changes:         - notification-service-api/*

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

2) Дополнительный .api-first.gitlab-ci.yml:

stages:   - validate   - prepare   - generate   - publish  variables:   OPENAPI_GENERATOR: registry.gitlab.com/dmitrii-demchenko/infrastructure/custom-docker-images/openapi-generator-cli:1.0.0   OPENAPI_GENERATOR_CLI: java -jar /opt/openapi-generator-cli.jar  # Stage: validate validate:   stage: validate   image: ${OPENAPI_GENERATOR}   script:     - cp ${OPENAPI_SPEC_PATH}/openapi.yaml openapi.yaml     - ${OPENAPI_GENERATOR_CLI} validate -i openapi.yaml   rules:     - if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"       when: always     - if: $CI_COMMIT_BRANCH == "main"       when: always   artifacts:     paths:       - openapi.yaml   tags:     - docker  # Stage: prepare .prepare:   stage: prepare   before_script:     - mkdir -p configs/shared     - |       cat > configs/shared/common.yaml <<EOF       inputSpec: openapi.yaml       EOF   rules:     - if: $CI_COMMIT_BRANCH == "main"       when: on_success   artifacts:     paths:       - openapi.yaml       - configs     expire_in: 1w   needs:     - job: validate   tags:     - docker  prepare:spring-cloud-interface:   extends: .prepare   script:     - |       cat > configs/shared/spring-cloud-interface.yaml <<EOF       ${SPRING_CLOUD_INTERFACE_INCLUDE_CONFIG}       EOF     - |       cat > configs/spring-cloud-interface.yaml <<EOF       '!include': 'shared/common.yaml'       outputDir: generated/spring-cloud-interface       generatorName: spring       '!include': 'shared/spring-cloud-interface.yaml'       additionalProperties:         artifactId                    : ${JAR_ARTIFACT_ID}         groupId                       : ${JAR_GROUP_ID}         apiPackage                    : ${JAR_API_PACKAGE}         modelPackage                  : ${JAR_MODEL_PACKAGE}         library                       : spring-cloud         dateLibrary                   : java8         useSpringBoot3                : true         useTags                       : true         interfaceOnly                 : true         openApiNullable               : false         documentationProvider         : none         hideGenerationTimestamp       : true         additionalModelTypeAnnotations: '@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) @lombok.Getter @lombok.Setter @lombok.AllArgsConstructor @lombok.NoArgsConstructor'       EOF  # Stage: generate generate:   stage: generate   image: ${OPENAPI_GENERATOR}   script:     # - *script-clone-common     - ${OPENAPI_GENERATOR_CLI} batch configs/*.yaml   rules:     - if: $CI_COMMIT_BRANCH == "main"       when: on_success   artifacts:     paths:       - generated     expire_in: 1w   needs:     - job: prepare:spring-cloud-interface       artifacts: true   tags:     - docker  # Stage: publish .publish:   stage: publish   rules:     - if: $CI_COMMIT_BRANCH == "main"       when: manual   allow_failure: true   tags:     - docker  .publish:maven:   extends: .publish   image: maven:3-eclipse-temurin-21-alpine   variables:     MAVEN_OPTS: "-Dmaven.repo.local=${CI_PROJECT_DIR}/.m2/repository"     MAVEN_CLI_OPTS: "-Dmaven.test.skip=true -s settings.xml"   script:     - echo ${MAVEN_SETTINGS_XML} > settings.xml     - mvn deploy ${MAVEN_CLI_OPTS} -DaltDeploymentRepository=${MAVEN_SERVER_ID}::${MAVEN_SANDBOX_URL}   cache:     paths:       - '.m2/repository'  publish:spring-cloud-interface:   extends: .publish:maven   before_script:     - cd generated/spring-cloud-interface   needs:     - job: generate       artifacts: true

Этот файл можно назвать основным, так как в нем описаны основные джобы.

3) Контрактный (на примере crud-service-api/.gitlab-ci.yml):

include:   - local: .api-first.gitlab-ci.yml  variables:   OPENAPI_SPEC_PATH: crud-service-api   JAR_GROUP_ID: com.example   JAR_ARTIFACT_ID: crud-service-api   JAR_API_PACKAGE: com.example.crud.api   JAR_MODEL_PACKAGE: com.example.crud.api.model   SPRING_CLOUD_INTERFACE_INCLUDE_CONFIG: |-     typeMappings:       'OrderPageDto': 'Page<OrderDto>'     importMappings:       'Page': 'org.springframework.data.domain.Page'       'Pageable': 'org.springframework.data.domain.Pageable'       'Page<OrderDto>': 'org.springframework.data.domain.Page'

В этом же файле уже идет передача конечных значения переменным, предопределенным в файле выше.

Да, чуть не забыл…

Что за образ такой registry.gitlab.com/dmitrii-demchenko/infrastructure/custom-docker-images/openapi-generator-cli:1.0.0?

Для удобства сборки контрактов я сделал небольшой образ с openapi-generator-cli на борту.

Dockerfile:

FROM eclipse-temurin:21-jre-alpine  ARG VERSION=7.10.0 ARG OPENAPI_URL=https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/${VERSION}/openapi-generator-cli-${VERSION}.jar  ADD ${OPENAPI_URL} /opt/openapi-generator-cli.jar

И еще у меня есть переменная MAVEN_SETTINGS_XML которой нигде не задано значение. Это постоянная переменная, которая добавлена в Gitlab CI/CI variables в настройках репозитория и содержит креды к maven-репозиторию. И выглядит она вот так:

<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0">     <servers>         <server>             <id>maven-sandbox</id>             <username>${env.MAVEN_SANDBOX_USERNAME}</username>             <password>${env.MAVEN_SANDBOX_PASSWORD}</password>         </server>     </servers> </settings>

Запускаем первый пайплайн (на самом деле не первый, но первый удачный, который не стыдно показать вам):

И получаем загруженный артефакт в любой удобный для вас maven-репозиторий.

Артефакты в репозитории

Артефакты в репозитории

Кто и как работает с контрактами

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

Версионирование реализовано по простому, через указание версии в спецификации конкретного сервиса.

Была версия 1.0.0:

openapi: 3.0.3  info:   title: CRUD service API   description: CRUD service API   version: 1.0.0

Стала 1.1.0:

openapi: 3.0.3  info:   title: CRUD service API   description: CRUD service API   version: 1.1.0

Далее обычная сборка и деплой из main ветки.

Итоги

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

Уверен, в этом подходе есть вещи, которые можно улучшить и я продолжаю исследовать этот вопрос.

PS: не буду вдаваться в подробности CLI команд openapi генератора, с документацией этого инструмента и тем, как я настраивал параметры для сборки контракта, можно ознакомиться по ссылкам:

https://openapi-generator.tech/docs/usage#batch

https://openapi-generator.tech/docs/generators/spring/

В планах сделать что-то похожее для асинхронных контрактов (Kafka, RabbitMQ, и тд).

Вопросы, критика и предложения в комментарии.

Спасибо за внимание.


ссылка на оригинал статьи https://habr.com/ru/articles/884412/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *