Зачем?
Ой, не спрашивайте. Хотя почему бы и не поддаться современным веяниям, и не реализовать REST api на лямбдах.
Проверить так ли уж необходимы Rails и попробовать минимизировать количество зависимостей.
Попробовать декомпозировать веб приложение в терминах облачных сервисов.
Oracle free tier, хм, насколько это бесплатно в реальности?
Стек
В отличие от Amazon, у Oracle не так много сервисов. С одной стороны это плохо, а с другой - будет проще прикинуть архитектуру.
Итак, если лямбда в Oracle, значит это https://fnproject.io/. Free tier предоставляет 2000000 вызовов лямбд в месяц бесплатно. А еще это значит что наши лямбды будут поставляться как doker образы.
Если лямбда для веба, значит роутить запросы в облаке будет Api Gateway, это бесплатный миллион запросов в месяц (~ 1 запрос в 2.5 секунды). Этот же сервис умеет проксировать запрос к статическому контенту, предоставит https соединение. Он же может авторизовывать запросы и обрабатывать path параметры. Считайте что это аналогия роутера в Rails.
В качестве ORM не хочется брать тяжеловесный ActiveRecord, благо есть Sequel. Он более модульный и имеет адаптеры для большинства БД. Берем.
Для поднятия инфраструктуры в облаке будем использовать Terraform. Вы только гляньте на oracle провайдер от облака, он умеет все, класс.
В качестве бд я возьму Postgresql. Делаю это потому что привык и потому что на бесплатных вариантах Оракл может удалить вашу бдшечку при отсутствии запросов к ней, что неприятно. Но вообще в облаке есть варианты и с документными бд и с оракл бд (куда уж без нее). Использование этих вариантов считаю предпочтительней, поскольку вы можете управлять их схемой через тот же терраформ и хранить ее в стейте.
Итак, поехали.
Начинать стоит с авторизации SDK запросов к облачному Oracle апи. Подробно не будем останавливаться (вот и вот ссылочки в помощь), надо лишь сделать ключ и сохранить конфиг.
Структура проекта
/app
- здесь лежит gem с логикой, которая шарится между всеми лямбдами. Согласитесь, хочется пошарить модели/сериалайзеры и прочее.
/client
- код SPA фронтенда
/functions
- здесь лежат наши лямбды
.tf файлы брошены в корень, терраформ любит знать про все зависимости сразу, пусть так, потом разберемся. В качестве бекенда можно использовать оракловые же бакеты, все как и в амазоне:
//infrastructure.tf
terraform {
required_version = ">= 1.0.0"
required_providers {
oci = {
version = ">= 4.76.0"
source = "oracle/oci"
}
}
}
provider "oci" {
config_file_profile = "DEFAULT"
}
terraform {
backend "s3" {
bucket = "terraform-states"
key = "terraform.tfstate"
region = "{region-name}"
profile = "terraform"
endpoint = "https://{namespace}.compat.objectstorage.{region-name}.oraclecloud.com".cm"
shared_credentials_file = "~/.oci/terraform.secret"
skip_region_validation = true
skip_credentials_validation = true
skip_metadata_api_check = true
force_path_style = true
}
}
Деплой приложения происходит в десколько шагов - сначала билдим app гем, потом билдим и заливаем в облако образы функций, применяем изменения в инфраструктуре. С инфраструктурой справляется terraform, апи гейтвей выглядит как то так:
resource "oci_apigateway_gateway" "api_gateway" {
compartment_id = oci_identity_compartment.compartment.id
endpoint_type = "PUBLIC"
subnet_id = data.oci_core_subnet.net.id
display_name = "API Gateway"
freeform_tags = local.default_tags
}
resource "oci_apigateway_deployment" "app" {
#Required
compartment_id = oci_identity_compartment.compartment.id
gateway_id = oci_apigateway_gateway.api_gateway.id
path_prefix = "/"
specification {
routes {
path = "/users.create"
methods = ["POST", "OPTIONS"]
backend {
type = "ORACLE_FUNCTIONS_BACKEND"
function_id = local.users_function_id
}
}
}
Типичная лямбда выглядит так
Yaml-файл описания фунции в fnproject. Под капотом собирается докер образ из указанных image, указаны параметры рантайма.
//func.yaml
schema_version: 20180708
name: users
version: 0.0.27
runtime: ruby
build_image: oci-app:latest // образ для билда
run_image: oci-app-run:latest // релиз-образ
entrypoint: ruby func.rb
memory: 256
timeout: 120
Гемфайл зависимостей лямбды. По-хорошему наш гем стоит указать здесь, я же хотел чтобы гем ставился из локальной папки в проекте, с этим возникли сложности, поэтому гем предустанавливается в базовые докер образы oci-app
, oci-app-run
# Gemfile
source "https://www.rubygems.org" do
gem "fdk", ">= 0.0.31"
gem "oci", ">= 2.17.0"
end
Сам код функции, все классы Application::*
берутся из нашего гема.
require "fdk"
require "oci/common"
require "oci/object_storage/object_storage"
require "oci/auth/auth"
require "oci/secrets/secrets"
require "application"
class UsersController < Application::Controller
def show(input:)
authenticate do
user = input.key?("id") ? Application::User.with_pk!(id: input["id"]) : current_user
Application::UserSerializer.new(user).serializable_hash
end
end
def create(input:)
user = Application::User.new(input["data"]["attributes"].slice("name"))
if user.save
Application::UserSerializer.new(user, include: [:jwt_token]).serializable_hash
else
context.http_context.status_code = 400
end
end
end
def route(context:, input:)
UsersController.new(context).route(input: input)
end
FDK.handle(target: :route)
Переходим к самому гему.
Докер файлы для базовых образов функций:
// Dockerfile
FROM fnproject/ruby:2.7-dev as build-stage
WORKDIR /app_lib
RUN microdnf install libpq-devel
COPY pkg/* /app_lib
RUN gem install /app_lib/*
// Dockerfile.run
FROM fnproject/ruby:2.7
RUN microdnf install tzdata libpq && microdnf clean all
Билд гема и его включение в базовые образы:
rake build
docker build -f Dockerfile -t oci-app:latest .
docker build -f Dockerfile.run -t oci-app-run:latest .
Управление лямбдами решил осуществлять средствами команд fnproject: fn start|build|deploy
. Удалось организовать локальную среду, то есть те же самые функции работают в локальном fn сервере и отвечают на такие же запросы как и ApiGateway в облаке.
Итог:
Имеем костяк приложения с базовым роутингом
Имеем локальную девелопмент среду
Имеем возможность локально запускать аналог rails консоли из папки гема с коннектом к локальной бд
Минимум зависимостей от сторонних библиотек, нет rails, нет activesupport
Практически готовый деплой и возможность деплоить по частям
Фронтенд и его сборка полностью отдельно и никак не зависит от бекенда, все живет в папке client и деплоится в бакет терраформом. При желании все билд команды можно прописать в терраформе и тогда деплой будет выглядеть просто как `terraform apply`.
Масштабирование из коробки. Забудьте про скейл группы и мониторинг метрик нагрузки.
Приложение практически готово к развертыванию в собственном kubernetes кластере
Благодаря щедротам Oracle, это все бесплатно в известных пределах)
Минусы тоже есть:
Время холодного старта функций может доходить до 20-30 секунд
У Оракла существенно меньше облачных сервисов, закрыт буквально минимальный набор
FnProject выглядит последнее время заброшенным, не знаю насколько у Оракла в планах поддерживать его.