diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..d75d1c3 --- /dev/null +++ b/Gemfile @@ -0,0 +1,13 @@ +source 'http://rubygems.org' + +gem 'sinatra' +gem 'json' +gem 'rack' +gem 'rack-contrib' +gem 'aws-record' + +# These are the dependencies that are used only for unit tests. +group :test do + gem "rspec" + gem "rack-test" +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..89ddd7a --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,61 @@ +GEM + remote: http://rubygems.org/ + specs: + aws-eventstream (1.0.1) + aws-partitions (1.114.0) + aws-record (2.1.2) + aws-sdk-dynamodb (~> 1.0) + aws-sdk-core (3.38.0) + aws-eventstream (~> 1.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-dynamodb (1.16.0) + aws-sdk-core (~> 3, >= 3.26.0) + aws-sigv4 (~> 1.0) + aws-sigv4 (1.0.3) + diff-lcs (1.3) + jmespath (1.4.0) + json (2.1.0) + mustermann (1.0.3) + rack (2.0.6) + rack-contrib (2.1.0) + rack (~> 2.0) + rack-protection (2.0.4) + rack + rack-test (1.1.0) + rack (>= 1.0, < 3) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) + sinatra (2.0.4) + mustermann (~> 1.0) + rack (~> 2.0) + rack-protection (= 2.0.4) + tilt (~> 2.0) + tilt (2.0.8) + +PLATFORMS + ruby + +DEPENDENCIES + aws-record + json + rack + rack-contrib + rack-test + rspec + sinatra + +BUNDLED WITH + 1.16.6 diff --git a/README.md b/README.md index 2bb8da5..3b9434e 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,90 @@ ## Running Ruby Sinatra on AWS Lambda -Demo code for running Ruby Sinatra on AWS Lambda +This sample code helps get you started with a simple Sinatra web app deployed on AWS Lambda + +What's Here +----------- + +This sample includes: + +* README.md - this file +* Gemfile - Gem requirements for the sample application +* app/config.ru - this file contains configuration for Rack middleware +* app/server.rb - this file contains the code for the sample service +* app/views - this directory has the template files +* spec/ - this directory contains the RSpec unit tests for the sample application +* template.yml - this file contains the description of AWS resources used by AWS + CloudFormation to deploy your infrastructure + +Getting Started +--------------- + +These directions assume you want to develop on your local computer, and not +from the Amazon EC2 instance itself. If you're on the Amazon EC2 instance, the +virtual environment is already set up for you, and you can start working on the +code. + +To work on the sample code, you'll need to clone your project's repository to your +local computer. If you haven't, do that first. You can find instructions in the +AWS CodeStar user guide. + +1. Install bundle + + $ gem install bundle + +2. Install Ruby dependencies for this service + + $ bundle install + +3. Download the Gems to the local vendor directory + + $ bundle install --deployment + +4. Create the deployment package (note: if you don't have a S3 bucket, you need to create one): + + $ aws cloudformation package \ + --template-file template.yaml \ + --output-template-file serverless-output.yaml \ + --s3-bucket { your-bucket-name } + + Alternatively, if you have SAM CLI installed, you can run the following command + which will do the same + + $ sam package \ + --template-file template.yaml \ + --output-template-file serverless-output.yaml \ + --s3-bucket { your-bucket-name } + +5. Deploying your application + + $ aws cloudformation deploy --template-file serverless-output.yaml \ + --stack-name { your-stack-name } \ + --capabilities CAPABILITY_IAM + + Or use SAM CLI + + $ sam deploy \ + --template-file serverless-output.yaml \ + --stack-name { your-stack-name } \ + --capabilities CAPABILITY_IAM + + +What Do I Do Next? +------------------ + +If you have checked out a local copy of your repository you can start making changes to the sample code. + +Learn more about Serverless Application Model (SAM) and how it works here: https://github.com/awslabs/serverless-application-model/blob/master/HOWTO.md + +AWS Lambda Developer Guide: http://docs.aws.amazon.com/lambda/latest/dg/deploying-lambda-apps.html + +How Do I Add Template Resources to My Project? +------------------ + +To add AWS resources to your project, you'll need to edit the `template.yml` +file in your project's repository. You may also need to modify permissions for +your project's worker roles. After you push the template change, AWS CloudFormation provisions the resources for you. ## License -This library is licensed under the Apache 2.0 License. +This library is licensed under the Apache 2.0 License. diff --git a/app/config.ru b/app/config.ru new file mode 100644 index 0000000..79d85a5 --- /dev/null +++ b/app/config.ru @@ -0,0 +1,8 @@ +require 'rack' +require 'rack/contrib' +require_relative './server' + +set :root, File.dirname(__FILE__) +set :views, Proc.new { File.join(root, "views") } + +run Sinatra::Application diff --git a/app/server.rb b/app/server.rb new file mode 100644 index 0000000..8b3bf10 --- /dev/null +++ b/app/server.rb @@ -0,0 +1,58 @@ +require 'sinatra' +require 'aws-record' + +################################## +# For the index page +################################## +get '/' do + erb :index +end + +################################## +# Return a Hello world JSON +################################## +get '/hello-world' do + content_type :json + { :Output => 'Hello World!' }.to_json +end + +post '/hello-world' do + content_type :json + { :Output => 'Hello World!' }.to_json +end + +################################## +# Web App with a DynamodDB table +################################## + +# Class for DynamoDB table +# This could also be another file you depend on locally. +class FeedbackServerlessSinatraTable + include Aws::Record + string_attr :id, hash_key: true + string_attr :data + epoch_time_attr :ts +end + +get '/feedback' do + erb :feedback +end + +get '/api/feedback' do + content_type :json + ret = [] + items = FeedbackServerlessSinatraTable.scan() + items.each do |r| + item = { :ts => r.ts, :data => r.data } + ret.push(item) + end + ret.sort { |a, b| a[:ts] <=> b[:ts] }.to_json +end + +post '/api/feedback' do + content_type :json + body = env["rack.input"].gets + item = FeedbackServerlessSinatraTable.new(id: SecureRandom.uuid, ts: Time.now, data: body) + item.save! # raise an exception if save fails + item.to_json +end \ No newline at end of file diff --git a/app/views/feedback.erb b/app/views/feedback.erb new file mode 100644 index 0000000..64b30c6 --- /dev/null +++ b/app/views/feedback.erb @@ -0,0 +1,94 @@ + + + + + +
+
+
+
+

Hope you enjoying programming with AWS Lambda! Please share your feedback or any random thoughts.

+
+ +
+
+ + +
+
+ + +
+
+ Success! Thank you! +
+ +
+

+
+
diff --git a/app/views/index.erb b/app/views/index.erb new file mode 100644 index 0000000..a75f91f --- /dev/null +++ b/app/views/index.erb @@ -0,0 +1,8 @@ +

+
<%= ['Hello', 'Hi', 'Hey', 'Yo'][rand(4)] %> World!
+
<%= ['Hello', 'Hi', 'Hey', 'Yo'][rand(4)] %> World!
+

+
- from SINATRA on AWS Lambda
+
+
Also checkout the API example: Hello World
+
And a web app example: Feedback
\ No newline at end of file diff --git a/app/views/layout.erb b/app/views/layout.erb new file mode 100644 index 0000000..fbbcfda --- /dev/null +++ b/app/views/layout.erb @@ -0,0 +1,9 @@ + + +Serverless Sinatra + + +<%= yield %> + + + diff --git a/buildspec.yml b/buildspec.yml new file mode 100644 index 0000000..6106fd3 --- /dev/null +++ b/buildspec.yml @@ -0,0 +1,21 @@ +version: 0.2 + +phases: + install: + commands: + # Upgrade AWS CLI to the latest version + - pip install --upgrade awscli + + # Install Ruby dependencies from the Gemfile. + - bundle install --deployment + + build: + commands: + # Run the tests using RSpec. + - rspec + + - aws cloudformation package --template template.yaml --s3-bucket $S3_BUCKET --output-template-file template-export.yaml +artifacts: + type: zip + files: + - template-export.yaml diff --git a/lambda.rb b/lambda.rb new file mode 100644 index 0000000..4c8a3b4 --- /dev/null +++ b/lambda.rb @@ -0,0 +1,49 @@ +require 'json' +require 'rack' + +$app ||= Rack::Builder.parse_file("#{File.dirname(__FILE__)}/app/config.ru").first + +def handler(event:, context:) + + p "Request received ..." + # enironment required by Rack (http://www.rubydoc.info/github/rack/rack/file/SPEC) + env = { + "REQUEST_METHOD" => event['httpMethod'], + "SCRIPT_NAME" => "", + "PATH_INFO" => event['path'] || "", + "QUERY_STRING" => event['queryStringParameters'] || "", + "SERVER_NAME" => "localhost", + "SERVER_PORT" => 443, + + "rack.version" => Rack::VERSION, + "rack.url_scheme" => "https", + "rack.input" => StringIO.new(event['body'] || ""), + "rack.errors" => $stderr, + } + + unless event['header'].nil? + event['headers'].each{ |key, value| env[key] = "HTTP_#{value}" } + end + + begin + status, headers, body = $app.call(env) + + body_content = "" + body.each do |item| + body_content += item.to_s + end + + response = { + "statusCode" => status, + "headers" => headers, + "body" => body_content + } + rescue Exception => msg + response = { + "statusCode" => 500, + "body" => msg + } + end + + response +end diff --git a/spec/server_spec.rb b/spec/server_spec.rb new file mode 100644 index 0000000..ded2675 --- /dev/null +++ b/spec/server_spec.rb @@ -0,0 +1,29 @@ +require_relative '../app/server.rb' +require 'rack/test' + +set :environment, :test + +# Tests for server.rb +describe 'HelloWorld Service' do + include Rack::Test::Methods + + def app + Sinatra::Application + end + + # Test for HTTP GET for URL-matching pattern '/' + it "should return successfully on GET" do + get '/hello-world' + expect(last_response).to be_ok + json_result = JSON.parse(last_response.body) + expect(json_result["Output"]).to eq("Hello World!") + end + + # Test for HTTP POST for URL-matching pattern '/' + it "should return successfully on POST" do + post '/hello-world' + expect(last_response).to be_ok + json_result = JSON.parse(last_response.body) + expect(json_result["Output"]).to eq("Hello World!") + end +end diff --git a/template.yaml b/template.yaml new file mode 100644 index 0000000..02ad833 --- /dev/null +++ b/template.yaml @@ -0,0 +1,74 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: 'AWS::Serverless-2016-10-31' + +Resources: + SinatraFunction: + Type: 'AWS::Serverless::Function' + Properties: + FunctionName: SinatraApp + Handler: lambda.handler + Runtime: ruby2.5 + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref FeedbackTable + CodeUri: "./" + MemorySize: 512 + Timeout: 30 + Events: + SinatraApi: + Type: Api + Properties: + Path: / + Method: ANY + RestApiId: !Ref SinatraAPI + SinatraAPI: + Type: AWS::Serverless::Api + Properties: + Name: SinatraAPI + StageName: Prod + DefinitionBody: + swagger: '2.0' + basePath: '/Prod' + info: + title: !Ref AWS::StackName + paths: + /{proxy+}: + x-amazon-apigateway-any-method: + responses: {} + x-amazon-apigateway-integration: + uri: + !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SinatraFunction.Arn}/invocations' + passthroughBehavior: "when_no_match" + httpMethod: POST + type: "aws_proxy" + /: + get: + responses: {} + x-amazon-apigateway-integration: + uri: + !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SinatraFunction.Arn}/invocations' + passthroughBehavior: "when_no_match" + httpMethod: POST + type: "aws_proxy" + ConfigLambdaPermission: + Type: "AWS::Lambda::Permission" + DependsOn: + - SinatraFunction + Properties: + Action: lambda:InvokeFunction + FunctionName: !Ref SinatraFunction + Principal: apigateway.amazonaws.com + FeedbackTable: + Type: AWS::Serverless::SimpleTable + Properties: + TableName: FeedbackServerlessSinatraTable + PrimaryKey: + Name: id + Type: String + ProvisionedThroughput: + ReadCapacityUnits: 5 + WriteCapacityUnits: 5 +Outputs: + SinatraAppUrl: + Description: App endpoint URL + Value: !Sub "https://${SinatraAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod/" \ No newline at end of file