Запускаем Express.js приложение в Yandex Cloud Functions
К сожалению просто так взять и запустить приложение написанное на любом популярном node.js фреймворке у нас не выйдет. Эти фреймворки пишут ответ в http(s) сокет. Рантайм функций ожидает получить от пользовательского кода функции объект определенного содержания.
{
"statusCode": <HTTP код ответа>,
"headers": <словарь со строковыми значениями HTTP-заголовков>,
"multiValueHeaders": <словарь со списками значений HTTP-заголовков>,
"body": "<содержимое ответа>",
"isBase64Encoded": <true или false>
}
К счастью если у нас уже есть работающее приложение, не нужно переписывать его целиком. Нужно лишь научить его возвращать ответ в этом ожидаемом формате.
Возьмем для примера простое приложение, которое будет иметь два эндпоинта.
Создадим новую директорию и инициируем в ней новый проект
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 есть плагинкоторый позволяет деплоить функции. К сожалению из него пока не получится развернуть еще один ключевой компонент системы — 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. Для этого можно посмотреть в сторону https://www.npmjs.com/package/express-oas-generator
Готово теперь наше приложение доступно по URL и полностью работает.
curl 'https://_%api-gw-id%_.apigw.yandexcloud.net/api/info'
{"application":"sample-app","version":"1"}
Кстати к API Gateway можно привязать свой домен, но про это я расскажу в другой раз.
upd: Вот пост про привязку домена к API Gateway
upd 2021–04–03: В Yandex Cloud 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/{Bproxy+}
и роутер Expressjs будет ломаться. Решения как минимум два:
- Написать честный provider для Yandex.Cloud по образу того что сейчас есть для AWS.
- Быстренько попатчить объект event, положив в
path
значение изurl
. В примере ниже можно найти на 13–20 строках.
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);
}