Запускаем приложение на Express.js в Yandex Cloud Functions
Node.js — удобная масштабируемая серверная платформа для работы с JavaScript. С помощью нее и различных поддерживаемых фреймворков, таких как Express, Connect или Koa, можно создавать полноценные приложения.
Если идти по пути упрощения администрирования, возникает желание загрузить приложение в Yandex Cloud Functions и вызывать его из облака. К сожалению, пока нельзя просто так взять и запустить в облаке приложение, написанное на любом популярном node.js-фреймворке. Фреймворки пишут ответ в сокет HTTP(S). Рантайм функций ожидает получить от пользовательского кода функции объект определенного содержания.
{
"statusCode": <HTTP код ответа>,
"headers": <словарь со строковыми значениями HTTP-заголовков>,
"multiValueHeaders": <словарь со списками значений HTTP-заголовков>,
"body": "<содержимое ответа>",
"isBase64Encoded": <true или false>
}
«Из коробки» это работать не будет, но можно научить приложение возвращать ответ в ожидаемом формате. Разберем, как это сделать, на примере приложения Express.js с двумя эндпоинтами.
Создаем и запускаем новый проект
Создаем новую директорию и инициируем в ней новый проект:
mkdir sample-app && cd sample-app
npm init -y
npm install express
touch index.js
Далее в index.js добавляем следующий код:
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get('/api/info', (req, res) => {
res.send({ application: 'sample-app', version: '1.0' });
});
app.post('/api/v1/getback', (req, res) => {
res.send({ ...req.body });
});
app.listen(3000, () => console.log(`Listening on: 3000`));
Запускаем проект и проверяем, что приходят ожидаемые ответы:
$ curl 'http://localhost:3000/api/info'
{"application":"sample-app","version":"1"}
Адаптируем проект под Serverless
Интегрируем модуль serverless-http:
npm i --save serverless-http
Это универсальный враппер, он поддерживает не только Express, но и Connect, Koa, restana, а также экспериментально другие фреймворки: Sails, Hapi, Fastify, Restify, Polka и LoopBack.
Затем модифицируем наш пример. Заменяем запуск сервера на порте 3000 экспортом функции-обработчика, которая будет вызываться serverless-рантаймом облака:
const express = require('express');
const app = express();
const serverless = require('serverless-http');
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get('/api/info', (req, res) => {
res.send({ application: 'sample-app', version: '1.0' });
});
app.post('/api/v1/getback', (req, res) => {
res.send({ ...req.body });
});
//app.listen(3000, () => console.log(`Listening on: 3000`));
module.exports.handler = serverless(app);
Теперь наше приложение готово к запуску в облаке.
Развертываем приложение в облаке
Для того чтобы развернуть код в облаке, проще всего воспользоваться утилитой serverless. У Yandex.Cloud есть свой плагин, который позволяет деплоить функции. Из него пока нельзя развернуть еще один ключевой компонент системы — Yandex API Gateway, мы чуть позже сделаем это вручную через консоль.
Устанавливаем Serverless Framework и плагин к нему:
npm i -g serverless serverless-yandex-cloud
Далее создаем в проекте файл serverless.yaml с содержимым:
service: sample-app
frameworkVersion: ">=1.1.0"
configValidationMode: off
provider:
name: yandex-cloud
runtime: nodejs12-preview
plugins:
- serverless-yandex-cloud
package:
exclude:
- ./**
include:
- ./package.json
- ./**/*.js
functions:
express:
# this is formatted as <FILENAME>.<HANDLER>
handler: index.handler
memory: 128
timeout: 5
Деплоим функцию командой:
serverless deploy
Если сделать функцию публичной и вызвать ее по предложенному URL, передав путь /api/info , то в ответ мы получим следующую ошибку:
$ curl 'https://functions.yandexcloud.net/%function-id%/api/info'
{"errorCode":400,"errorMessage":"Invalid functionID: /%function-id%/api/info",
"errorType":"ProxyIntegrationError"}
необходима настройка API Gateway.
Создание API Gateway
Спецификация должна соответствовать стандарту OpenAPI 3.0, для нашего простого API ее можно написать руками:
openapi: 3.0.0
info:
title: Sample API
version: 1.0.0
paths:
/api/info:
get:
responses:
'200':
description: Ok
x-yc-apigateway-integration:
type: cloud_functions
function_id: %function_id%
tag: $latest
service_account_id: %service_account_id%
/api/v1/getback:
post:
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/Test'
requestBody:
required: false
content:
application/json:
schema:
$ref: '#/components/schemas/Test'
x-yc-apigateway-integration:
type: cloud_functions
function_id: %function_id%
tag: $latest
service_account_id: %service_account_id%
components:
schemas:
Test:
type: object
Не забудьте поменять %function_id% и %service_account_id% на ваши значения. У сервисного аккаунта должна быть роль serverless.functions.invoker или выше, если вы оставили функцию без публичного доступа.
В более сложных случаях можно попробовать сгенерировать спецификацию OpenAPI на основе уже имеющегося кода API. Для этого подойдет express-oas-generator.
Теперь наше приложение работает и доступно по URL.
$ curl 'https://%api-gw-id%.apigw.yandexcloud.net/api/info'
{"application":"sample-app","version":"1"}
Кстати, к API Gateway можно привязать свой домен. Как приязать домен — читайте в этом посте.
Новый параметр API Gateway
Совсем недавно в API Gateway появилась возможность указать параметр вида {param+}. В этом случае будут матчиться и вложенные пути.
paths:
/api/{proxy+}:
get:
x-yc-apigateway-integration:
type: cloud_functions
function_id: d4e***
tag: $latest
service_account_id: aje***
responses:
200:
description: Ok
parameters:
- explode: true
in: path
name: proxy
required: true
schema:
type: string
style: simple
В первом параметре функции event в проперти path будет лежать значение вида /api/%7Bproxy+%7D и роутер Express.js будет ломаться.
Решения как минимум два:
написать честный provider для Yandex.Cloud по образу того, что сейчас есть для AWS;
пропатчить объект event, положив в path значение из url (строки 13–19 в примере ниже).
Пример готового скрипта можно скачать.
const express = require('express');
const serverless = require('serverless-http');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.get('/api/info', (req, res) => {
res.send({ application: 'sample-app', version: '1.0' });
});
app.get('/api/pet/:name?', (req, res) => {
res.send({ ...req.params });
});
module.exports.handler = (event, context) => {
const patchedEvent = {
...event,
path: event.url,
originalPath: event.path,
}
return serverless(app)(patchedEvent, context);
}
Вы можете бесплатно попробовать запустить приложений Express.js на Yandex Cloud Functions по программе free tier: сервис не тарифицирует первый миллион вызовов функций и первые 10 ГБ×час выполнения функций. А любые вопросы о работе сервисов можно обсудить как с их пользователями, так и с их создателями в чате Yandex Serverless Ecosystem.