Anatomy of a Lamby Project
Lamby can be installed within legacy Rails/Rack applications or pre-installed in a fresh new Rails application using our AWS SAM cookiecutter project. This guide outlines how Lamby works and how to integrate our files within your project.
- How Lamby Works
- Core Lamby Files
- Our SAM Cookiecutter's Features
- Switch To REST API
- Switch To Application Load Balancer
- Switch To REST API (private)
- Installing for Legacy Applications
- Inspiration
How Lamby Works
Lamby is as a Rack adapter that converts AWS Lambda integration events into native Rack Environment objects which are sent directly to your application. Lamby can do this when using either API Gateway HTTP API's (the default) v1/v2 payloads, API Gateway REST API, or even Application Load Balancer (ALB) integrations.
This means Lamby removes the need for any Rack Web Server including WEBrick, Passenger, or Puma. It also means that Lamby can be used by any Rack Web Application such as Sinatra, Hanami, and even Rails as long as that framework is using Rack v2.0
or higher. For Rails, this means you need to be running v5.0
or higher. It does all of this in a simple one line interface.
def handler(event:, context:)
Lamby.handler $app, event, context
end
Core Lamby Files
These files are core to Lamby allowing your Rails application to work on AWS Lambda. Some are specific to AWS SAM while others are opinionated files included if you used our Cookiecutter getting started project. These mostly help with Docker.
File - app.rb
The app.rb
file is similar to Rails' config.ru
for Rack. Commonly called your handler, this file should remain relatively simple and look something like this.
require_relative 'config/boot'
require 'lamby'
require_relative 'config/application'
require_relative 'config/environment'
$app = Rack::Builder.new { run Rails.application }.to_app
def handler(event:, context:)
Lamby.handler $app, event, context, rack: :http
end
Any code outside the handler
method is loaded only once, which includes booting your Rails application. After that, Lamby does all the work to convert the event
and context
objects to Rack messages that get sent to your Rails application. The details of the AWS Lambda Function Handler in Ruby should be left to Lamby, but please learn about this topic if you are interested.
File - template.yaml
This YAML file at the root of your project describes your SAM application. Don't worry, we have done some heavy lifting for you. But as your application grows you may end up adding resources like S3 Buckets, DynamoDB, or IAM Policies. Please take some time to learn how SAM & CloudFormation works.
- What Is the AWS Serverless Application Model (AWS SAM)?
- Quick Intro & Tech Spec for SAM File
- What is AWS CloudFormation?
Files - Dockerfile
, Dockerfile-build
, & docker-compose.yml
Your Dockerfile
should use one of the AWS provided runtimes from their public ECR repository and typically do a simple copy of your built project and other tasks. For example:
FROM public.ecr.aws/lambda/ruby:2.7
ARG RAILS_ENV
ENV RAILS_ENV=$RAILS_ENV
COPY . .
CMD ["app.handler"]
The Dockerfile-build
facilitates local development/test for your Rails application. It also serves as the build/deploy environment. This follows a typical good practice for Docker called multi-stage builds. We recommend using SAM's build images) (ex: public.ecr.aws/sam/build-ruby2.7
) for your development needs. Installing additional tooling like a SAM version and JavaScript for compiling assets should be done in this image. All docker compose commands leverage this image.
Files - bin
Because we encourage use of the Lambda docker containers using the Docker files above, we include a host of bin scripts that make development easy for you. All files with a leading _
should be run in the container. For example, bin/server
is just a docker-compose run command to bin/_server
. Overall, here is what you will find in our cookiecutter project.
- Normalized
bootstrap
,setup
, andtest
scripts. - Wrappers around
sam build
,package
, anddeploy
. - A
bin/run
convenience script to run any other command in the container.
Our SAM Cookiecutter's Features
Our cookiecutter makes starting a Rails application with API Gateway HTTP API so easy, you may have missed some of the interesting things we have done for you. Here is a small list:
- Docker docker docker! Using AWS SAM build container for development since it closely mirrors the production Runtime container environment. Shares your project directory, environment variables, and allows easy use of container resources like MySQL.
- Encourage good CI/CD practices with GitHub Actions. Includes a test and deploy workflow with caching for fast integration and code delivery. See the Quick Start for more details.
- Simple
SECRET_KEY_BASE
and SSM Parameter Store placeholders. See Environment & Configuration for details. - Compiles CSS/JS Assets with LibSass & Webpacker. See Precompiling CSS & JavaScript Assets for details.
- Creates an Amazon Elastic Container Registry (ECR) repository for you automatically in
./bin/bootstrap
. - Serving static assets using
RAILS_SERVE_STATIC_FILES
&config.public_file_server
. - Sets the
RAILS_LOG_TO_STDOUT
environment variable. Also ensures that allLogger
objects are forced to useSTDOUT
. - Adds the lograge gem to reduce CloudWatch data costs while easily allowing CloudWatch Insights or Embedded Metrics to be used. These are major observability wins!
Switch To REST API
By default our starter uses API Gateway's latest HTTP API which is faster, cheaper, and far easier to configure. However, you may have a need to use REST API. Here is how to switch your project to using it.
Open your template.yaml
file and replace your RailsHttpApi
resource with this RailsApi
one. It is a little more verbose, but it essentially sets up a simple proxy that Lamby can use.
RailsApi:
Type: AWS::Serverless::Api
Properties:
DefinitionBody:
swagger: 2.0
info:
title: !Ref "AWS::StackName"
basePath: !Sub "/${RailsEnv}"
schemes: ["https"]
paths:
/:
x-amazon-apigateway-any-method:
x-amazon-apigateway-integration:
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RailsLambda.Arn}:live/invocations
httpMethod: POST
type: aws_proxy
/{resource+}:
x-amazon-apigateway-any-method:
parameters:
- name: resource
in: path
required: true
type: string
x-amazon-apigateway-integration:
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RailsLambda.Arn}:live/invocations
httpMethod: POST
type: aws_proxy
x-amazon-apigateway-binary-media-types:
- "*/*"
StageName: !Ref RailsEnv
The integration uri
lines above use the :live
alias since our starter defaults to using an AutoPublishAlias: live
. Now your Lambda function needs to respond to these REST API events. Find your RailsLambda
resource and change the Events
to use these vs the HttpApiProxy
from our starter.
RailsRoot:
Type: Api
Properties:
Path: /
Method: ANY
RestApiId: !Ref RailsApi
RailsAll:
Type: Api
Properties:
Path: /{resource+}
Method: ANY
RestApiId: !Ref RailsApi
Lastly, update your RailsApiUrl
in the Outputs
section. Replace the resource RailsHttpApi
in the Value
section with the new logical name of RailsApi
. Example:
RailsApiUrl:
Description: API Gateway Endpoint
Value: !Sub "https://${RailsApi}.execute-api.${AWS::Region}.amazonaws.com/${RailsEnv}/"
Switch To Application Load Balancer
Using Lambda's ALB Integration is a great way to setup your application on a private VPC. ⚠️ Currently ALBs limit response payloads to less than 1MB vs. the 6MB limit for API Gateway. Using Rack::Deflater can help with this if needed.
Next, replace your RailsHttpApi
resource with these load balancer resources. These make use of the VPC subnets and security groups from above. It also assumes your Lambda function in your template has a Logical ID of RailsLambda
. Make sure to use the same SubnetIds
and SecurityGroupIds
described in our Database & VPCs guide. Likewise you will need to ensure the CertificateArn
value is a valid Certificate Manager ARN as described in our Custom Domain Names, CloudFront, & SSL guide.
RailsLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internal
SubnetIds:
- subnet-09792e6cd06dd59ad
- subnet-0501f3136415021da
SecurityGroupIds:
- sg-07be99aff5fb14557
RailsLoadBalancerHttpsListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
Certificates:
- CertificateArn: arn:aws:acm:us-east-1:123456789012:certificate/38613b58-c21e-11eb-8529-0242ac130003
DefaultActions:
- TargetGroupArn: !Ref RailsLoadBalancerTargetGroup
Type: forward
LoadBalancerArn: !Ref RailsLoadBalancer
Port: 443
Protocol: HTTPS
RailsLoadBalancerTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
DependsOn: RailsLambdaInvokePermission
Properties:
TargetType: lambda
TargetGroupAttributes:
- Key: lambda.multi_value_headers.enabled
Value: true
Targets:
- Id: !GetAtt RailsLambda.Arn
RailsLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt RailsLambda.Arn
Action: "lambda:InvokeFunction"
Principal: elasticloadbalancing.amazonaws.com
Find your RailsLambda
resource and remove the Events
property completely. An ALB will trigger your Lambda for you via the target group.
Lastly, update your RailsApiUrl
in the Outputs
section. With the example below that uses the Application Load Balancer's DNS name. Example:
RailsApiUrl:
Description: Load Balancer DNS Name
Value: !GetAtt RailsLoadBalancer.DNSName
Switch To REST API (private)
Unfortunately there is no turn-key way to setup the modern HTTP API to be private. However there is a way to use REST API on a private VPC if your application needs the 6MB response limit capability. ⚠️ This setup is a mix of the REST API & ALB setup above and can be more costly since it requires a handful of resources. It is also not trivial to setup! Using this Building a Private REST API tutorial can walk you thru a few of the steps like creating a VPC Endpoint.
The CloudFormation changes to your template.yaml
file must include BOTH the REST API & ALB changes above with the following changes. First, find your RailsLambda
resource and remove the Events
property. This will not be needed since the ALB integration will take care of this for us once you map a Custom Domain Names, CloudFront, & SSL that matches the API Gateway DNS name pointed to the ALB's DNS Name.
Now, add these to the Properties
section of your REST API RailsApi
resource. Make sure to use your own VPC Endpoint ID created in the tutorial above.
Auth:
ResourcePolicy:
CustomStatements:
- Effect: Allow
Principal: "*"
Action: execute-api:Invoke
Resource: ["execute-api:/*"]
EndpointConfiguration:
Type: PRIVATE
VPCEndpointIds:
- vpce-17d53d19eb38b879c
Installing for Legacy Applications
The most comprehensive method to install Lamby is to perform the same steps within our getting started cookiecutter project. We break these into two steps, the first adds Lamby files, the second inserts additional code. Follow the steps and files in these scripts/directories.
The above files assume the most common use case of a public facing Rails application using API Gateway's HTTP API. This serves as the base for the guides above to switch to REST API and/or an ALB. Alternatively, Lamby provides a simple Rake task to install very basic starter files needed to use AWS Lambda for your application. One for each integration method.
$ ./bin/rake -r lamby lamby:install:http
$ ./bin/rake -r lamby lamby:install:rest
$ ./bin/rake -r lamby lamby:install:alb
This task will install app.rb
, template.yaml
, and starter bin
files. Please review our cookiecutter's scripts and files above for a complete integration reference.
Inspiration
Thanks to the projects and people below which inspired our code and implementation.