Русский
Русский
English
Статистика
Реклама

Блог компании мир plat.form (нспк)

Создание приложений на Angular с использованием продвинутых возможностей DI

01.06.2021 10:11:39 | Автор: admin

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

Слоеный пирог приложения

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

  1. Хранение данных и осуществление операций с данными (слой данных).

  2. Преобразование информации к виду, требуемому для отображения, обработка действий пользователя (слой управления или контроллер).

  3. Визуализация данных и делегация событий (слой представления).

В контексте фреймворка они будут обладать следующими характерными особенностями:

  • элементы слоя представления компоненты;

  • зависимости слоя управления находятся в элементных инжекторах, а слоя данных в модульных;

  • связь между слоями осуществляется средствами системы DI;

  • элементы каждого уровня могут иметь дополнительные зависимости, которые непосредственно к слою не относятся;

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

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

Вообще говоря, под данными, передаваемыми между слоями, имеются в виду произвольные объекты. Однако, в большинстве случаев ими будут Observable, которые идеально подходят к описываемому подходу. Как правило, слой данных отдает Observable с частью состояния приложения. Затем в слое управления с помощью операторов rxjs данные преобразовываются к нужному формату, и в шаблоне компонента осуществляется подписка через async pipe. События на странице связываются с обработчиком в контроллере. Он может иметь сложную логику управления запросами к слою данных и подписывается на Observable, которые возвращают асинхронные команды. Подписка позволяет гибко реагировать на результат выполнения отдельных команд и обрабатывать ошибки, например, открывая всплывающие сообщения. Элементы слоя управления я буду дальше называть контроллерами, хотя они отличаются от таковых в MVC паттерне.

Слой данных

Сервисы слоя данных хранят состояние приложения (бизнес-данные, состояние интерфейса) в удобном для работы с ним виде. В качестве дополнительных зависимостей используются сервисы для работы с данными (например: http клиент и менеджеры состояния). Для непосредственного хранения данных удобно использовать BehaviourSubject в простых случаях, и такие библиотеки как akita, Rxjs или ngxs для более сложных. Однако, на мой взгляд, последние две избыточны при данном подходе. Лучше всего для предлагаемой архитектуры подходит akita. Ее преимуществами являются отсутствие бойлерплейта и возможность переиспользовать стейты обычным наследованием. При этом обновлять стейт можно непосредственно в операторах rxjs запросов, что гораздо удобнее, чем создание экшенов.

@Injectable({providedIn: 'root'})export class HeroState {  private hero = new BehaviorSubject(null);  constructor(private heroService: HeroService) {}  load(id: string) {    return this.heroService.load(id).pipe(tap(hero => this.hero.next(hero)));  }  save(hero: Hero) {    return this.heroService.save(hero).pipe(tap(hero => this.hero.next(hero)));  }  get hero$(): Observable<Hero> {    return this.hero.asObservable();  }}

Слой управления

Так как каждый сервис слоя относится к конкретному компоненту с его поддеревом, логично назвать сервис контроллером компонента. Благодаря тому, что контроллер компонента находится в элементном инжекторе, в нем можно использовать OnDestroy hook и внедрять те же зависимости, что и в компоненте, например ActivatedRoute. Безусловно, можно не создавать отдельный сервис для контроллера в тех случаях, где это равноценно вынесению кода из компонента.

Помимо зависимостей из слоя данных, в контроллере могут быть внедрены зависимости управляющие визуализацией (например: открытие диалогов, роутер) и помогающие с преобразованием данных (например: FormBuilder).

@Injectable()export class HeroController implements OnDestroy {  private heroSubscription: Subscription;    heroForm = this.fb.group({    id: [],    name: ['', Validators.required],    power: ['', Validators.required]  });  constructor(private heroState: HeroState, private route: ActivatedRoute, private fb: FormBuilder) { }  save() {    this.heroState.save(this.heroForm.value).subscribe();  }  initialize() {    this.route.paramMap.pipe(      map(params => params.get('id')),      switchMap(id => this.heroState.load(id)),    ).subscribe();    this.heroSubscription = this.heroState.selectHero().subscribe(hero => this.heroForm.reset(hero));  }    ngOnDestroy() {    this.heroSubscription.unsubscribe();  }}

Слой представления

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

@Component({  selector: 'hero',  template: `    <hero-form [form]="heroController.heroForm"></hero-form>    <button (click)="heroController.save()">Save</button>  `,  providers: [HeroController]})export class HeroComponent {  constructor(public heroController: HeroController) {    this.heroController.initialize();  }}

Повторное использование кода

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

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

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

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

export abstract class EntityState<T> {    abstract get entities$(): Observable<T[]>; // список сущностей    abstract get selectedId$(): Observable<string>; // id выбранного элемента    abstract get selected$(): Observable<T>; // выбранный элемент    abstract select(id: string); // выбрать элемент с указанным id    abstract load(): Observable<T[]> // загрузить список    abstract save(entity: T): Observable<T>; // сохранить сущность}

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

@Injectable()export class EntityCardController {    isSelected$ = this.entityState.selectedId$.pipe(map(id => id !== null));    constructor(private entityState: EntityState<any>, private snackBar: MatSnackBar) {    }    save(form: FormGroup) {        this.entityState.save(form.value).subscribe({            next: () => this.snackBar.open('Saved successfully', null, { duration: 2000 }),            error: () => this.snackBar.open('Error occurred while saving', null, { duration: 2000 })        })    }}

В самом компоненте используем еще один способ внедрения зависимости через директиву @ContentChild.

@Component({    selector: 'entity-card',    template: `        <mat-card>            <ng-container *ngIf="entityCardController.isSelected$ | async; else notSelected">                <mat-card-title>                    <ng-content select=".header"></ng-content>                </mat-card-title>                <mat-card-content>                    <ng-content></ng-content>                </mat-card-content>                <mat-card-actions>                    <button mat-button (click)="entityCardController.save(entityFormController.entityForm)">SAVE</button>                </mat-card-actions>            </ng-container>            <ng-template #notSelected>Select Item</ng-template>        </mat-card>    `,    providers: [EntityCardController]})export class EntityCardComponent {    @ContentChild(EntityFormController) entityFormController: EntityFormController<any>;    constructor(public entityCardController: EntityCardController) {        this.entityCardController.initialize();    }}

Для того чтобы это было возможно, необходимо в провайдерах компонента, который проецируется в entity-card, указать реализацию EntityFormController:

providers: [{ provide: EntityFormController, useClass: HeroFormController }]

Шаблон компонента, использующего эту карточку, будет выглядеть следующим образом:

<entity-card><hero-form></hero-form></entity-card>

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

export interface Entity {    value: string;    label: string;}@Injectable()export abstract class EntityListController<T> {    constructor(protected entityState: EntityState<T>) {}    select(value: string) {        this.entityState.select(value);    }    selected$ = this.entityState.selectedId$;    abstract get entityList$(): Observable<Entity[]>;}

Для уточнения преобразования конкретной модели данных к отображаемому виду теперь достаточно объявить наследника и переопределить абстрактное свойство.

@Injectable()export class FilmsListController extends EntityListController<Film> {    entityList$ = this.entityState.entities$.pipe(        map(films => films.map(f => ({ value: f.id, label: f.title })))    )}

Компонент списка использует этот сервис, однако его реализация будет предоставлена внешним компонентом.

@Component({    selector: 'entity-list',    template: `        <mat-selection-list [multiple]="false"                             (selectionChange)="entityListController.select($event.options[0].value)">            <mat-list-option *ngFor="let item of entityListController.entityList$ | async"                             [selected]="item.value === (entityListController.selected$ | async)"                             [value]="item.value">                {{ item.label }}            </mat-list-option>        </mat-selection-list>    `})export class EntityListComponent {    constructor(public entityListController: EntityListController<any>) {}}

Компонент, являющийся абстракцией всей вкладки, включает список сущностей и проецирует содержимое с формой.

@Component({    selector: 'entity-page',    template: `        <mat-sidenav-container>            <mat-sidenav opened mode="side">                <entity-list></entity-list>            </mat-sidenav>            <ng-content></ng-content>        </mat-sidenav-container>    `,})export class EntityPageComponent {}

Использование компонента entity-page:

@Component({    selector: 'film-page',    template: `        <entity-page>            <entity-card>                <span class="header">Film</span>                <film-form></film-form>            </entity-card>        </entity-page>    `,    providers: [        { provide: EntityState, useExisting: FilmsState },        { provide: EntityListController, useClass: FilmsListController }    ]})export class FilmPageComponent {}

Компонент entity-card передается через проекцию содержимого для возможности использования ContentChild.

Послесловие

Описанный подход позволил мне значительно упростить процесс проектирования и ускорить разработку без ущерба качеству и читаемости кода. Он отлично масштабируется к реальным задачам. В примерах были продемонстрированы лишь базовые техники переиспользования. Их комбинация с такими фичами как multi-провайдеры и модификаторы доступа (Optional, Self, SkipSelf, Host) позволяет гибко выделять абстракции в сложных случаях, используя меньше кода, чем обычное переиспользование компонентов.

Подробнее..

Концепт декларативного IaC подхода для автоматизации инструментов CI

18.09.2020 18:11:01 | Автор: admin

Привет! Меня зовут Игорь Николаев, я пью за любовь работаю в отделе автоматизации процессов разработки Мир Plat.Form в НСПК. В этой статье я поделюсь тем, как наш отдел решал задачу по автоматизации предоставления различных ресурсов для команд разработки. Эта задача свойственна организациям с большим количеством проектов, инфраструктура которых состоит из распределенных и, возможно, слабо связанных сетевых сегментов.
В статье описан PoC (Proof of concept) решения задачи выделения ресурсов в рамках сервисов CI/CD (Continuous Integration & Continuous Delivery) и предоставления привилегий для пользователей этих сервисов.


Описание


Часто в организациях используются сложные и дорогостоящие IDM - класс систем Identity Management (как в рамках лицензирования, так и внедрения и обслуживания) для управления доступами. Нам хотелось совместить процессы запроса и предоставления ресурсов на сервисах CI/CD и предоставления доступов к этим ресурсам. Хотелось получить максимально прозрачное и простое в поддержке и реализации решение, которое обеспечивает следующий функционал:


  • Создание и управление сущностями сервисов CI/CD
  • Использование удобных для нас инструментов
  • Легкая интеграция с уже развернутыми у нас системами
  • Простота эксплуатации
  • Возможность тиражирования

Про тиражирование стоит сказать подробнее, у нас есть несколько сетевых сегментов с разными сервисами CI/CD, и иногда они имеют минимум сетевой связанности. Система должна с минимальными затратами обслуживать несколько окружений, которые могут отличаться друг от друга.


Что мы выбрали для PoC:


Как подход был выбран IaC (Infrastructure-as-Code) с описанием желаемых состояний в виде yaml файлов.
Python язык для написания автоматизации (подходящий вариант для прототипа);
Bitbucket веб-сервис для хостинга проектов и их совместной разработки;
Jenkins сервис непрерывной интеграции (необходим нам для визуализации выполнения задач).


Как пилотные системы для автоматизации были выбраны:


Active Directory всем известные службы каталогов (нам понадобятся группы и пользователи);
Bitbucket часто запрашивают создание проектов, предоставление привилегий;
Nexus 3 OSS (не реклама, нет страницы в Wiki) корпоративная система хранения артефактов, при появлении проектов создаются персональные репозитории проекта и выдаются привилегии.


Немного про Bitbucket и GitOps
(Перевод замечательной статьи про GitOps от коллег из Флант)


Разговор об автоматизации следует начать с описания общей концепции.
В Bitbucket есть две важные сущности: проект(project) и репозиторий (repository), который входит в состав проекта. Для описания доступов в рамках концепта мы решили ограничиться доступами к проекту (более сегментированное предоставление привилегий (на репозиторий) в рамках концепта не потребуется).
У project в Bitbucket есть параметр project key, который понадобится для дальнейших манипуляций, мы взяли его за связующую основу. Именно он и будет являться названием директории в git-репозитории meta. В директории проекта будут размещаться meta-файлы (карты) проекта описанные в формате yaml.
Проектов в Мир Plat.Form много, и у каждого есть своя специфика. Возникает мысль держать в одном месте информацию о группах, инструментах, требуемых проекту, стендах (наборах серверов) и прочего, что имеет отношение к проекту. Для этого отлично подходит git репозиторий.


Какие задачи это решает?


  • В первую очередь, мы получаем стандартный интерфейс описания, который создает однотипные сущности сервисов CI/CD.
  • В любой момент времени мы можем увидеть реальное описание пользователей и их привилегий в рамках проекта.
  • Выполняя автоматизацию по расписанию можно гарантированно получать именно то, что описано в проектных метаданных (защита от очумелых ручек).
  • Расширяя описание проекта в виде определенной структуры meta-данных, можно автоматизировать другие сущности.

Структура meta репозитория git:

DEV наименование сетевого сегмента
project1 ключ проекта в Bitbucket
project1_meta.yaml карта проекта
examples директория примера описания


Такая структура позволит описать несколько различных сетевых сегментов, при этом сохраняя гибкую возможность изменений и различий между ними.
Скрипты автоматизации в рамках концепта будут находиться в проекте в отдельных репозиториях (названия не принципиальны):



О назначении первых трех репозиториев легко догадаться. Последний репозиторий jjb-core репозиторий в котором мы будем хранить описание Jenkins Job в виде рецептов для Jenkins Job builder (о нем будет рассказано ниже).


Автоматизация Microsoft AD


Active Directory используется во многих организациях. Большое количество рабочих процессов организаций начинаются именно с него. У нас в Мир Plat.Form все сотрудники имеют учетные записи в AD и включены в различные группы.
За AD отвечает подразделение инфраструктуры. Для наших нужд была выделена техническая учетная запись (ТУЗ), которой делегировано управление одним из Organization unit (OU). Именно в нем с помощью простой автоматизации мы будем создавать группы и наполнять их пользователями.


Часть содержимого project1_meta.yaml, которая отвечает за AD:


---READY: True  # Защита от "дурака", если не True, то автоматизация проигнорирует весь файлTEAM:  # Описание состава команды (роли)  USER_LOCATION: ldap  # local or ldap  ROLES:    owner:      - owner1    developer:      - developer1      - developer2    qa:      - qa1      - qa2GLOBAL_PRIVILEGES: &global_privileges  # Базовый набор привилегий для каждой роли в команде  owner: [read, write, delete]  developer: [read, write]  qa: [read]

READY булево значение и позволяет, в случае необходимости, выключить автоматизацию обработки данного мета файла
TEAM секция, описывающая сущность проекта
ROLES произвольные названия ролей на проекте, отображающие суть
GLOBAL_PRIVELEGES секция описывает, какая роль будет обладать какими привилегиями
Пример мета репозитория


В рамках предоставления прав для окружения разработки, чтобы не усложнять пример, остановимся на 3х основных ролях: owner, developer, qa (в целом, количество и наименование ролей является произвольным). Для дальнейшей автоматизации эти роли позволят покрыть большую часть повседневных потребностей (у нас сразу появились роль tech, для ТУЗ, но для примера обойдемся без нее).
В рамках OU проекта будем автоматически, на основании meta-файлов проекта, создавать необходимые SG (Security group) и наполнять их пользователями.


На схеме структура выглядит так:


В AD используем плоскую иерархическую структуру, это позволит ее легко обслуживать, и выглядит она весьма наглядно.
Скрипт автоматизации получился очень простой. Он позволяет отслеживать изменения в составе групп (добавление/удаление пользователей) и создавать OU/SG.
Для запуска потребуется установить зависимости из requirements.txt (ldap3, PyYAML).


Пример скрипта


Автоматизация Sonatype Nexus3 OSS


Что такое Nexus? Nexus это менеджер репозиториев, позволяющий обслуживать разные типы и форматы репозиториев через единый интерфейс (Maven, Docker, NPM и другие).
На момент написания статьи версия была OSS 3.25.1-04


Почему именно Nexus?
Есть community версия, которая обладает богатым функционалом, достаточным для выполнения большинства задач, связанных с хранением артефактов и проксирования внешних репозиториев.
Процесс хранения артефактов является важным при проектировании конвейера тестирования и развертывания.


Что потребуется автоматизировать?
Blobstore
Все двоичные файлы, загружаемые через proxy репозитории (мы не предоставляем прямого доступа к интернет репозиториям, используем исключительно прокисрование через nexus), опубликованные в hosted (локальные репозитории) репозитории хранятся в хранилищах Blob-объектов, связанном с репозиторием. В базовом развертывании Nexus, с одним узлом, обычно связаны с локальным каталогом на файловой системе, как правило, а каталоге sonatype-work.
Nexus версии >= 3.19 поддерживает два типа хранилищ File и S3.


UI Blob stores:

Как мы видим, по умолчанию нам уже доступно хранилище default. Из информации выше мы можем понять, что данный blob находится на диске и ему доступен весь объем дискового раздела, на котором находится директория sonatype-work.


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


  1. В случае, если все репозитории будут привязаны к одному blob, у нас могут появиться проблемы с тем, что хранилище может побиться.
  2. Если мы предполагаем, что наш Nexus будет использоваться несколькими командами разработки, то стоит сразу задуматься о том, что в некоторых ситуациях чрезмерная генерация артефактов может забить весь раздел и проблема будет не только у команды, которая генерирует большой объем артефактов, но и у других команд.

Простое решение
Первое, что приходит в голову это создание отдельных blob stores. Очевидно, это не решает проблему расположения на одном дисковом разделе. Подходящим решением является "нарезать" разделы для каждого проекта. Забегая вперед, это решит еще и вопрос мониторинга и отправки уведомлений ответственным за проект. Удобное решение второго пункта описанных проблем.
По первому пункту наиболее правильным решением является создание отдельных blob store для каждого репозитория.


UI создания Blob stores:


Nexus позволяет настроить Soft quota, штука сомнительная. Она уведомляет о том, что с местом что-то не так, но не производит каких-либо действий. При правильном применении шагов, описанных выше, удается добиться большего функционала (Появляется простой способ отслеживания объема и обращений к диску, а переполнение не создает неприятности "соседям").
В поле path мы можем указать раздел, который примонтирован, например, как nfs.
Что позволяет держать раздел непосредственно на сетевом хранилище. Это может снизить скорость, но дает ряд преимуществ с точки зрения простоты.
Nexus у нас запускается в Docker, для этого используется compose файл. Для подключения новых точек монтирования, простым решением будет добавить в compose файле монтирование родительского каталога точек монтирования.


Пример docker-compose:


version: "3"services:  nexus3:    container_name: nexus3    image: sonatype/nexus3:3.27.0    ports:      - 8443:8443      - 50011:50011 # project1-docker-releases      - 20012:50012 # project2-docker-releases    volumes:      - /nexus/sonatyep-work:/nexus-data      - /mnt-blobs:/mnt-blobs      - /etc/timezione:/etc/timezone      - /etc/localtime:/etc/localtime    logging:      driver: "json-file"      options:        max-size: "10m"        max-file: "10"

Repositories
Nexus позволяет создавать репозитории почти всех распространенных форматов. Если идти в сторону идеального хранения, то целесообразно для каждого проекта создавать минимум release и snapshot репозиторий, хотя идеальный вариант может содержать еще и release-candidat репозиторий. Это позволит настроить удобный механизм чистки репозиториев.
Определенно, release репозиторий должен во многих случаях иметь максимальную глубину хранения, как требование, в релизах не должно оказаться "мусора". Напротив, с репозиториями snapshot мы должны иметь возможность очищать без опасений в любое удобное время и без рисков.
Ко всем форматам репозиториев доступ осуществляется по 80 и/или 443 портам, за исключением docker. Репозиторий Docker, для доступа к нему, должен иметь персональный порт. Это приводит к некоторым сложностям. Каждый раз публикуя новый порт, мы должны добавлять его публикацию в compose файле.


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


Roles
Для удобства роли создаются под проект, лучше идти от минимума, и для себя мы выбрали три роли для каждого проекта:
qa обладают правами достаточными для read
developers read, write
owners read, write, delete
Группы из AD матчатся в локальные группы Nexus.


API
Начиная с версии Nexus OSS 3.19 появилось весьма удобное API для управления Nexus, это значимое нововведение, которое многие пользователи ждали позволит нам управлять Nexus и приводить его в нужное состояние.


Swagger UI API:


На момент написания статьи API, по большей части, в статусе beta, но не смотря на это, работает без больших проблем и позволяет автоматизировать почти все необходимое.


Часть содержимого project1_meta.yaml, которая отвечает за nexus:


RESURCES:  # Ресурсы, обслуживаемые автоматизацией  nexus:    repository:  # Сущности        # Maven        - name: test-maven-releases          locationType: hosted          repoType: maven        - name: test-maven-proxy          locationType: proxy          blobStoreName: test          remoteUrl: http://test.ru          repoType: maven        # Docker        - name: test-docker-releases          locationType: hosted          repoType: docker        - name: test-docker-proxy          locationType: proxy          blobStoreName: test-blob          remoteUrl: http://test.ru          repoType: docker        - name: test-docker-group          blobStoreName: test-blob          locationType: group          httpPort: 10555          repoType: docker          memberNames:            - test-docker-releases            - test-docker-proxy        # Npm        - name: test-npm-proxy          locationType: proxy          remoteUrl: http://test.ru          repoType: npm    blob:      - name: test-blob        path: test-blob    privileges:      <<: *global_privileges

На основании такого файла система автоматизации создает все обслуживаемые сущности. В наших командах принято, что teamlead отвечает за наполнение файла проекта, однако, создать его может любой желающий. После создания pull request следует согласование вовлеченными в процесс участниками, после мерджа с master веткой, отрабатывает автоматизация.


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


Пример кода модели для maven hosted repository:


def maven_model_hosted(params):    model = {        'name': params.get('name'),        'online': params.get('online', True),        'storage': {            'blobStoreName': params.get('blobStoreName', params['name']),            'strictContentTypeValidation': params.get('strictContentTypeValidation', True),            'writePolicy': params.get('writePolicy', 'ALLOW')        },        'cleanup': {            'policyNames': params.get('policyNames', [])        },        'maven': {            'versionPolicy': params.get('versionPolicy', 'MIXED'),            'layoutPolicy': params.get('layoutPolicy', 'PERMISSIVE')        }    }    return model

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


Пример скрипта


Автоматизация Atlassian Bitbucket


Для концепта достаточно будет автоматизировать создание проекта и предоставление привилегий к нему.


Часть содержимого project1_meta.yaml, которая отвечает за Bitbucket:


...  bitbucket:    name: project1-bitbucket-project # Это не project key!                                                     # project_key получается из имени файла    description: "Описание проекта в свободной форме"    privileges:      <<: *global_privileges

Это все, что потребуется при заведении нового проекта. Project key будет взят из названия yaml файла (в данном примере project1).


Как это выглядит в UI:


Пример скрипта


Jenkins Job Builder


JJB является python утилитой для описания сущностей jenkins в виде yaml манифестов, которые преобразуются в понятные jenkins API запросы. Это позволяет великолепно решать задачу управления большим количеством однотипных задач.
Jenkins в данном контексте является интерфейсом для отображения успешности выполняемых задач автоматизации и контроля над ними. Сами задачи на первом этапе планируем выполнять по расписанию, например, каждый час. Это позволит избавиться большой части неконтролируемых ручных изменений и будет каждый час приводить систему к описанному состоянию.


Структура репозитория jjb-core:


Каждая директория содержит описание Jenkins job состоящее из двух файлов.


Yaml файл описывает шаблон jenkins job имеет следующее наполнение:


---- job:  # Создаем директорию CORE    name: CORE    project-type: folder- job:  # Создаем поддиректорию ad-core-automation в CORE    name: CORE/ad-core-automation    project-type: folder# Описание темплэйта- job-template:    name: 'CORE/ad-core-automation/{name}-{project_key}'    project-type: pipeline    job_description: Упралвение OU и SG для {project_key}    # Defaults    GIT_BRANCH: master    GIT_CRED_ID: jenkins-bitbucket-integration    triggers:        - timed: 'H * * * *'    parameters:        - string:            name: GIT_BRANCH            default: '{GIT_BRANCH}'            description: Git ref (branch/tag/SHA)        - string:            name: GIT_CRED_ID            default: '{GIT_CRED_ID}'            description: Jenkins credentials ID for BitBucket        - string:            name: META_LOCATION            default: 'DEV/{project_key}/{project_key}_meta.yaml'            description: Meta file location if CORE/meta repository    dsl: !include-raw-escape: ./ad-core-automation.groovy- project:    name: ad-ou    project_key:        - project1        - project2        - project3    jobs:        - 'CORE/ad-core-automation/{name}-{project_key}'

Файл groovy это простой jenkinsfile:


def meta_location = params.META_LOCATIONdef git_cred_id = params.GIT_CRED_IDdef git_branch = params.GIT_BRANCHpipeline {    agent {        label 'centos'    }    stages {        stage('Clone git repos') {            steps {                echo 'Clone meta'                dir('meta') {                    git credentialsId: "${git_cred_id}",                    url: 'ssh://git@bitbucket.mir/core/meta.git'                }                echo 'Clone ad-core-automation'                dir('auto') {                    git credentialsId: "${git_cred_id}",                    branch: git_branch,                    url: 'ssh://git@bitbucket/core/ad-core-automation.git'                }            }        }        stage('Install and run') {            steps {                echo 'Install requirements'                withDockerContainer('python:3.8.2-slim') {                    withEnv(["HOME=${env.WORKSPACE}"]) {                    sh 'pip install --user --upgrade -r auto/requirements.txt  &> /dev/null'                    echo 'Run automation'                    withCredentials([usernamePassword(credentialsId: 'ssp_ad_tech', passwordVariable: 'ad_pass', usernameVariable: 'ad_user')]) {                        dir('auto') {                                sh "./run.py -u $ad_user -p $ad_pass -f ../meta/${meta_location}"                            }                        }                    }                }            }        }    }}

Пример скрипта


Все это описывает создание следующей структуры Jenkins:


Общий алгоритм работы автоматизации:


  • Инициатор создает в репозитории meta новую директорию с картой проекта и создает pull-request в мастер ветку(1).
  • Pull-request попадает на проверку согласующих (2)
  • В случае, если проект новый, пока в ручном режиме инженер прописывает Bitbucket project key для JJB (данное действие нужно произвести единожды)
  • Автоматизация после внесения изменений в шаблоны JJB генерирует описанные job для проекта(4, 5).
  • Jenkins запускает автоматизацию AD(6), которая создает необходимые сущности в виде OU и SG в AD. В случае, если все сущности уже созданы, приводит состав пользователей к описанному (удаляет/добавляет)
  • Jenkins запускает автоматизацию Bitbucket(4), если проекта нет в Bitbucket, то создает его и предоставляет доступ для групп команды проекта. Если проект уже существует, то добавляет к нему группы AD с необходимыми привилегиями.
  • Jenkins запускает автоматизацию для Nexus(7). Создаются описанные сущности Nexus и к ним предоставляется доступ на основе групп AD

Результат и развитие


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


На текущий момент описанный PoC перешел в стадию промышленной эксплуатации и претерпел значительные доработки. Мы переписали core систему автоматизации, к которой в виде плагинов подключаются модули для автоматизации новых сервисов. Появились тесты.
Всего автоматизацией обслуживается около 50 проектов и подключаются новые. Планируем тиражирование в другие сетевые сегменты.

Подробнее..

Концепт подхода инфраструктура как код в автоматизации сервисов CICD

18.09.2020 20:23:44 | Автор: admin

Привет! Меня зовут Игорь Николаев, я пью за любовь работаю в отделе автоматизации процессов разработки Мир Plat.Form в НСПК. В этой статье я поделюсь тем, как наш отдел решал задачу по автоматизации предоставления различных ресурсов для команд разработки. Эта задача свойственна организациям с большим количеством проектов, инфраструктура которых состоит из распределенных и, возможно, слабо связанных сетевых сегментов.


В статье описан PoC (Proof of concept) решения задачи выделения ресурсов в рамках сервисов CI/CD (Continuous Integration & Continuous Delivery) и предоставления привилегий для пользователей этих сервисов.



Описание


Часто в организациях используются сложные и дорогостоящие IDM - класс систем Identity Management (как в рамках лицензирования, так и внедрения и обслуживания) для управления доступами. Нам хотелось совместить процессы запроса и предоставления ресурсов на сервисах CI/CD и предоставления доступов к этим ресурсам. Хотелось получить максимально прозрачное и простое в поддержке и реализации решение, которое обеспечивает следующий функционал:


  • Создание и управление сущностями сервисов CI/CD
  • Использование удобных для нас инструментов
  • Легкая интеграция с уже развернутыми у нас системами
  • Простота эксплуатации
  • Возможность тиражирования

Про тиражирование стоит сказать подробнее, у нас есть несколько сетевых сегментов с разными сервисами CI/CD, и иногда они имеют минимум сетевой связанности. Система должна с минимальными затратами обслуживать несколько окружений, которые могут отличаться друг от друга.


Что мы выбрали для PoC:


Как подход был выбран IaC (Infrastructure-as-Code) с описанием желаемых состояний в виде yaml файлов.
Python язык для написания автоматизации (подходящий вариант для прототипа);
Bitbucket веб-сервис для хостинга проектов и их совместной разработки;
Jenkins сервис непрерывной интеграции (необходим нам для визуализации выполнения задач).


Как пилотные системы для автоматизации были выбраны:


Active Directory всем известные службы каталогов (нам понадобятся группы и пользователи);
Bitbucket часто запрашивают создание проектов, предоставление привилегий;
Nexus 3 OSS (не реклама, нет страницы в Wiki) корпоративная система хранения артефактов, при появлении проектов создаются персональные репозитории проекта и выдаются привилегии.


Немного про Bitbucket и GitOps
(Перевод замечательной статьи про GitOps от коллег из Флант)


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


В Bitbucket есть две важные сущности: проект(project) и репозиторий (repository), который входит в состав проекта. Для описания доступов в рамках концепта мы решили ограничиться доступами к проекту (более сегментированное предоставление привилегий (на репозиторий) в рамках концепта не потребуется).


У project в Bitbucket есть параметр project key, который понадобится для дальнейших манипуляций, мы взяли его за связующую основу. Именно он и будет являться названием директории в git-репозитории meta. В директории проекта будут размещаться meta-файлы (карты) проекта описанные в формате yaml.


Проектов в Мир Plat.Form много, и у каждого есть своя специфика. Возникает мысль держать в одном месте информацию о группах, инструментах, требуемых проекту, стендах (наборах серверов) и прочего, что имеет отношение к проекту. Для этого отлично подходит git репозиторий.


Какие задачи это решает?


  • В первую очередь, мы получаем стандартный интерфейс описания, который создает однотипные сущности сервисов CI/CD.
  • В любой момент времени мы можем увидеть реальное описание пользователей и их привилегий в рамках проекта.
  • Выполняя автоматизацию по расписанию можно гарантированно получать именно то, что описано в проектных метаданных (защита от очумелых ручек).
  • Расширяя описание проекта в виде определенной структуры meta-данных, можно автоматизировать другие сущности.

Структура meta репозитория git:

DEV наименование сетевого сегмента
project1 ключ проекта в Bitbucket
project1_meta.yaml карта проекта
examples директория примера описания


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


Скрипты автоматизации в рамках концепта будут находиться в проекте в отдельных репозиториях (названия не принципиальны):



О назначении первых трех репозиториев легко догадаться. Последний репозиторий jjb-core репозиторий в котором мы будем хранить описание Jenkins Job в виде рецептов для Jenkins Job builder (о нем будет рассказано ниже).


Автоматизация Microsoft AD


Active Directory используется во многих организациях. Большое количество рабочих процессов организаций начинаются именно с него. У нас в Мир Plat.Form все сотрудники имеют учетные записи в AD и включены в различные группы.


За AD отвечает подразделение инфраструктуры. Для наших нужд была выделена техническая учетная запись (ТУЗ), которой делегировано управление одним из Organization unit (OU). Именно в нем с помощью простой автоматизации мы будем создавать группы и наполнять их пользователями.


Часть содержимого project1_meta.yaml, которая отвечает за AD:


---READY: True  # Защита от "дурака", если не True, то автоматизация проигнорирует весь файлTEAM:  # Описание состава команды (роли)  USER_LOCATION: ldap  # local or ldap  ROLES:    owner:      - owner1    developer:      - developer1      - developer2    qa:      - qa1      - qa2GLOBAL_PRIVILEGES: &global_privileges  # Базовый набор привилегий для каждой роли в команде  owner: [read, write, delete]  developer: [read, write]  qa: [read]

READY булево значение и позволяет, в случае необходимости, выключить автоматизацию обработки данного мета файла
TEAM секция, описывающая сущность проекта
ROLES произвольные названия ролей на проекте, отображающие суть
GLOBAL_PRIVELEGES секция описывает, какая роль будет обладать какими привилегиями
Пример мета репозитория


В рамках предоставления прав для окружения разработки, чтобы не усложнять пример, остановимся на 3х основных ролях: owner, developer, qa (в целом, количество и наименование ролей является произвольным). Для дальнейшей автоматизации эти роли позволят покрыть большую часть повседневных потребностей (у нас сразу появились роль tech, для ТУЗ, но для примера обойдемся без нее).


В рамках OU проекта будем автоматически, на основании meta-файлов проекта, создавать необходимые SG (Security group) и наполнять их пользователями.


На схеме структура выглядит так:


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


Скрипт автоматизации получился очень простой. Он позволяет отслеживать изменения в составе групп (добавление/удаление пользователей) и создавать OU/SG.


Для запуска потребуется установить зависимости из requirements.txt (ldap3, PyYAML).


Пример скрипта


Автоматизация Sonatype Nexus3 OSS


Что такое Nexus? Nexus это менеджер репозиториев, позволяющий обслуживать разные типы и форматы репозиториев через единый интерфейс (Maven, Docker, NPM и другие).


На момент написания статьи версия была OSS 3.25.1-04


Почему именно Nexus?


Есть community версия, которая обладает богатым функционалом, достаточным для выполнения большинства задач, связанных с хранением артефактов и проксирования внешних репозиториев.


Процесс хранения артефактов является важным при проектировании конвейера тестирования и развертывания.


Что потребуется автоматизировать?


Blobstore
Все двоичные файлы, загружаемые через proxy репозитории (мы не предоставляем прямого доступа к интернет репозиториям, используем исключительно прокисрование через nexus), опубликованные в hosted (локальные репозитории) репозитории хранятся в хранилищах Blob-объектов, связанном с репозиторием. В базовом развертывании Nexus, с одним узлом, обычно связаны с локальным каталогом на файловой системе, как правило, а каталоге sonatype-work.
Nexus версии >= 3.19 поддерживает два типа хранилищ File и S3.


UI Blob stores:



Как мы видим, по умолчанию нам уже доступно хранилище default. Из информации выше мы можем понять, что данный blob находится на диске и ему доступен весь объем дискового раздела, на котором находится директория sonatype-work.


Проблематика


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


  1. В случае, если все репозитории будут привязаны к одному blob, у нас могут появиться проблемы с тем, что хранилище может побиться.
  2. Если мы предполагаем, что наш Nexus будет использоваться несколькими командами разработки, то стоит сразу задуматься о том, что в некоторых ситуациях чрезмерная генерация артефактов может забить весь раздел и проблема будет не только у команды, которая генерирует большой объем артефактов, но и у других команд.

Простое решение


Первое, что приходит в голову это создание отдельных blob stores. Очевидно, это не решает проблему расположения на одном дисковом разделе. Подходящим решением является "нарезать" разделы для каждого проекта. Забегая вперед, это решит еще и вопрос мониторинга и отправки уведомлений ответственным за проект. Удобное решение второго пункта описанных проблем.
По первому пункту наиболее правильным решением является создание отдельных blob store для каждого репозитория.


UI создания Blob stores:



Nexus позволяет настроить Soft quota, штука сомнительная. Она уведомляет о том, что с местом что-то не так, но не производит каких-либо действий. При правильном применении шагов, описанных выше, удается добиться большего функционала (Появляется простой способ отслеживания объема и обращений к диску, а переполнение не создает неприятности "соседям").


В поле path мы можем указать раздел, который примонтирован, например, как nfs.
Что позволяет держать раздел непосредственно на сетевом хранилище. Это может снизить скорость, но дает ряд преимуществ с точки зрения простоты.


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


Пример docker-compose:


version: "3"services:  nexus3:    container_name: nexus3    image: sonatype/nexus3:3.27.0    ports:      - 8443:8443      - 50011:50011 # project1-docker-releases      - 20012:50012 # project2-docker-releases    volumes:      - /nexus/sonatyep-work:/nexus-data      - /mnt-blobs:/mnt-blobs      - /etc/timezione:/etc/timezone      - /etc/localtime:/etc/localtime    logging:      driver: "json-file"      options:        max-size: "10m"        max-file: "10"

Repositories
Nexus позволяет создавать репозитории почти всех распространенных форматов. Если идти в сторону идеального хранения, то целесообразно для каждого проекта создавать минимум release и snapshot репозиторий, хотя идеальный вариант может содержать еще и release-candidat репозиторий. Это позволит настроить удобный механизм чистки репозиториев.


Определенно, release репозиторий должен во многих случаях иметь максимальную глубину хранения, как требование, в релизах не должно оказаться "мусора". Напротив, с репозиториями snapshot мы должны иметь возможность очищать без опасений в любое удобное время и без рисков.


Ко всем форматам репозиториев доступ осуществляется по 80 и/или 443 портам, за исключением docker. Репозиторий Docker, для доступа к нему, должен иметь персональный порт. Это приводит к некоторым сложностям. Каждый раз публикуя новый порт, мы должны добавлять его публикацию в compose файле.


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


Roles
Для удобства роли создаются под проект, лучше идти от минимума, и для себя мы выбрали три роли для каждого проекта:
qa обладают правами достаточными для read
developers read, write
owners read, write, delete
Группы из AD матчатся в локальные группы Nexus.


API
Начиная с версии Nexus OSS 3.19 появилось весьма удобное API для управления Nexus, это значимое нововведение, которое многие пользователи ждали позволит нам управлять Nexus и приводить его в нужное состояние.


Swagger UI API:



На момент написания статьи API, по большей части, в статусе beta, но не смотря на это, работает без больших проблем и позволяет автоматизировать почти все необходимое.


Часть содержимого project1_meta.yaml, которая отвечает за nexus:


RESURCES:  # Ресурсы, обслуживаемые автоматизацией  nexus:    repository:  # Сущности        # Maven        - name: test-maven-releases          locationType: hosted          repoType: maven        - name: test-maven-proxy          locationType: proxy          blobStoreName: test          remoteUrl: http://test.ru          repoType: maven        # Docker        - name: test-docker-releases          locationType: hosted          repoType: docker        - name: test-docker-proxy          locationType: proxy          blobStoreName: test-blob          remoteUrl: http://test.ru          repoType: docker        - name: test-docker-group          blobStoreName: test-blob          locationType: group          httpPort: 10555          repoType: docker          memberNames:            - test-docker-releases            - test-docker-proxy        # Npm        - name: test-npm-proxy          locationType: proxy          remoteUrl: http://test.ru          repoType: npm    blob:      - name: test-blob        path: test-blob    privileges:      <<: *global_privileges

На основании такого файла система автоматизации создает все обслуживаемые сущности. В наших командах принято, что teamlead отвечает за наполнение файла проекта, однако, создать его может любой желающий. После создания pull request следует согласование вовлеченными в процесс участниками, после мерджа с master веткой, отрабатывает автоматизация.


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


Пример кода модели для maven hosted repository:


def maven_model_hosted(params):    model = {        'name': params.get('name'),        'online': params.get('online', True),        'storage': {            'blobStoreName': params.get('blobStoreName', params['name']),            'strictContentTypeValidation': params.get('strictContentTypeValidation', True),            'writePolicy': params.get('writePolicy', 'ALLOW')        },        'cleanup': {            'policyNames': params.get('policyNames', [])        },        'maven': {            'versionPolicy': params.get('versionPolicy', 'MIXED'),            'layoutPolicy': params.get('layoutPolicy', 'PERMISSIVE')        }    }    return model

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


Пример скрипта


Автоматизация Atlassian Bitbucket


Для концепта достаточно будет автоматизировать создание проекта и предоставление привилегий к нему.


Часть содержимого project1_meta.yaml, которая отвечает за Bitbucket:


...  bitbucket:    name: project1-bitbucket-project # Это не project key!                                                     # project_key получается из имени файла    description: "Описание проекта в свободной форме"    privileges:      <<: *global_privileges

Это все, что потребуется при заведении нового проекта. Project key будет взят из названия yaml файла (в данном примере project1).


Как это выглядит в UI:



Пример скрипта


Jenkins Job Builder


JJB является python утилитой для описания сущностей jenkins в виде yaml манифестов, которые преобразуются в понятные jenkins API запросы. Это позволяет великолепно решать задачу управления большим количеством однотипных задач.


Jenkins в данном контексте является интерфейсом для отображения успешности выполняемых задач автоматизации и контроля над ними. Сами задачи на первом этапе планируем выполнять по расписанию, например, каждый час. Это позволит избавиться большой части неконтролируемых ручных изменений и будет каждый час приводить систему к описанному состоянию.


Структура репозитория jjb-core:



Каждая директория содержит описание Jenkins job состоящее из двух файлов.


Yaml файл описывает шаблон jenkins job имеет следующее наполнение:


---- job:  # Создаем директорию CORE    name: CORE    project-type: folder- job:  # Создаем поддиректорию ad-core-automation в CORE    name: CORE/ad-core-automation    project-type: folder# Описание темплэйта- job-template:    name: 'CORE/ad-core-automation/{name}-{project_key}'    project-type: pipeline    job_description: Упралвение OU и SG для {project_key}    # Defaults    GIT_BRANCH: master    GIT_CRED_ID: jenkins-bitbucket-integration    triggers:        - timed: 'H * * * *'    parameters:        - string:            name: GIT_BRANCH            default: '{GIT_BRANCH}'            description: Git ref (branch/tag/SHA)        - string:            name: GIT_CRED_ID            default: '{GIT_CRED_ID}'            description: Jenkins credentials ID for BitBucket        - string:            name: META_LOCATION            default: 'DEV/{project_key}/{project_key}_meta.yaml'            description: Meta file location if CORE/meta repository    dsl: !include-raw-escape: ./ad-core-automation.groovy- project:    name: ad-ou    project_key:        - project1        - project2        - project3    jobs:        - 'CORE/ad-core-automation/{name}-{project_key}'

Файл groovy это простой jenkinsfile:


def meta_location = params.META_LOCATIONdef git_cred_id = params.GIT_CRED_IDdef git_branch = params.GIT_BRANCHpipeline {    agent {        label 'centos'    }    stages {        stage('Clone git repos') {            steps {                echo 'Clone meta'                dir('meta') {                    git credentialsId: "${git_cred_id}",                    url: 'ssh://git@bitbucket.mir/core/meta.git'                }                echo 'Clone ad-core-automation'                dir('auto') {                    git credentialsId: "${git_cred_id}",                    branch: git_branch,                    url: 'ssh://git@bitbucket/core/ad-core-automation.git'                }            }        }        stage('Install and run') {            steps {                echo 'Install requirements'                withDockerContainer('python:3.8.2-slim') {                    withEnv(["HOME=${env.WORKSPACE}"]) {                    sh 'pip install --user --upgrade -r auto/requirements.txt  &> /dev/null'                    echo 'Run automation'                    withCredentials([usernamePassword(credentialsId: 'ssp_ad_tech', passwordVariable: 'ad_pass', usernameVariable: 'ad_user')]) {                        dir('auto') {                                sh "./run.py -u $ad_user -p $ad_pass -f ../meta/${meta_location}"                            }                        }                    }                }            }        }    }}

Пример скрипта


Все это описывает создание следующей структуры Jenkins:



Общий алгоритм работы автоматизации:



  • Инициатор создает в репозитории meta новую директорию с картой проекта и создает pull-request в мастер ветку(1).
  • Pull-request попадает на проверку согласующих (2)
  • В случае, если проект новый, пока в ручном режиме инженер прописывает Bitbucket project key для JJB (данное действие нужно произвести единожды)
  • Автоматизация после внесения изменений в шаблоны JJB генерирует описанные job для проекта(4, 5).
  • Jenkins запускает автоматизацию AD(6), которая создает необходимые сущности в виде OU и SG в AD. В случае, если все сущности уже созданы, приводит состав пользователей к описанному (удаляет/добавляет)
  • Jenkins запускает автоматизацию Bitbucket(4), если проекта нет в Bitbucket, то создает его и предоставляет доступ для групп команды проекта. Если проект уже существует, то добавляет к нему группы AD с необходимыми привилегиями.
  • Jenkins запускает автоматизацию для Nexus(7). Создаются описанные сущности Nexus и к ним предоставляется доступ на основе групп AD

Результат и развитие


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


На текущий момент описанный PoC перешел в стадию промышленной эксплуатации и претерпел значительные доработки. Мы переписали core систему автоматизации, к которой в виде плагинов подключаются модули для автоматизации новых сервисов. Появились тесты.
Всего автоматизацией обслуживается около 50 проектов и подключаются новые. Планируем тиражирование в другие сетевые сегменты.

Подробнее..

Как устроен прикладной и бизнес-мониторинг сервисов НСПК

20.10.2020 12:19:01 | Автор: admin
image

НСПК сегодня это не просто операционно-клиринговый центр для карточных операций, но и современная технологическая платформа для продвижения и развития платёжных инструментов и сервисов, как на территории России, так и за её пределами. НСПК это платёжная система Мир, Система быстрых платежей и обработка внутрироссийских операций по картам международных платёжных систем. Мы обеспечиваем миллиарды транзакций в год при отказоустойчивости и доступности на уровне 99,999%.

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

Меня зовут Липкин Иван, в НСПК я руковожу управлением прикладного мониторинга, и сегодня я хочу поделиться опытом построения прикладного и бизнес-мониторинга, полученным нашей командой за несколько лет развития национальных платёжных сервисов.

Идеология


image

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

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

Разделение сервиса на уровни происходит по принципу отнесения снимаемых с него данных к описанным трём категориям:

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

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

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

Требования к системе мониторинга


Мониторинг это служебный сервис, который не зарабатывает деньги в явном виде. При этом все понимают, что без качественного мониторинга невозможно обеспечить требования по уровню доступности, времени восстановления после сбоев, различные SLA перед партнёрами, рынком, регулятором и т.д. Тут и возникает важная задача максимально покрыть все потребности в мониторинге бизнес-сервисов, минимизировав при этом расходы на создание такой системы.

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

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

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

Гибкость
Система должна уметь получать данные от сервисов в разных режимах (push\pull), иметь богатый набор экспортёров данных и встроенные механизмы интеграции с современным ПО. То есть любая поставленная перед мониторингом задача должна решаться максимально быстро и, по возможности, штатными средствами, без долгих интеграционных процессов или разработки.

Открытость
Идеально, если весь используемый стек это open source. Но тут нужно представить известную картинку с тремя пересекающимися кругами цена, качество и время. Эта характеристика имеет свои плюсы и минусы, не всегда имеется время и профессиональный ресурс для решения всего спектра задач с использованием открытого ПО. Здесь мы за здравый баланс, но с прицелом на идеал open source и компетенции его приготовления.

В настоящее время нам удалось достичь определённой гармонии в совмещении всех этих требований. За последние несколько лет сервис качественно изменялся по форме, содержанию и задачам, но обо всем по порядку.

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


В 2015 году, когда всё только начиналось, мониторинг представлял собой всего лишь один элемент Zabbix, функциональных возможностей которого очень быстро стало не хватать. Во-первых, появилась потребность в централизованном сборе и хранении логов. Во-вторых, возникла необходимость визуализации и контроля качества прикладного авторизационного трафика. Очевидно, что решить такие задачи с помощью одного только Zabbix было невозможно и в инфраструктуру мониторинга добавился стек ELK (Elasticsearch, Logstash, Kibana), который прекрасно решает вопрос с централизованным сбором логов, и Grafana, хорошо справляющаяся с визуализацией данных. В итоге к середине 2016 года в стек уже входит: Zabbix, ELK, Grafana и много Perl-a.

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

В конце 2017 года мы открываем внутренний проект по выбору платформы операционной аналитики, и в фокусе нашего внимания оказывается Splunk.

В 2018 году Splunk уже внедрён в систему прикладного мониторинга НСПК. На текущий момент он по-прежнему остается аналитическим ядром в стеке, осуществляющим комплексный анализ данных, что позволяет фиксировать даже незначительные случаи деградации в процессах.

Все бы хорошо, но в начале 2019 года Splunk уходит с российского рынка, вынуждая нас искать альтернативу. Такой альтернативой видится ELK++ привычный нам стек, но с определёнными расширениями (про ++ дальше).

Уход Splunk из России не означает, что им нельзя пользоваться. У нас имеется постоянная лицензия, ограниченная только объёмом индексируемых данных в сутки. Понятно, что со временем это станет проблемой, поэтому у нас имеется стратегия по миграции на альтернативное решение.

Архитектура и прикладной стек


image atl

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

Но вернемся к архитектуре. Как видно на схеме, система условно раскладывается на четыре слоя: источники данных, транспорт, хранение\анализ и клиентская часть.

Источники данных и транспорт

Ключевую роль в сборе и транспортировке данных играют компоненты ELK-stack, Zabbix, а также брокер Kafka с клиентской библиотекой обработки потоков Kafka Streaming (очень важно, что это полностью open source). Богатый набор экспортёров данных Beats data shippers и input\output плагинов Logstash покрывает практически весь набор потребностей по снятию данных с объектов мониторинга. Также Logstash выступает в качестве посредника между шиной или конечной аналитической системой и системой алертинга Zabbix. Таким образом Logstash выполняет функции агрегатора метрик и планировщика запросов в сторонние API, откуда получает результаты сложной аналитики данных (например, из Splunk) для передачи их в Zabbix.

Zabbix, по такому же принципу, использует готовые шаблоны для экспорта метрик с сервиса. Иногда одну и туже задачу можно решить, как средствами ELK, так и Zabbix. В зависимости от ситуации, выбор делается в пользу того или иного приложения в стеке.

Чтобы отдавать в несколько конечных аналитических систем одни и те же данные, в транспортном слое присутствует Apache Kafka. Брокер получает данные как от Logstash, так и через собственные коннекторы Kafka Connect или напрямую от приложений, которые подключаются к Producer API Kafka. Использование единой шины решает ряд проблем. В качестве потребителя можно поставить любое хранилище данных и со стороны транспорта ничего менять не нужно. Получается своего рода стандарт, где любая новая потребность в данных решается просто, используя готовые рельсы.

На слое транспорта происходит ещё и трансформация данных. Несложные преобразования и обогащения делаются на Logstash формирование новых вычислительных атрибутов в данных, приклеивание справочников и т.д. Более сложные концепции обработки потоков реализуются в Kafka Streaming. Пока у нас имеется только одно приложение, обрабатывающее поток бизнес-лога авторизационных систем, формируя на лету модель данных для мониторинга. Но мы активно работаем в этом направлении, усиливая компетенции в области инженерии данных.

Как отдельное направление мониторинга, но полностью интегрированное в стек, Elastic развивает APM (application performance monitoring профилирование запросов и мониторинг приложений), который мы пробуем применять под задачи сбора и анализа прикладных трасс. В транспортном слое этот элемент представлен APM-агентом (в нашем случае java) и APM-сервером, в задачи которого входит обработка данных от агентов и подготовка их для передачи в Elasticsearch.

Конечные аналитические системы

Аналитическим ядром системы является Splunk готовое коробочное решение: база данных, язык взаимодействия с данными и пользовательский интерфейс.

Это очень гибкий и универсальный инструмент, особенностью которого можно назвать сильный высокоуровневый язык SPL (search processing language). Он сочетает в себе возможности SQL и Unix pipeline syntax. На этом языке можно очень быстро писать аналитические запросы к данным, а в сочетании с UI конструировать сложнейшие параметрические отчёты.

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

image

Но действительно важную и сложную аналитическую задачу Splunk решает в фоновом режиме непрерывное обнаружение аномалий. Постоянно работающие статистические анализаторы выдают сигналы об аномальном поведении метрик контролируемого операционного процесса. В основе лежит алгоритм median absolute deviation, а ключевая особенность подхода заключается в том, что алгоритм в моменте применяется к десяткам тысяч метрик, анализируя многомерные временные ряды.

image

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

Еще один пример интересного анализа это оценка объёма потерь по недополученным данным с использованием алгоритма прогнозирования временного ряда. Представьте ситуацию, когда банк ломается и перестает отправлять запросы в НСПК. В этом случае на мониторах видим просадку трафика и нам необходимо оценить количество данных, которое мы недополучаем, чтобы уведомить об этом участника или руководство компании (если речь идёт о крупном игроке рынка). В Splunk есть целое приложение с набором алгоритмов ML (в частности Forecast time series), которые делают прогноз трафика в системе и позволяют определить объем потерь.

Алгоритмы прогнозирования временных рядов также используются в методиках расчёта доступности сервисов. Когда происходит инцидент, то ключевая метрика (или метрики) производительности сервиса начинает деградировать, причем деградации бывают двух видов полная (Рис.1) или частичная (Рис.2). Очевидно, что считать временем недоступности сервиса весь интервал инцидента при частичной деградации это неправильно, потому что сервис свою задачу выполнял (в каком-то процентном отношении). Но деградация была и повлияла на определённую часть конечных пользователей. Splunk позволяет очень просто подсчитать область между прогнозом и фактом и сконвертировать это значение во время полной недоступности сервиса.

image
Рис.1

image
Рис.2

Описанные выше примеры показывают, как быстро и удобно можно делать сложное в Splunk, но есть с ним и определенные трудности. В паре cо Splunk работает Elasticsearch, который в стратегическом смысле видится как альтернатива, а в тактическом смысле имеет определенные преимущества перед ним. Ещё он выступает в роли резервной системы, если со Splunk что-то идет не так.

Сначала расскажу про плюсы с точки зрения аналитика и бизнес-заказчика. Сейчас для меня ключевым преимуществом Elasticsearch перед Splunk является то, что он умеет делать обновление документов в индексе. Отсутствие этой возможности в Splunk порождает на практике ряд трудностей. Например, в сервисе 3-D Secure имеется довольно сложный сценарий прохождения аутентификационных запросов. За одной бизнес-транзакцией стоит с десяток событий, предшествующих ей. Чтобы понимать качество оказания сервиса все события не нужны, только последние те, на которых конечные пользователи перестали взаимодействовать с сервисом. Elasticsearch делает обновление документов в индексе по ключевому полю, что дает на выходе нужный вид данных (последние статусы) и на порядок меньший объем хранимых данных, следовательно, быстрее и легче начинают работать статистические выборки. Splunk не умеет делать обновления записей, такова идеология решения, все индексируется как отдельные события. Чтобы вернуть только последние статусы, нужно делать дедупликацию событий по ключевому полю с сортировкой по времени, что при значительных RPS (количество запросов в секунду) вычислительно затратно.

Для некоторых сервисов возможность обновления данных крайне важна, и здесь очевидно преимущество Elasticsearch.

Теперь про недостатки. У Elasticsearch нет гибкого аналитического языка взаимодействия с данными, такого как SPL в Splunk. Это ключевая проблема. Elastic предоставляет целую группу языков запросов (DSL, LQS, SQL, EQL), но все это пока очень далеко от возможностей SPL. ELK хорош, как инфраструктура сбора, транспорта и хранения, но мне, как аналитику, нужен язык для работы с данными.

Какое здесь может быть решение? Внешний framework для аналитики. Берем Python pandas и пишем в нём любую сложную обработку, взаимодействуя с Elasticsearch API как с источником данных. Концепция рабочая, но порог вхождения намного выше чем в случае с SPL, нужны другие профессиональные навыки для развития и эксплуатации подобной конструкции.

Когда я писал про альтернативу Splunk, то отметил, что альтернативой будет ELK++, где первый плюс это аналитика во внешнем framework. Без этого невозможно решать задачи аналогично тому, как мы их решаем в Splunk. Вторым плюсом приставки является подписка платное расширение функциональности, которое существенно усиливает бесплатный уровень basic. В подписке решаются вопросы ИБ (интеграция с IDM, ролевая модель доступа), alerting, reporting, machine learning, расширенный мониторинг стека и управление pipeline Logstash из Kibana, JDBC\ODBC, cross cluster replication и т.д. Подписка не снимает первого плюса (аналитика во внешнем framework), но при несложных аналитических концепциях (не наш случай) можно обойтись комплектом ELK subscription + BI Tableau.

В слое хранения и анализа данных та же присутствует Zabbix, который выполняет роль центральной консоли событий (триггеров) в системе мониторинга. Как было сказано выше, данные в Zabbix собираются через собственные агенты и с помощью Logstash, который взаимодействует с внутренними интерфейсами системы мониторинга и интерфейсами контролируемых сервисов. Всю активность по триггерам отслеживает дежурная служба, но для удобства эксплуатации и поддержки сервисов реализована маршрутизация нотификаций в корпоративном мессенджере.

Короткий вывод по слою конечных аналитических систем. Вся самая сложная аналитика делается сейчас в Splunk. Учитывая, что вендор ушел с российского рынка, мы сосредотачиваем усилия на альтернативном решении ELK++. Почему же ELK, а не что-то другое? Ответ в критерии компактности прикладного стека и задачах: Elastic покрывает все домены обратной связи с сервисом (метрики, логи, прикладные трассы), он легко масштабируется и конфигурируется, имеет богатый набор экспортеров данных и плагинов Logstash, что делает его наиболее универсальным и привлекательным для нас инструментом.

Интерфейсы пользователя

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

К программным интерфейсам относятся Grafana, Kibana, Splunk, Zabbix и Telegram. Splunk и Zabbix для дежурной службы являются основными, Grafana выступает в качестве резерва. Zabbix центральная консоль событий в системе. Вкладка с триггерами постоянно открыта у каждого дежурного. В Splunk подготовлены все необходимые визуализации для анализа ситуаций по любому сервису, также дается возможность всем линиям работать с данными в режиме ad hoc, так как не всё можно заранее заложить в подготовленные аналитические панели.

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

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

Заключение


Хотелось бы закончить статью описанием главных качеств инженера SRE (Site Reliability Engineering). Знакомство с этой концепцией когда-то меня поразило очень интересная и сильная методология, снимающая абсолютно все барьеры между разработкой и эксплуатацией. В своей работе мы стараемся применять важные и полезные для нас части этой концепции.

Что же это за качества? По версии авторов книги Site Reliability Engineering. Надежность и безотказность как в Google это:

  • Обратное проектирование
  • Статистическое мышление
  • Импровизация в сложных\нестандартных ситуациях

Где во всём этом мониторинг? В статистическом мышлении! Высоконагруженные распределенные системы невозможно мониторить без этого качества. Десятки, сотни, тысячи хостов и приложений генерируют о себе какие-то метаданные, понимание которых нельзя сформировать без техник централизованного сбора и статистики это как минимум.
Мониторинг современного программного проекта это сам по себе программный проект
Очень точная формулировка. Техническая и логическая сложность мониторинга возникает не от того, что нечем заняться, а от того, что по мере развития задачи становятся всё сложнее. Необходимо отвечать на эти вызовы, потому что 99.999% доступности не просто красивая цифра, а тяжёлая работа большого числа специалистов компании и достичь её можно только максимальной отдачей и постоянным развитием.
Подробнее..

Обновление фронтальных систем НСПК без прерывания сервиса

23.12.2020 08:22:02 | Автор: admin

Фронтальные офисные (ФО) системы одни из основных MissionCriticalсистем, эксплуатируемых в НСПК сегодня. Они отвечают за обработку и маршрутизацию авторизационных запросов между Банком-эквайрером и Банком-эмитентом. Именно через них производят обмен данными банки пока вы проводите операцию по карте. Через ФО проходит до 60 миллионов авторизаций в сутки, при этом в пике они обрабатывают 1800TPS(transactionpersecond).

Меня зовут Пашин Вадим, в НСПК я руковожу управлением фронт-офисных решений и сегодня я хочу поделиться опытом внедрения системы управления соединениями банков.

ФО обладают достаточно сложной архитектурой и имеют 4-кратное резервирование каждого сервера.

Мы используем 2 ЦОД для георезервирования. В каждом ЦОД расположены ноды, принимающие соединения и обрабатывающие трафик от банков. Каждая нода обслуживает часть банков. Имеются следующие резервирования нода, обслуживающая трафик участников (нода А), имеет копию внутри ЦОД (нода В), а также копии этих двух нод существуют и в другом ЦОД.

Существует 3 типа подключения участников:

  • Участник имеет одно активное соединение к 1 ЦОД (Active-Passive);

  • Участник имеет два активных соединения к 2 ЦОД (Active-Active);

  • Участник имеет четыре активных соединения к 2 ЦОД (4Active).

Как и любые другиеIT-системы, ФО требуют периодических обновлений. Мы разделяем обновления на следующие типы:

  • Релиз;

  • Hotfix.

Релиз рождается в рамках 2-недельных спринтов и может содержать в себе следующие изменения:

  • Businessfeatures внедрение новой бизнес-функциональности в платежную систему. Например, такие сервисы какПокупка с выдачей наличных, возможность использования новыхWalletproviders(MirPay,Samsungpay,etc.);

  • Technicalfeatures внедрение технических изменений, упрощающих сопровождение системы, повышающих ее скорость работы, переход на новые технические решения;

  • Bugfixing устранение багов, не оказывающих влияния на бизнес компании.

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

Как правило, все изменения в виде релиза илиhotfixтребуют полной остановки приложений, отвечающих за обработку трафика на ноде. Это требуется для дистрибуции новых библиотек, перезапуска приложений, а также контроля по логам и через систему мониторинга, что ошибок при старте не образовалось, и модули ФО запущены в полном составе. Но мы не можем останавливать обработку трафика от банков, так как их клиенты не могут ждать у кассы и/или банкоматов, когда мы завершим обновление, и они смогут совершить покупку или снять наличные. Также мы стремимся к доступности нашего сервиса в 99,999%.

Обновление происходит следующим образом:

  1. Остановка приклада на резервных нодах В, где нет трафика от участников.

  2. Обновление ФО на нодах В.

  3. Перевод трафика с активных нод А на обновленные ноды В путем остановки нод А.

  4. Контроль правильности обработки трафика, отсутствия возросших отказов, ошибок в логах.

  5. Обновление нод А.

  6. Ноды В теперь становятся активными, а ноды А резервными.

Участники обмениваются авторизационными сообщениями по прикладному протоколу, основанному на ISO 8583. Он описывает формат финансовых сообщений и процесс передачи системами, обрабатывающими данные банковских платежных карт. Транспортным протоколом выступаетTCP/IP. Участник имеет только дваIPдля подключения (по одному на каждый ЦОД) и не знает, на какую ноду (А или В) уходит его трафик. Раньше мы использовали так называемый балансировщик, который проверял доступность ноды А при установке соединения со стороны банка. В случае ее доступности, устанавливалось соединение с нодой А, при недоступности ноды А, происходило установление соединения с нодой В.

Схема с балансировщиком имела следующий вид:

Использование балансировщиков было удобным и простым для сопровождения, при выключении нод происходило переустановление сессий на резервные ноды, однако опыт эксплуатации выявил следующие недостатки:

  • доступность ноды определяется балансировщиком только во время установления сессии от банка;

  • невозможность проведения обновлений ФО без разрывов соединений. Чтобы перевести трафик на резервные ноды В, происходит разрыв всех соединений, и всему рынку необходимо заново устанавливать свои сессии. Так как после установления сессий на транспортном уровне необходимо также установление прикладного уровня. Большинство банков умеют восстанавливать свои соединения в автоматическом режиме, но разные ПО банков это делают с разной скоростью. Неизбежно происходят потери авторизаций на время переключения. Это негативно влияет на нашу доступность.

  • в случае некорректной обработки трафика на нодах В во время обновления, обратное переключение на ноды А требует времени.

Мы стремимся к доступности 99,999% для наших ФО,поэтомув компании было принято решение и запущен проект разработки нового комплекса по управлению трафиком участника. К нему предъявлялись следующие требования:

  • возможность быстрого ручного или автоматического переключения трафика между нодами А и В;

  • переключение между нодами не должно порождать разрыв существующейTCPсессии с банками;

  • отказоустойчивость. Новый модуль должен быть зарезервирован, его падение также не должно вызывать разрываTCP-сессии с банками;

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

В итоге мы получили новую подсистему управления соединениями с участниками МУПС/ПУПС.

Схема подключения преобразилась следующим образом:

Название система получила от имени двух модулей, из которых она состоит:

  • ПУПС Прокси управления прикладными соединениями;

  • МУПС Модуль управления прикладными соединениями.

Мы вывели точки терминации трафика от банков из ЦОД в точки обмена трафиком М9 и М10, где располагается наше коммуникационное оборудование. Оборудование для реализации нового умного балансировщика мы также расположили на этих площадках.

В каждой точке обмена трафиком М9/М10 мы расположили по активной и резервной паре МУПС/ПУПС. Перейдем к описанию этих компонент и принципа работы нового комплекса. Серверы с этими парами объединены в VRRP-кластер с помощью keepalived и делят между собой один виртуальный IP.

ПУПС отвечает за TCP-взаимодействие узла балансировки с процессинговым ПО банков. Реализует механизм репликации и прозрачного восстановления TCP-соединений с организацией-участником на случай штатного переключения:

  • принимает TCP-соединения;

  • инициирует обмен данными между МУПС, ПУПС и банком;

  • отправляет и получает прикладные сообщения;

  • обрабатывает управляющие соединения между МУПС и ПУПС;

  • восстанавливает TCP-подключения и обеспечивает переключение между основными и резервными МУПС/ПУПС.

МУПС, второй компонент системы, предназначен для:

  • поддержания соединений с нодами ФО;

  • управления соединениями банков (включить/выключить, подключиться к ноде А или ноде B);

  • оборачивания сообщения ISO 8583 (авторизационная информация от банка) в свой протокол взаимодействия МУПС и ноды ФО;

  • получения сообщений от ноды ФО, разворачивания сообщения ISO 8583 и отправка в ПУПС;

  • подачи команды ПУПС о миграции на резервный сервер.

Одна из самых важных функций МУПС, ради чего он создавался, это переключение обработки трафика на резервную ноду ФО и обратно без разрыва соединения с банком-участником. Это работает благодаря тому, что МУПС стоит между ПУПС, который "держит" соединение с банком, и ФО, который обрабатывает трафик. МУПС управляет тем, куда именно этот трафик направляется в данный момент, и по команде от администратора осуществляет незаметное для банка и безопасное для обработки операций переключение между серверами.

Происходит это следующим образом:

  • фронтальные модули по команде от МУПС переходят в состояние синхронизации

  • активный модуль, который в данный момент обрабатывает операции, загружает контексты in-flight операций (для которых он ожидает, но ещё не получил ответных сообщений от банка) из своей памяти в общий in-memory data grid

  • резервный модуль забирает к себе эти контексты

  • по завершении выгрузки МУПС деактивирует активный модуль и передаёт на резервный его новый статус и ряд runtime-параметров, с которым работал прошлый активный модуль

  • с этого момента МУПС начинает направлять трафик от участника на новый активный модуль

Для передачи данных и управления МУПС используется два соединения. Первое это Data-соединение. Используется для передачи данных по авторизациям от банка (ISO8583) в ФО и обратно. Второе соединение это Control-соединение. Используется для обмена управляющими сообщениями между ПУПС и МУПС. В управляющем соединении используются команды для проверки жива ли активная пара МУПС/ПУПС командаheartbeat, а также ряд команд для осуществления переезда соединений на резервную пару МУПС/ПУПС в рамках площадки.

В узле балансировки активный ПУПС взаимодействует только с МУПС, установленном на том же сервере, что и ПУПС.

Если сигналы heartbeat от МУПС отсутствуют в течение заданного времени, ПУПС на активном узле начинает процедуру активации второго узла в кластере (при его доступности), а затем деактивируется.

Процесс миграции с основного сервера на резервный сервер происходит следующим образом:

  • ПУПС на основном сервере устанавливает флаг готовности к миграции;

  • на основном сервере создается дамп процесса, далее ПУПС переносит его образ на резервный сервер, а также устанавливает флаг готовности к миграции и восстановлению на резервном сервере;

  • ПУПС на резервном сервере при обнаружении образа переносит правила iptables и увеличивает приоритет Keepalived на узле, тем самым запускается процедура переноса IP-адреса;

  • после переноса IP-адреса Keepalived на резервном сервере из образа восстанавливается работающий процесс. Также восстанавливается приоритет самого Keepalived.

Таким образом, обеспечивается отказоустойчивость пары МУПС/ПУПС в рамках одной площадки.

Взаимодействие МУПС и ноды ФО происходит по собственному протоколу. В протоколе передается как платежная информация, так и проверяется доступность нод ФО с помощьюheartbeat, а также может передаваться ряд команд, необходимых для переезда трафика на неактивные ноды ФО. Очень важно: при переезде из активной ноды необходимо получить всю платежную информацию и передать ее уже в резервную нодуB. Наличие постоянныхheartbeatмежду МУПС и нодами ФО позволяет в автоматическом режиме диагностировать проблему с нодой и осуществлять мгновенные переводы трафика участника на резервную ноду без разрыва соединения с участником.

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

Заключение

Созданный комплекс МУПС/ПУПС позволил решить ряд существенных вопросов компании по управлению прикладными соединениями банков с компанией:

  • все работы на ФО остаются незамеченными для участников, не происходит разрыва соединений и потери транзакций;

  • при проблеме на ноде ФО, перевод трафика на резерв осуществляется автоматически и мгновенно, банк также не видит разрыва соединения;

  • дежурные службы и администраторы ФО получили удобный и наглядный инструмент для управления соединениями. Вывод ноды из работы для обновления ОС, замены железных компонент также не замечается участником и не приводит к транзакционным потерям.

Подробнее..

Одна кнопка, чтобы тестировать их всех. Как не упустить все интеграции из поля зрения

24.09.2020 12:15:14 | Автор: admin
Привет, Хабровчане! Мы Владимир Мясников и Владислав Егоров представители команды интеграционного тестирования Mir Plat.Form (АО НСПК). Сегодня мы расскажем про разработанный и развиваемый нами инструмент автоматизации, позволивший сократить рутину во внутренних процессах команды.

Предисловие


Платёжная экосистема Mir Plat.Form включает в себя несколько десятков систем, большинство из которых взаимодействуют между собой по различным протоколам и форматам. Мы, команда интеграционного тестирования, проверяем соответствие этих взаимодействий установленным требованиям.



На данный момент команда работает с 13 системами уровня mission и business critical. Mission critical системы обеспечивают выполнение Mir Plat.Form своих основных функций, обеспечивающих стабильность и непрерывность функционирования банковской карточной системы РФ. Системы уровня business critical отвечают за поддержку предоставляемых клиентам Mir Plat.form дополнительных сервисов, от которых зависит непосредственная операционная деятельность компании. Частота выкатывания релизов в ПРОД варьируется от раза в неделю до раза в квартал, всё зависит от системы и готовности участников к частоте обновлений. В общей сложности мы насчитали около 200 релизов, прошедших через нашу команду в прошлом году.

Простая математика гласит следующее: количество проверяемых цепочек это N-систем * M-интеграций между ними * K-релизов. Даже на примере 13 систем * 11 интеграций * 27 версий релизов получается примерно 3 861 возможных вариантов совместимости систем. Кажется, ответ очевиден автотесты? Но проблема чуть серьезнее, только автотесты не спасут. Учитывая растущее количество систем и их интеграций, а также различную частотность релизов, всегда имеется риск протестировать неправильную цепочку версий систем. Следовательно, имеется риск пропустить дефект в межсистемном взаимодействии, например, влияющий на корректность работы платежной системы (ПС) Мир.

Естественно, в ПРОДЕ наличие такого рода багов недопустимо, и задача нашей команды свести такой риск до нуля. Если помните текст выше, любой чих влияет не только на внутренние системы Mir Plat.form, но и на участников рынка: банки, торгово-сервисные предприятия (ТСП), физические лица и даже на другие платежные системы. Поэтому для устранения рисков мы пошли следующим путем:

Ввели единую базу выпуска релизов. Для этой задачи вполне хватило календаря релизов в Confluence с указанием версий систем, установленных в ПРОД;

Отслеживаем интеграционные цепочки в соответствие с релизными датами. Здесь мы тоже не стали изобретать велосипед, он нам потребуется дальше. Для решение данной задачи использовали Epic структуры в JIRA для интеграционного тестирования релизов. Пример структуры для релиза 1.111.0 системы System3:



С одной стороны, все эти действия позволили улучшить понимание команды о тестируемых интеграциях, версиях систем и последовательности их выхода в ПРОД. С другой все равно осталась вероятность некорректного тестирования вследствие человеческого фактора:
  1. В случае, если дату релиза какой-нибудь системы подвинули, то члену команды необходимо вручную поправить календарь и всю структуру в JIRA, в том числе сроки выполнения задач и, возможно, версии тестируемых систем;
  2. Перед тестированием интеграции необходимо убедиться, что окружение для тестирования состоит из нужных версий систем. Для этого необходимо вручную пробежаться по тестовым стендам и выполнить пару консольных команд.

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

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

Какие опции хотелось реализовать в разрабатываемой системе?
1. Наглядный календарь релизов с возможностью вывода версий всех систем на конкретную дату;
2. Мониторинг окружений для интеграционного тестирования:
список окружений;
наглядное отображение тестовых стендов и систем, входящих в состав отдельного окружения;
контроль версий систем, развернутых на тестовых стендах.
3. Автоматизированную работу с задачами в Jira:
создание Epic структуры релиза;
управление жизненным циклом задач на тестирование;
актуализация задач в случае сдвига даты релиза;
подкладывание allure-отчетов в задачи на тестирование.
4. Автоматизированную работу с ветками в Bitbucket, а именно создание релизных веток в проектах:
интеграционных автотестов;
автодеплоя интеграционного окружения.
5. Интуитивно понятный UI для запуска автотестов и обновления версий систем.

Что есть СМИТ


Так как система несложная, мы не стали особо мудрить с технологиями. Бэкенд написали на Java с использованием Spring Boot. Фронтенд на React. К базе данных особенных требований не было, поэтому мы выбрали MySql. Поскольку у нас принято работать с контейнерами, то все вышеперечисленные составляющие завернули в Docker, собирая при помощи Docker Compose. Работает СМИТ быстро и так же надежно, как остальные системы Mir Plat.Form.



Интеграции


Atlassian Jira. В джире создаются, открываются, принимаются в работу и закрываются задачи на тестирование каждой конкретной интеграции, если все тесты прошли успешно прикладывается ссылка на allure отчет в комментарии.
Atlassian BitBucket. В битбакете лежит код проекта автотестов, где инженеры по автоматизации ведут список окружений, настраивают его и добавляют/убирают системы в определенные окружения. Также там создаются релизные ветки под каждую новую версию системы, где будут вестись работы по актуализации кода и бизнес логики тестовых сценариев.
Jenkins. Все тесты из проекта автотестирования можно запускать через Jenkins, для каждого набора тегов у нас предусмотрена своя джоба. Отдельные джобы нужны для того, чтобы не грузить все шаги каждый раз, а загружать только нужные с помощью указания glue для Cucumber.
Системы. У СМИТа есть необходимость взаимодействовать с самими системами. Так СМИТ узнает актуальные версии систем путем выполнения определенных команд по ssh.

Ведение списков систем


Перед тем как в СМИТе вести календарь и мониторить состояние окружений, необходимо завести список тестируемых систем и взаимосвязи между ними. Все настройки можно произвести через веб-интерфейс:



После добавления тестируемой системы в список СМИТ:
  1. постучится на все хосты систем, имеющих название SYS_CMD в списке окружений;
  2. узнает версию этой системы с помощью команды, указанной в конфигурации;
  3. запишет к себе в базу текущую версию данной системы и окружения, в которых она фигурирует.


В итоге в СМИТе будет информация о всех системах, развернутых на используемых окружениях, включая номера их версий. На основе этой информации можно визуализировать календарь релизов.

Календарь релизов


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



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

Также на странице с календарем имеется функция вывода версий всех систем на конкретную дату:



Стоит отметить, что при регистрации нового релиза в календаре СМИТ автоматически создает Epic структуру в Jira и релизные ветки в проектах в Bitbucket.

Состояние окружений


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



Как видно на скриншоте, СМИТ обнаружил на хосте host-4.nspk.ru неактуальную версию System 4 и предлагает обновить её. Если нажать красную кнопку с белой стрелкой, то СМИТ вызовет Jenkins джоб на деплой актуальной версии системы в текущем окружении. Также есть возможность обновить все системы после нажатия соответствующей кнопки.

Окружения для интеграционного тестирования


Стоит немного рассказать про то, как мы задаем тестовые окружения. Одно окружение представляет собой некий набор стендов с развернутыми системами Mir Plat.form и настроенной интеграцией (на одном стенде одна система). В общей сложности у нас 70 стендов, разбитых на 12 окружений.

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

{     "properties":{        "comment":"Общие system property для всех Environment. Могут быть переопределены персональными property, а также всем, что при запуске тестов в System.getProperties()",      "common.property":"some global property"   },   "environments":[        {           "comment":"Если отсутствует name, то Environment получит имя common + порядковый номер. Например common1",         "name":"env_1",         "properties":{              "comment":"Персональные system property данного Environment. Могут переопределять общие property. Могут быть переопределены всем, что при запуске тестов в System.getProperties()",            "env1.property":"some personal property"         },         "DB":{              "comment":"Пример TestResource'а DbTestResource. Если не указано поле id, то оно автоматически будет взято из ключа",            "url":"jdbc:mysql://11.111.111.111:3306/erouter?useUnicode=yes&characterEncoding=UTF-8&useSSL=false",            "driver":"com.mysql.jdbc.Driver",            "user":"fo",            "password":"somepass"         },         "SYS_CMD":{              "comment":"Пример TestResource'а на основе RemoteExecCmd. Должен иметь параметр type = remote",            "type":"remote",            "host":"10.111.111.111",            "username":"user",            "password":"somepass"         }      }   ]}


Помимо того, что данный файл необходим для работы проекта интеграционных автотестов, он также является дополнительным конфигурационным файлом для СМИТа. При запросе обновлении информации об окружениях в СМИТе отправляется HTTP запрос в API нашего bitbucket, где мы храним проект с интеграционными автотестами. Таким путем СМИТ получает актуальное содержимое файла конфигураций из master ветки.

Запуск тестов


Одной из целей создания СМИТа было максимальное упрощение процедуры запуска интеграционных автотестов. Рассмотрим, что же у нас получилось в итоге на примере:



На странице тестирования системы (в данном примере System 3) можно выбрать перечень систем, с которыми нужно проверить интеграцию. После выбора нужных интеграций и нажатия на кнопку Запустить тестирование, СМИТ:
1. Сформирует очередь и последовательно запустит соответствующие Jenkins джобы;
2. мониторит выполнение джоб;
3. меняет статус у соответствующих задач в Jira:
Если джоба отработала успешно задача в Jira будет автоматически закрыта, к ней будет приложена ссылка на allure-отчет и комментарий о том, что дефектов в данной интеграции не обнаружено.
Если джоба зафейлена задача в Jira останется открытой и будет ожидать решения от ответственного за интеграцию сотрудника, который сможет определить причину падения тестов. Ответственного за интеграцию можно подсмотреть в карточке интеграции.

Вывод


СМИТ был создан для минимизации рисков интеграционного тестирования, но нам как команде хотелось бОльшего! В частности, одним из желаний было, чтобы по одному нажатию кнопки автотесты запускались с правильным тестовым окружением, проверялось все в нужных интеграционных соответствиях, задачи в Jira сами открывались и закрывались вместе с отчетами. Такая утопия автотестеров: скажи системе, что проверить, и иди пить кофе :)

Подведем итог, что у нас получилось реализовать:
1. Наглядный календарь релизов с возможностью вывода версий всех систем на конкретную дату;
2. UI для отслеживания состояния наших окружений, позволяющий посмотреть перечень и версии систем, установленных на конкретном окружении;
3. Оповещение пользователей о неактуальных версиях систем с возможностью обновления до актуальной;
4. UI с интуитивно понятным запуском интеграционных автотестов для всей системы или для отдельных интеграций на определенном окружении;
5. Автоматическое создание и закрытие Epic и Task в Jira, прикладывание Allure отчетов к ним;
6. Автоматическое создание релизных веток в Bitbucket.

О планах на будущее


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

Kotlin. Автоматизация тестирования (Часть 2). Kotest. Deep Diving

20.02.2021 10:19:11 | Автор: admin

Kotest


Продолжение цикла публикаций про автоматизацию функционального тестирования на Kotlin
с использованием фреймворка Kotest совместно с наиболее полезными дополнительными библиотеками, существенно облегчающими и ускоряющими создание тестов.


В этой части мы углубимся в возможности Kotest:


  • покажу все варианты группировки тесты
  • расскажу про последовательность выполнения тестов и спецификаций
  • изучим возможности параллельного запуска
  • настроим таймауты на выполнение тестов
  • проговорим про ожидания и Flaky-тесты
  • рассмотрим использование Фабрик тестов
  • и напоследок исследуем тему Property Testing

Все части руководства:



О себе


Я являюсь QA Лидом на одном из проектов Мир Plat.Form (НСПК). Проект зародился около года назад и уже вырос до четырех команд, где трудится в общей сложности около 10 разработчиков в тестировании (SDET), без учета остальных участников в лице аналитиков, разработчиков и технологов.
Наша задача автоматизировать функциональные тесты на уровне отдельных сервисов, интеграций между ними и E2E до попадания функционала в master всего порядка 30 микро-сервисов. Взаимодействие между сервисами Kafka, внешний API REST, а также 2 фронтовых Web приложения.
Разработка самой системы и тестов ведется на языке Kotlin, а движок для тестов был выбран Kotest.


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


Мотивация и цели


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


Уже в следующей части я расскажу про интеграцию со сторонними библиотеками:


  • Spring Core/Test. Dependencies Injection и конфигурирование профилей для тестов
  • Spring Data JPA. Работа с БД
  • TestContainers. Управление Docker контейнерами
  • Allure. Отчетность
  • Awaitility. Ожидания

Группировка тестов


В публикации много примеров с пояснениями двух видов: комментарии в коде либо объяснения вне блока кода со ссылками на конкретные места примера.
Куски кода больше половины стандартного экрана скрыты спойлером.
Рассматриваемая версия Kotest 4.3.2

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


  1. По уровню. Модульные -> На один сервис (в контексте микро-сервисов) -> Интеграционные -> E2E


  2. По платформе. Windows\Linux\MacOS | Desktop\Web\IOS\Android


  3. По функциональности. Frontend\Backend В контексте целевого приложения: Авторизация\Администрирование\Отчетность...


  4. По релизам. Sprint-1\Sprint-2 1.0.0\2.0.0



Далее описание реализации группировки в Kotest


Теги


Для группировки тестов используются теги объекты класса abstract class io.kotest.core.Tag. Есть несколько вариантов декларирования меток:


  • расширить класс Tag, тогда в качестве имени метки будет использовано simpleName расширяющего класса, либо переопределенное свойство name.
  • использовать объект класса io.kotest.core.NamedTag, где имя метки передается в конструкторе.
  • создать String константу и использовать в аннотации @io.kotest.core.annotation.Tags, однако в прошлых версиях Kotest эта аннотация принимала тип KClass<T> класса тегов, а сейчас String имя тега.

Ниже представлены все варианты декларирования тегов:


/** * TAG for annotation @Tag only. */const val LINUX_TAG = "Linux"/** * Name will be class simple name=Windows */object Windows : Tag()/** * Override name to Linux. */object LinuxTag : Tag() {    override val name: String = LINUX_TAG}/** * Create [NamedTag] object with name by constructor. * Substitute deprecated [io.kotest.core.StringTag] */val regressTag = NamedTag("regress")

Применить тег к тесту можно несколькими путями:


  • через аннотацию @Tags на классе спецификации
  • extension функцию String.config в тесте или контейнере теста
  • переопределив метод tags(): Set<Tag> у спецификации

Однако, последний вариант я не рекомендую, т.к. тестовому движку для получения тега и проверки придется создать экземпляр класса Spec и выполнить init блок, а еще выполнить все инъекции зависимостей, если используется Dependency injection в конструкторе.

Пример с 2-мя классами спецификаций:


@Tags(LINUX_TAG) // Аннотацияclass LinuxSpec : FreeSpec() {    init {        "1-Test for Linux" { }                                        /* конфигурация теста String.config */        "2-Test for Linux and Regress only".config(tags = setOf(regressTag)) { }    }}class WindowsSpec : FreeSpec() {    /** Override tags method */    override fun tags(): Set<Tag> = setOf(Windows) // переопределение метода    init {        "Test for Windows" { }    }}

Остается задать выражение тегов для запуска ожидаемого набора тестов через системную переменную -Dkotest.tags=<выражение для выборки тестов по тегам>.
В выражении можно использовать ограниченный набор операторов: (, ), |, &


Теги чувствительны к регистру

Привожу несколько вариантов запуска в виде Gradle task


Набор Gradle Tasks для запуска групп тестов
// Используется встроенный в Gradle фильтр тестов по пакетам без фильтрации теговtask gradleBuildInFilterTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties    filter { includeTestsMatching("ru.iopump.qa.sample.tag.*") }}// Запустить только тест с тегами regress (в тесте) и Linux (в аннотации спецификации) в LinuxSpectask linuxWithRegressOnlyTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties + ["kotest.tags": "Linux & regress"]}// Запустить 2 теста с тегом Linux (в аннотации спецификации) в LinuxSpectask linuxAllTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties + ["kotest.tags": "Linux"]}// Исключить из запуска тесты с тегом Linux, а также Windows. То есть запуститься 0 тестов task noTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties + ["kotest.tags": "!Linux & !Windows"]}// Запустить тесты, у которых имеется тег Linux либо Windows. То есть все тестыtask linuxAndWindowsTest(type: Test) {    group "test"    useJUnitPlatform()    systemProperties = System.properties + ["kotest.tags": "Linux | Windows"]}

Обращаю внимание на 2 момента:


  • Kotest запускается через Junit5 Runner, поэтому декларирован useJUnitPlatform()
  • Gradle не копирует системные переменные в тест, поэтому необходимо явно указать systemProperties = System.properties

Условный запуск


В коде спецификации можно задать динамические правила включения/выключения тестов двумя путями:


  • на уровне спецификации через аннотацию @io.kotest.core.annotation.EnabledIf и реализацию интерфейса EnabledCondition
  • на уровне теста через расширение String.config(enabledIf = (TestCase) -> Boolean)
    Вот пример:

/** [io.kotest.core.annotation.EnabledIf] annotation with [io.kotest.core.annotation.EnabledCondition] */@EnabledIf(OnCICondition::class) // Аннотация принимает класс с логикой включениеclass CIOnlySpec : FreeSpec() {    init {                            /* Логика включения передается в конфигурацию теста */        "Test for Jenkins".config(enabledIf = jenkinsTestCase) { }    }}/** typealias EnabledIf = (TestCase) -> Boolean */val jenkinsTestCase: io.kotest.core.test.EnabledIf = { testCase: TestCase -> testCase.displayName.contains("Jenkins") }/** Separate class implementation [io.kotest.core.annotation.EnabledCondition] */class OnCICondition : EnabledCondition {    override fun enabled(specKlass: KClass<out Spec>) = System.getProperty("CI") == "true"}

Тест запустится, если:


  1. Среди системных переменных будет переменная CI=true
  2. В отображаемом имени TestCase встретится строка Jenkins

Функционал не самый используемый.
Есть более простой, но менее гибкий вариант через параметр enabled.
Например: "My test".config(enabled = System.getProperty("CI") == "true") { }

Последовательность выполнения


Уровень спецификации


По-умолчанию спецификации выполняются в порядке загрузки классов JVM. Это зависит от платформы, но скорее всего порядок будет алфавитный. Есть возможность задать порядок через конфигурацию уровня проекта используя enum io.kotest.core.spec.SpecExecutionOrder


object ProjectConfig : AbstractProjectConfig() {    override val specExecutionOrder = SpecExecutionOrder.Annotated}

  • SpecExecutionOrder.Undefined. Используется по-умолчанию и зависит от загрузки классов на платформе.
  • SpecExecutionOrder.Lexicographic. В алфавитном порядке имен классов спецификаций
  • SpecExecutionOrder.Random. В случайном порядке.
  • SpecExecutionOrder.Annotated. На основе аннотаций @Order над классами спецификаций с номерным аргументом. Меньше номер раньше выполнение. Не помеченные выполняются в конце по стратегии Undefined
  • SpecExecutionOrder.FailureFirst. Новая стратегия. Сначала выполняет упавшие в предыдущем прогоне тесты, а остальные спецификации по стратегии Lexicographic

Для использования FailureFirst необходимо включить сохранение результатов прогона в конфигурации проекта. По-умолчанию результаты сохраняются по пути на файловой системе ./.kotest/spec_failures в директории проекта.


object ProjectConfig : AbstractProjectConfig() {    override val specExecutionOrder = SpecExecutionOrder.FailureFirst    /**     * Save execution results to file for [SpecExecutionOrder.FailureFirst] strategy.     * File location: [io.kotest.core.config.Configuration.specFailureFilePath] = "./.kotest/spec_failures"     */    override val writeSpecFailureFile = true}

Привожу пример использования стратегии Annotated:


object ProjectConfig : AbstractProjectConfig() {    override val specExecutionOrder = SpecExecutionOrder.Annotated}@Order(Int.MIN_VALUE) // Аннотация на классеclass FirstSpec : FreeSpec() {    init {        "FirstSpec-Test" { }    }}@Order(Int.MIN_VALUE + 1) // Аннотация на классеclass SecondSpec : FreeSpec() {    init {        "SecondSpec-Test" { }    }}@Order(Int.MAX_VALUE) // Аннотация на классеclass LastSpec : FreeSpec() {    init {        "LastSpec-Test" { }    }}

Порядок выполнения будет такой: FirstSpec -> SecondSpec -> LastSpec. Чем больше число в @Order тем позже выполнение.


Уровень тестов


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


Для всего проекта можно настроить последовательность в конфигурации проекта:


object ProjectConfig : AbstractProjectConfig() {    override val testCaseOrder: TestCaseOrder = TestCaseOrder.Random}

В рамках одного класса через переопределение метода testCaseOrder:


class TestOrderingSpec : FreeSpec() {    override fun testCaseOrder(): TestCaseOrder = TestCaseOrder.Lexicographic}

Параллельность


Очень интересная и сложная тема, как в контексте разработки, так и в контексте тестирования.
Рекомендую ознакомиться с книгой Java Concurrency in Practice она как раз недавно появилась на русском языке.
Основой для принятия решения запуска тестов параллельно служит свойство теста быть независимым в контексте Фреймворка тестов и тестируемой системы.
Недостаточно обеспечить очистку состояния тестируемой системы, в которое она перешла после выполнения теста,- необходимо обеспечить неизменное состояние во время выполнения теста, либо гарантию, что изменение состояния не имеет инвариантов с другими воздействиями на систему в других тестах.


Для гарантии изолированности необходимо:


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

Есть системы, которые потенциально независимо обрабатывают запросы или сессии, можно положиться на это, однако 100% гарантии никто не даст.
Тест может воздействовать на систему в обход публичного API (например что-то записать в БД напрямую), что также может нарушить изоляцию.


Kotest не решает проблемы обеспечения условий для параллельного запуска, но предоставляет инструменты для организации параллельного выполнения на уровне тестов и спецификаций


Уровень тестов


Можно задать кол-во потоков для тестов в рамках одного класса спецификации переопределив метод fun threads()


Пример спецификации с 2-мя тестами и 2-мя потоками:


class OneParallelOnTestLevelSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(OneParallelOnTestLevelSpec::class.java)    }    override fun threads(): Int = 2    init {        "parallel on test level 1" {            log.info("test 1 started")            delay(500)            log.info("test 1 finished")        }        "parallel on test level 2" {            log.info("test 2 started")            delay(1000)            log.info("test 2 finished")        }    }}

Запустим с помощью gradle задачи:


task parallelismTest(type: Test) {    group "test"    useJUnitPlatform()    filter { includeTestsMatching("ru.iopump.qa.sample.parallelism.OneParallelOnTestLevelSpec") }}

Имеем вывод:


21:15:44:979 OneParallelOnTestLevelSpec - test 2 started21:15:44:979 OneParallelOnTestLevelSpec - test 1 started21:15:45:490 OneParallelOnTestLevelSpec - test 1 finished21:15:45:990 OneParallelOnTestLevelSpec - test 2 finished

По логу видно, что test 1 и test 2 запустились одновременно в 21:15:44:979 и завершились:
первый в 21:15:45:490, второй через 500мс, все по-плану.


Уровень спецификации


Чтобы запустить классы спецификаций в несколько потоков нужно задать настройки параллельности до их запуска, то есть на уровне конфигурации проекта:


object ProjectConfig : AbstractProjectConfig() {    override val parallelism: Int = 3}

Либо через системную переменную -Dkotest.framework.parallelism=3 или прямо в задаче gradle:


task parallelismTest(type: Test) {    group "test"    useJUnitPlatform()    doFirst { systemProperties = System.properties + ["kotest.framework.parallelism": 3] }    filter { includeTestsMatching("ru.iopump.qa.sample.parallelism.*") }}

Используем одну спецификацию с прошлого раздела OneParallelOnTestLevelSpec и 2 новые, где сами тесты внутри будут выполняться последовательно:


Еще 2 спецификации для параллельного запуска
class TwoParallelSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(TwoParallelSpec::class.java)    }    init {        "sequential test 1" {            log.info("test 1 started")            delay(1000)            log.info("test 1 finished")        }        "sequential test 2" {            log.info("test 2 started")            delay(1000)            log.info("test 2 finished")        }    }}class ThreeParallelSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(ThreeParallelSpec::class.java)    }    init {        "sequential test 1" {            log.info("test 1 started")            delay(1000)            log.info("test 1 finished")        }        "sequential test 2" {            log.info("test 2 started")            delay(1000)            log.info("test 2 finished")        }    }}

Все 3 спецификации должны запуститься параллельно, а также тесты в OneParallelOnTestLevelSpec выполняться в своих 2-ух потоках:


21:44:16:216 [kotest-engine-1] CustomKotestExtension - [BEFORE] prepareSpec class ru.iopump.qa.sample.parallelism.ThreeParallelSpec21:44:16:216 [kotest-engine-2] CustomKotestExtension - [BEFORE] prepareSpec class ru.iopump.qa.sample.parallelism.TwoParallelSpec21:44:16:216 [kotest-engine-0] CustomKotestExtension - [BEFORE] prepareSpec class ru.iopump.qa.sample.parallelism.OneParallelOnTestLevelSpec21:44:18:448 [SpecRunner-3] ThreeParallelSpec - test 2 started21:44:18:448 [SpecRunner-6] OneParallelOnTestLevelSpec - test 1 started21:44:18:448 [SpecRunner-5] TwoParallelSpec - test 1 started21:44:18:448 [SpecRunner-4] OneParallelOnTestLevelSpec - test 2 started21:44:18:959 [SpecRunner-6] OneParallelOnTestLevelSpec - test 1 finished21:44:19:465 [SpecRunner-5] TwoParallelSpec - test 1 finished21:44:19:465 [SpecRunner-3] ThreeParallelSpec - test 2 finished21:44:19:465 [SpecRunner-4] OneParallelOnTestLevelSpec - test 2 finished21:44:19:471 [SpecRunner-5] TwoParallelSpec - test 2 started21:44:19:472 [SpecRunner-3] ThreeParallelSpec - test 1 started21:44:20:484 [SpecRunner-3] ThreeParallelSpec - test 1 finished21:44:20:484 [SpecRunner-5] TwoParallelSpec - test 2 finished

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


  • пул потоков для спецификаций NamedThreadFactory("kotest-engine-%d")
  • пул дочерних потоков для тестов NamedThreadFactory("SpecRunner-%d")

Все это запускается с помощью Executors.newFixedThreadPool и особенности работы с корутинами конкретно в этой конфигурации не используются, т.к. suspend функции выполняются через runBlocking:


executor.submit {    runBlocking {        run(testCase)    }}

Подробности реализации в методах io.kotest.engine.KotestEngine.submitBatch и io.kotest.engine.spec.SpecRunner.runParallel

Исключение из параллельного запуска


Если спецификацию по каким-то причинам следует выполнять в единственном потоке, то есть аннотация @DoNotParallelize.
Для отдельного теста на текущий момент подобного не предусмотрено.


Таймауты


Имеется большой набор вариантов для установки таймаутов на время выполнения теста.


Привожу пример спецификации со всеми вариантами таймаута:


Для пояснений в коде я буду использовать метки формата /*номер*/ и после примера рассказывать про каждую в виде: номер -

Варианты описания таймаутов
@ExperimentalTimeclass TimeoutSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(TimeoutSpec::class.java)    }    /*1*/override fun timeout(): Long = 2000 // Spec Level timeout for each TestCase not total    /*2*/override fun invocationTimeout(): Long =        2000 // Spec Level invocation timeout for each TestCase invocation not total    init {        /*3*/timeout = 1500 // Spec Level timeout for each TestCase not total        /*4*/invocationTimeout = 1500 // Spec Level invocation timeout for each TestCase invocation not total        "should be invoked 2 times and will be successful at all".config(            /*5*/invocations = 2,            /*6*/invocationTimeout = 550.milliseconds,            /*7*/timeout = 1100.milliseconds        ) {            log.info("test 1")            delay(500)        }        "should be invoked 2 times and every will fail by invocationTimeout".config(            invocations = 2,            invocationTimeout = 400.milliseconds,            timeout = 1050.milliseconds        ) {            log.info("test 2")            delay(500)        }        "should be invoked 2 times and last will fail by total timeout".config(            invocations = 2,            invocationTimeout = 525.milliseconds,            timeout = 1000.milliseconds        ) {            log.info("test 3")            delay(500)        }    }}

Агрегирующего таймаута на всю спецификацию нет. Все варианты устанавливают таймаут только на тест.

5 invocations = 2 В Kotest есть возможность задать ожидаемое кол-во успешных выполнений одного теста, чтобы считать его успешным.
Например, чтобы проверить требование отсутствия состояния в системы, можно выполнить тест несколько раз, если хотя бы одно выполнение будет неуспешным, то тест будет считаться неуспешным. Не путайте с fluky это обратная ситуация.


1 override fun timeout(): Long = 2000 Через переопределение метода. Установить таймаут на тест в мс.


2 override fun invocationTimeout(): Long = 2000 Через переопределение метода. Установить таймаут на один вызов теста в мс (см 5).


3 timeout = 1500 Через свойство. Установить таймаут на тест в мс и переписать 1


4 invocationTimeout = 1500 Через свойство. Установить таймаут на один вызов теста в мс и переписать 2 (см 5).


6 invocationTimeout = 550.milliseconds Через метод конфигурации теста. Установить таймаут на тест в kotlin.time.Duration и переписать 3.


7 timeout = 1100.milliseconds Через метод конфигурации теста. Установить таймаут на один вызов теста в kotlin.time.Duration и переписать 4 (см 5).


kotlin.time.Duration имеет статус Experimental и при использовании требует установки @ExperimentalTime над классом спецификации

Разберем вывод запуска TimeoutSpec:


08:23:35:183 TimeoutSpec - test 108:23:35:698 TimeoutSpec - test 108:23:36:212 CustomKotestExtension - [AFTER] afterTest. Test case duration: 1047 ms08:23:36:217 TimeoutSpec - test 2Test did not complete within 400msTimeoutException(duration=400)08:23:36:625 TimeoutSpec - test 308:23:37:141 TimeoutSpec - test 3Test did not complete within 1000msTimeoutException(duration=1000)

Первый тест прошел успешно 2 раза, каждое выполнение уместилось invocationTimeout и весь тест в timeout
Второй тест упал на первом выполнении и не стал далее выполняться по invocationTimeout TimeoutException(duration=400)
Третий тест упал на втором выполнении по своему timeout TimeoutException(duration=1000)


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


@ExperimentalTimeobject ProjectConfig : AbstractProjectConfig() {    /** -Dkotest.framework.timeout in ms */    override val timeout = 60.seconds    /** -Dkotest.framework.invocation.timeout in ms */    override val invocationTimeout: Long = 10_000}

Встроенные ожидания


Во время тестов система реагирует на наши действия не мгновенно, особенно в E2E. Поэтому для всех проверок результатов взаимодействия с системой необходимо использовать ожидание. Приведу неполный список основных:


  • ожидание HTTP ответа (уже есть на уровне HTTP клиента, достаточно только указать таймаут)
  • ожидание появления запроса на заглушке
  • ожидание появления/изменения записи в БД
  • ожидание сообщения в очереди
  • ожидание реакции UI (для Web реализовано в Selenide)
  • ожидание письма на SMTP сервере

Если не понятно, почему нужно ждать в заглушке или в БД, отвечу в комментариях или сделаю отдельную публикацию

Есть отличная утилита для тонкой настройки ожиданий: Awaitility о ней пойдет речь в будущих частях. Но Kotest из коробки предоставляет простой функционал ожиданий.
В документации движка этот раздел называется Non-deterministic Testing.


Eventually


Подождать пока блок кода пройдет без Исключений, то есть успешно, в случае неуспеха повторять блок еще раз.
Функция из пакета io.kotest.assertions.timing: suspend fun <T, E : Throwable> eventually(duration: Duration, poll: Duration, exceptionClass: KClass<E>, f: suspend () -> T): T


f это блок кода, который может выбросить Исключение и этот метод будет его перезапускать, пока не выполнит успешно либо не пройдет таймаут duration.
poll это промежуток бездействия между попытками выполнения.
exceptionClass класс исключения по которому нужно попытаться еще раз.
Если есть уверенность, что когда код выбрасывает IllegalStateException, то нужно пробовать еще раз, но если, класс исключения другой, то успешно код точно никогда не выполниться и пытаться еще раз выполнить код не нужно, тогда указываем конкретный класс IllegalStateException::class.


Continually


Выполнять переданный блок в цикле, пока не закончится выделенное время либо пока код не выбросит Исключение.
Функция из пакета io.kotest.assertions.timing: suspend fun <T> continually(duration: Duration, poll: Duration, f: suspend () -> T): T?


f блок кода, который будет запускаться в цикле пока не пройдет таймаут duration либо не будет выброшено Исключение.
poll промежуток бездействия между попытками выполнения


Рассмотрим пример из четырех тестов (2 eventually + 2 continually)


Варианты использования ожидания
@ExperimentalTimeclass WaitSpec : FreeSpec() {    private companion object {        private val log = LoggerFactory.getLogger(WaitSpec::class.java)    }    /*1*/    private lateinit var tries: Iterator<Boolean>    private lateinit var counter: AtomicInteger    private val num: Int get() = counter.incrementAndGet()    init {        /*2*/        beforeTest {            tries = listOf(true, true, false).iterator()            counter = AtomicInteger()        }        "eventually waiting should be success" {            /*3*/eventually(200.milliseconds, 50.milliseconds, IllegalStateException::class) {            log.info("Try #$num")            if (tries.next()) /*4*/ throw IllegalStateException("Try #$counter")        }        }        "eventually waiting should be failed on second try" {            /*5*/shouldThrow<AssertionError> {            eventually(/*6*/100.milliseconds, 50.milliseconds, IllegalStateException::class) {                log.info("Try #$num")                if (tries.next()) throw IllegalStateException("Try #$counter")            }        }.toString().also(log::error)        }        "continually waiting should be success" - {            /*7*/continually(200.milliseconds, 50.milliseconds) {            log.info("Try #$num")        }        }        "continually waiting should be failed on third try" {            /*8*/shouldThrow<IllegalStateException> {            continually(200.milliseconds, 50.milliseconds) {                log.info("Try #$num")                if (tries.next()) throw IllegalStateException("Try #$counter")            }        }.toString().also(log::error)        }    }}

1 Блок с полями для подсчета попыток и описания итератора из трех элементов


2 Перед каждым тестовым контейнером сбрасывать счетчики


3 Вызывается функция eventually, которая завершится успешно. Общий таймаут 200 мс, перерыв между попытками 50 мс, игнорировать исключение IllegalStateException


4 Первые две итерации выбрасывают IllegalStateException, а 3-я завершается успешно.


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


6 таймаута 100 мс и перерыв между попытками 50 мс, позволяют выполнить только 2 неудачный попытки, в итоге ожидание завершается неуспешно


7 continually выполнит 4 попытки, все из которых будут успешные и само ожидание завершится успехом


8 Ожидается, что continually закончится неуспешно и выполняется проверка выброшенного исключения. При неудаче continually
перебрасывает последнее Исключение от кода внутри, то есть IllegalStateException, чем отличается от eventually


Лог выполнения с пояснениями
/////////////////////////////////////////////////////////////////// 1 ////////////////////////////////////////////////////////////////////////21:12:14:796 INFO CustomKotestExtension - [BEFORE] test eventually waiting should be success21:12:14:812 INFO WaitSpec - Try #121:12:14:875 INFO WaitSpec - Try #221:12:14:940 INFO WaitSpec - Try #3/////////////////////////////////////////////////////////////////// 2 ////////////////////////////////////////////////////////////////////////21:12:14:940 INFO CustomKotestExtension - [BEFORE] test eventually waiting should be failed on second try21:12:14:956 INFO WaitSpec - Try #121:12:15:018 INFO WaitSpec - Try #2/* Сообщение в ошибке содержит информацию о настройках ожидания */21:12:15:081 ERROR WaitSpec - java.lang.AssertionError: Eventually block failed after 100ms; attempted 2 time(s); 50.0ms delay between attempts/* Сообщение в ошибке содержит первое выброшенное кодом Исключение и последнее */ The first error was caused by: Try #1java.lang.IllegalStateException: Try #1The last error was caused by: Try #2java.lang.IllegalStateException: Try #2//////////////////////////////////////////////////////////////////// 3 ///////////////////////////////////////////////////////////////////////21:12:15:081 INFO CustomKotestExtension - [BEFORE] test continually waiting should be success21:12:15:081 INFO WaitSpec - Try #121:12:15:159 INFO WaitSpec - Try #221:12:15:221 INFO WaitSpec - Try #321:12:15:284 INFO WaitSpec - Try #4///////////////////////////////////////////////////////////////////// 4 //////////////////////////////////////////////////////////////////////21:12:15:346 INFO CustomKotestExtension - [BEFORE] test continually waiting should be failed on third try21:12:15:346 INFO WaitSpec - Try #121:12:15:409 INFO WaitSpec - Try #221:12:15:469 INFO WaitSpec - Try #3/* Здесь выбрасывается Исключение из выполняемого блока в continually */21:12:15:469 ERROR WaitSpec - java.lang.IllegalStateException: Try #3

Flaky тесты


Flaky тесты это нестабильные тесты, которые могут случайным образом проходить неудачно. Причин этому крайне много и разбирать мы их конечно же не будем в рамках этой статьи, обсудим варианты перезапуска подобных тестов. Можно выделить несколько уровней для перезапуска:


  • на уровне CI перезапускать всю Задачу прогона тестов, если она неуспешна
  • на уровне Gradle сборки, используя плагин test-retry-gradle-plugin или аналог
  • на уровне Gradle сборки, с помощью своей реализации, например сохранять упавшие тесты и запускать их в другой задаче

Другие инструменты для сборки Java/Kotlin/Groovy кроме Gradle не рассматриваю

  • на уровне тестового движка
  • на уровне блоков кода в тесте (см. раздел Встроенные ожидания и retry)

На текущей момент в версии Kotest 4.3.2 нет функционала для перезапуска нестабильных тестов. И не работает интеграция с Gradle плагином test-retry-gradle-plugin.


Есть возможность перезапустить отдельный блок кода. Это особенно актуально для UI тестирования, когда клик на некоторые элементы иногда не срабатывает.
Представьте ситуацию с Dropdown внизу страницы, после скролла происходит клик, далее ожидание раскрытия элементов и клик на элемент списка.
Так вот, независимо от Фронтового фреймворка, иногда клик для раскрытия списка может не сработать и список не появляется, нужно еще раз попробовать начиная с клика.


Можно сделать повтор через try/catch, но лучше использовать
функцию suspend fun <T, E : Throwable> retry(maxRetry: Int, timeout: Duration, delay: Duration = 1.seconds, multiplier: Int = 1, exceptionClass: KClass<E>, f: suspend () -> T)::


// using Selenidedropdown.scrollTo()retry(2, 20.seconds, exceptionClass = Throwable::class) {    dropdown.click()    dropdownItems.find(Condition.text("1")).apply {        click()        should(Condition.disappear)    }}

Действие scrollTo повторять не нужно. После перехода к элементу блок с раскрытием списка и выбором повторить 2 раза, но не дольше 20 сек игнорируя все исключения (или более конкретные).


В Kotest возможно создать свое расширение для перезапуска упавших спецификаций/контейнеров теста/тестов.
В 3 части руководства я покажу как создать самый простой вариант для уровня тестов.
Остальные варианты я планирую реализовать в своем GitHub репозиторие и выпустить в MavenCentral, если к тому моменту разработчик Kotest не добавит поддержку 'из коробки'.


Фабрики тестов


Представим, что в каждом тесте нужно выполнять одни и те же действия для подготовки окружения: наполнять БД, очереди, конфиги на файловой системе.
Для этих действий хочется создать шаблон теста с адекватным именем, возможно с описанием в виде javadoc/kdoc и несколькими аргументами, например именем и паролем тестового пользователя.
В Kotest такой подход называется Test Factories и позволяет вставлять куски тестов в корневой тест.
Это ни функции, ни методы, ни абстракции это параметризованные части теста с той же структурой, что основной тест, но используемые в нескольких местах кода.


Я придерживаюсь правила, что спецификация теста должна быть понятна любому человеку, имеющему экспертизу в предметной области теста.
Под спецификацией теста я понимаю не реализацию, а описания шагов в виде строк в BDD стиле. В 1-ой части руководства я раскрывал тему форматирования теста более подробно.
Даже если требуется много copy/paste для сохранения формата теста, нужно это делать.
А вот тестовый движок/фреймворк позволяет спецификацию теста запустить, провести взаимодействия с системой автоматически, как если бы все что написано в тесте делал человек.

Очень важно не нарушать читабельность теста использованием шаблонов:


  • имя шаблона должно быть понятное и сопровождаться описанием, а так же описанием всех параметров.
  • шаблон должен выполнять одну функцию, например настройка БД (принцип единственной ответственности)
  • шаблон должен быть описан в BDD стиле
  • шаблон не должен быть слишком абстрактным (субъективно)

Теперь к реализации в Kotest. Она очень ограничена, поэтому я приведу пример того, как рекомендует делать шаблоны официальная документация и как это удобнее делать своими силами через scope-функции и функции-расширения.
Пример кода с пояснениями:


2 вида фабрик тестов. TestFactory и scope-функции
class FactorySpec : FreeSpec() {    init {        /*1.1*/include(containerFactory(1, 2, log))        "root container" - {            /*2.1*/containerTemplate()        }    }}/** Kotest factory */fun containerFactory(argument1: Any, argument2: Any, logger: Logger) =    /*1.2*/freeSpec {    beforeContainer { logger.info("This 'beforeContainer' callback located in the test factory") }    "factory container" - {        "factory test with argument1 = $argument1" { }        "factory test with argument2 = $argument2" { }    }}/** Add [TestType.Container] by scope function extension *//*2.2*/suspend inline fun FreeScope.containerTemplate(): Unit {    "template container with FreeScope context" - {        /*2.3*/testCaseTemplate()    }}/** Add [TestType.Test] by scope function extension *//*2.4*/suspend inline fun FreeScope.testCaseTemplate() {    "nested template testcase with FreeScope context" { }}private val log = LoggerFactory.getLogger(FactorySpec::class.java)

TestFactory

1.1 С помощью метода fun include(factory: TestFactory) включаем шаблон теста в спецификацию в качестве самостоятельного теста.


1.2 Определена функция containerFactory, которая возвращает экземпляр TestFactory и принимает несколько параметров. Для создания объекта TestFactory используем функцию fun freeSpec(block: FreeSpecTestFactoryConfiguration.() -> Unit): TestFactory, которая принимает блок тестов и предоставляет контекст FreeSpecTestFactoryConfiguration.
Внутри этого блока пишем тест, как обычно, в BDD силе. Далее с помощью includeшаблон вставляется как обычный контейнер теста.


Для TestFactory выполняются все обратные вызовы, что определены для спецификации, куда встраивается шаблон.
А также доступны все методы для создания обратных вызовов внутри фабрики, например beforeContainer.
У TestFactory есть большие ограничения:
  • нельзя вставлять include друг в друга
  • встраивать можно только на уровень спецификации, то есть фабрика должна быть полным тестом, а не частью теста.


Шаблоны через scope-функции и функции-расширения.

2.1 Внутри контейнера вызывается пользовательская функция containerTemplate и добавляет к контексту этого контейнера новые шаги теста.


2.2 Функция suspend inline fun FreeScope.containerTemplate(): Unit выполняет свой код в контексте внешнего теста FreeScope и просто изменяет этот контекст, добавляя новый вложенный контейнер.
Функция ничего не возвращает, а именно изменяет (side-effect) переданный контекст. Тесты пишем так, как будто они не в отдельной функции, а в спецификации.
suspend обязателен, т.к. все тесты запускаются в корутине.
inline для скорости, не обязателен. Указывает компилятору на то, что код этой функции нужно просто скопировать вместо вызова, то есть в байт-коде, не будет containerTemplate, а будет вставленный код в спецификации.
FreeScope. внутренности вызываются в контексте этого объекта, в данном случае контейнера


2.3 Внутри шаблона вызывается другой шаблон, который добавлен новые шаги. В реальных тестах так делать не следует


2.4 Функция suspend inline fun FreeScope.testCaseTemplate(): Unit добавляет вложенные шаги теста вместо вызова. Все то же самое, что и 2.2


В итоге имеем следующую структуру теста после выполнения:



Property тестирование


Представим, что мы в 90-х годах на большом и сложном проекте, где критично качество и есть ресурсы на всеобъемлющее тестирование, на всех вариантах тестовых данных и окружений.
В этом случае невозможно использовать ограниченный тестовых набор, если только тип входных данных не ограничен парой вариантов Data-Driver не подходит.
В таких случаях необходимо генерировать тестовые данные случайным образом, либо вообще покрывать абсолютно все возможные значения.
Необходимо не просто уметь генерировать данные, но и легко менять параметры генерации: функции распределения значений, граничные значения и т.п.
Не менее важна возможность повторить последовательность с прошлого прогона, чтобы воспроизвести ошибку. А также требуются инструменты для создания своих генераторов.


Генераторы данных


В Kotest существует 2 типа генераторов данных:


  • Arb (Arbitrary Случайный) генерирует бесконечные последовательности из которых по-умолчанию в тесте будет используется 1000 значений
  • Exhaustive (Исчерпывающий) служит для полного перебора ограниченного набора значений

Полный список генерируемых типов довольно большой и хорошо описан в официальной документации


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


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


Применение Arb генераторов для генерации случайных данных
/** For string generator with leading zero *//*1*/val numberCodepoint: Arb<Codepoint> = Arb.int(0x0030..0x0039)        .map { Codepoint(it) }/** For english string generator *//*2*/val engCodepoint: Arb<Codepoint> = Arb.int('a'.toInt()..'z'.toInt())    .merge(Arb.int('A'.toInt()..'Z'.toInt()))    .map { Codepoint(it) }class GeneratorSpec : FreeSpec() {    init {        /*3*/"random number supported leading zero" {            Arb.string(10, numberCodepoint).next()                .also(::println)        }        /*4*/"random english string" {            Arb.string(10, engCodepoint).orNull(0.5).next()                .also(::println)        }        /*5*/"random russian mobile number" {            Arb.stringPattern("+7\\(\\d{3}\\)\\d{3}-\\d{2}-\\d{2}").next()                .also(::println)        }        /*6*/"exhaustive collection and enum multiply" {            Exhaustive.ints(1..5).times(Exhaustive.enum<Level>()).values                .also(::println)        }        /*7*/"exhaustive collection and enum merge" {            Exhaustive.ints(1..5).merge(Exhaustive.enum<Level>()).values                .also(::println)        }    }}

1 Класс Codepoint задает набор символов в виде кодов Unicode. Этот класс используется в Arb для генерации строк.
В Kotest есть встроенные наборы символов в файле io.kotest.property.arbitrary.codepoints.
В примере определен набор Unicode цифр (такого набор нет среди встроенных). В дальнейших шагах этот набор будет использован для генерации номеров, которые могут начинаться на 0


2 Определен набор букв английского алфавита. Т.к. в Unicode они идут не по порядку, то сначала создается набор малого регистра, потом merge с набором большого регистра.


3 Использование набора символов цифр numberCodepoint для генерации номеров, где возможен 0 в начале типа String, длина 10 символов.


4 Использование набора символов английского алфавит engCodepoint для генерации слов длиной 10 символов. А также с вероятностью 50% вместо строки сгенерируется null метод orNull(0.5)


5 Метод Arb.stringPattern позволяет генерировать строки на основе RegEx паттерна очень полезная функциональность. Реализация на основе Generex. Производительность оставляет желать лучшего и зависит от сложности регулярного выражения.
Для номера телефона генерация происходит в 10 раз медленнее, чем при использовании Arb.string(10,numberCodepoint) с форматированием. У меня 1000 генераций телефона по паттерну -> 294 мс и 1000 генераций телефона из строки цифр с последующим форматированием -> 32 мс.
Замерять удобно методом measureTimeMillis.


6 Exhaustive фактически просто обертка для перечислимых типов и актуальна только в интеграции с Property-тестами, однако также имеет интересные возможности. Здесь происходит умножение набора Int на объекты enum Level и на выходы получается 25 Pair. Если интегрировать в тест, то будет 25 запусков для каждой пары.


7 Exhaustive объединение в одну коллекцию из 10 разнородных элементов вида: [1, ERROR, 2, WARN ...]


Вся функциональность генераторов доступна не только для Property-тестов, но и для простых тестов, где нужны случайные значения тестовых данных.
Для генерации одного значения используется терминальный метод Arb.next

Результаты запуска:


// Test 12198463900// Test 2tcMPeaTeXG// Test 3+7(670)792-05-16// Test 4[(1, ERROR), (1, WARN), (1, INFO), (1, DEBUG), (1, TRACE), (2, ERROR), (2, WARN), (2, INFO), (2, DEBUG), (2, TRACE), (3, ERROR), (3, WARN), (3, INFO), (3, DEBUG), (3, TRACE), (4, ERROR), (4, WARN), (4, INFO), (4, DEBUG), (4, TRACE), (5, ERROR), (5, WARN), (5, INFO), (5, DEBUG), (5, TRACE)]// Test 5[1, ERROR, 2, WARN, 3, INFO, 4, DEBUG, 5, TRACE]

Написание и конфигурирование Property тестов


Kotest предоставляет две функции для запуска Property-тестов, а также их перегруженные вариации:


  • suspend inline fun <reified A> forAll(crossinline property: PropertyContext.(A) -> Boolean)
  • suspend inline fun <reified A> checkAll(noinline property: suspend PropertyContext.(A) -> Unit)

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


Выполнить 1000 итераций кода со случайным входным числом типа Long:


checkAll<Long> { long: Long ->    val attempt = this.attempts()    println("#$attempt - $long")    long.shouldNotBeNull()}

Обращаю внимание на то, что функциональное выражение в аргументе forAll и checkAll не простое, а с дополнительным контекстом!


suspend PropertyContext.(A) -> Unit имеем аргумент generic типа (в примере это long: Long), но также имеем доступ к контексту PropertyContext, через ключевое слово this (для красоты его можно опустить).


val attempt = this.attempts() здесь возвращается кол-во пройденных попыток из PropertyContext, но там есть и другие полезные методы.


Пользовательские генераторы

У обоих видов генераторов (Arb и Exhaustive) есть терминальные методы forAll и checkAll, которые запускают блок теста, принимающий сгенерированные значения от пользовательских генераторов в качестве аргумента.


Arb

Задача: Рассмотрим написание Property-теста на примере основной теоремы арифметики: каждое натуральное число можно представить в виде произведения простых чисел. Также это называется факторизация.
Допустим имеется реализованный на Kotlin алгоритм разложения и его необходимо протестировать на достаточном наборе данных.
Код алгоритма можно найти в примерах


Тестовый сценарий: сгенерированное число, разложить с помощью алгоритма на простые множители и проверить, что полученные множители простые и их произведение равно исходному числу тем самым мы проверим реализацию алгоритма.
Очевидно, что одного прогона недостаточно и 10 прогонов недостаточно, как минимум нужно прогнать тест на 1000 случайных величин, а может и больше.


Параметры генератора данных: Кол-во = 1000. Сгенерированное число должно быть больше 1. В сгенерированной последовательности первые два числа должны быть граничные: 2 и 2147483647.


"Basic theorem of arithmetic. Any number can be factorized to list of prime" {    /*1*/Arb.int(2..Int.MAX_VALUE).withEdgecases(2, Int.MAX_VALUE).forAll(1000) { number ->    val primeFactors = number.primeFactors    println("#${attempts()} Source number '$number' = $primeFactors")    /*2*/primeFactors.all(Int::isPrime) && primeFactors.reduce(Int::times) == number}    /*3*/Arb.int(2..Int.MAX_VALUE).checkAll(1000) { number ->    val primeFactors = number.primeFactors    println("#${attempts()} Source number '$number' = $primeFactors")    /*4*/primeFactors.onEach { it.isPrime.shouldBeTrue() }.reduce(Int::times) shouldBe number}}

1 создается генератор на 1000 итераций, множество значений от 2 до Int.MAX_VALUE включительно, отдельным методом withEdgecasesнеобходимо явно указать 2 и Int.MAX_VALUE граничными значениями.
Методом forAll запускаем тестирование, выходной результат блока теста Boolean. AssertionError Движок выбросит за нас, если будет результат false.


2 метод all принимает ссылку на метод и проверяет, что каждый множитель простой, метод reduce выполняет умножение, а также выполняется конъюнкцию двух проверок.


3 все абсолютно аналогично 1. Отличие только в том, что блок кода не должен возвращать Boolean и используются стандартные Assertions, как в обычном тесте.


4 вместо логических операций используем проверки Kotest


Exhaustive

Для исчерпывающего тестирования все аналогично Arb подходу.


Допустим, реализован метод, который в зависимости от enum UUIDVersion генерирует указанный тип UUID. А также же метод должен принимать null и генерировать UUID типа UUIDVersion.ANY.
Сигнатура: fun UUIDVersion?.generateUuid(): UUID


Чтобы проверить этот функционал, нужно перебрать абсолютно все значения UUIDVersion + null.
Не имея знаний о возможностях Exhaustive можно просто перебрать данные в цикле. Однако Exhaustive упрощает и без того несложную задачу:


"UUIDVersion should be matched with regexp" {    /*1*/Exhaustive.enum<UUIDVersion>().andNull().checkAll { uuidVersion ->    /*2*/uuidVersion.generateUuid().toString()    /*3*/.shouldBeUUID(uuidVersion ?: UUIDVersion.ANY)    .also { println("${attempts()} $uuidVersion: $it") }}}

1 reified функция Exhaustive.enum создаст последовательность всех значений UUIDVersion и добавит null, а далее будет вызван Property-тест


2 вызов тестируемой функции-расширения для генерации UUID


3 встроенная в Kotest проверка на соответствие UUID регулярному выражению


Генераторы по-умолчанию

Если использовать функции forAll и checkAll без явного указания генератора типа Arb или Exhaustive, то будет использоваться генератор по-умолчанию в зависимости от Generic-типа.
Например для forAll<String>{ } будет использован генератор Arb.string(). Вот полный набор из внутренностей Kotest:


fun <A> defaultForClass(kClass: KClass<*>): Arb<A>? {    return when (kClass.bestName()) {        "java.lang.String", "kotlin.String", "String" -> Arb.string() as Arb<A>        "java.lang.Character", "kotlin.Char", "Char" -> Arb.char() as Arb<A>        "java.lang.Long", "kotlin.Long", "Long" -> Arb.long() as Arb<A>        "java.lang.Integer", "kotlin.Int", "Int" -> Arb.int() as Arb<A>        "java.lang.Short", "kotlin.Short", "Short" -> Arb.short() as Arb<A>        "java.lang.Byte", "kotlin.Byte", "Byte" -> Arb.byte() as Arb<A>        "java.lang.Double", "kotlin.Double", "Double" -> Arb.double() as Arb<A>        "java.lang.Float", "kotlin.Float", "Float" -> Arb.float() as Arb<A>        "java.lang.Boolean", "kotlin.Boolean", "Boolean" -> Arb.bool() as Arb<A>        else -> null    }}

Примеры кода:


"check 1000 Long numbers" {    checkAll<Long> { long ->        long.shouldNotBeNull()    }}

Seed

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


"print seed on fail" {    /*1*/shouldThrow<AssertionError> {    checkAll<Int> { number ->        println("#${attempts()} $number")        /*2*/number.shouldBeGreaterThanOrEqual(0)    }}./*3*/message.shouldContain("Repeat this test by using seed -?\\d+".toRegex())}"test with seed will generate the same sequence" {    Arb.int().checkAll(/*4*/ PropTestConfig(1234567890)) { number ->        /*5*/if (attempts() == 24) number shouldBe 196548668        if (attempts() == 428) number shouldBe -601350461        if (attempts() == 866) number shouldBe 1742824805    }}

1 Ожидаем исключение AssertionError с семенем.


2 Из 1000 чисел последовательности примерно половина будет точно меньше 0 и проверка должна сработать почти сразу


3 Демонстрация, что сообщение исключения содержит информацию о числе seed


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


5 attempts() возвращает номер итерации. Для итераций 24, 428, 866 будут всегда сгенерированы одинаковые числа


Заключение


Во-первых, привожу ссылку на все примеры qa-kotest-articles/kotest-second.


Был рассмотрен практически весь функционал Kotest


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


Ресурсы


Kotlin. Автоматизация тестирования (часть 1). Kotest: Начало


Примеры кода


Официальная документация Kotest


Kotest GitHub


Kotlin Lang


Coroutines Tutorial


Gradle Testing

Подробнее..

Из песочницы Мир на ладони или как мы с помощью чат-бота оптимизируем рабочие процессы сотрудников

13.10.2020 18:18:35 | Автор: admin

Когда численность сотрудников в НСПК стала измеряться сотнями, а количество корпоративных систем, в которых они работают десятками, то взаимодействие с ними стало не всегда удобным, временные затраты на доступ к ним с рабочей машины начали превышать непосредственное время самой работы в них. А что, если сотрудник находится не на своем рабочем месте, но, скажем, срочно необходимо оформить гостевой пропуск для партнера компании? И это лишь один кейс из множества.


Привет, меня зовут Иван Фисунов. Я расскажу, как мы в компании успешно решили и продолжаем решать, задачи по оптимизации рабочего процесса сотрудников, а также делаем рутинные процессы удобнее, проще и даже чуточку веселее, а следовательно и эффективнее.


Начало


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


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


В первом приближении мы решили опробовать подход, понять его жизнеспособность, собрать отклики пользователей, оценить будет ли такой продукт популярен среди сотрудников и есть ли у него будущее в компании. Для первоначальной апробации сервиса и его скорейшего создания был использован инструмент Node-RED, который позволил в достаточно сжатые сроки создать рабочий прототип, наполнить его первичным функционалом, такими сервисами как парковка, поиск контактов сотрудников, переадресация звонков, печать и некоторыми другими. Мы дали ему имя Шурик, запустили в продакшн и отдали на растерзание пользователям.


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


Новая жизнь


Новый статус системы обязал нас более серьезно подойти к вопросу разработки архитектуры приложения и используемым в его основе технологиям. На первом этапе мы решили реализовать монолитную архитектуру приложения, что позволило нам упростить разработку и последующее развертывание, а в качестве технологического стека выбрали JavaScript, Node.js и MySQL. Выбор стека обусловлен рядом причин. Во-первых, мы уже используем Node.js и React в ряде проектов, по созданию дашбордов и визуализации метрик для мониторинга инфраструктуры. Во-вторых, экосистема Node.js хорошо развита и имеет достаточное количество качественных пакетов, позволяющих не изобретать свой велосипед.


Здесь я бы мог погрузиться в технические детали разработки, выбора фреймворков и библиотек, но на просторах Хабра вы и без меня найдете подробные гайды о том, как создать чат-бота на Python, JavaScript, C# и т.д., нужное подчеркнуть. Я же расскажу о том, во что превратился сервис, как он выглядит сейчас и что с его помощью можно делать.


Регистрация


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


Когда сотрудник впервые становится пользователем Шурика он проходит аутентификацию. Для этого он должен на корпоративном портале указать свой актуальный номер мобильного телефона, привязанный к Telegram аккаунту, после чего отправить свою карточку контакта боту. Чтобы предотвратить ситуацию когда мошенник отправляет не свою карточку контакта, представившись каким-то другим реальным сотрудником НСПК и получает доступ в систему, нами предусмотрен защитный механизм в виде отправки СМС с кодом на предоставленный номер.



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


Офис


Сервис объединяющий под собой несколько полезных офисных задач.
Печать. Часто сотруднику необходимо распечатать документ, который находится на телефоне. Возможно его прислал партнер в один из мессенджеров и заниматься пересылкой его на почту, чтобы потом пустить на печать довольно неудобный процесс. Теперь же, попросив бота напечатать документ, а это могут быть как текстовые документы или таблицы Excel, так и графические файлы, остается лишь проходя мимо любого МФУ, приложить к нему свой пропуск и дождаться завершения печати. Интеграция бота с серверами печати позволила сделать процесс печати быстрым, дистанционным и главное удобным.
Звонки. Проблему включения переадресации звонков с внутреннего номера на мобильный без доступа к рабочему месту, решили путем интеграции бота с сервером телефонии. Раньше этот процесс был не оптимален, требовал обращения в службу поддержки, которая в свою очередь обращалась к администратору телефонии. Тем самым процесс занимал достаточно продолжительное время. Теперь же, по просьбе сотрудника, бот любезно "сходит" на сервер телефонии, подкрутит соответствующие настройки и включит переадресацию на дефолтный мобильный номер или иной, указанный сотрудником. Займет это считанные секунды и пройдёт в фоновом режиме.
Переговорные. В компании ежедневно проходят десятки переговоров с партнерами и гостями. Сейчас у нас почти 20 переговорных. Чтобы быстро забронировать свободную, нужно только сообщить боту желаемое время встречи, а он уже сам сходит в Outlook и создаст в календаре встречу. Сделать это можно даже на лету, например, если хорошая идея возникла при встрече в коридоре, что тоже бывает не редко.


Сотрудники


В НСПК уже почти тысяча сотрудников, поэтому мы добавили возможность оперативного поиска карточки сотрудника и его контактов. Причем сделать это возможность из любого места, например, находясь в командировке.
Это позволило нам значительно сократить время на поиск контактов нужного сотрудника и избавиться от необходимости получения информации из разных источников.
Указав несколько букв фамилии искомого человека, получаем список совпадений, находим в нем нужного. Дальше бот отобразит всю доступную информацию, сопроводит карточку контакта актуальной фотографией, а также дополнительно сообщит, если сотрудник сейчас в отпуске или уехал в командировку.
Под капотом этого сервиса реализован отдельный микросервис, в реальном времени агрегирующий информацию о сотрудниках с ряда корпоративных систем, таких как MS Exchange, 1C, Active Directory, корпоративного портала и предоставляющий боту исчерпывающую информацию о сотруднике.


Парковка


В офисе НСПК имеются несколько парковок, доступные сотрудникам. Находятся они в ведении службы физической безопасности, задача которой, в том числе, организовать удобный и беспроблемный процесс парковки сотрудников. Раньше для парковки своего ТС требовалось либо заранее писать письмо уполномоченному сотруднику, либо звонить на внутренний номер для того чтобы забронировать место. В таком виде процесс нельзя было назвать удобным, оперативным и главное надежным.
Одноименный сервис чат-бота позволил решить данную проблему и значительно упростить остальные процессы. Как сотрудники, так и служба безопасности получили в распоряжение единый канал коммуникации, а забронировать место и получить подтверждение теперь можно за 5 10 минут до подъезда к офису.
Всего за пару нажатий можно посмотреть сколько осталось свободных мест на уличной парковке или подземной, а также на мотопарковке. В конце рабочего дня можно освободить ранее занятое место. И пожаловаться тоже можно, если вас не пускают по какой-то причине.
Если вы часто приезжаете в офис на своем транспортном средстве, возможно даже каждый день на новом, бот позволит сохранить их все и по мере появления нового или продажи старого ТС, вносить соответствующее изменение в базу данных, а при очередной парковке просто выбрать нужное авто из вашего списка. Дополнительным плюсом для службы безопасности, помимо единого канала обращений, стало появление возможности оперативного формирования аналитических справок по использованию парковок.


Фитнес


В компании есть тренажерный зал , открытый 24 часа 7 дней в неделю. Но его заполняемость была неравномерной: он мог то пустовать, то, наоборот, в нем создавались очереди к тому или иному тренажеру. Причина была в том, что для бронирования приходилось открывать календарь Outlook, выбирать интересующий тренажер, искать свободное время, а если не удавалось найти, то повторять процедуру для другого тренажера. Такой процесс требовал непосредственного нахождения сотрудника за своим рабочим местом, был неудобен и часто приводил к тому, что зал посещали без предварительной брони.
Благодаря интеграции с почтовым сервером MS Exchange нам удалось решить данную проблему. Теперь забронировать тренажер можно с помощью чат-бота, выбрав день и доступное время тренировки. Бот гарантирует, что никто не займет его к вашему приходу. Всю работу в Outlook за вас сделает бот, а вам останется только заняться спортом.
И что важно, теперь это можно сделать, например, сидя в корпоративной столовой в момент, когда вы едите вкусную булочку. Кстати, что сегодня на обед тоже может подсказать бот!


Сервис IT


Здесь чат-бот подскажет какие технические работы Департамента ИТ запланированы в ближайшем будущем или уже завершены. Интеграция с JiraSD и Splunk позволяет получать актуальную информацию и быть в курсе событий.
Если пользователь сталкивается с трудностями входа в систему под своей учётной записью, можно уточнить не заблокирована ли она. Бот обратится к Active Directory и проверит это. Если учетная запись все же заблокирована, то тут же можно оформит заявку на ее разблокировку или сброс пароля после удавшегося отпуска.
Если сотрудник обнаружил неполадки с одним из многочисленных МФУ разбросанных по офисам, он может с помощью бота оформить инцидент и сообщить информацию в отдел поддержки пользователей. Бот задаст пару вопросов и попросит сфотографировать штрих-код на МФУ. Далее он распарсит его, обратится к инвентарной базе 1С, определит его местоположение и тут же проинформирует ответственное подразделение.


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


  • Заказать себе справку с места работы для визы или иных целей, копию трудовой или 2-НДФЛ. Сотруднику необходимо сделать всего пару нажатий, а дальше ему остается лишь дождаться уведомления о готовности и забрать документы. Интеграция с JiraSD позволяет делать это прямо с телефона.
  • Поболели с электронным больничным? По выходу предоставьте его номер боту.
  • Офис НСПК имеет централизованную систему кондиционирования помещений. С помощью бота можно управлять микроклиматом в кабинетах и переговорных. Если на совещании стало слишком "жарко", то можно попросить его включить кондиционер.
  • Собрались в отпуск? Всего за пару нажатий можно настроить процедуру замещения в ряде корпоративных систем и поставить почтовый автоответ в Outlook.

Бот в эпоху Короны


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


В первую очередь, чат-бот стал сервисом по получению актуальных инструкций по работе из хоум-офиса, подключению к удаленному рабочему месту и т.д.
Ввиду того, что чат-бот уже имел интеграцию с JiraSD, оперативно добавить еще несколько сервисов для удобства работы на удаленке не составило труда. Пользователи получили возможность, при необходимости, создавать заявки на перезагрузку/включение своих рабочих машин прямо с телефона. Аналогично была решена проблема разблокировки учётных записей пользователей. Появилась возможность создать заявку на разблокировку без лишних звонков и обращений, так как при заблокированной УЗ оформить заявку в системе самостоятельно просто невозможно.


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


Это еще не конец истории


Сейчас уже с уверенностью можно сказать, что чат-бот "поселился" в карманах практически всех наших сотрудников из всех подразделений. База активных пользователей насчитывает более 80% сотрудников компании. Это один из самых высоких показателей среди всех корпоративных систем. Ежедневно бот обрабатывает множество различных запросов. Сотрудники не только используют сервис, но и принимают активное участие в его развитии, как в части новых предложений по функционалу, так и доработки существующих сервисов.


С технической точки зрения за это время сервис из монолитной архитектуры превратился в систему микросервисов, каждая из частей которой выполняет свою работу независимо. А некоторые ее части со временем даже стали источником данных и предоставляют API для ряда сторонних сервисов. Бот интегрирован со множеством корпоративных систем: Active Directory, почтовыми серверами, телефонией, серверами печати, JiraSD, DocVision, 1С и рядом других.
Просматривается тенденция, когда целые подразделения выступают заказчиками того, либо иного функционала, необходимого для их нужд. Служба мониторинга за инфраструктурой получила в лице бота помощника, который отслеживает ряд систем и сигнализирует о наступлении определённых событий, а служба поддержки пользователей, например, получает уведомление о новых открытых инцидентах.


Создание чат-бота с нуля на первоначальном этапе активной разработки с минимальным количеством сервисов и последующим запуском в продакшн, заняло приблизительно три месяца. Говоря о трудозатратах, в этот период потребовалась работа одного разработчика в режиме full-time.


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


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

Подробнее..

Архитектура экосистем

15.12.2020 10:11:51 | Автор: admin

Термин Экосистема появился в бизнес-лексиконе в 1993 году. Американский ученый Джеймс Мур в статье Хищники и жертва: новая экология конкуренции так обозначил модель объединения компаний вокруг решения единой стратегической задачи. Последнее время термин особенно популярен. Упоминаемость экосистемной бизнес-модели на пике в деловых новостях, бизнес-публикациях, финансовых отчетах и программах развития корпораций. Бизнес-экосистемам посвящаются деловые форумы и конференции.

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

Для начала все-таки придется посвятить пару слов лирике - природе и этапам становления экосистем. Это поможет выровняться в понимании самого термина.

В ретроспективе 25-30 лет экосистемная бизнес-модель эволюционировала. На этапе зарождения этого понятия под экосистемой понималось в большей степени объединение вокруг одного продукта конкурирующих между собой поставщиков и производителей. Пример - разработчики клиентского ПО для компьютеров Apple или производители аппаратных компонентов для ПК IBM. Превалировала классическая платформенная модель, которая решала задачу расширения и максимизации ассортиментного состава клиентских продуктов или составных компонентов одного продукта. Сегодня экосистемы приобрели сложный сетевой характер.Бизнес-экосистема выполняет роль источника ресурсов и знаний для развития компаний-участников. Синергетический эффект от участия в экосистеме стал проявляться в намного большем объеме. Продукты и сервисы этой бизнес-модели обогащают друг друга технологиями, функциями и операционными данными.Технологии - главный драйвер эволюции и становления экосистемной бизнес-модели. Тридцать лет назад в розничном бизнесе преобладал Product-centric подход. Главной задачей было грамотно сегментировать клиентскую аудиторию, правильно позиционировать товар, сформировать стратегию продвижения и дистрибуции. С ростом популярности персональных компьютеров, развитием телекоммуникаций, Интернет-технологий и появлением смартфонов возникла ориентация на каналы продаж - WEB-first, Mobile-first, Voice-first. Появилась электронная торговля и продвижение. Золотая полка, статичная и ограниченная в размерах в офлайн-ритейле по причине расположения на уровне глаз покупателя, в электронных каналах продаж стала безграничной и кастомизируемой под каждого клиента. Бизнес представил взору клиента весь товарный ассортимент. Взрывной рост и отрыв от конкурентов получили компании, которые быстро освоили новые каналы продаж и переориентировались на платформенную электронную бизнес-модель. Netflix и Zappos вырвались вперед в конкурентной борьбе, когда предложили клиентам больший ассортимент через онлайн-каналы. Крупнейшим розничным банкам взаимодействие через личные кабинеты клиентов помогло расширить набор финансовых продуктов.

Дальнейший рост вычислительных возможностей, доступности хранилищ данных и их логистики привели к появлению клиенто-центричного подхода в розничном бизнесе. Каждый клиент компании стал отдельным самостоятельным сегментом. Благодаря технологиям регистрации, обработки и анализа неструктурированных операционных данных, бизнес научился предугадывать клиентское поведение и предвосхищать ожидание клиента. Дополнительным катализатором послужило появление CEP (Complex Event Processing) и RTDM (Real-Time Decision Manager) -решений, которые обеспечили анализ информации на лету. Большие данные перестали анализировать по ночам. Интернет-компании за мгновения узнают пользователя и отображают таргетированную рекламу или цену товара уже после обращения к WEB-странице. Благодаря предиктивной аналитике физическое формирование посылки с товарами начинается одновременно с наполнением корзины на сайте - до момента оплаты товара клиентом. А предложение международной страховки направляется клиенту финансовой компании сразу после оплаты покупки в аэропорту.

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

Накопленные подходы и технологии позволили бизнесу создать новые способы конкуренции и, как следствие, новые бизнес-модели. Если раньше корпорации конкурировали с помощью технологий, то теперь - с помощью инновационных бизнес-моделей. Онлайн-магазины становятся Маркетплейсами, поставщики услуг и контента создают мультисервисные Онлайн-платформы, мобильные приложения трансформируются в Супераппы, а поставщики life-style сервисов объединяются вокруг клиента в единую электронную микросреду.

Эти профильные бизнес-модели и объединяет понятие Экосистема.

К текущему моменту сложилось две модели появления экосистем Европейская и Американо-Китайская. Первая модель предполагает децентрализованное объединение компаний - чаще стартапов - на основе единых правил, утверждаемых глобальным государственным или межгосударственным регулятором Центральным банком. Вторая модель предполагает объединение вокруг одного глобального финтех или бигтех игрока десятков меньших по объему бизнеса продуктов и сервисов. Примеры таких экосистем - Facebook, Amazon, Microsoft, Google, Apple (FAMGA) и Baidu, Alibaba, Tencent (BAT).

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

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

Для экосистем характерен ряд свойств, которые отличают их от стратегических альянсов, а также вертикально- и горизонтально-интегрированных компаний:

  • Наличие больших ресурсов для регулярных исследований, опытов и развития решений

  • Использование новых технологий, архитектуры и подходов к разработке ПО

  • Регулярная работа с большими данными

  • Цифровые бизнес-процессы

  • Отсутствие бюрократии в производственном процессе, сокращенный Time-to-market

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

Для клиента такая бесшовная мультисервисная среда включает, например:

  • Возможность использовать единый логин и пароль в разных продуктах

  • Возможность не вводить многократно свои данные в профилях разных сервисов

  • Доступность нужных сервисов в разных интерфейсах (каналах, продуктах) экосистемы

  • Использование единого платежного инструмента, подписки, бонусной программы

  • Просмотр релевантного контента и предложений

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

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

Среди таких глобальных технологических сервисов и подходов можно выделить:

  • Сервисы обеспечения омниканальности.

  • Единую учетную запись.

  • Единый ID клиента и клиентский профиль.

  • Доступность основных сервисов и функций через API.

  • Централизованный клиентский биллинг экосистемы.

  • Ориентацию на событийную модель интеграции (Event-Driven Architecture).

  • Единый контакт центр и службу поддержки.

  • Единый аналитический и операционный CRM.

Рассмотрим некоторые из них подробней.

Омниканальность

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

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

Для НСПК банковская карта это одновременно продукт и канал обслуживания клиента. Омниканальный подход реализуется набором клиентских сервисов, которые поставляются Платежной системой внутри данного клиентского канала. НСПК выступает платформой, связывающей держателей карты Мир с поставщиками финансовых и около-финансовых сервисов. Например, с банками-эмитентами, компаниями-партнерами программы лояльности, сторонними сервисами лояльности, государственными организациями, с собственным продуктом MirPay.

Единая учетная запись

Продукты экосистемы включают большой набор профильных функций. Например, банковское приложение содержит ряд инструментов для работы с текущими счетами, а приложение для инвестирования той же финансовой организации - другой широкий состав доступных операций. Совмещение двух функционалов в одном приложении было бы нецелесообразным с точки зрения UX/UI. При этом вынуждать клиента использовать разные логин и пароль для двух и более сервисов было бы отступлением от принципов экосистемы. Поэтому клиенты экосистемы используют единый логин и пароль.

С точки зрения архитектуры важно использовать единый для продуктов экосистемы сервис аутентификации и авторизации. Это условие выглядит очевидным в случае, когда компоненты экосистемы создаются одновременно. Но часто сама экосистема собирается из разрозненных самостоятельных клиентских сервисов, которые уже располагают авторизующими решениями. В этом случае возникает дилемма. C одной стороны в разных сервисах уже зарегистрированы одни и те же клиенты, которые на момент регистрации не давали согласия и не ожидали, что учетная запись в сервисе A в какой-то момент заработает в сервисе B. С другой стороны, новым клиентам должна быть доступна регистрация сразу во всех бизнес-доменах экосистемы. Оптимальный вариант - создание дополнительного глобального универсального для всей экосистемы способа регистрации и аутентификации, доступного клиентам наряду со стандартной регистрацией в отдельных сервисах (продуктах).

Единый ID клиента и клиентский профиль

Как уже говорилось, важнейший принцип экосистемы - максимизация знаний о клиенте. Здесь важным является взаимный обмен данными о клиентах между сервисами. Эту задачу сложно решить без уникального идентификатора клиента, единого для всех информационных систем и сервисов экосистемы. Если одна информационная система знает клиента по номеру паспорта, а другая по номеру телефона, то синхронизация данных возможна только с реализацией справочников соответствий идентификаторов на стороне каждой из систем. Если информационных систем много, то задача репликации клиентских данных усложняется, а нагрузка на системы возрастает кратно. Поэтому важно формировать уникальный ID клиента централизованно и в момент его регистрации в любом продукте экосистемы.

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

Для платежной системы Мир единый ID клиента и клиентский профиль важен по нескольким причинам. Во-первых, контакт-центр должен иметь возможность оказать поддержку держателю карты по различным вопросам от начисления кэшбэка до токенизации карты в мобильном кошельке MirPay. Для этого информация о клиентских событиях должна сохраняться централизованно и привязываться к единому сквозному клиентскому идентификатору. Во-вторых, в рамках программы лояльности важно уметь предвосхищать клиентские ожидания и понимать, какие категории товаров и услуг интересны клиенту. Данные задачи как раз и помогает решать единый клиентский профиль, сквозной для всех доменов ID клиента и единый аналитический CRM. Новые клиентские продукты проектируются также с учетом использования кросс-доменных глобальных сервисов ID, профиля, учетной записи.

Единый платежный инструмент и централизованный клиентский биллинг экосистемы

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

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

Событийная интеграция систем (Event-Driven Architecture)

Используя перекрестное обогащение знаниями о клиенте компании создают сложные механики анализа клиентского поведения. Они помогают предвосхищать желания и потребности клиентов и предлагать релевантную продукцию товары, контент, услуги. На таком подходе построены концепции Next Best Offer (NBO) и Next Best Action (NBA). В рамках этих решений определяется, какой товар клиент с высокой вероятностью приобретет в конкретный момент (или период) времени. И, соответственно, какое действие клиент будет готов совершить в следующий момент. Для принятия таких решений компании анализируют в режиме real-time до тысячи триггеров клиентского поведения состав покупок, суммы, тип ТСП, запрашиваемый контент, проставленные в соцсетях лайки, среднее время просмотра роликов, контакты и многое другое. Но главное, решение на основе такого анализа необходимо принимать на лету, так как спустя время готовность клиента к приобретению товара или действию может сильно снизиться и предложение станет не актуальным. Поэтому для такого рода задач важна событийно-ориентированная интеграционная архитектура. Каждый домен экосистемы (как совокупность информационных систем) должен уведомлять другие домены о событиях в жизни клиента. Поэтому необходима организация супермаркета операционных данных - решения, которое позволяет информационной системе в онлайн-режиме получать важные для себя данные (например, на базе брокера сообщений Apache Kafka). Прямая интеграция систем для получения данных по запросу или рассылки сообщений о событиях создаст спагетти-архитектуру и, как следствие: существенный прирост нагрузки на системы, более сложное сопровождение, а также предпосылки для большего количества доработок в случае расширения атрибутного состава клиентских данных.


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

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

Поэтому включение нового клиента в экосистему происходит по заранее и детально спроектированному клиентскому пути (Customer Journey). А работа с одним сервисом упрощает клиенту работу с другими сервисами.

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

Подробнее..

Как работают мобильные кошельки на примере приложения Mir Pay

31.08.2020 18:16:08 | Автор: admin
Как известно, в 2015 году мы запустили в эксплуатацию платежную систему Мир, и карты Мир в России принимаются повсеместно. Это, конечно, очень здорово, но сейчас набирает популярность использование мобильных кошельков для оплаты покупок. Согласно статистике, в 2019 году 19% всех операций составляют платежи при помощи смартфона. В 2017 году их было всего 3%. В 2018 году собственное платёжное приложение Mir Pay представила и платежная система Мир. Mir Pay написан на Kotlin, может работать на телефонах с поддержкой NFC и операционной системой Android 6.0 и выше.



Меня зовут Богданов Валерий, я являюсь руководителем группы тестирования в команде мобильных платежей департамента информационных технологий НСПК, и я расскажу о том, как работают мобильные кошельки на примере нашего приложения Mir Pay.

Сначала рассмотрим, как работает оплата с использованием пластиковой карты. В классическом случае карта выдается держателю банком-эмитентом. При этом карта в защищенной области памяти хранит общий с эмитентом ключ MK-AC (Application Cryptogram Master Key). Во время совершения оплаты (при online-операции) карта генерирует на основе MK-AC сессионный ключ SK-AC (Application Cryptogram Session Key) и на нем, с использованием данных карты и данных об операции, полученных с терминала, генерирует криптограмму ARQC (Authorization Request Cryptogram). В основе генерации криптограммы лежит алгоритм 3DES (Triple DES). В общем случае данные по операции поступают от карты к терминалу, далее на хост банка-эквайрера (т.е. обслуживающего торговую точку), затем к платежной системе и на самом последнем этапе к банку-эмитенту (т.е. выдавшему карту) для авторизации.



Эмитент проверяет криптограмму, сгенерировав ее сам на основе данных об операции, пришедших вместе с ARQC и сравнив ее со значением из полученных данных. Банк-эмитент может одобрить или отклонить операцию по результатам анализа карточных данных, криптограммы, установленных лимитов, оценки рисков, а также других параметров.

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



Держатель карты вводит данные в приложение (1), которое передает их в зашифрованном виде (об этом чуть позже) через хосты поставщика услуг мобильного кошелька (WSP Wallet Service Provider) в платежную систему. В случае с Mir Pay поставщиком услуг кошелька является НСПК, поэтому данные сразу попадают в платежную систему (2). Далее обработка происходит на платформе мобильных платежей (ПМП). ПМП расшифровывает данные, по номеру карты определяет, каким эмитентом она была выдана, и запрашивает у него подтверждение на возможность добавления карты в кошелек (3). В случае положительного ответа (4) для данной карты происходит процедура генерации токен-профайла (5) и отправка его на телефон (6). Таким образом, вместо карточных данных на мобильном устройстве будет храниться токен-профайл, привязанный к данной карте и данному устройству. Отметим, что преобразование токен-профайла в исходные карточные данные вне платформы мобильных платежей невозможно. После сохранения токен-профайла на устройстве пользователя Mir Pay запрашивает у ПМП (7) пачку одноразовых ключей, которые будут использоваться приложением при совершении покупки в качестве сессионных ключей, аналогичных упомянутым выше SK-AC. Как видно из названия, одноразовый ключ не может быть применен более одного раза, поэтому в процессе использования приложение Mir Pay периодически подгружает из ПМП новые порции ключей. На этом добавление карты в приложение завершается.

Теперь рассмотрим, как изменился процесс оплаты по сравнению с оплатой по пластиковой карте.



Первый этап почти такой же, только вместо данных карты используются данные токен-профайла, а криптограмма ARQC генерируется на одноразовом ключе, полученном от ПМП в качестве сессионного SK-AC. Еще одно отличие Mir Pay от пластиковых карт состоит в том, что при генерации криптограммы вместо 3DES используется более современный симметричный алгоритм блочного шифрования AES (Advanced Encryption Standard).

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

В Mir Pay используется схема с одноразовыми ключами, но существует и другой подход хранение одного ключа на устройстве. Такой подход требует наличия элемента безопасности на устройстве и некоторые кошельки могут его применять с учетом того, что на определенных устройствах такой элемент безопасности присутствует. В нашем случае, учитывая огромное многообразие телефонов с ОС Android это просто не достижимо. Данная специфика и объясняет выбранную схему.

Рассматривая процесс токенизации, описанный выше, можно заметить один тонкий момент: при оплате кошельком используются данные токен-профайла, однако при добавлении карты ее данные отправляются на хосты платежной системы, а эти данные являются строго конфиденциальными. Для защиты карточных данных в Mir Pay предусмотрена многоступенчатая система защиты. При запуске автоматически включается механизм контроля целостности приложения и проверки окружения, не допускающий использование подложного приложения, модифицированного посторонними лицами. В случае обнаружения существенного риска, приложение сообщает об этом пользователю и автоматически удаляет все хранимые токен-профайлы. Дополнительно результаты данных проверок анализируются еще и на стороне ПМП.

Для обмена конфиденциальными данными ПМП и Mir Pay генерируют ключевые пары и обмениваются публичными компонентами. В силу того, что мы не можем на 100% доверять встроенному хранилищу ключей, была разработана схема с хранением разных ключевых компонент в разных местах: как в ключевом хранилище, так и в оперативной памяти. То есть для инициирования мошеннической операции необходимо, во-первых, извлечь криптограммы всех этих ключей, а во-вторых их нужно еще и расшифровать! Но это не так-то просто и не особо эффективно, поскольку для проведения операций используются строго одноразовые ключи. И только после того, как Mir Pay и ПМП обменялись публичными ключами, то есть фактически создали защищенный канал, допускается передача чувствительных данных, которые шифруются крипто-стойкими алгоритмами. По этому механизму на устройство пользователя доставляются и токен-профайл, и одноразовые ключи для проведения операций, и данные по уже совершенным операциям.

Как видно из этого описания, безопасность платежей на базе мобильных кошельков не только сохраняется на уровне пластиковых карт, а в некоторых случаях даже его превосходит! Приложение Mir Pay соответствует международным и отечественным требованиям к безопасности и позволяет держателям карт Мир использовать мобильный телефон для оплаты, не опасаясь утечки личных данных.

На текущий момент разработка Mir Pay продолжается выпустив в сжатые сроки первые версии, мы уже внедряем новые разработки в приложение, не забывая улучшать то, что уже сделано.

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

Как мы поощряем и развиваем ключевых сотрудников

04.02.2021 10:09:11 | Автор: admin

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

А с чего все началось?

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

Анализируя полученную обратную связь, мы запустили проект Ключевые люди (Key People). Благодаря ему желающие развиваться и обучаться могли бы получить конкретные рекомендации по своему развитию и уверенно двигаться к намеченной цели. Сотрудники, демонстрирующие прорывные результаты работы, поняли, что компания ценит их вклад, выражая им признание и индивидуальный подход в системе премирования. А сотрудников носителей уникальной экспертизы компания готова поощрять за передачу знаний коллегам.

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


High Potential (HiPo) высокопотенциальные сотрудники. Они проактивны в собственном развитии, инициативны, с лидерскими задатками;

Best Performers сотрудники, регулярно достигающие очень высоких результатов работы в своем подразделении. Они могут решить любую сложную, нестандартную задачу;

Key Expert сотрудники, обладающие уникальными знаниями не только в рамках компании, но на рынке труда в целом.


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

После этого мы провели интервью с руководителями, спросив их о конкретных качествах High Potential, Best Performers и Key Experts. Что эти люди должны делать, чтобы руководитель увидел в одном большой потенциал, а в другом большую эффективность? Как описать их поведение?

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

Таким образом, мы не стали использовать чужие наработки, а получили перечень компетенций, который был максимально про нас. Дальнейший отбор мы также вели собственными силами. Каждый руководитель структурного подразделения определял своих номинантов, а после этого представлял его на общей встрече руководителей. Все участники встречи обсуждали, соответствует ли данный сотрудник разработанным критериям или нет.

Для HiPo мы проводим дополнительный этап оценки, помогающий ребятам понять свои сильные стороны и зоны развития. Важно, что только в категорию HiPo сотрудники могли сами выдвинуть свою кандидатуру.

Итак, к сентябрю 2018 года мы провели большой отборочный этап и определили пул ключевых сотрудников по трем категориям. В рассмотрении участвовали 196 уникальных номинантов.

Один человек мог попасть в несколько категорий сразу.

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

С каждой категорией ключевых сотрудников мы активно взаимодействуем в течение года.

High Potential:
Для каждого участника, прошедшего отбор:

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

Планируем индивидуальное обучение. Для каждого HiPo мы выделяем свой персональный бюджет на обучение (помимо того, что планируем на профессиональное обучение в рамках общей программы компании);

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

Best Performer:

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

Устанавливаем персональные задачи на год, по итогам выполнения которых проводится коллегиальная оценка результатов с возможным премированием сотрудника.

Key Expert:

Обучаем экспертов навыкам передачи знаний. Как правильно составить программу передачи знаний, как работать с подопечным и т.д.;

Награждаем наставников по результатам передачи знаний;

Пишем про коллег в корпоративных СМИ. Рассказываем о том, что они делают в компании, в чем уникальность их опыта и экспертизы.

Прошедшие 2 года программы принесли много позитивных отзывов. Мы не стоим на месте и постоянно совершенствуем проект. Очень радует, что каждый год количество желающих выдвинуть свою кандидатуру в HiPo прирастает. Наша самая первая группа HiPo сейчас уже работает над полезным для компании проектом, на подходе следующая группа, которая будет реализовывать своей проект. Также мы видим, что результаты обучения и развития сотрудников положительно влияют на их продвижение в компании (30% HiPo получили продвижение за последние 2 года).

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

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

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

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

Подробнее..

Что было раньше код или документация? OpenApi (OAS 3.0) и проблемы кодогенерации на Java

12.11.2020 14:14:33 | Автор: admin

Задача интеграции сервисов и различных систем является чуть ли не одной из основных проблем современного IT. На сегодняшний день самым популярным архитектурным стилем для проектирования распределенных систем является REST. Но, как известно, единого стандарта для RESTfulсервисов нет, а у кажущейся простоты и свободы есть обратная сторона медали. Не менее важной является задача интеграции систем разными командами или даже компаниями, с которой приходит вопрос об актуальности документации на протяжении жизни всего проекта, и удобного способа передачи потребителю.

Эту проблему отчасти удалось решить при помощи спецификации OpenAPI(OAS3.0)[1], но все равно часто встает вопрос о правильном применении и подводных камнях кодогенерации, например на языке Java. И можно ли полностью предоставить аналитикам написание функциональных требований, документации и моделей в ymlформе для OpenAPI, а разработчикам предоставить возможность только написание бизнес-логики?

Что было раньше: код или документация?

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

В некоторых случаях такие документы составляются на этапе проектирования архитектуры в обычном текстовом формате, а потом передаются разработчику для реализации. Однако, для разработки REST-APIтакой подход не очень удобен и усложняет жизнь как программистам, которые должны еще правильно понять написанное, так и командам, которые будут с получившимся продуктом интегрироваться.К тому же документы могут интерпретироваться людьми по-разному, что обязательно приведет к необходимости дополнительных обсуждений, согласований и переделок, что в конечном счете срывает сроки и добавляет дефекты в продукт. Хотя, метод не очень удобен, иногда он до сих пор применяется при проектировании проектов без OpenAPI(например файловая интеграция, интеграция через БД, интеграция через очереди и общую шину).

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

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

В случае крупных и серьезных проектов, ручное документирование приводит к ряду больших проблем. В SOAPсервисах потребители, получая ссылку на WSDL-документ, генерируют на своей стороне модель данных и клиентский код. В отличие от SOAPпри интеграции OAS3.0 или OpenAPISpecificationявляется не просто спецификацией, а целой экосистемой инструментов для создания RESTсервисов, и она позволяет решить как задачу генерации документации по существующему коду, так и обратную - генерацию кода по документации. Нопрежде чем ответить на вечный вопрос, что первично, нужно определиться с целями и потребностями проекта.

Генерация документации по коду

Первым рассмотрим подход с автодокументируемым кодом[2,3]. Простота здесь заключается в том, что при использовании SpringMVCот разработчиков требуется всего лишь подключить пару библиотек, и на выходе уже получается вполне сносная документация с возможностью интерактивного взаимодействия с API.

Зависимости:

Spoiler

Maven

<dependency>   <groupId>org.springdoc</groupId>   <artifactId>springdoc-openapi-ui</artifactId>   <version>1.4.8</version></dependency>
<dependency>   <groupId>org.springdoc</groupId>   <artifactId>springdoc-openapi-webflux-ui</artifactId>   <version>1.4.8</version></dependency>

Gradle

compile group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.4.8'
compile group: 'org.springdoc', name: 'springdoc-openapi-webflux-ui', version: '1.4.8'

Результат доступен на: http://localhost:8080/swagger-ui.html

Сгенерированная документация по ссылке: http://localhost:8080/v3/api-docs

Плюсы подхода:

  • Возможность быстро сгенерировать документацию по существующему коду

  • Простота интеграции

  • Актуальная онлайн документация

  • Встроенная возможность интерактивного взаимодействия

Минусы подхода:

  • Документация написанная для разработчиков, содержит немного бизнес информации

  • При требовании подробного описания моделей и API возникает необходимость внедрять в код аннотации.

  • Для любых изменений требуется участие разработчика и правки кода

  • Требует от разработчика полного понимания архитектуры приложения или дополнительного документа с ее описанием

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

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

Генерация кода по документации

Однако есть и обратный подход, когда код объекты модели данных и интерфейсы контроллеров генерируются на основе описанной документации в yamlформате. Основной целью такого подхода является предоставление аналитикам возможности создать для потребителей не только общее описание, но и техническую документацию системы, по которой генерируется исходный код, а разработчик привлекается только при крупных изменениях, которые затрагивают бизнес логику системы.

Плюсы подхода

  • Ведение документации становится доступно не только разработчикам, но и техническим и системным аналитикам

  • Есть возможность разделить код и документацию

  • Можно объединить описание моделей, API и функциональных/бизнес требований

  • Любые изменения, инициированные бизнесом, сразу же применяются к модели и контроллерам

  • В случае критического несоответствия сервиса документации, сервис не соберется уже на этапе компиляции и потребует привести интерфейсы в соответствие с реализацией

  • Документацию в yaml формате легко предоставить потребителям сервиса при помощи сторонних библиотек, например redoc[4]

  • На основе yaml файлов можно описать сервисы на стороне потребителя, например, OpenApi для получения нотификаций от основного сервиса

  • На основе этой документации генерировать у себя тестовые сервисы для интеграционных и функциональных тестов

Минусы подхода

  • Более актуально для крупных и долгоживущих проектов (от одного года разработки)

  • Требует знания yaml формата для описания моделей

  • Требует навыки работы с markdown для описания функциональных требований

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

Подводные камни и опыт реализации:

Задача по применению второго подхода впервые появилась, когда потребовалось предоставить потребителям актуальную веб-версию документации с подробным описанием функциональных требований и бизнес-кейсов. Написание такой документации и ее актуализацию предстояло вести техническим аналитикам без возможности напрямую редактировать код. И еще одним важным требованием было красивое визуальное представление документации. Далее опишу базовый набор конфигураций для достижения этой цели и возможные подводные камни и проблемы[2,3]

Первым делом добавляем зависимость на mavenплагин

Spoiler
<dependency>   <groupId>io.swagger.codegen.v3</groupId>   <artifactId>swagger-codegen-maven-plugin</artifactId>   <version>3.0.21</version></dependency>
   <artifactId>springdoc-openapi-webflux-ui</artifactId>
   <version>1.4.8</version>

И конфигурируем генератор кода

Spoiler
<plugin>   <groupId>io.swagger.codegen.v3</groupId>   <artifactId>swagger-codegen-maven-plugin</artifactId>   <version>3.0.21</version>   <executions>       <execution>           <id>1</id>           <goals>               <goal>generate</goal>           </goals>           <configuration>               <groupId>com.habr</groupId>               <artifactId>oas3</artifactId>               <inputSpec>${project.basedir}/src/main/resources/habr-1.yaml</inputSpec>               <output>${project.build.directory}/generated-sources/</output>               <language>spring</language>               <configOptions>                   <sourceFolder>src/gen/java/main</sourceFolder>                   <library>spring-mvc</library>                   <interfaceOnly>true</interfaceOnly>                   <useBeanValidation>true</useBeanValidation>                   <dateLibrary>java8</dateLibrary>                   <java8>true</java8>                   <apiTests>false</apiTests>                   <modelTests>false</modelTests>               </configOptions>               <modelPackage>com.habr.oas3.model</modelPackage>               <apiPackage>com.habr.oas3.controller</apiPackage>           </configuration>       </execution>       <execution>...</execution>     ...   </executions>

В случае наличия в проекте нескольких файлов документации следует указывать их в разных executionблоках, однако, каждый запуск перетирает результаты предыдущего запуска (либо игнорирует при включении определенного флага). При наличии нескольких yamlфайлов рекомендуется избегать коллизий в моделях и интерфейсах чтобы запуски не перетирали уже сгенерированные модели.

InputSpecсодержит путь к документации, а modelPackageи apiPackageпакеты для сгенерированных моделей. Также можно генерировать моковые реализации интерфейсов для тестов (при генерации некоторый контроллер, что отвечает типичным ответом).

Флаг interfaceOnlyпозволяет генерировать только интерфейсы контроллеров и предоставить разработчику лишь контракты для реализации.

Общая структура yaml документации:

  1. openApi - Содержит версию

  2. info - метаданные для api

  3. servers - информация о серверах c api

  4. tags - дополнительные метаданные, в этом блоке нужно писать текст ФТ и регламента взаимодействия

  5. paths - описание endpoints и интерфейсов контроллеров

  6. components - описание модели данных

  7. security - схема безопасности

Исходный код на github и пример документации можно посмотреть здесь

Визуализация и предоставление потребителю: swagger-ui vs redoc

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

Swagger-ui

Удобная реализация с возможностью интерактивного взаимодействия. Идет из коробки в проекте swagger.io[5]. Можно подключить как библиотеку вместе с сервисом или развернуть статикой отдельно.

Пример визуализации документации тестового сервиса:

Redoc

Он позволяет отобразить и кастомизировать документацию альтернативным способом. Более удобная и красивая структура документации[4].

Пример визуализации документации тестового сервиса:

Хотя, redocреализован на react, его также можно использовать и как VueJSкомпонент:

Spoiler
<template>    <div v-if="hasYmlContent">        <RedocStandalone :spec='ymlObject' :options='ropt' />    </div></template><style...><script>    import AuthService from '../services/auth.service';    import DocumentationService from '../services/documentation.service'    import {RedocStandalone} from 'redoc'    import {API_URL} from "../services/auth-header";    import YAML from "yamljs"    import Jquery from "jquery"    export default {        name: 'documentation',        components: {'RedocStandalone': RedocStandalone},        props: ['yml'],        data: function() {            return {                ymlContent: false,                ropt:{...},            }        },        created() {...},        computed: {...}    };</script>

Выводы

Проведение аналитики и проектирование системы до начала этапа разработки помогает лучше понять задачи и избежать многих ошибок при реализации. Проект OAS3.0 показал себя очень удобным для описания документации и поставки ее потребителям сервиса.

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

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

Источники

  1. https://swagger.io/specification/

  2. https://www.baeldung.com/spring-rest-openapi-documentation

  3. https://github.com/springdoc/springdoc-openapi

  4. https://redocly.github.io/redoc/

  5. https://swagger.io/tools/swagger-ui/

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru