Перейти к основному содержимому

Запускаем Express.js приложение в Yandex Cloud Functions

· 5 мин. чтения

К сожалению просто так взять и запустить приложение написанное на любом популярном 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.

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.yaml
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 будет ломаться. Решения как минимум два:

  1. Написать честный provider для Yandex.Cloud по образу того что сейчас есть для AWS.
  2. Быстренько попатчить объект 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);
}