Try running SlackBot made with Ruby x Sinatra on AWS Lambda

Configuration to aim for in this article

Untitled Diagram(1).png

① Perform a specific action in Slack. (* This time, the slash command) (2) Start Lambda via API Gateway. ③ Execute the function placed in Lambda and return the information.

slackbot(2).gif

↑ It looks like this as an operation image. This time, I will move SlackBot, which returns the current temperature in a certain area.

Target audience

--People who want to make a simple SlackBot --People who want to touch AWS Lambda

What is Lambda?

AWS Lambda is a computing service that allows you to execute code without having to provision or manage servers. AWS Lambda runs your code only when you need it, and automatically scales from a few requests per day to thousands of requests per second. You only pay for the compute time you use-no charges unless the code is running. AWS Lambda allows you to virtually execute code in any application or back-end service with no management required. AWS Lambda runs code on a highly available computing infrastructure and manages all of your computing resources, including server and operating systems, system maintenance, capacity provisioning and autoscaling, code monitoring and logging. To execute. All you have to do is specify the code in one of the languages AWS Lambda supports. (Quote: Amazon official document)

It's hard to understand if it's just this, but in short, it means that you don't need a server to execute the program.

Normally, if you want to run some program, it takes a lot of trouble to purchase a server and install various middleware, but in Lambda, AWS manages all of that, so developers It seems that you only have to focus on creating the source code.

Benefits of using Lambda

--No need to manage servers and various middleware --For the reasons mentioned above.

In the case of a SlackBot like this one, I think it's just right to use Lambda because it doesn't have to be always up and running only when needed.

If the implementation is as light as SlackBot, the deployment method using Heroku etc. is standard, but I feel like using AWS nowadays.

specification

Language: Ruby2.5 Framework: Sinatra Infrastructure: AWS Lambda

Completed form: slack-bot-on-aws-lambda

Create SlackBot

First, create the essential SlackBot.

Create directory

$ mkdir slack-bot-on-aws-lambda
$ cd slack-bot-on-aws-lambda

Specify Ruby version

# 2.Any 5 series is OK
$ rbenv local 2.5.1 

Install Sinatra

$ bundle init

Create a Gemfile with the ↑ command and edit it as follows.

./Gemfile


# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'sinatra'

Then install Gem.

$ bundle install --path vendor/bundle

To check the operation, I will implement a page that returns "Hello World!" For the time being.

$ touch main.rb

ruby:./main.rb


require 'sinatra'

get '/' do
  'Hello World!'
end

After that, start Sinatra.

$ bundle exec ruby main.rb

[2020-09-21 20:47:35] INFO  WEBrick 1.4.2
[2020-09-21 20:47:35] INFO  ruby 2.5.1 (2018-03-29) [x86_64-darwin19]
== Sinatra (v2.1.0) has taken the stage on 4567 for development with backup from WEBrick
[2020-09-21 20:47:35] INFO  WEBrick::HTTPServer#start: pid=50418 port=4567

By default, Sinatra works on port number "4567", so access "[localhost: 4567](localhost: 4567)".

スクリーンショット 2020-09-21 20.50.33.png

Success if "Hello World!" Is displayed.

Implemented a program that returns weather information

We will implement a program that returns weather information, which is the main function of the SlackBot created this time.

Get OpenWeather API key

https://openweathermap.org/ スクリーンショット 2020-09-21 20.54.33.png

Register as a member on the above site and get an API key.

スクリーンショット 2020-09-21 20.59.28_censored.jpg

Although the service is written in English, detailed explanation is omitted because it can be operated intuitively to some extent. If you really don't understand, you can get as many articles as you want by google.

Install various gems

Since there are some gems required for future processing, install them at once at this timing.

./Gemfile


gem 'faraday'
gem 'rack'
gem 'rack-contrib'
gem 'rubysl-base64'
gem 'slack-ruby-bot'

Don't forget to "bundle install".

$ bundle install --path vendor/bundle

Create src / weather.rb

$ mkdir src
$ touch src/weather.rb

Create src / weather.rb and write as follows.

ruby:./src/weather.rb


require 'json'

class Weather
  def current_temp(locate)
    end_point_url = 'http://api.openweathermap.org/data/2.5/weather'
    api_key = #The OpenWeather API key you got earlier

    res = Faraday.get(end_point_url + "?q=#{locate},jp&APPID=#{api_key}")
    res_body = JSON.parse(res.body)

    temp = res_body['main']['temp']
    celsius = temp - 273.15
    celsius_round = celsius.round

    return "The current temperature in Nerima#{celsius_round.to_s}It is ℃."
  end
end

Edit main.rb

ruby:./main.rb


require 'slack-ruby-client'
require 'sinatra'
require './src/weather'

Slack.configure do |conf|
  conf.token = #SlackBot token
end

get '/' do
  'This is SlackBot on AWS Lambda'
end

post '/webhook' do
  client = Slack::Web::Client.new

  channel_id = params['channel_id']
  command = params['command']

  case command
    when '/nerima'
    #Slash command "/When "nerima" is executed, the following processing is executed.
      weather = Weather.new
      client.chat_postMessage channel: channel_id, text: weather.current_temp('Nerima'), as_user: true
      # 'Nerima'You can change the part of. If you change to "Shinjuku", the temperature in Shinjuku should be returned.
  end

  return
end

See the following article for how to get a SlackBot token.

See also: Creating a bot for your workspace (https://slack.com/intl/ja-jp/help/articles/115005265703-%E3%83%AF%E3%83%BC%E3%82% AF% E3% 82% B9% E3% 83% 9A% E3% 83% BC% E3% 82% B9% E3% 81% A7% E5% 88% A9% E7% 94% A8% E3% 81% 99% E3% 82% 8B% E3% 83% 9C% E3% 83% 83% E3% 83% 88% E3% 81% AE% E4% BD% 9C% E6% 88% 90) See also: API Token Generation and Regeneration (https://slack.com/intl/ja-jp/help/articles/215770388-API-%E3%83%88%E3%83%BC%E3%82 % AF% E3% 83% B3% E3% 81% AE% E7% 94% 9F% E6% 88% 90% E3% 81% A8% E5% 86% 8D% E7% 94% 9F% E6% 88% 90 )

Actual operation check

There are a few things you need to do to call SlackBot with the slash command.

https://api.slack.com/apps/ スクリーンショット 2020-09-21 21.33.08.png Access the URL above and select the relevant bot.

スクリーンショット 2020-09-21 21.37.35.png There is an item called "Slash Commands" in the left side menu, so select it and click "Create New Command". スクリーンショット 2020-09-21 21.40.08.png Enter each item.

--Command: Any slash command. --This time, "/ nerima" is used because it is assumed that the current temperature in Nerima-ku, Tokyo will be returned, but in Shinjuku-ku, for example, "/ shinjuku" is OK. ) --Request URL: The URL you want to request when you execute the slash command. --Since it doesn't work on "localhost", this time I'm using ngrok to assign my own domain. --Reference: How to use ngrok --After starting Sinatra with "bundle exec ruby main.rb", tap "ngrok http 4567" in another terminal and use the displayed URL. --I want to make a request with the post method, so enter "https: //********. Ngrok.io/webhook". --Short Description: A brief description of the slash command.

Click "Save" at the bottom right when the input is completed.

スクリーンショット 2020-09-21 21.53.38.png After creating the slash command, try typing "/ nerima" on the channel where you added the SlackBot. Hopefully you will get a response from SlackBot like the image. (It is also possible to change the image and name in the settings.) スクリーンショット 2020-09-21 22.04.20.png If something goes wrong, the log should be output to the terminal, so debug as appropriate.

Deploy to AWS Lambda

After confirming the operation normally, it is finally time to put it into production on AWS Lambda.

Install AWS CLI

This time, we will deploy using a tool called "AWS CLI", so if you haven't installed it yet, install it.

$ brew install awscli

Create an IAM user

Create an IAM user to perform the deployment work. スクリーンショット 2020-09-21 22.11.08.png First, go to "IAM"-> "Policy"-> "Create Policy" and paste the following statement from the JSON tab. スクリーンショット 2020-09-21 22.13.26.png

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "apigateway:*",
                "cloudformation:*",
                "dynamodb:*",
                "events:*",
                "iam:*",
                "lambda:*",
                "logs:*",
                "route53:*",
                "s3:*"
             ],
            "Resource": [
                "*"
            ]
        }
    ]
}

See also: Minimal Deploy IAM Policy (https://rubyonjets.com/docs/extras/minimal-deploy-iam/)

スクリーンショット 2020-09-21 22.15.47.png Enter the policy name and description as appropriate, and click "Create Policy".

スクリーンショット 2020-09-21 22.17.27.png Next, go to "IAM"-> "User"-> "Create User", give it an appropriate name, check "Access by Program", and proceed to the next.

スクリーンショット 2020-09-21 22.17.47.png Select the "Minimal Deploy IAM Policy" created earlier from "Attach existing policy directly" and proceed to the next.

スクリーンショット 2020-09-21 22.17.59.png

(Tags are optional) A confirmation screen will be displayed at the end, so if there are no problems, click "Create User".

スクリーンショット 2020-09-21 22.18.13_censored.jpg Then, two "access key ID" and "secret access key" will be issued, so keep it in a safe place as you download the csv file or make a note of it.

AWS CLI settings

$ aws configure

AWS Access Key ID #The access key ID created earlier
AWS Secret Access Key #The secret access key you created earlier
Default region name # ap-northeast-1 
Default output format # json 

When you type "aws configure" in the terminal, you will be asked various things interactively, so enter the necessary information for each.

Create various files

After setting the AWS CLI, create various files required for deployment.

ruby:./config.ru


require 'rack'
require 'rack/contrib'
require_relative './main'

set :root, File.dirname(__FILE__)

run Sinatra::Application

ruby:./lambda.rb


require 'json'
require 'rack'
require 'base64'

$app ||= Rack::Builder.parse_file("#{__dir__}/config.ru").first
ENV['RACK_ENV'] ||= 'production'

def handler(event:, context:)
  body = if event['isBase64Encoded']
    Base64.decode64 event['body']
  else
    event['body']
  end || ''

  headers = event.fetch 'headers', {}

  env = {
    'REQUEST_METHOD' => event.fetch('httpMethod'),
    'SCRIPT_NAME' => '',
    'PATH_INFO' => event.fetch('path', ''),
    'QUERY_STRING' => Rack::Utils.build_query(event['queryStringParameters'] || {}),
    'SERVER_NAME' => headers.fetch('Host', 'localhost'),
    'SERVER_PORT' => headers.fetch('X-Forwarded-Port', 443).to_s,

    'rack.version' => Rack::VERSION,
    'rack.url_scheme' => headers.fetch('CloudFront-Forwarded-Proto') { headers.fetch('X-Forwarded-Proto', 'https') },
    'rack.input' => StringIO.new(body),
    'rack.errors' => $stderr,
  }

  headers.each_pair do |key, value|
    name = key.upcase.gsub '-', '_'
    header = case name
      when 'CONTENT_TYPE', 'CONTENT_LENGTH'
        name
      else
        "HTTP_#{name}"
    end
    env[header] = value.to_s
  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
    }
    if event['requestContext'].has_key?('elb')
      response['isBase64Encoded'] = false
    end
  rescue Exception => exception
    response = {
      'statusCode' => 500,
      'body' => exception.message
    }
  end

  response
end

template.yaml


AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Resources:
  SinatraFunction:
    Type: 'AWS::Serverless::Function'
    Properties:
      FunctionName: SlackBot
      Handler: lambda.handler
      Runtime: ruby2.5
      CodeUri: './'
      MemorySize: 512
      Timeout: 30
      Events:
        SinatraApi:
            Type: Api
            Properties:
                Path: /
                Method: ANY
                RestApiId: !Ref SinatraAPI
  SinatraAPI:
    Type: AWS::Serverless::Api
    Properties:
      Name: SlackBotAPI
      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
Outputs:
  SinatraAppUrl:
    Description: App endpoint URL
    Value: !Sub "https://${SinatraAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod/"

I will omit here what each represents. (Because this area uses the official AWS documentation almost as it is) See also: aws-samples / serverless-sinatra-sample

Creating an S3 bucket

Since it is necessary to prepare the AWS S3 bucket in advance, create an S3 bucket appropriately.

See: How to make a bucket for AWS S3

Deploy

Execute the following command.

$ aws cloudformation package \
     --template-file template.yaml \
     --output-template-file serverless-output.yaml \
     --s3-bucket #The S3 bucket name you created earlier

Uploading to a3a55f6abf5f21a2e1161442e53b27a8  12970487 / 12970487.0  (100.00%)
Successfully packaged artifacts and wrote output template to file serverless-output.yaml.
Execute the following command to deploy the packaged template
aws cloudformation deploy --template-file /Users/username/Directory name/serverless-output.yaml --stack-name <YOUR STACK NAME>

Then, a file called "serverless-output.yaml" should be automatically generated in the directory, so execute the following command based on this.

$ aws cloudformation deploy --template-file serverless-output.yaml \
     --stack-name slack-bot \
     --capabilities CAPABILITY_IAM

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - slack-bot

If "Successfully" is displayed, the deployment is successful.

スクリーンショット 2020-09-21 22.49.32_censored.jpg

If you go to "Lambda"-> "Function", the contents deployed earlier will be displayed, so access the API endpoint described in "API Gateway".

スクリーンショット 2020-09-21 22.53.09.png

ruby:./main.rb


get '/' do
  'This is SlackBot on AWS Lambda'
end

If "This is SlackBot on AWS Lambda" is returned as expected in the get'/' request in main.rb, it is OK to judge that it is working normally.

Change Request URL for Slash Commands

https://api.slack.com/apps/

スクリーンショット 2020-09-21 23.00.35_censored.jpg Go to the SlackBot settings page again and change the Request URL from "Slash Commands" to the endpoint you just created. (Https: //********.execute-api.ap-northeast-1.amazonaws.com/Prod/webhook ")

スクリーンショット 2020-09-21 23.06.41.png Finally, I typed "/ nerima" on the Slack channel again, and I was happy if the response came back properly.

スクリーンショット 2020-09-21 23.07.45.png If it doesn't work, the log should be output to CloudWatch, so debug it appropriately.

Afterword

Thank you for your hard work!

This time, I touched AWS Lambda with the theme of running a simple SlackBot with Lambda. I couldn't really realize all the greatness of Lamda because I couldn't implement a lot of functions, but I feel that it is a very convenient service if used well.

I haven't done any difficult operations, so basically it should work if you follow the procedure, but if there is something stuck, I'd appreciate it if you could point it out in the comments.

Recommended Posts

Try running SlackBot made with Ruby x Sinatra on AWS Lambda
Try running Word2vec model on AWS Lambda
Try running an app made with Quarkus on Heroku
Create a periodical program with Ruby x AWS Lambda x CloudWatch Events
Try running MPLS-VPN with FR Routing on Docker
Introducing Rspec with Ruby on Rails x Docker
Publish the app made with ruby on rails
Try running OSPF with FR Routing on Docker
I made a portfolio with Ruby On Rails
Try running the Embulk command with your Lambda function
Notify Slack of AWS bills daily with Lambda for Ruby
Regularly post imaged tweets on Twitter with AWS Lambda + Java
Create a SlackBot with AWS lambda & API Gateway in Java
Build AWS Lambda with Quarkus
When I try to use the AWS SDK with Ruby + Lambda, `sam local` is messed up.
With [AWS] CodeStar, you can build a Spring (Java) project running on Lambda in just 3 minutes! !!
Install gem in Serverless Framework and AWS Lambda with Ruby environment
Try AWS Lambda Runtime Interface Emulator with Docker Desktop for M1
Install Ruby on MSYS2 with pacman
[Ruby on Rails] Read try (: [],: key)
Try running cloudera manager with docker
Is Java on AWS Lambda slow?
Programming with ruby (on the way)
Hello World on AWS Lambda + Java
Try running Spring Boot on Kubernetes
Try running AWS X-Ray in Java
Install ruby on Ubuntu 20.04 with rbenv
Run C binaries on AWS Lambda
How to build a Ruby on Rails development environment with Docker (Rails 6.x)
Getting Started with Micronaut 2.x ~ Native Build and Deploy to AWS Lambda ~
How to build a Ruby on Rails development environment with Docker (Rails 5.x)