Pull to refresh
0
AB-DOC
Cервис хранения и систематизации любой информации

Фреймворк для бессерверных приложений в AWS

Reading time 10 min
Views 5.9K
Мы решили создать небольшой фреймворк для бессерверных веб-приложений в AWS. Может более правильно назвать это не фреймворком, а заготовкой, — я не знаю. Но суть в том, чтобы создать основу для быстрой разработки бессерверных приложений в AWS. Код выложен на GitHub и открыт для любых усовершенствований, коих предстоит немало.


В статье речь пойдет о том, как разрабатывать и тестировать бессерверные приложения локально, о роутинге на фронтенде и бекенде, о сервисах 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-функция, соответственно, должна взять на себя роль роутера, который будет выполнять разный код в зависимости от запросов.

Код 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 для отрисовки дерева каталогов
Tags:
Hubs:
+8
Comments 9
Comments Comments 9

Articles

Information

Website
ab-doc.com
Registered
Founded
Employees
2–10 employees
Location
Россия