Jets allows you to run Rails applications on AWS Lambda via Mega Mode. The name reminds me of a few things:

  • Mega Monolith: Rails applications can sometimes become a Majestic Monolith. Or sometimes it just becomes a Monolith 😁
  • Power Rangers Mega Mode: The Power Rangers can combine their Zords into one huge Megazord.
  • Mega Man: This superhero can change his arm to use different special weapons. One of the things that makes him Mega Man.

Mega Mode combines a Jets app and a Rack app together to allow you to run Rails on AWS Lambda with little effort. The docs for Mega Mode and Rails Support are on the Ruby On Jets site. Here’s a Live Mega Mode Demo. We’ll go through an example of Mega Mode and get a Rails application running on AWS Lambda in this post.

Setup: New Jets App

To demonstrate Jets Rails Support, we’ll first create a brand new Jets app. Then we’ll add an existing Rails application to it. Here are the commands based on the Quick Start to generate a new Jets app.

gem install jets
jets new demo
cd demo
jets generate scaffold Post title:string
vim .env.development # edit with local db settings
jets db:create db:migrate
jets runner '3.times {|i| Post.create(title: "Title #{i+1}") }' # seed some data
jets server # starts server on localhost:8888

You should see a Jets welcome page on the localhost:8888 and simple CRUD actions under localhost:8888/posts. The Jets welcome page looks something like this:

And the CRUD posts index page like this:

Here’s the initial tongueroo/jets-mega-demo repo. It’s been tagged with before-megamode.

Add Rails App: jets import:rails

Zords Combine! -Power Rangers

Now it’s time to add a Demo Rails app to the Jets project. Jets provides a handy jets import:rails command that makes this a simple task.

jets import:rails http://github.com/tongueroo/demo-rails.git

The import command essentially copies the project to a rack folder within the demo Jets app. The import command also adds an example route to config/routes.rb to enable Mega Mode. It looks something like this:

Jets.application.routes.draw do
  # ...
  # Enables Mega Mode Rails integration
  any "*catchall", to: "jets/rack#process"
end

The newly added catchall route forwards requests to a special jets/rack#process controller. This controller forwards requests to the Rails application in the rack subfolder.

Here’s the Jets project after the Mega Mode has been set up: tongueroo/jets-mega-demo.

Local Testing

First, it is good to check that the imported Rails app in the rack subfolder works locally.

cd rack # in the imported Rails app
bundle exec rails db:create db:migrate # needed if you havent ran the demo app before
bundle exec rackup # starts Rails app on http://localhost:9292

The demo-rails app has a /info route that provides some info about the Rails application. You should see something like this:

Once the Rails app looks good, we can shut down the rack server and test the Jets app with Mega Mode. Let’s start the jets server:

jets server

Notice that 2 servers start up. The jets server starts up on localhost:8888 and the rack server starts on localhost:9292

Now hit the /info route again except this time on port 8888. It shows the same info page, but this time it’s going through the Jets app via Mega Mode.

Let’s also visit localhost:8888 and localhost:8888/posts again. These urls are served by the Jets app as defined by config/routes.rb. Only when the routes hit the catchall Mega Mode route will it get forwarded to the Rails app. To make it clear we can adjust the app/views/posts/index.html.erb and replace <h1>Posts</h1> with <h1>Jets App: Posts</h1>. Now we can see the difference easily:

http://localhost:8888/posts:

http://localhost:8888/info:

Mega Mode is controlled via routing. You can selectively route requests to the Rails app by adding routes send to jets/rack#process.

Where to Find Logs

It is useful to know where logs are. The rack server will send Rails logs to whatever has been set up in the Rails app, usually a log file like rack/log/development.log. Here’s an example:

$ tail -f rack/log/development.log

Started GET "/info" for 127.0.0.1 at 2018-11-03 21:18:34 +0000
Processing by DemoController#index as HTML
   (0.2ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
   (0.1ms)  SELECT version()
  Rendering demo/index.html.erb within layouts/application
  Rendered demo/index.html.erb within layouts/application (0.5ms)
Completed 200 OK in 13ms (Views: 10.0ms | ActiveRecord: 0.9ms)

The main Jets app sends the logs to stdout as part of the jets server command. Example:

$ jets s --host 0.0.0.0
* Environment: development
* Listening on tcp://localhost:9292
Use Ctrl-C to stop
I, [2018-11-03T21:28:51.363402 #18894]  INFO -- : Processing by PostsController#index
I, [2018-11-03T21:28:51.364730 #18894]  INFO -- :   Event: {"resource"=>"/posts", "path"=>"/posts", "httpMethod"=>"GET", ...}
I, [2018-11-03T21:28:51.364801 #18894]  INFO -- :   Parameters: {}
I, [2018-11-03T21:28:51.587102 #18894]  INFO -- : Completed Status Code 200 in 0.224176506s
104.220.39.135 - - [03/Nov/2018:21:28:51 +0000] "GET /posts HTTP/1.1" 200 - 0.4635

When the app is deployed to AWS Lambda, both Jets and Rails logs are streamed to CloudWatch Logs instead.

Deploy to AWS Lambda

We’re almost ready to deploy to AWS Lambda. You will need an RDS database set up for this. You can follow the AWS docs, Create an RDS DB Instance, wizard steps to set up an RDS database. For this tutorial, we’re using MySQL 5.7.23. Also for simplicity, we’ll make it publicly accessible and open up security group port 3306. This is a throwaway database and will have been deleted by the time this post has been published. It usually takes less than 5 minutes for the RDS database to be available. Once it’s available you can grab the RDS endpoint from the RDS console:

To set up the RDS endpoint with the app, create a .env.development.remote file at the top level of your project and updated it with the DATABASE_URL. The format of DATABASE_URL looks like this:

DATABASE_URL=mysql2://dbuser:dbpass@host/dbname?pool=5
DATABASE_URL=mysql2://dbuser:dbpass@demotest.cbuqdmc3nqvb.us-west-2.rds.amazonaws.com/dbname?pool=5 # another example with an rds host
vim .env.development.remote # edit and add DATABASE_URL

We’re using an env.development.remote file because we want this specific value for DATABASE_URL to be set remotely on AWS Lambda only. More info about Env Files and Remote Only Variables are available on the docs.

One way to test the RDS database locally is by using the JETS_ENV_REMOTE=1 flag. We’ll also use this to create and migrate the database on RDS.

$ JETS_ENV_REMOTE=1 jets db:create db:migrate
$ JETS_ENV_REMOTE=1 jets console
>> ActiveRecord::Base.connection.tables
=> ["ar_internal_metadata", "posts", "schema_migrations"]
>>

We’re now ready to deploy AWS Lambda! Let’s run jets deploy:

$ jets deploy
...
10:15:26PM UPDATE_COMPLETE_CLEANUP_IN_PROGRESS AWS::CloudFormation::Stack demo-dev
10:15:27PM UPDATE_COMPLETE AWS::CloudFormation::Stack demo-dev
Stack success status: UPDATE_COMPLETE
Time took for stack deployment: 2m 1s.
Prewarming application.
API Gateway Endpoint: https://u3egh0sere.execute-api.us-west-2.amazonaws.com/dev/
$

After the deploy completes, you can check it out in the browser. Using your specific API Gateway endpoint, visit /dev/posts and /dev/info. You should see something like this:

https://api-gateway-endpoint/dev/posts:

https://api-gateway-endpoint/dev/info:

Explore the AWS Console

It is useful to check out the Lambda console because you can see the function code and even test it live in the editor. Lambda Functions:

Let’s take a look at the demo-dev-posts_controller-index function and test it with the Lambda Console:

Click on the “Test” button on the upper right-hand corner and configure a test event. For the test event payload, it does not matter what we use since the PostsController#index doesn’t use the parameters so just use the provided example. After you’ve configured an event in hit “Test” again you should see something like this:

For other actions, the test event payload does matter. Controller Lambda functions expect a payload from API Gateway. So you need a similar payload structure to test with the Lambda Console. Adding a puts JSON.dump(event) in the controller is a great way grab a live API Gateway event payload for testing. Also here are some examples to help you get an idea of the minimal structure:

Action Payload Notes
posts#index {"path": "/posts"} Path is always required.
posts#new {"path": "/posts/new"} The new action uses the path to generate the new form.
posts#show {"path": "/posts/123"} The post id must be provided and exist in the database or you’ll get a “Couldn’t find Post without an ID” error. 123 is an example.
posts#edit {"path": "/posts/123/edit", "pathParameters": {"id": "123"}} You will also need pathParameters because that’s how the controller gets the id parameter.
jets/rack#process {"path": "/info"} The rack controller requires the path to be forwarded to the Rails app.

Notice how the jets/rack#process controller just needs a {"path": "/info"} payload since that’s all the Rails app /info url requires.

CloudWatch Logs

As mentioned early in the “Where to Find Logs” section, when the app is deployed to AWS Lambda, the logs are streamed to Cloudwatch logs instead. Here’s what the CloudWatch Log groups look like:

If you click into the posts_controller-index log group and hit the /dev/posts url in the browser you should see something like this:

For Mega Mode requests, go to the jets-rack_controller-process log group and hit the /dev/info url, you’ll notice both Rails and Jets logs:

Jets prepends “Rails: “ to the Rails logging output to distinguish them from the Jets app logging.

Live Demo

Here’s a Live Demo to see and use Mega Mode in action.

That’s it! You can run a Rails application on AWS Lambda today. Hope you like Mega Mode and give it try. Also if you find Jets interesting, please give it ⭐️ it on GitHub. 👍

More info