LFA - Lambda Function Adapter
LFA is under development, and not released yet.
LFA is a web application framework in Ruby (Rack framework), to run AWS Lambda functions (migrated from AWS API Gateway integration) on Ruby's application servers (Unicorn, Puma, etc).
The main purposes of LFA are:
- run Lambda functions on EC2 (or container hosting services) temporarily
- test Lambda function as web request handler on our laptop
LFA was initially designed to host webapps migrated from AWS Lambda to EC2/ECS/k8s/etc by moving functions to app servers as-is. This provides time to engineers for re-implemention of native stand alone web applications.
The 2nd purpose (testing on laptop) was found eventually during the development of LFA. We can't test web request handlers of AWS Lambda functions on our laptop directly (because we don't have local API Gateway), but we can do it locally with LFA on laptop. This MAY improve our dev-experience a little, or more.
Features
LFA mounts Lambda functions on request paths, and routes requests to those functions.
Lambda functions are:
- mounted on specified paths, by AWS Lambda's handler specification
funcfile.Modname.method_name
- configured by
ENV
environment variables (just like AWS Lambda) - called with
event
andcontext
arguments per HTTP request (translated from Rackenv
)
LFA uses a YAML file to:
- configure functions with those environment variables
- configure resource-function relations (just like AWS API Gateway)
Functions on LFA will be loaded in (semi-)isolated module spaces. Functions will not effect to other functions (at least, unintentionally). See "Limitations" section below about the function isolation.
Features not supported yet
The features below are not supported yet, but will be implemented eventually.
- CORS support and built-in
OPTIONS
method request handler - Loading functions from
.zip
archives
How to run LFA
Installation
Add LFA to your application's Gemfile:
gem 'LFA'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install LFA
Configuration
Write a YAML configuration file, and a config.ru
Rack app file.
Rack app file is just to load LFA with the following YAML file.
# config.ru
require 'LFA'
run LFA.ignition!('config.yaml')
The YAML configuration file is to describe application resources, and functions to be called per request. The YAML requires 2 child elements:
-
resources
: the list of nested resources, almost equal to the resources on AWS API Gateway -
functions
: the list of functions, referred from resources
# config.yaml
---
resources:
- path: /api
resources:
- path: /country
methods:
GET: myfunc-countries
- path: /data
resources:
- path: /csv
methods:
GET: myfunc-data-csv
- path: /json
methods:
GET: myfunc-data-json
- path: /web
resources:
- path: /blog/{entry_id}
methods:
GET: blog-entry-get
- path: /assets/{asset_path+}
methods:
ANY: blog-asset-access
functions:
- name: myfunc-countries
handler: myfunc.Countries.process
env:
DATABASE_HOSTNAME: mydb.local
DATABASE_PASSWORD: this-is-the-top-secret
- name: myfunc-data-csv
handler: myfunc.Data.process
env:
OUTPUT_DATA_TYPE: csv
- name: myfunc-data-json
handler: myfunc.Data.process
env:
OUTPUT_DATA_TYPE: json
# omit blog-entry-get and blog-asset-access
The actual Lambda functions should be placed on the same directory with those configuration files.
LFA will load Countries
module from myfunc.rb
, then call its process
method for the request path /api/country
.
Ignition
Run your Rack application as usual (for example, with puma
):
$ puma config.ru
Or, using rackup
(requires gem i rackup
)
$ rackup --server puma config.ru
Limitation
The separation of Lambda functions is not perfect. That means:
- The Ruby script
funcfile
of Lambda handlerfuncfile.Modname.method_name
is loaded in an isolated namespace - Libraries
require
-ed from the Lambda file are NOT isolated, and be shared by all Lambda functions in the process -
ENV
access out of Lambda handler context inrequire
-ed files will see the originalENV
, instead ofenv
configured
To avoid those limitations, the Lambda functions loaded by LFA should take care of the following things.
Use common set of libraries
Lambda functions should use the common set of libraries. That means, Lambda functions should:
- have a common
lib
directory for its internal libraries - have a single
Gemfile
(andGemfile.lock
) to use the common set of gems
If the Lambda functions are of single application, these things will be satisfied usually. Otherwise, don't share a single LFA process.
Create handler object/module in function files
If your Lambda function's handler module is defined in funcfile
, it's totally OK.
# func.rb
module Modname
def self.method_name(event:, context:)
# ...
end
end
# OK
But if the funcfile
just requires your library and the library defines the handler module or instantiate the handler object, it MAY be NOT OK because that module/object are shared between different functions. It SHOULD be BAD when the handler object has internal states.
# func.rb
require_relative './lib/myapp/handler'
# and it provides Modname module
# MAY be NOT OK
If your Lambda handler has to have internal states, you should define a class, and instantiate it in funcfile
, as following:
# lib/myapp/handler.rb
class MyHandler
def initialize
@internal_cache = {}
end
def process(event:, context:)
# ...
end
end
# func.rb
require_relative './lib/myapp/handler'
Handler = MyHandler.new
# and specify the handler: func.Handler.process
# OK!
Refer ENV in dynamic manner
LFA overrides the ENV
reference in funcfile
.
# func.rb
module HandlerA
DB_HOST = ENV['DB_HOST'] # this refers configured `env`
def process(event:, context:)
# ...
end
end
But the ENV
reference in the static context (code not in methods) in required libraries will refer the original environment variables of the LFA process, instead of configured env
key-value pairs.
# func.rb
require_relative './lib/myapp/handler'
Handler = HandlerB.new
# lib/myapp/handler.rb
class HandlerB
DB_HOST = ENV['DB_HOST'] # this refers the process's environment variables
def process(event:, context:)
# ...
end
end
Even in libraries, code in methods called from funcfile
will refer the configured env
. So, ENV
references should be written in methods, called dynamically.
# func.rb
require_relative './lib/myapp/handler'
Handler = HandlerB.new
# lib/myapp/handler.rb
class HandlerB
def initialize
@db_host = ENV['DB_HOST'] # this refers the `env` configured
end
def process(event:, context:)
# Or, refer ENV in this handler method
# ...
end
end
Gem libraries referring ENV
should be initiated in methods, dynamically, as well.
Predefined Handlers
CORS: Preflight Request Handler
Just like enabling CORS
on AWS API Gateway, LFA has the built-in CORS preflight request handler. It can respond to OPTIONS
request on resources, instead of Lambda functions.
For the details of CORS requests/responses, see MDN documents or any other resources.
To enable CORS preflight request handler, specify CORS
as handler
:
resources:
- path: /a
methods:
GET: func1-myapp1-yay
OPTIONS: cors-1
functions:
- name: cors-1
handler: CORS
params:
allowOrigins:
- '*'
allowMethods:
- GET
- OPTIONS
allowHeaders:
- Content-Type
- Authorization
All parameters of the handler should be under params
key. Available parameters are:
allowOrigins
Fixed values for the response header access-control-allow-origin
. A string, or list of strings.
allowOrigins: '*'
allowOrigins: 'https://example.com, https://www.example.com'
allowOrigins:
- 'https://example.com'
- 'https://www.example.com'
Exclusive with mirrorAllowOrigin
. One of allowOrigins
or mirrorAllowOrigin
should be specified.
mirrorAllowOrigin
true
or false
(or missing) value, which specifies to respond access-control-allow-origin
value with the value of the request header Origin
.
mirrorAllowOrigin: true
The handler with mirrorAllowOrigin: true
will respond access-control-allow-origin: https://example.com
for the request with a header origin: https://example.com
.
Exclusive with allowOrigins
. One of allowOrigins
or mirrorAllowOrigin
should be specified.
allowMethods
Fixed values for the response header access-control-allow-methods
. A string, or list of strings. Should be specified.
allowMethods: GET, POST, PUT, DELETE, OPTIONS
allowMethods:
- GET
- POST
- OPTIONS
allowHeaders
Fixed values for the response header access-control-allow-headers
. A string, or list of strings. Optional.
allowHeaders: x-my-custom-header
allowHeaders:
- authorization
- x-my-custom-header
allowCredentials
true
or false
(or missing) to specify access-control-allow-credentials
. Optional.
allowCredentials: true
exposeHeaders
Fixed values for the response header access-control-expose-headers
. A string, or list of strings. Optional.
exposeHeaders: x-my-custom-header
exposeHeaders:
- x-my-custom-header
maxAge
A fixed number (seconds) for the response header access-control-max-age
. Optional.
maxAge: 86400
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/tagomoris/LFA.
License
The gem is available as open source under the terms of the MIT License.