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

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.

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.

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:

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.