Serverless в первый раз
Я давно приглядывался к Serverless-технологиям, но все не доходили руки. В М2, как и во многих компаниях, есть строгое разделение на бэкендеров и фронтендеров. Проблемы у этого известные, но самая неприятная — надо договариваться, а разработчики далеко не всегда самые общительные люди.
Ну сами знаете, бекендеры с марса, фронтендеры с твикса.
Вместо предисловия
Моя профессия — фронтендер. Не я это придумал и не я сформулировал, в нашей компании есть фраза: «Любой фронтендер немножечко фулстек». И кроется под этим, конечно же nodejs. Мы решили, что некоторые сервисы можем писать без помощи бекенд-разработчиков и начали…
Скоро сказка сказывается, да не скоро дело делается, часть фронтенд-тусовки перешло на темную сторону и, вооружившись nestjs и mongodb, мы сделали несколько очень важных сервисов. Я тоже оказался по ту сторону сумрака. Но другая часть (она оказалась много больше чем первая) сердцем была с нами, но делами…
«слишком высокий порог входа», «нужно изучать новые концепции», «нужно не бояться ошибаться», «нужно что-то делать, а всегда много рабочих задач, жена, дети, друзья».
В общем, чтобы ни делать, лишь бы не бэкенд.
Революционно настроенный, я не смирился в ссылке и решил во что бы то ни стало повысить производительность команд, сделав их участников более самодостаточными чем вчера. И на помощь мне пришли Cloud Function от Yandex, так как мы используем Yandex Cloud, но то же самое можно сделать и с Firebase и c другими современными клаудами.
Моя первая функция
О том, как сделать маленькую функцию написано уже множество инструкций и статей — не буду повторяться. Мне же хотелось сделать репозиторий, куда любой разработчик М2 сможет добавить функцию и она, передвигаясь по пайплайну 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
CLI на страже галактики
Процессы в нашей компании устроены так, что любая мало мальская библиотека даже на фронтенде проходит через тернии CI/CD. Знаю, что передавать сборку на флешке гораздо кинематографичнее, но мы не гонимся за картинкой, а стараемся все и по максимуму автоматизировать.
Бок обок с CI, идет CLI: иногда надо переложить, переименовать файлы, а иногда и преобразовать и даже обогатить. Тут действует принцип «кто во что горазд», кто-то использует bash, а я и моя команда используем clipanion. На хабре есть статьи на эту тему, поэтому не буду останавливаться на библиотеке подробно, но штука просто бомба, и мы ее используем в каждом проекте.
В нашем CI есть этап обогащения или enrich. Фокус состоит в том, чтобы на этапе перед сборкой инкапсулировать все данные, необходимые для выполнения функции в нее. Для каждой функции есть соответствующая команда enrich-template, которая умеет ходить в базу, умеет скачивать что-то из интернета, затем делает какие-то преобразования и складывает все в data.json.
Изоляция во благо
Функция абсолютно изолирована, никуда не ходит, ни к чему не обращается Соответственно, не может иметь дыр в безопасности, ее не надо согласовывать с «первым» отделом и не надо подвергать дополнительным проверкам. А главное мы экономим на сетевых соединениях и прочих ресурсах.
Но на мое место придет другой
Еще не нужно думать про идемпотентность и другие бекендерские шалости: функция ничего не меняет, а только что-то возвращает. Она никогда не остановит общий процесс, так как при зависании функции на ее место тут же придет другая и заменит ее.
Тестирование
Изолированные функции легко поддаются тестированию юнит-тестами. Для этого мы написали простую функцию-обертку:
export function makeHandlerParams(method: YC.HttpMethod, params: Record): [event: YC.CloudFunctionsHttpEvent, context: YC.CloudFunctionsHttpContext] {
const event: YC.CloudFunctionsHttpEvent = {
httpMethod: method
} as YC.CloudFunctionsHttpEvent;
const context: YC.CloudFunctionsHttpContext = {
_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 шаблонизатор.
А вы пользуетесь клауд функциями в таком контексте? Если да, поделитесь в комментариях для каких задач.