Как стать автором
Обновить
m2_tech
Строим лучшую PropTech-компанию в России

Serverless в первый раз

Уровень сложностиСредний
Время на прочтение4 мин
Количество просмотров7.1K

Я давно приглядывался к serverless-технологиям, но всё не доходили руки. В М2 есть строгое разделение на бэкендеров и фронтендеров, как и во многих компаниях. Это вызывает много сложностей, одна из которых — необходимость договариваться, а разработчики — далеко не всегда самые общительные люди.

Ну, сами знаете, бэкендеры — с Марса, фронтендеры — с Твикса
Ну, сами знаете, бэкендеры — с Марса, фронтендеры — с Твикса

Моя профессия — фронтендер. В нашей компании есть фраза: «Любой фронтендер немножечко фулстек» — не я это придумал и не я сформулировал. И кроется под этим, конечно же, nodejs. Мы с командой решили, что некоторые сервисы можем писать без помощи бэкенд-разработчиков, и начали...

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

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

В общем, чтобы ни делать, лишь бы не бэкенд.

Революционно настроенный я не смирился и решил во что бы то ни стало повысить производительность команд, сделав их участников более самодостаточными, чем вчера. Мне на помощь пришли Cloud Function от Yandex, так как мы используем Yandex.Cloud, но то же самое можно сделать с Firebase и с другими современными облаками.

Моя первая функция

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

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

/template
    jest.config.js
    package.json
    tsconfig.json
    /src
        index.ts
        index.test.ts
        /data
            data.json

index.ts

import { YC } from '../../yc';

import json from './data/data.json';

export type Payload = {
  data: any;
}

export const handler: YC.Handler<"POST", any, Payload> = async (event, context) => {

  const data = context.getPayload().data;

    return {
        statusCode: 200,
      headers: {
          'Content-Type': 'application/json; charset=utf-8',
        },
      body: { event, millis: context.getRemainingTimeInMillis(), data, json},
      isBase64Encoded: false,
      };
}

Функция в практическом смысле бесполезная, самое интересное в ней — это import json (о нём напишу ниже). Сама же функция — это шаблон для таких же функций. То есть, при вызове cli create <name> создается новая функция по образу и подобию текущей, которую дальше следует модифицировать, то есть наделить бизнес логикой.

CLI на страже галактики

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

Бок о бок с CI идет CLI: иногда надо переложить, переименовать файлы, а иногда и преобразовать, и даже обогатить. Тут действует принцип «кто во что горазд»: кто-то использует bash, я и моя команда — clipanion. На «Хабре» есть статьи на эту тему, поэтому не буду останавливаться на библиотеке подробно, но штука — просто бомба, мы её используем в каждом проекте.

В нашем CI есть этап обогащения, или enrich. Фокус состоит в том, чтобы на этапе перед сборкой инкапсулировать все данные, необходимые для выполнения функции, в неё. Для каждой функции есть соответствующая команда enrich-template, которая умеет ходить в базу, скачивать что-то из интернета, затем делает какие-то преобразования и складывает всё в data.json. То есть в функции есть полный слепок данных, необходимых ей для выполнения. Такой подход можно использовать, если данные меняются редко и их не очень много.

Изоляция во благо

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

Но на мое место придет другой
Но на мое место придет другой

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

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

Изолированные функции легко поддаются тестированию юнит-тестами. Для этого мы написали простую функцию-обёртку:

export function makeHandlerParams<TMETHOD extends YC.HttpMethod, TIN>(method: YC.HttpMethod, params: Record<string, any>): [event: YC.CloudFunctionsHttpEvent<TMETHOD>, context: YC.CloudFunctionsHttpContext<TMETHOD, TIN>] {

    const event: YC.CloudFunctionsHttpEvent<TMETHOD> = {
        httpMethod: method
    } as YC.CloudFunctionsHttpEvent<TMETHOD>;
    const context: YC.CloudFunctionsHttpContext<YC.HttpMethod, TIN> = {
        _data: event,
        requestId: 'requestId',
        awsRequestId: 'awsRequestId',
        uberTraceId: 'uberTraceId',
        deadlineMs: 3000,

        functionFolderId: 'functionFolderId',
        functionName: 'functionName', // "<идентификатор функции>",
        functionVersion: 'functionVersion', // "<идентификатор версии функции>",
        invokedFunctionArn: 'invokedFunctionArn',
        logGroupName: 'logGroupName',

        memoryLimitInMB: '120', // "<объем памяти версии функции, МБ>",
        getRemainingTimeInMillis: () => 3000, // возвращает время, оставшееся на выполнение текущего запроса в миллисекундах;
        getPayload: () => params as TIN
    }
    return [event, context];
}

И обычный тест выглядит следующим образом:

test('template', async () => {
    const data = await handler(...makeHandlerParams<'POST', Payload>('POST',
         {
        data: 99
    }));
    expect(99).toBe(data.body.data);
});

Вместо эпитафии

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

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

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

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

А вы пользуетесь клауд-функциями в таком контексте? Если да, поделитесь в комментариях, для каких задач.

Теги:
Хабы:
Всего голосов 17: ↑16 и ↓1+16
Комментарии24

Публикации

Информация

Сайт
m2.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия
Представитель
Макс Дмитров