本記事で目差す構成
① Slackで特定のアクションを実行する。(※今回はスラッシュコマンド)
② API Gatewayを介してLambdaを起動。
③ Lambdaに配置した関数を実行し情報を返す。
↑動作イメージとしてはこんな感じ。
今回はとある地域の現在気温を返してくれるSlackBotを動かしてみる。
対象読者
- 簡単なSlackBotを作ってみたい人
- AWSのLambdaに触れてみたい人
Lambdaとは?
AWS Lambda はサーバーをプロビジョニングしたり管理する必要なくコードを実行できるコンピューティングサービスです。 AWS Lambda は必要時にのみてコードを実行し、1 日あたり数個のリクエストから 1 秒あたり数千のリクエストまで自動的にスケーリングします。使用したコンピューティング時間に対してのみお支払いいただきます- コードが実行中でなければ料金はかかりません。AWS Lambda では、管理を全く必要とせずに、任意のアプリケーションやバックエンドサービスで仮想的にコードを実行できます。AWS Lambda は、高度な可用性のコンピューティングインフラストラクチャでコードを実行し、サーバーとオペレーティングシステム、システムのメンテナンス、容量のプロビショニングと自動スケーリング、コードのモニタリングやログ記録など、コンピューティングリソースのすべての管理を実行します。必要な操作は、AWS Lambda がサポートするいずれかの言語でコードを指定するだけです。(引用: Amazon公式ドキュメント)
これだけだとイマイチわかりにくいが、要するに「プログラムを実行するためのサーバーがいらない」という事。
通常、何かしらのプログラムを実行しようと思った場合、サーバーを購入したり、各種ミドルウェアをインストールしたりと色々手間がかかるものだが、Lambdaにおいてそういったものは全てAWSが管理してくれるため、開発者はソースコードの作成にだけ力を注げば良くなるらしい。
Lambdaを使うメリット
- サーバーや各種ミドルウェアの管理が不要
- 上述の理由から。
- コスト削減
- リクエスト数やプログラムの実行時間によって課金される仕組みとなっており、待機時間には課金されないため、使用する局面によっては大幅なコストダウンが可能。(常に課金され続けるEC2とは対照的)
- オートスケーリング
- アクセス数や負荷に応じて自動的にサーバーの数を増減してくれる。
今回のようなSlackBotの場合、常に稼働させたいというよりは必要な時のみ動いてくれれば構わないため、Lambdaを利用するにはちょうど良いと思った。
SlackBotくらい軽い実装であればHerokuなどを使ったデプロイ方法も定番だが、今時のAWSを使ってみたい感がある。
仕様
言語: Ruby2.5
フレームワーク: Sinatra
インフラ: AWS Lambda
SlackBotを作成
まず、肝心のSlackBotを作成していく。
ディレクトリを作成
$ mkdir slack-bot-on-aws-lambda
$ cd slack-bot-on-aws-lambda
Rubyのバージョンを指定
# 2.5系なら何でもOK
$ rbenv local 2.5.1
Sinatraをインストール
$ bundle init
↑のコマンドでGemfileを生成し、以下のように編集する。
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem 'sinatra'
その後、Gemをインストール。
$ bundle install --path vendor/bundle
動作確認のため、とりあえず「Hello World!」と返すページを実装してみる。
$ touch main.rb
require 'sinatra'
get '/' do
'Hello World!'
end
その後、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
Sinatraはデフォルトだとポート番号「4567」で動くため、「localhost:4567」にアクセス。
「Hello World!」と表示されれば成功。
天気情報を返すプログラムを実装
今回作るSlackBotの主な機能である天気情報を返すプログラムを実装していく。
OpenWeatherのAPIキーを取得
上記サイトに会員登録し、APIキーを取得。
英語で書かれたサービスだが、ある程度は直感的に操作できるので詳しい説明は省略。どうしてもわからなかったらググればいくらでも記事が出てくるはず。
各種Gemをインストール
この先の処理を行う上で必要なGemがいくつかあるため、このタイミングで一気にインストールしておく。
gem 'faraday'
gem 'rack'
gem 'rack-contrib'
gem 'rubysl-base64'
gem 'slack-ruby-bot'
「bundle install」も忘れずに。
$ bundle install --path vendor/bundle
src/weather.rbを作成
$ mkdir src
$ touch src/weather.rb
src/weather.rbを作成し、次のように記述。
require 'json'
class Weather
def current_temp(locate)
end_point_url = 'http://api.openweathermap.org/data/2.5/weather'
api_key = # 先ほど取得したOpenWeatherのAPIキー
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 "現在の練馬の気温は#{celsius_round.to_s}℃です。"
end
end
main.rbを編集
require 'slack-ruby-client'
require 'sinatra'
require './src/weather'
Slack.configure do |conf|
conf.token = # SlackBotのトークン
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'
# スラッシュコマンド「/nerima」が実行された場合に以下の処理が走る。
weather = Weather.new
client.chat_postMessage channel: channel_id, text: weather.current_temp('Nerima'), as_user: true
# 'Nerima'の部分は各自変更してOK。「Shinjuku」に変えれば新宿の気温を返すはず。
end
return
end
SlackBotのトークンを取得する方法については次の記事を参照。
参照: ワークスペースで利用するボットの作成
参照: API トークンの生成と再生成
実際に動作確認
SlackBotをスラッシュコマンドで呼び出すためにいくつか設定しなければならない事がある。
https://api.slack.com/apps/
↑のURLにアクセスし、該当のBotを選択。
左サイドメニューに「Slash Commands」という項目があるので選択し、「Create New Command」をクリック。
各項目を入力していく。
- Command: 任意のスラッシュコマンド。
- 今回は東京度練馬区の現在気温を返す事を想定しているので「/nerima」としているが、たとえば新宿区であれば「/shinjuku」とかでもOK。)
- Request URL: スラッシュコマンドを実行した際にリクエストしたいURL。
- 「localhost」では動かないため、今回はngrokを使って独自のドメインを割り当てている。
- 参照: ngrokの利用方法
- 「bundle exec ruby main.rb」でSinatraを起動した後、別のターミナルで「ngrok http 4567」と叩いて表示されたURLを使用する。
- postメソッドでリクエストしたいので、「https://********.ngrok.io/webhook」と入力する。
- Short Description: スラッシュコマンドの簡単な説明。
入力が完了したら右下の「Save」をクリック。
スラッシュコマンドの作成が終わったら、SlackBotを追加したチャンネルで「/nerima」と打ち込んでみる。上手くいけば画像のようにSlackBotから返答が来る。(設定で画像や名前を変えたりする事も可能。)
何か不具合があった場合はターミナルにログが出力されているはずなので、適宜デバッグ。
AWS Lambdaにデプロイ
正常に動作確認できたら、いよいよAWS Lambdaで本番稼働させる。
AWS CLIをインストール
今回は「AWS CLI」と呼ばれるツールを使いながらデプロイしていくので、まだインストールできてないない場合はインストールしておく。
$ brew install awscli
IAMユーザーを作成
デプロイ作業を行うためのIAMユーザーを作成していく。
まずは「IAM」→「ポリシー」→「ポリシーの作成」へと進み、JSONタブから以下の文を貼り付ける。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"apigateway:*",
"cloudformation:*",
"dynamodb:*",
"events:*",
"iam:*",
"lambda:*",
"logs:*",
"route53:*",
"s3:*"
],
"Resource": [
"*"
]
}
]
}
適当にポリシー名や説明を記述し、「ポリシーの作成」をクリック。
次に「IAM」→「ユーザー」→「ユーザーの作成」へと進み、適当な名前を付けた後「プログラムによるアクセス」にチェックを入れて次へ進む。
「既存のポリシーを直接アタッチ」から先ほど作成した「MinimalDeployIAMPolicy」を選択し、次へ進む。
(タグは任意でOK)最後に確認画面が表示されるので、問題無ければ「ユーザーの作成」をクリック。
すると「アクセスキーID」と「シークレットアクセスキー」の2つが発行されるので、csvファイルをダウンロードするなりメモするなり大事に保管しておく。
AWS CLIの設定
$ aws configure
AWS Access Key ID # 先ほど作成したアクセスキーID
AWS Secret Access Key # 先ほど作成したシークレットアクセスキー
Default region name # ap-northeast-1
Default output format # json
ターミナルで「aws configure」と打ち込むと対話形式で色々聞かれるので、それぞれ必要な情報を入力していく。
各種ファイルを作成
AWS CLIの設定が終わったら、デプロイに必要な各種ファイルの作成を行う。
- config.ru
- lambda.rb
- template.yaml
require 'rack'
require 'rack/contrib'
require_relative './main'
set :root, File.dirname(__FILE__)
run Sinatra::Application
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
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/"
それぞれが何を表しているのかについてここでは割愛。(この辺はawsの公式ドキュメントをほぼそのまま使っているため)
参照: aws-samples/serverless-sinatra-sample
S3バケットの作成
事前にAWS S3バケットを準備しておく必要があるため、適当にS3バケットを作成。
参照: AWS S3のバケットの作り方
デプロイ
次のコマンドを実行。
$ aws cloudformation package \
--template-file template.yaml \
--output-template-file serverless-output.yaml \
--s3-bucket # 先ほど作成したS3バケット名
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/ユーザー名/ディレクトリ名/serverless-output.yaml --stack-name <YOUR STACK NAME>
すると、ディレクトリ内に「serverless-output.yaml」というファイルが自動生成されているはずなので、こちらを元に次のコマンドを実行。
$ 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
「Successfully」と表示されればデプロイ成功。
「Lambda」→「関数」と進むと先ほどデプロイした内容が表示されるので、「API Gateway」内に記載されているAPIエンドポイントにアクセス。
get '/' do
'This is SlackBot on AWS Lambda'
end
main.rb内のget '/' リクエストの期待通り「This is SlackBot on AWS Lambda」が返ってくれば正常に動作していると判断してOK。
Slash CommandsのRequest URLを変更
再びSlackBotの設定ページにアクセスし、「Slash Commands」からRequest URLを先ほど作成されたエンドポイントに変更する。(https://********.execute-api.ap-northeast-1.amazonaws.com/Prod/webhook」)
最後にもう一度、Slackチャンネルで「/nerima」と打ち込み、ちゃんとレスポンスが返ってくればめでたしめでたし。
もし上手く行かなかった場合はCloudWatchにログが出力されているはずなので、適宜デバッグ。
あとがき
お疲れ様でした!
今回、簡単なSlackBotをLambdaで動かすというテーマでAWS Lambdaに触れてみました。
大した機能は実装できていないのでLamdaの素晴らしさを全て実感というわけにはいきませんでしたが、上手く使えばかなり便利なサービスだと素人ながら感じています。
難しい操作はしていないため、基本的には手順通りに進めていけば動くはずですが、もし詰まるところがあった場合はコメント欄などで指摘していただけると嬉しいです。