В статье речь пойдет о том, как разрабатывать и тестировать бессерверные приложения локально, о роутинге на фронтенде и бекенде, о сервисах Amazon и тому подобных вещах. Кому интересно, добро пожаловать под кат!
Что-то вроде предисловия
До недавнего времени разработка бессерверных приложений сильно осложнялась тем, что не было средств полноценного локального тестирования lambda-функций и API. При создании приложений нужно было или работать все время онлайн, редактируя код в браузере, или постоянно архивировать и загружать в облако исходный код lambda-функций.
Летом 2017 произошел прорыв. AWS создал новый упрощенный стандарт шаблонов CloudFormation, который они назвали Serverless Application Model (SAM) и одновременно запустили проект sam-local. Обо всем по порядку.
Amazon CloudFormation — это такой сервис, который позволят описывать всю нужную для вашего приложения инфраструктуру AWS с помощью файла шаблона в формате JSON или YAML. Это очень-очень удобная штука. Потому что без нее вам нужно вручную через веб-консоль или командный интерфейс создавать множество нужных вам ресурсов: ламбда-функции, база данных, API, роли и политики…
С помощью CloudFormation инфраструктуру можно нарисовать или в специальном дизайнере, или написать ее руками в шаблоне. В любом случае в итоге получается файл шаблона, с помощью которого дальше можно в пару-тройку кликов или одной командой поднять все, что нужно для приложения. А дальше при необходимости вносить изменения в этот шаблон и применять их опять же одной командой. Это делает поддержку инфраструктуры приложения гораздо легче. Получается, инфраструктура, как код.
CloudFormation прекрасен, его шаблоны позволяют описать практически 100% ресурсов AWS. Но из-за его универсальности это достаточно «многословный» формат — шаблоны могут быстро вырастать до приличных размеров. Осознавая это и преследуя цель сделать создание бессерверных приложений проще, AWS создали новый формат SAM.
Можно условно считать, что обычные шаблоны CloudFormation пишутся на языке низкого уровня. А шаблоны SAM — на языке высокого уровня, таким образом, позволяя описывать инфраструктуру бессерверных приложений при помощи упрощенного синтаксиса. Шаблоны SAM трансформируются CloudFront в обычные шаблоны при деплое.
Что же такое sam-local? Это инструмент командной строки, позволяющий работать локально с бессерверными приложениями, описанными шаблонами SAM. Sam-local позволяет тестировать lambda-функции, генерировать события от различных сервисов AWS, запускать API Gateway, проверять шаблоны SAM — и всё это локально!
Sam-local использует docker-контейнер для эмуляции API Gateway и Lambda. Принцип работы следующий. При запуске sam-local ищет файл шаблона SAM папке проекта. Он анализирует файл шаблона и запускает в docker-контейнере выделенные в шаблоне ресурсы: открывает API и подключает к ним ламбда-функции. Причем поддержка очень близкая к работе реальных lambda-функций (лимиты, показывается объем использованной памяти и длительность выполнения).
Выглядит это примерно так
Georgiy@Baltimore MINGW64 /h/dropbox/projects/aberp/lambda (master)
$ sam local start-api --docker-volume-basedir /h/Dropbox/Projects/aberp/lambda "aberp"
<[34mINFO<[0m[0000] Unable to use system certificate pool: crypto/x509: system root pool is not available on Windows
2018/04/04 22:33:49 Connected to Docker 1.35
<[34mINFO<[0m[0001] Unable to use system certificate pool: crypto/x509: system root pool is not available on Windows
2018/04/04 22:33:50 Fetching lambci/lambda:nodejs6.10 image for nodejs6.10 runtime...
nodejs6.10: Pulling from lambci/lambda
<[1B06c3813f: Already exists
<[1B967675e1: Already exists
<[1Bdaa0d714: Pulling fs layer
<[1BDigest: sha256:56205b1ec69e0fa6c32e9658d94ef6f3f5ec08b2d60876deefcbbd72fc8cb12f52kB/2.052kBB
Status: Downloaded newer image for lambci/lambda:nodejs6.10
<[32;1mMounting index.handler (nodejs6.10) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH]<[0
m
You can now browse to the above endpoints to invoke your functions.
You do not need to restart/reload SAM CLI while working on your functions,
changes will be reflected instantly/automatically. You only need to restart
SAM CLI if you update your AWS SAM template.
Далее обращение к локальному API и вызов соответствующих lambda-функции отображается в консоли в общем-то также, как lambda-функции выводят информацию в логи CloudWatch:
2018/04/04 22:36:06 Invoking index.handler (nodejs6.10)
2018/04/04 22:36:06 Mounting /h/Dropbox/Projects/aberp/lambda as /var/task:ro inside runtime container
<[32mSTART RequestId: 9fee783c-285c-127d-b5b5-491bff5d4df5 Version: $LATEST<[0m
<[32mEND RequestId: 9fee783c-285c-127d-b5b5-491bff5d4df5<[0m
<[32mREPORT RequestId: 9fee783c-285c-127d-b5b5-491bff5d4df5 Duration: 476.26 ms Billed Duration: 500 ms Memory S
ize: 128 MB Max Memory Used: 37 MB <[0m
Sam-local все еще находится в статусе публичной беты, но мне показалось, что работает он достаточно стабильно.
Все это в целом позволяет работать над созданием бессерверного приложения на локальном компьютере и это не сложнее, чем создавать традиционные веб-приложения.
Не могу не упомянуть. У sam-local есть аналог — это Serverless framework. Serverless framework довольно популярен, во многом благодаря тому, что раньше альтернатив не было. У меня нет особого опыта его использования, но насколько я знаю, такой полноценной локальной среды, как sam-local он не дает. Sam-local разрабатывается в самом AWS, а serverless framework делает отдельная команда энтузиастов. В пользу serverless framework, правда, можно отнести то, что он позволяет делать приложения менее привязанными к конкретному вендору.
О фреймворке
Как я уже писал, он нужен для того, чтобы обеспечивать быстрый старт при создании новых бессерверных приложений. На текущий момент в нем реализована только авторизация на веб-токенах. Дальше планируем добавить обработку ошибок, работу с формами и вывод табличных данных, настроить механизм развертывания. В общем, чтобы можно было в будущем клонировать репозиторий AB-ERP и быстро начинать работу над приложениями.
Мы создаем ERP-системы, поэтому назвали его AB-ERP по аналогии с названиями других наших продуктов: AB-TASKS и AB-DOC. При этом AB-ERP — это не обязательно для создания ERP-систем, на базе него можно делать любые бессерверные веб-приложения.
У приложения есть код фронтенда и код бекенда. Соответственно, в корне проекта 2 папки: lambda (бекенд) и public (фронтент):
+---lambda
| +---api
| +---core
\---public
+---css
| \---core
+---img
+---js
| \---core
\---views
AB-ERP работает по принципу одностраничного веб-приложения (SPA). При развертывании приложения код фронтенда нужно будет размещать в AWS S3 и настраивать перед ним CloudFront. Это было описано в моей предыдущей статье про AB-DOC в разделе «Разработка и развертывание».
Код бекенда при развертывании будет загружаться в сервис AWS Lambda.
В качестве базы данных AB-ERP использует MariaDB. MariaDB разворачивается в сервисе AWS RDS. При желании AB-ERP можно перенастроить, например, на работу с AWS DynamoDB.
Файлы пользователей будут сохраняться в AWS S3.
Вот так выглядит выглядит архитектура приложения:
Бекенд
На текущий момент все очень-очень просто. Всего один ресурс API Gateway и всего одна lambda-функция.
Вот так выглядит шаблон SAM:
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: An example RESTful service
Resources:
ABLambdaRouter:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs6.10
Handler: index.handler
Events:
ABAPI:
Type: Api
Properties:
Path: /{proxy+}
Method: any
В шаблоне SAM мы видим один наш ресурс ABLambdaRouter, который является lambda-функцией. ABLambdaRouter вызывается только одним событием ABAPI, которое поступает от API.
Наш ресурс API Gateway принимает запросы любыми методами (ANY) к любым путям в URL: /{proxy+}. То есть другими словами, выступает в роли обычного двух-стороннего прокси. Lambda-функция, соответственно, должна взять на себя роль роутера, который будет выполнять разный код в зависимости от запросов.
'use strict';
const jwt = require('jsonwebtoken');
//process.env.PROD and other env.vars are set in production only
if(process.env.PROD === undefined){
process.env.PROD = 0;
process.env.SECRET = 'SOME_SECRET_CODE_672967256';
process.env.DB_HOST = '192.168.1.5';
process.env.DB_NAME = 'ab-erp';
process.env.DB_USER = 'ab-erp';
process.env.DB_PASSWORD = 'ab-erp';
}
//core modules
const HTTP = require('core/http');
const DB = require('core/db');
//main handler
exports.handler = (event, context, callback) => {
context.callbackWaitsForEmptyEventLoop = false;
let api;
const [resource, action] = event.pathParameters['proxy'].split('/');
//OPTIONS requests are proccessed by API GateWay using mock
//sam-local can't do it, so for local development we need this
if(event.httpMethod === 'OPTIONS'){
return callback(null, HTTP.response());
}
//require resource module
try {
api = require('api/' + resource)(HTTP, DB);
} catch(e) {
if (e.code === 'MODULE_NOT_FOUND') {
return callback(null, HTTP.response(404, {error: 'Resource not found.'}));
}
return callback(null, HTTP.response(500));
}
//call resource action
if(api.hasOwnProperty(action)) {
if(api[action].protected === 0){
api[action](event, context, callback);
} else if (event.headers['X-Access-Token'] !== undefined) {
let token = event.headers['X-Access-Token'];
try {
event.userData = jwt.verify(token, process.env.SECRET);
api[action](event, context, callback);
} catch(error) {
return callback(null, HTTP.response(403, {error: 'Failed to verify token.'}));
}
} else {
return callback(null, HTTP.response(403, {error: 'No token provided.'}));
}
} else {
return callback(null, HTTP.response(404, {error: 'Action not found.'}));
}
}
API имеет двухуровневую иерархию: первый уровень — это модуль, второй уровень — это действие. URL имеют следующий вид api.app.com/module/action. Функция-роутер анализирует pathParameters поступившего запроса, пытается подключить нужный модуль из папки lambda/api и дальше передать запрос в нужную функцию в этом модуле.
По умолчанию для функций в модулях требуется авторизация, поэтому перед вызовом функции из модуля, наш роутер проверит наличие валидного токена в X-Access-Token заголовке запроса. Если токен валидный, будет вызвана функция из модуля, если нет — будет возвращена ошибка 403.
Почему мы выбрали такой подход, вместо создания множества отдельных ресурсов API Gateway и множества lambda-функций? Во-первых, и это самое главное, простота настройки, развертывания и собственно работы с такой архитектурой. Во-вторых, такой подход минимизирует холодные старты функции. Дело в том, что если к функции нет долго обращений, AWS удаляет ее контейнер и тогда при новом обращении требуется больше времени для обработки запроса.
Минусы в этом подходе тоже есть. У нас не будет возможности на уровне API Gateway делать какие-то особые настройки для разных ресурсов API.
Может у кого-то возникает вопрос, зачем тогда вообще нужен API Gateway, почему бы не обращаться к lambda напрямую из браузера? API Gateway предоставляет множество преимуществ. Он может работать, как CDN, в режиме Edge Optimized, есть кэширование ответов, на запросы OPTION он может отвечать сам без обращений к бекенду (MOCK-интеграция) — все это существенно ускоряет работу приложения. Также у него есть защита от DDOS и возможность регулирования трафика с использованием ограничений. Ну и еще он позволяет открыть API приложения для сторонних разработчиков.
Фронтенд
Для фронтенда мы решили не использовать «большие» фреймворки, вроде React, Vue.js или Angular.js, поэтому написали маленький роутер для нашего SPA-приложения.
Роутер хранит описание каждой страницы: какой html-шаблон и какие css, js-файлы ей нужны. При запросе к странице роутер загружается все необходимые файлы в виде простого текста, объединяет их и вставляет в div-контейнер интерфейса приложения. При вставке в контейнер происходит выполнение JavaScript открываемой страницы.
"use strict";
//ROUTER object
const ROUTER = {
pages: {
"index": ["css/index.css", "views/index.html", "js/index.js"],
"login": ["css/login.css", "views/login.html", "js/login.js"]
},
open: function(page){
let self = this;
$container.html(big_preloader_html);
if(self.pages.hasOwnProperty(page)){
const parts = self.pages[page];
let getters = [];
let wrappers = [];
for (let i = 0; i < parts.length; i++) {
if( /^.*\.css$/i.test(parts[i]) ){
wrappers.push('style');
} else if ( /^.*\.js$/i.test(parts[i]) ){
wrappers.push('script');
} else {
wrappers.push('');
}
getters.push(
$.get(parts[i], null, null, 'text').promise()
);
}
Promise.all(getters).then(function(results) {
let html = '';
for (let i = 0; i < results.length; i++) {
if(wrappers[i] === ''){
html += results[i];
} else {
html += `<${wrappers[i]}>${results[i]}</${wrappers[i]}>`;
}
}
self.updatePath(page);
$container.html(html);
});
} else {
//TODO
console.log('404');
}
},
updatePath: function(newPath){
if(newPath !== window.location.pathname) {
history.pushState({}, null, newPath);
}
}
}
Настройка окружения
Всё, что потребуется, чтобы запустить проект у себя на компьютере, я постарался изложить по шагам в README на гитхабе проекта. Если что-то не будет получаться, пишите в комментариях — постараемся помочь. Соответственно, README будем пополнять.
Для локального тестирования я написал маленький HTTP-сервер на Node.js:
const express = require('express');
const app = express();
app.use(express.static('public'));
app.use(function(req, res, next) {
req.url = 'app.html';
next();
});
app.use(express.static('public'));
app.listen(80, () => console.log('Listening..'))
Перед началом работы надо его запустить командой node abserver.js. При поступлении запроса он ищет файл в папке public и отдает его, если нашел. Если файл не найден, он отдает главный файл приложения public\app.html. Этого вполне достаточно для работы SPA-приложения. В продакшн эту же задачу решает Amazon CloudFront.
Заключение
AB-ERP пока очень «сырой». Будем рады любым предложениям и комментариям, а еще больше коммитам.
На текущий момент в AB-ERP более-менее реализована только авторизация — о ней планирую рассказать в одной из следующих статей. Какие варианты авторизации есть при работе с API Gateway и почему мы не стали реализовывать custom authorizer или интеграцию с Cognito.
Некоторые планы по дальнейшему развитию проекта.
Ключевые компоненты для любого приложения, работающего с данными, — это формы для ввода данных и таблицы для их вывода. Поэтому функционал по работе с формами и таблицами будет добавлен в первую очередь.
Есть идея стандартизовать работу с формами (построение форм на странице, валидация на бекенд и на фронтенд, сохранение в базе данных) через использование YAML-шаблонов. То есть сделать возможность в YAML-шаблонах описывать формы, а дальше вся остальная работа на фронтенде и на бекенде чтобы производилась кодом AB-ERP. Для таблиц будем использовать библиотеку Datatables, которую мы использовали в нашем таск-трекере AB-TASKS.
При написании статьи мне помогли следующие инструменты:
- Онлайн-сервис рисования диаграмм draw.io
- Команда tree командной строки Windows для отрисовки дерева каталогов
Комментарии (8)
random1st
11.04.2018 08:57Первое, API Gateway стоимость имеет на количество вызовов, независимо от того, кэшированы они или нет. А без кэша использование реляционной базы не имеет смысла, потому что Lambda не умеет шарить коннекшены. Не надо везде пихать серверлесс. Второе, ну допустим описали связку AG + Lambda, а остальную инфраструктуру что, руками менеджить? Имея терраформ, написать инфраструктуру и даже provisioning лямбды труда особого не составит. Я гораздо более оправданным считаю например такой фреймворк как zappa — он умеет адаптировать классическое WSGI приложение к работе внутри Lambda, ну и может как целиком CloudFormation stack развернуть, так и использоваться исключительно для создания package. Какой-то хайп нездоровый попер с этим серверлессом. «Запекать» AMI пакером и использовать споты всегда выгоднее будет, как и накрывать API CDN.
gnemtsov Автор
11.04.2018 22:19Переменные, объявленные вне вызова функции handler, при повторных вызовах обычно не инициализируются повторно. То есть соединение с БД не будет каждый раз устанавливаться заново, если функция вызывается достаточно часто. Lambda сохраняет контейнер функции в «горячем» состоянии. Вот интересный пост на эту тему: https://www.jeremydaly.com/reuse-database-connections-aws-lambda/.
Разворачивать инфраструктуру я предполагаю командами aws cli на основе шаблона CloudFormation + bash код. Подобный скрипт я приводил в своей предыдущей статье про то, как мы делаем AB-DOC.
Насчет zappa и WSGI — не знал. Почитал сейчас. Во-первых, я никогда не программировал на Python (может зря). Во-вторых, мне априори не нравятся сторонние фреймворки, не поддерживаемые вендором. Они могут быстро становится ненужными и лишаться поддержки, если сам вендор предложит полноценное решение. А SAM и sam-local похоже всерьез и надолго.
Последнее предложение вашего комментария я не в силах до конца понять.random1st
12.04.2018 06:21Интересное предположение, на скорую руку набросал простейший тест и нет, я не получил сохранения использования переменных в Python. К тому же мы говорим о затратах на установление и обслуживание TCP сессии.
Для развертывания и одновления инфраструктуры не CLI надо использовать. AWS имеет свой CI/CD ( см. CodeDeploy, CodeCommit, CodePipeline), но, честно говоря, я его не использовал.
По поводу сторонних фреймворков — Вы переносите риски саппорта на вендора, да, но целиком и полностью впадаете в вендор-лок. Серверлесс очень неплохо влетает в копеечку на высоких нагрузках. Я лично как альтернативу невнятному CloudFormation и SAM использую Terraform. И это работает.
Вот пример пайплайна, о котором я говорю
hippoage
11.04.2018 10:09Интересно, но смущает несколько моментов:
— непонятно зачем свой велосипед на фроненде. Вроде бы vue как раз и позиционируется как достаточно простой фреймворк. На это же уйдёт достаточно много времени в будущем.
— действительно, реляционная БД вряд ли будет хорошо работать с функциями
— нужно переименовывать проект, если есть расчёт на популярность в open source. Ab-erp — вообще не понятно, что это прямой конкурент Serverless.
То, что получается напоминает Google Firebase. Вы его пробовали? Там и хостинг статики есть, и авторизации из коробки, и функции. И много чего ещё (например, аналитика). Можно локально разрабатывать. Плюс, SDK для мобильных на всякий случай.gnemtsov Автор
11.04.2018 22:361. Насчет Vue.js — согласен, отличный фреймворк. Но вы смотрите на вопрос, как программист. Да, Vue.js может здорово повысить продуктивность написания кода для некоторых типов приложений. Но сколько программистов знают Vue.js и сколько знают jQuery? На сколько больше нужно платить программистам Vue.js? В общем это не столь однозначный вопрос. И я больше склоняюсь к тому, что нам не нужен фреймворк, если взвешивать все за и против и оценивать пользу фреймворка для конкретного типа приложений. Наши приложения — это ERP-системы, которые состоят в основном из форм и таблиц.
2. По-моему с этим все ОК, написал выше.
3. Мы вовсе не делаем конкурента Serverless framework. Скорее можно сказать, что Serverless framework и sam-local конкуренты, но никак не AB-ERP. А название AB-ERP выбрано из-за того, что наша заготовка в первую очередь ориентирована на создание ERP-систем и тому подобных приложений.
Google Firebase — это конкурент AWS. Не берусь сравнивать их с AWS, с Firebase я не работал. Много лет назад «подсел» на AWS и с тех пор не перестаю восхищаться ими, это целая вселенная :) Мне кажется, в целом они опережают своих конкурентов на годы. Об этом красноречиво говорят шильдики «бета» напротив многих сервисов Firebase.
zaikin-andrew
11.04.2018 21:42Так есть уже и Claudia.js и Serverless.js. Там вроде не сложно всё.
По поводу тестирования, serverless.js с пол пинка заводится на карме и для локальной разработке есть serverless-offline(добавьте туда что захочется, типо локальной MySQL или DynamoDB, S3 тоже локально работает).
gnemtsov Автор
11.04.2018 22:43Пишу сугубо свое мнение, могу ошибаться. Я считаю, что эти проекты — следствие того, что AWS вовремя не создал адекватные средства для удобной разработки бессерверных приложений. Люди не хотели ждать, и это понятно, поэтому появились эти проекты. По своей сути, это «костыли» или временные решения. Автор плагина serverless-offline уже объявил о своем нежелании продолжать проект и просит кого-то его подхватить. В общем будущее за SAM и sam-local, именно на этих технологиях стоит выстраивать процесс разработки в долгосрочной перспективе.
Упс. Промахнулся. Отвечал zaikin-andrew.
Yak52
> До недавнего времени разработка бессерверных приложений сильно осложнялась тем, что не было средств полноценного локального тестирования lambda-функций и API. При создании приложений нужно было или работать все время онлайн, редактируя код в браузере, или постоянно архивировать и загружать в облако исходный код lambda-функций.
По крайней мере для AWS lambda-функция на Python, можно всегда поставить на свой комп boto3 и спокойно отлаживать лямбду в любимом PyCharm.