はじめに
前回、AWS SAMのサンプルを実行してたので、今回はそれを利用してline worksのオウム返しbotを構築した備忘録になります
前回の記事はコチラ
初期設定
まずは、環境の新規構築を行います
$ sam init
$ 省略
Project name [sam-app]: oumu
$ 省略
$ cd oumu
$ mv hello_world/ oumu/
テンプレート改修
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
oumu
Sample SAM Template for oumu
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3
MemorySize: 128
Resources:
- HelloWorldFunction:
+ OumuFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
- CodeUri: hello_world/
+ CodeUri: oumu/
Handler: app.lambda_handler
Runtime: ruby3.2
Architectures:
- x86_64
Events:
- HelloWorld:
+ OumuApi:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
- Path: /hello
+ Path: /oumu
- Method: get
+ Method: post
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
- HelloWorldApi:
+ OumuApi:
- Description: "API Gateway endpoint URL for Prod stage for Hello World function"
+ Description: "API Gateway endpoint URL for Prod stage for oumu function"
- Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
+ Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/oumu/"
- HelloWorldFunction:
+ OumuFunction:
- Description: "Hello World Lambda Function ARN"
+ Description: "oumu Lambda Function ARN"
- Value: !GetAtt HelloWorldFunction.Arn
+ Value: !GetAtt OumuFunction.Arn
- HelloWorldFunctionIamRole:
+ OumuFunctionIamRole:
- Description: "Implicit IAM Role created for Hello World function"
+ Description: "Implicit IAM Role created for oumu function"
- Value: !GetAtt HelloWorldFunctionRole.Arn
+ Value: !GetAtt OumuFunctionRole.Arn
デプロイ
正常に動くか動作検証がてらにデプロイを実施します
$ sam build
$ sam deploy --guided
省略
Successfully created/updated stack - oumu in us-west-2
動作確認
$ curl -X POST 'デプロイされたエンドポイント'
{"message":"Hello World!"}
これで、postのAPIが正常に動いてるのが分かります
エンドポイントは次でも利用するのでメモしといてください
line worksの準備
こちらの記事に詳細が書いてあるので、こちらを参考にしてください(botの発行などは割愛)
bot作成時にコールバックを設定
こちらに、samでデプロイ時に出力されたエンドポイントを登録してください
開発開始
$ sam sync --watch --profile hogehoge
省略
Stack update succeeded. Sync infra completed.
CodeTrigger not created as CodeUri or DefinitionUri is missing for ServerlessRestApi.
Infra sync completed.
※開発時は同期してると楽なので、ここでは同期コマンド「sam sync」を利用しています。
準備が整ったので、ここから本格的に開発になります。
まず、ログが出力できるように修正します
# require 'httparty'
require 'json'
require 'logger'
def logger
@logger ||= Logger.new($stdout, level: Logger::Severity::DEBUG)
end
def lambda_handler(event:, context:)
logger.debug(event)
logger.debug(context)
{ statusCode: 200 }
end
line works botからlambdaにpostが届いているか確認します
cloudWatchで確認すると、ログが出力されていることが分かります
line works bot api
source 'https://rubygems.org'
gem 'httparty'
ruby '~> 3.2.0'
gem 'jwt'
line works bot の api を別ファイルで作成します。
CLIENT_ID、CLIENT_SECRET、SERVICE_ACCOUNT_ID、PRIVATE_KEY、BOT_IDは自分が発行したものに変更してから利用してください。
require 'jwt'
CLIENT_ID = 'hogehoge'
CLIENT_SECRET = 'hogehoge'
SERVICE_ACCOUNT_ID = 'hogehoge'
PRIVATE_KEY = "hogehoge"
SCOPE = 'bot'
BOT_ID = '1'
def jwt
current_time = Time.now.to_i
rsa_private = OpenSSL::PKey::RSA.new(PRIVATE_KEY)
payload = { iss: CLIENT_ID, sub: SERVICE_ACCOUNT_ID, iat: current_time, exp: current_time + 3600 }
JWT.encode(payload, rsa_private, 'RS256') # 暗号化
end
def create_access_token
url = 'https://auth.worksmobile.com/oauth2/v2.0/token'
headers = { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }
params = {
assertion: jwt, grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
client_id: CLIENT_ID, client_secret: CLIENT_SECRET, scope: SCOPE
}
response = Net::HTTP.post(URI.parse(url), URI.encode_www_form(params), headers)
JSON.parse(response.body)
end
def lw_api_headers_params
access_token = create_access_token
{
'Content-type': 'application/json',
Authorization: "#{access_token['token_type']} #{access_token['access_token']}"
}
end
def lw_api_message_params(message)
{
content: {
type: 'text',
text: message.to_s
}
}
end
def bot_send_message(user_id, message)
url = "https://www.worksapis.com/v1.0/bots/#{BOT_ID}/users/#{user_id}/messages"
params = lw_api_message_params(message)
response = Net::HTTP.post(URI.parse(url), params.to_json, lw_api_headers_params)
response.code
end
上記で作ったものを呼び出し、lambda_handlerに設定します
# require 'httparty'
require 'json'
require 'logger'
require './line_works_send'
def logger
@logger ||= Logger.new($stdout, level: Logger::Severity::DEBUG)
end
def lambda_handler(event:, context:)
line_post = JSON.parse(event['body'])
logger.debug(line_post)
user_id = line_post.dig('source', 'userId')
logger.debug(user_id)
message = line_post.dig('content', 'text')
logger.debug(message)
s_code = bot_send_message(user_id, message)
logger.debug(s_code)
{ statusCode: s_code }
end
動作検証
Bot に話しかけると、オウム返しされます。
署名検証
公式ページを見ると、セキュリティの観点から署名検証をするように記載がありますので、追加します
公式に記載されているBot Secretと比較するために、lambdaに渡す必要があるので、テンプレートを修正します
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
oumu
Sample SAM Template for oumu
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
Function:
Timeout: 3
MemorySize: 128
+ Parameters:
+ LineWorksBotSecret:
+ Type: String
+ Description: Line Works Bot Secret
Resources:
OumuFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: oumu/
Handler: app.lambda_handler
Runtime: ruby3.2
+ Environment:
+ Variables:
+ LINE_WORKS_BOT_SECRET: !Ref LineWorksBotSecret
Architectures:
- x86_64
Events:
OumuApi:
Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
Properties:
Path: /oumu
Method: post
Outputs:
# ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
# Find out more about other implicit resources you can reference within SAM
# https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
OumuApi:
Description: "API Gateway endpoint URL for Prod stage for oumu function"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/oumu/"
OumuFunction:
Description: "oumu Lambda Function ARN"
Value: !GetAtt OumuFunction.Arn
OumuFunctionIamRole:
Description: "Implicit IAM Role created for oumu function"
Value: !GetAtt OumuFunctionRole.Arn
反映させます
$ sam build
$ sam deploy --guided
すると、デプロイ時にパラメータを聞かれますので、bot画面のbot secretを入力します
# require 'httparty'
require 'json'
require 'logger'
require './line_works_send'
# require 'rack'
require 'openssl'
require 'base64'
def logger
@logger ||= Logger.new($stdout, level: Logger::Severity::DEBUG)
end
+ def verify_signature(event)
+ hmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), ENV.fetch('LINE_WORKS_BOT_SECRET'), event['body'])
+ hash2 = Base64.strict_encode64(hmac)
+ hash2 != event.dig('headers', 'x-works-signature')
+ end
def lambda_handler(event:, context:)
+ return { statusCode: 200 } if verify_signature(event) # 違う場合は、何もせずに200を返す
request_body = JSON.parse(event['body'])
user_id = request_body.dig('source', 'userId')
message = request_body.dig('content', 'text')
s_code = user_send_message(user_id, message)
{ statusCode: s_code }
end
これで完成となります
再デプロイを行いますのでエンドポイントも変わります。bot側の変更も必要なのでご注意ください
さいごに
今回は簡単なオウムbotを作成しました。
lambda側でopenAIなどと繋げば色々な応用が作れそうとは思っています。
が、もう少し学習が必要だと感じていますので、少しずつ何かを作っていこうと考えております。
拙いですが、最後までご拝読頂けますと幸いです。