Я давно приглядывался к 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)
beoserg
29.12.2023 10:05+3Вы написали, что клауд функции не являются универсальным решением. Можете ли вы привести примеры задач, для которых они наиболее подходят?
sssrgei Автор
29.12.2023 10:05+3Сами клауд функции являются универсальным решением, просто другой подход к решению задач. Но в таком изолированном виде они годятся только для очень ограниченного списка задач, честно говоря, не приходит в голову других задач, кроме тех, что я описал. А так можно писать телеграм бота на клауд функции. У Google Cloud из коробки можно Next приложение развернуть. Как раз хотелесь бы чтобы кто-то поделился опытом такого изолированного использования.
m3rcury_87
29.12.2023 10:05+1а как происходит мониторинг и отладка клауд функций? прямо там же в яндекс клауде получается?
sssrgei Автор
29.12.2023 10:05+2Вообще, есть такой пакет: https://www.npmjs.com/package/@yandex-cloud/serverless-live-debug
Но я отлаживал через TDD, а когда что-то непонятное было, то да в яндекс клауде много инструментов. Как и приведенный мной, тоже хотелось бы опробовать, вероятнее, если будет проект с состоянием на сервере, то придется воспользоваться им, а в моих кейсах не было необходимости.
maximpozharskiy238
29.12.2023 10:05+1Спасибо, звучит здорово. А какой принцип работы cloud-функций?
sssrgei Автор
29.12.2023 10:05+1при каждом вызове поднимается nodejs, обрабатывает запрос и потухает через какое-то время, если не приходит новый запрос. Но лучше почитать об этом в документации.
The_KOPACb
29.12.2023 10:05+3То есть по факту народ заново изобретает CGI?
onyxmaster
29.12.2023 10:05+1Основное отличие — биллинг по времени исполнения. Провайдеру это удобно, так как он может исполнять функции на простаивающем оборудовании, и за счёт этого давать привлекательные цены. Но не у всех провайдеров получается хорошее соотношение, как обычно нужно считать, потому что для некоторых задач виртуалка будет дешевле.
sssrgei Автор
29.12.2023 10:05и отлично подходит для петпроектов, когда трафик маленький, у яндекса куча условно бесплатного.
anonymous
29.12.2023 10:05НЛО прилетело и опубликовало эту надпись здесь
sssrgei Автор
29.12.2023 10:05функция пустышка это как пример итона исподняет роль шаблона для команды create. используя этот подход мы сделали поиск и ротацию для баннеров: вот пример поиска https://m2.ru/support/ на том же сайте на главной есть баннеры, они от регионов зависят и тп. но я же не могу весь код к статье приложить, она же не про поиск.
anonymous
29.12.2023 10:05НЛО прилетело и опубликовало эту надпись здесь
sssrgei Автор
29.12.2023 10:05Статья не о том, что такое серверлес, а о подходе по включению например поискового индекса в тело функции. Если контент статичный, то можно не хранить данные в базе данных, а при изменении пересобирать функцию с новым индексом.
sssrgei Автор
29.12.2023 10:05функция пустышка это как пример итона исподняет роль шаблона для команды create. используя этот подход мы сделали поиск и ротацию для баннеров: вот пример поиска https://m2.ru/support/ на том же сайте на главной есть баннеры, они от регионов зависят и тп. но я же не могу весь код к статье приложить, она же не про поиск.
Thomas_Hanniball
29.12.2023 10:05+1В М2 есть строгое разделение на бэкендеров и фронтендеров, как и во многих компаниях. Это вызывает много сложностей, одна из которых — необходимость договариваться, а разработчики — далеко не всегда самые общительные люди.
Есть решение. Нужно придумать новую роль в команде, которая будет объединять между собой бэкендеров и фронтендеров, например, FrontBack, по аналогии с DevOps. :)
Alesh
29.12.2023 10:05+1То есть то что фронты стали писать бакенд. Это какое-то чудо, и достижение которым следует поделится с миром, при том что бакенд команда имеется.
А я вам скажу, что это не чудо, и не достижение. Это провал вашего менеджмента, который вы разгребаете.
sssrgei Автор
29.12.2023 10:05А какие этот может иметь негативные последствия? У вас был опыт?
Alesh
29.12.2023 10:05+1Опыта такого к счастью не было.
А какие могут быть негативные последствия еще Иван Андреевич Крылов в своей басне обозначил)
Беда, коль пироги начнет печи сапожник,
А сапоги тачать пирожник,
И дело не пойдет на лад.
Да и примечено стократ,
Что кто за ремесло чужое браться любит,
Тот завсегда других упрямей и вздорней:
Он лучше дело все погубит,
И рад скорей
Посмешищем стать света,
Чем у честных и знающих людей
Спросить иль выслушать совета.
latrommi137
Спасибо! Очень интересно, как раз тоже искали такую возможность
sssrgei Автор
А для каких задач вы собираетесь использовать функции?