Я давно приглядывался к 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-шаблонизатор.

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

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


  1. latrommi137
    29.12.2023 10:05
    +6

    Спасибо! Очень интересно, как раз тоже искали такую возможность


    1. sssrgei Автор
      29.12.2023 10:05
      +3

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


  1. beoserg
    29.12.2023 10:05
    +3

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


    1. sssrgei Автор
      29.12.2023 10:05
      +3

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


  1. m3rcury_87
    29.12.2023 10:05
    +1

    а как происходит мониторинг и отладка клауд функций? прямо там же в яндекс клауде получается?


    1. sssrgei Автор
      29.12.2023 10:05
      +2

      Вообще, есть такой пакет: https://www.npmjs.com/package/@yandex-cloud/serverless-live-debug
      Но я отлаживал через TDD, а когда что-то непонятное было, то да в яндекс клауде много инструментов. Как и приведенный мной, тоже хотелось бы опробовать, вероятнее, если будет проект с состоянием на сервере, то придется воспользоваться им, а в моих кейсах не было необходимости.


  1. maximpozharskiy238
    29.12.2023 10:05
    +1

    Спасибо, звучит здорово. А какой принцип работы cloud-функций?


    1. sssrgei Автор
      29.12.2023 10:05
      +1

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


      1. The_KOPACb
        29.12.2023 10:05
        +3

        То есть по факту народ заново изобретает CGI?


        1. slonopotamus
          29.12.2023 10:05

          Да.


        1. onyxmaster
          29.12.2023 10:05
          +1

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


          1. sssrgei Автор
            29.12.2023 10:05

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


  1. AnaSergeeva
    29.12.2023 10:05
    +2

    Наконец-то я поняла, зачем нужны клауд-функции, спасибо????


  1. anonymous
    29.12.2023 10:05

    НЛО прилетело и опубликовало эту надпись здесь


    1. sssrgei Автор
      29.12.2023 10:05

      функция пустышка это как пример итона исподняет роль шаблона для команды create. используя этот подход мы сделали поиск и ротацию для баннеров: вот пример поиска https://m2.ru/support/ на том же сайте на главной есть баннеры, они от регионов зависят и тп. но я же не могу весь код к статье приложить, она же не про поиск.


      1. anonymous
        29.12.2023 10:05

        НЛО прилетело и опубликовало эту надпись здесь


        1. sssrgei Автор
          29.12.2023 10:05

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


  1. sssrgei Автор
    29.12.2023 10:05

    функция пустышка это как пример итона исподняет роль шаблона для команды create. используя этот подход мы сделали поиск и ротацию для баннеров: вот пример поиска https://m2.ru/support/ на том же сайте на главной есть баннеры, они от регионов зависят и тп. но я же не могу весь код к статье приложить, она же не про поиск.


  1. Thomas_Hanniball
    29.12.2023 10:05
    +1

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

    Есть решение. Нужно придумать новую роль в команде, которая будет объединять между собой бэкендеров и фронтендеров, например, FrontBack, по аналогии с DevOps. :)


  1. Alesh
    29.12.2023 10:05
    +1

    То есть то что фронты стали писать бакенд. Это какое-то чудо, и достижение которым следует поделится с миром, при том что бакенд команда имеется.

    А я вам скажу, что это не чудо, и не достижение. Это провал вашего менеджмента, который вы разгребаете.


    1. sssrgei Автор
      29.12.2023 10:05

      А какие этот может иметь негативные последствия? У вас был опыт?


      1. Alesh
        29.12.2023 10:05
        +1

        Опыта такого к счастью не было.
        А какие могут быть негативные последствия еще Иван Андреевич Крылов в своей басне обозначил)

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