Мы решили создать небольшой фреймворк для бессерверных веб-приложений в 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-функций (лимиты, показывается объем использованной памяти и длительность выполнения).
Выглядит это примерно так
Далее обращение к локальному API и вызов соответствующих lambda-функции отображается в консоли в общем-то также, как lambda-функции выводят информацию в логи CloudWatch:
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 (фронтент):
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:
В шаблоне SAM мы видим один наш ресурс ABLambdaRouter, который является lambda-функцией. ABLambdaRouter вызывается только одним событием ABAPI, которое поступает от API.
Наш ресурс API Gateway принимает запросы любыми методами (ANY) к любым путям в URL: /{proxy+}. То есть другими словами, выступает в роли обычного двух-стороннего прокси. Lambda-функция, соответственно, должна взять на себя роль роутера, который будет выполнять разный код в зависимости от запросов.
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 открываемой страницы.
Всё, что потребуется, чтобы запустить проект у себя на компьютере, я постарался изложить по шагам в README на гитхабе проекта. Если что-то не будет получаться, пишите в комментариях — постараемся помочь. Соответственно, README будем пополнять.
Для локального тестирования я написал маленький HTTP-сервер на Node.js:
Перед началом работы надо его запустить командой 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.
При написании статьи мне помогли следующие инструменты:

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

