This article is a translation of the original article.
In November of last year, AWS Lambda's Ruby runtime support was announced and a demo code to run Sinatra was released. I tried running Rails API with reference to these, so I will summarize the procedure.
What is this
A demo code of Rails API using Serverless Framework that runs on AWS Lambda. Includes API Gateway receiving requests and DynamoDB storing data too. Serverless deploy to AWS Lambda is executed by CodeBuild.
The REST API that can be used is below.
- GET /todos
- POST /todos
- GET /todos/:id
- PUT /todos/:id
- DELETE /todos/:id
The library version to be used is below.
- ruby 2.5.3p105 (2018-10-18 revision 65156) [x86_64-linux] (docker image: lambci/lambda:build-ruby2.5)
- Rails 5.2.2
- dynamoid 3.1
- Serverless Framework 1.35.1
The source code is below. The tag of this article is v0.0.0
.
How to use Demo Code
You can build an AWS environment with setup.sh.
$ git clone https://github.com/nihemak/serverless-rails-sample.git $ cd serverless-rails-sample $ ./setup.sh
setup.sh
will do the following.
- Create CodeCommit repository
- Create DynamoDB table
- Create IAM lambda execute role
- Create S3 deploy bucket
- Create ECR build image repository
- Create IAM build role
- Create CodeBuild
- Create Lambda and API Gateway
- Test Lambda and API Gateway
Specifically, it is as follows.
#!/bin/bash # This is the master key for this demo. RAILS_MASTER_KEY="743c44757a18175254895f68b1369aa5" AWS_IDENTITY=$(aws sts get-caller-identity) AWS_ACCOUNT_ID=$(echo ${AWS_IDENTITY} | jq -r ".Account") REGION="ap-northeast-1" SERVICE_NAME="serverless-rails-sample" STAGE_ENV="demo" CODECOMMIT_REPO_NAME=${SERVICE_NAME} CODECOMMIT_BRANCH="master" CODEBUILD_PROJ_NAME="${SERVICE_NAME}-${STAGE_ENV}" SERVICE_STACK_NAME="${SERVICE_NAME}-${STAGE_ENV}" ECR_REPO_NAME=${SERVICE_NAME} ROLE_BUILD_NAME="${SERVICE_NAME}-build" ROLE_EXECUTE_NAME="${SERVICE_NAME}-execute" BUCKET_DEPLOY_NAME="${SERVICE_NAME}-deploy-bucket" DYNAMO_PREFIX="${SERVICE_NAME}-${STAGE_ENV}" rm -rf work mkdir work cd ./work ## Create CodeCommit repository aws codecommit create-repository --repository-name ${CODECOMMIT_REPO_NAME} git clone --mirror https://github.com/nihemak/serverless-rails-sample.git cd serverless-rails-sample git push ssh://git-codecommit.${REGION}.amazonaws.com/v1/repos/${CODECOMMIT_REPO_NAME} --all cd .. ## Create DynamoDB table aws dynamodb create-table --table-name ${DYNAMO_PREFIX}-todos \ --attribute-definitions AttributeName=id,AttributeType=S \ --key-schema AttributeName=id,KeyType=HASH \ --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 ## Create IAM lambda execute role cat <<EOF > Trust-Policy.json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF ROLE_EXECUTE=$(aws iam create-role --role-name ${ROLE_EXECUTE_NAME} \ --assume-role-policy-document file://Trust-Policy.json) ROLE_EXECUTE_ARN=$(echo ${ROLE_EXECUTE} | jq -r ".Role.Arn") cat <<EOF > Permissions.json { "Version": "2012-10-17", "Statement": [ { "Action": [ "logs:CreateLogStream", "logs:PutLogEvents" ], "Resource": [ "arn:aws:logs:${REGION}:${AWS_ACCOUNT_ID}:log-group:/aws/lambda/${SERVICE_NAME}-*" ], "Effect": "Allow" }, { "Action": [ "dynamodb:DescribeTable", "dynamodb:Query", "dynamodb:Scan", "dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:DeleteItem" ], "Resource": "arn:aws:dynamodb:${REGION}:${AWS_ACCOUNT_ID}:table/${DYNAMO_PREFIX}-*", "Effect": "Allow" }, { "Action": [ "dynamodb:ListTables" ], "Resource": "*", "Effect": "Allow" } ] } EOF aws iam put-role-policy \ --role-name ${ROLE_EXECUTE_NAME} \ --policy-name ${ROLE_EXECUTE_NAME} \ --policy-document file://Permissions.json ## Create S3 deploy bucket aws s3 mb s3://${BUCKET_DEPLOY_NAME} --region ${REGION} ## Create ECR build image repository ECR_LOGIN=$(aws ecr get-login --no-include-email) echo ${ECR_LOGIN} > ecr_login.sh chmod 755 ecr_login.sh bash ./ecr_login.sh ECR_REPO=$(aws ecr create-repository --repository-name ${ECR_REPO_NAME}) ECR_REPO_URL=$(echo ${ECR_REPO} | jq -r ".repository.repositoryUri") docker pull lambci/lambda:build-ruby2.5 docker tag lambci/lambda:build-ruby2.5 ${ECR_REPO_URL}:latest docker push ${ECR_REPO_URL}:latest cat <<EOF > ecr_policy.json { "Version": "2008-10-17", "Statement": [ { "Sid": "CodeBuildAccess", "Effect": "Allow", "Principal": { "Service": "codebuild.amazonaws.com" }, "Action": [ "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability" ] } ] } EOF aws ecr set-repository-policy --repository-name ${ECR_REPO_NAME} --policy-text file://ecr_policy.json ## Create IAM build role cat <<EOF > Trust-Policy.json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "codebuild.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF ROLE_BUILD=$(aws iam create-role --role-name ${ROLE_BUILD_NAME} \ --assume-role-policy-document file://Trust-Policy.json) aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AdministratorAccess \ --role-name ${ROLE_BUILD_NAME} ROLE_BUILD_ARN=$(echo ${ROLE_BUILD} |jq -r ".Role.Arn") ## Create CodeBuild cat <<EOF > Source.json { "type": "CODECOMMIT", "location": "https://git-codecommit.${REGION}.amazonaws.com/v1/repos/${CODECOMMIT_REPO_NAME}", "buildspec": "buildspec.yml" } EOF cat <<EOF > Artifacts.json { "type": "NO_ARTIFACTS" } EOF cat <<EOF > Environment.json { "type": "LINUX_CONTAINER", "image": "${ECR_REPO_URL}:latest", "computeType": "BUILD_GENERAL1_SMALL", "environmentVariables": [ { "name": "REGION", "value": "${REGION}", "type": "PLAINTEXT" }, { "name": "STAGE_ENV", "value": "${STAGE_ENV}", "type": "PLAINTEXT" }, { "name": "SERVICE_NAME", "value": "${SERVICE_NAME}", "type": "PLAINTEXT" }, { "name": "LAMBDA_ROLE", "value": "${ROLE_EXECUTE_ARN}", "type": "PLAINTEXT" }, { "name": "DEPLOY_BUCKET", "value": "${BUCKET_DEPLOY_NAME}", "type": "PLAINTEXT" }, { "name": "DYNAMO_PREFIX", "value": "${DYNAMO_PREFIX}", "type": "PLAINTEXT" }, { "name": "RAILS_ENV", "value": "production", "type": "PLAINTEXT" }, { "name": "RAILS_MASTER_KEY", "value": "${RAILS_MASTER_KEY}", "type": "PLAINTEXT" } ] } EOF aws codebuild create-project --name ${CODEBUILD_PROJ_NAME} \ --source file://Source.json \ --artifacts file://Artifacts.json \ --environment file://Environment.json \ --service-role ${ROLE_BUILD_ARN} ## Create Lambda and API Gateway aws codebuild start-build --project-name ${CODEBUILD_PROJ_NAME} \ --source-version ${CODECOMMIT_BRANCH} REST_API_ID=$(aws cloudformation describe-stack-resources --stack-name ${SERVICE_STACK_NAME} | jq -r '.StackResources[] | select(.ResourceType == "AWS::ApiGateway::RestApi") | .PhysicalResourceId') REST_API_RESOURCE_ID=$(aws apigateway get-resources --rest-api-id ${REST_API_ID} | jq -r '.items[] | select(.path == "/{proxy+}") | .id') ## Test Lambda and API Gateway ### GET /todos aws apigateway test-invoke-method --rest-api-id ${REST_API_ID} \ --resource-id ${REST_API_RESOURCE_ID} \ --http-method GET \ --path-with-query-string '/todos' \ --headers 'Content-Type=application/json,charset=utf-8' # curl -X GET https://${REST_API_ID}.execute-api.${REGION}.amazonaws.com/${STAGE_ENV}/todos ### POST /todos aws apigateway test-invoke-method --rest-api-id ${REST_API_ID} \ --resource-id ${REST_API_RESOURCE_ID} \ --http-method POST \ --path-with-query-string '/todos?text=foo' \ --headers 'Content-Type=application/json,charset=utf-8' # curl -X POST https://${REST_API_ID}.execute-api.${REGION}.amazonaws.com/${STAGE_ENV}/todos --data "text=foo" TODO_ID=$(aws apigateway test-invoke-method --rest-api-id ${REST_API_ID} \ --resource-id ${REST_API_RESOURCE_ID} \ --http-method GET \ --path-with-query-string '/todos' \ --headers 'Content-Type=application/json,charset=utf-8' | jq -r '.body' | jq -r '.[].id') # TODO_ID=$(curl -X GET https://${REST_API_ID}.execute-api.${REGION}.amazonaws.com/${STAGE_ENV}/todos | jq -r '.[].id') ### GET /todos/:id aws apigateway test-invoke-method --rest-api-id ${REST_API_ID} \ --resource-id ${REST_API_RESOURCE_ID} \ --http-method GET \ --path-with-query-string "/todos/${TODO_ID}" \ --headers 'Content-Type=application/json,charset=utf-8' # curl -X GET https://${REST_API_ID}.execute-api.${REGION}.amazonaws.com/${STAGE_ENV}/todos/${TODO_ID} ### PUT /todos/:id aws apigateway test-invoke-method --rest-api-id ${REST_API_ID} \ --resource-id ${REST_API_RESOURCE_ID} \ --http-method PUT \ --path-with-query-string "/todos/${TODO_ID}?text=bar" \ --headers 'Content-Type=application/json,charset=utf-8' # curl -X PUT https://${REST_API_ID}.execute-api.${REGION}.amazonaws.com/${STAGE_ENV}/todos/${TODO_ID}?text=bar ### DELETE /todos/:id aws apigateway test-invoke-method --rest-api-id ${REST_API_ID} \ --resource-id ${REST_API_RESOURCE_ID} \ --http-method DELETE \ --path-with-query-string "/todos/${TODO_ID}" \ --headers 'Content-Type=application/json,charset=utf-8' # curl -X DELETE https://${REST_API_ID}.execute-api.${REGION}.amazonaws.com/${STAGE_ENV}/todos/${TODO_ID}
How to create a Rails on Lambda project
I will explain the procedure of creating a Rails API project that can be run with Lambda.
First, rails new
of API mode which skipped unnecessary setup.
$ rbenv shell 2.5.3 $ mkdir project_dir $ cd project_dir $ rails new . --api --skip-yarn --skip-active-record --skip-active-storage --skip-puma --skip-action-cable --skip-sprockets --skip-coffee --skip-javascript --skip-turbolinks --skip-bootsnap --skip-bundle
Then write the required gem in Gemfile and generate Gemfile.lock. Since bundle install
wants to be executed in the same environment as Lambda, use the environment of lambci/lambda:build-ruby2.5 of Docker.
$ docker run -v `pwd`:`pwd` -w `pwd` -it lambci/lambda:build-ruby2.5 bundle install --no-deployment $ docker run -v `pwd`:`pwd` -w `pwd` -it lambci/lambda:build-ruby2.5 bundle --deployment
Next, prepare lambda.rb to run Rails via Rack. It uses serverless-sinatra-sample lambda.rb. However, because the path of config.ru is different, need to change it for Rails.
$ git diff 61d42e36e5ddac4eaf5ee2538529142ffedc74e4 fd078722a2e73326ad57f48321a1e485865dc1ec diff --git a/lambda.rb b/lambda.rb index 05f7f59..0122927 100644 --- a/lambda.rb +++ b/lambda.rb @@ -19,7 +19,7 @@ require 'base64' # Global object that responds to the call method. Stay outside of the handler # to take advantage of container reuse -$app ||= Rack::Builder.parse_file("#{File.dirname(__FILE__)}/app/config.ru").first +$app ||= Rack::Builder.parse_file("#{File.dirname(__FILE__)}/config.ru").first def handler(event:, context:) # Check if the body is base64 encoded. If it is, try to decode it
Then prepare a serverless.yml that passes all http requests to lambda.rb
.
functions: app: handler: lambda.handler events: - http: ANY / - http: 'ANY {proxy+}'
Finally, prepare the buildspec.yml for serverless deploy
with CodeBuild.
version: 0.2 phases: install: commands: - curl -sL https://rpm.nodesource.com/setup_10.x | bash - - yum install -y nodejs - npm install -g serverless@1.35.1 build: commands: - bundle --deployment - serverless deploy post_build: commands: - touch dummy.zip artifacts: files: - dummy.zip
You are now ready to run with Lambda. The rest is the same as normal Rails development.
First, create a Controller.
$ docker run -v `pwd`:`pwd` -w `pwd` -it lambci/lambda:build-ruby2.5 bin/rails g controller Todos
Register the Action in the created todos_controller.rb.
class TodosController < ApplicationController # GET /todos def index # ... end # POST /todos def create # ... end # GET /todos/:id def show # ... end # PUT /todos/:id def update # ... end # DELETE /todos/:id def destroy # ... end end
Then register the routing in routes.rb.
Rails.application.routes.draw do # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html resources :todos end
You are now ready to start a project on Ruby on Rails on AWS Lambda and API Gateway by Serverless Framework
. How easy it is!
Enjoy Rails on Lambda!