Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9050

【今日から始めるAWS】Serverless Frameworkを使ってLINEのbotをつくる

$
0
0

はじめに

30代未経験からエンジニア転職をめざすコーディング初学者のYNと申します。お読みいただきありがとうございます。
前に作ったLINEのオウム返しbotを少しだけ発展させた内容でbotを作りましたので学習ログとして投稿させていただきました。

Bot内容

下記のように、ユーザーが送ったタンパク質摂取量を記録できるbotをつくりました。
↓一応友達登録もできます。(思いついたタイミングで勝手に消去する可能性があります。)
スクリーンショット 2020-08-14 20.27.13.png

スクリーンショット 2020-08-14 19.17.13.png

やったこと

Serverless Flameworkを使って、ローカルPC上でサーバレスのAWSのバックエンドを構築した後デプロイし、LINEのbotをつくりました。
イメージはこんな感じです。
スクリーンショット 2020-08-14 21.36.24.png

手順

  • 事前準備
  • Serverless Flameworkの初期設定
  • DynamoDBでデータベースをつくる
  • API-GatewayでLambda関数を公開する
  • Lambda関数を記述する
  • Serverless Frameworkを使ってローカル環境で動作確認する

事前準備

LINEデベロッパー登録

こちらを参照ください。

Serverless Flameworkのインストール

  • ローカルPCにグローバルインストール
$ npm i -g serverless
  • 動作確認
$ sls -v

(slsserverlessのどちらのコマンドでもOKです)

Serverless Flameworkの初期設定

雛形の作成

今回はnode.jsの雛形を作成します。

$ mkdir line-bot
$ cd line-bot
$ serverless create --template aws-nodejs

下記2つのファイルが作成されます。

  • handler.js => Lambdaで実行する関数の中身を記述します。
  • serverless.yml => AWSで構築するバックエンドの設定を記述します。

クレデンシャル情報の設定

初めてServerless Flameworkを使うとき、まず最初にAWSのクレデンシャル情報を設定する必要があります。
具体的には、(おそらくホームディレクトリにある).aws/credentialsというファイルの中身を設定します。
(以前AWS-CLIをいじったことがある方は、すでに設定されているかもしれません。その場合は上書きすることが出来ます。)
スクリーンショット 2020-08-15 10.04.14.png

まずはIAMコンソールでアクセスキーを作成します。
このアクセスキーは慎重に扱い、くれぐれもGithubなどにupなさらぬよう。。
スクリーンショット 2020-08-15 9.46.07.png

次に、クレデンシャル情報を設定します。

$ serverless config credentials --provider aws --key aws_access_key_id --secret aws_secret_access_key

これで、クレデンシャル情報の設定は完了です。
クレデンシャル情報を変更する必要がない限り、この設定は初回のみで大丈夫です。

とりあえず試しにデプロイ

serverless.ymlにデプロイ先のリージョンの設定を追記すればとりあえずデプロイすることができます。

serverless.yml
# 抜粋provider:name:awsruntime:nodejs12.xregion:us-east-2#ここにデプロイ先のリージョンを指定します。(今回はオハイオを選択) 

これで、下記デプロイコマンドを打てばデプロイできます。(簡単!)

$ sls deploy

この状態では、handler.jsに記載されたLambda関数のみがデプロイされた状態になります。
Severless Flameworkを使えば面倒なコンソール処理をしなくてもコマンド一発でAWSバックエンドをデプロイできます。これはserverless.ymlファイルに記載された設定をよみこみ、Cloud FormationというAWSのサービスを使って環境を構築しているということのようです。

DynamoDBでデータベースをつくる

そもそも、なぜDynamoDB?

ユーザー情報を管理するためにはデータベースが必要です。個人的にはSQLの方がユーザーデータを管理しやすいと思っているのですが、LambdaとRDSは相性が悪いという噂を聞いたので、今回はnoSQLであるDynamoDBを使います。

DynamoDBの設定

serverless.ymlファイルに設定を追記してDynamoDBの設定を行います。

まずは、AWS全体の設定についてです。

serverless.yml
# AWS周りの設定provider:name:awsruntime:nodejs12.xregion:us-east-2 stage:devenvironment:#環境変数をここに定義できます。今回は「DYNAMODB_TABLE」という環境変数を定義しています。DYNAMODB_TABLE:${self:service}-${self:provider.stage}#「self」とは、このファイルに記載されているクラスそのものです。iamRoleStatements:#ここでは、Lambda関数にDynamoDBへのアクセス権限を与えています。-Effect:AllowAction:-dynamodb:Query#条件検索-dynamodb:Scan#全件取得-dynamodb:GetItem#一件取得-dynamodb:PutItem#一件登録-dynamodb:UpdateItem#修正-dynamodb:DeleteItem#削除Resource:"arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}*"

そして、DynamoDBの設定をします。

serverless.yml
# DynamoDBの設定resources:Resources:Protein:Type:AWS::DynamoDB::TableProperties:TableName:${self:provider.environment.DYNAMODB_TABLE}-protein#作成するデーブル名です。どんな名前でもいいのですが、「line-bot-dev-protein」としています。AttributeDefinitions:-AttributeName:userIdAttributeType:S-AttributeName:sentAtAttributeType:NKeySchema:-AttributeName:userIdKeyType:HASH-AttributeName:sentAtKeyType:RANGEProvisionedThroughput:ReadCapacityUnits:1WriteCapacityUnits:1
  • まずは適当なテーブル名を付けます。
    今回は「line-bot-dev-protein」としました。

  • AttributeDefinitionsKeySchemaは非常に大事です。
    主キー(HASH)とソートキー(RANGE)を組み合わせて、一意に定まるように設定します。それ以外の記録情報(今回はタンパク質摂取量)は定義する必要はありません。ここら辺がSQLとの大きな違いですね。
    スクリーンショット 2020-08-15 11.59.52.png

テーブルをデプロイする

追記したserverless.ymlをデプロイするとテーブルが作成されているのが分かります。

$ sls deploy

スクリーンショット 2020-08-15 12.06.11.png

API-GatewayでLambda関数を公開する

今回、Lambdaで実装したい関数をlogProteinとして、

  1. LINEでユーザーからメッセージ(タンパク質摂取量)が送られてくる
  2. DynamoDBのデーブルからユーザーの情報(24時間以内のタンパク質摂取量)をよみとる
  3. ユーザーにメッセージを返す

という内容としますが、Lambda関数を記述する前に、API-Gatewayを使ってLambda関数を使って外部から関数を呼び出せるようにします。

serverless.ymlのLambda設定部分を記述します。

serverless.yml
# Lambdaの設定functions:logProtein:handler:handler.logProtein# handler.jsにlogProteinという言う関数を定義してLambdaにデプロイします。environment:# Lambda関数で有効な環境変数を定義。handler.jsで参照する。TableName:${self:provider.environment.DYNAMODB_TABLE}-proteinevents:-http:path:protein#アクセスするときのPathmethod:post#HTTPメソッドを指定

logProteinの概形を記述します。

handler.js
"use strict";module.exports.logProtein=async(event,context)=>{};

この状態でデプロイすると、

$ sls deploy

API-Gatewayにdev-line-botというAPIが作成されており、/proteinというエンドポイントにlogProteinというLambda関数と統合されていることが分かります。
スクリーンショット 2020-08-15 13.27.38.png

Lambda関数を記述する

前に作ったLINEのオウム返しbotを発展させて、
1. LINEでユーザーからメッセージ(タンパク質摂取量)が送られてくる
2. DynamoDBのデーブルからユーザーの情報(24時間以内のタンパク質摂取量)をよみとる
3. ユーザーにメッセージを返す
という内容を記述します。

前回からの変更部分にコメントを加えていきます。

handler.js
"use strict";constline=require("@line/bot-sdk");constcrypto=require("crypto");constclient=newline.Client({channelAccessToken:process.env.ACCESSTOKEN,});constAWS=require("aws-sdk");// AWSのSDKをインポートconstdynamo=newAWS.DynamoDB.DocumentClient();// DynamoDBと接続constTableName=process.env.TableName;// 接続先のテーブル名を設定。ymlファイルに記述された環境変数を参照している。module.exports.logProtein=async(event,context)=>{constbody=JSON.parse(event.body);constsignature=crypto.createHmac("sha256",process.env.CHANNELSECRET).update(event.body).digest("base64");constcheckHeader=(event.headers||{})["X-Line-Signature"];if(checkHeader===signature){if(body.events[0].replyToken==="00000000000000000000000000000000"){letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:'{"result":"connect check"}',};context.succeed(lambdaResponse);}else{try{constuserId=body.events[0].source.userId;constsentAt=body.events[0].timestamp;constprotein=Number(body.events[0].message.text);// ユーザーからのリクエストに含まれるWebhookイベントオブジェクトの情報をよみこむ。// https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objectsを参照constyesterday=sentAt-24*3600*1000;// リクエストが送られてきた24時間前のタイムスタンプを指定。constputParams={TableName,Item:{userId,sentAt,protein,},};constputResult=awaitdynamo.put(putParams).promise();// リクエストに含まれるタンパク質摂取量をDynamoDBに書き込む。詳細下記。constqueryParams={TableName,ExpressionAttributeValues:{":y":yesterday,":u":userId},KeyConditionExpression:"userId = :u and sentAt > :y",};constresult=awaitdynamo.query(queryParams).promise();consttotalProtein=result.Items.map((item)=>item.protein).reduce((a,b)=>a+b);// ユーザーが24時間以内に摂取したタンパク質の量ををDynamoDBから読み込む。詳細下記。constmessage1={type:"text",text:`この24時間で${totalProtein}gのタンパク質を摂取したぞ`,};constmessage2=totalProtein<100?{type:"text",text:`引き続き、高タンパク/低脂質/低糖質の食事を心掛けろ`,}:{type:"text",text:`タンパク質の摂りすぎも禁物だ。過剰摂取は腸内環境を悪化させる危険があるぞ`,};returnclient.replyMessage(body.events[0].replyToken,[message1,message2])//ユーザーにメッセージを返信する.then((response)=>{letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:'{"result":"completed"}',};context.succeed(lambdaResponse);}).catch((err)=>console.log(err));}catch(error){return{statusCode:error.statusCode,body:error.message,};}}}else{console.log("署名認証エラー");}};
  • リクエストに含まれるタンパク質摂取量をDynamoDBに書き込む
    DynamoDBにデータを1件書き込むためには、putメソッドを使います。
    詳細は公式ドキュメントを参照ください。

  • ユーザーが24時間以内に摂取したタンパク質の量ををDynamoDBから読み込む
    今回は、「条件に合致するデータすべて」を取得するために、queryメソッドを使っています。
    今回の条件とは、「①任意のユーザーの」「②24時間以内の」すべてのデータとなるため、下記のようにパラメータを設定しました。
    詳細は公式ドキュメントを参照ください。

constqueryParams={TableName,ExpressionAttributeValues:{":y":yesterday,":u":userId},KeyConditionExpression:"userId = :u and sentAt > :y",};

最後に、LINEのMessaging APIで使っているアクセストークンとチャンネルシークレットをymlファイルに追記します。
(Githubにあげる場合は、ymlファイルに書かずにLambdaコンソールで直接入力するのがいいと思います。)

serverless.yml
provider:name:awsruntime:nodejs12.xregion:us-east-2stage:devenvironment:DYNAMODB_TABLE:${self:service}-${self:provider.stage}ACCESSTOKEN:your-access-token#ここにアクセストークンを記述CHANNELSECRET:your-channel-secret#ここにチャンネルシークレットを記述

これで、デプロイしてLINEのWebhookのurlを設定すれば完成です。

Serverless Frameworkを使ってローカル環境で動作確認する

アプリ自体は、上記手順の後に、デプロイしてLINEのWebhookのurlを設定すれば完成なのですが、Serverless Flameworkの素晴らしい点は、なんと言ってもローカルで動作確認できる点です。
以下のように、AWSバックエンドでAPI-Gateway/Lambda/DynamoDBの動作をローカルで確認することができます。

ローカル開発用のライブラリをインストール

ライブラリをインストールし、プラグインの設定serverless.ymlに追記します。

$ yarn add -D serverless-dynamodb-local serverless-offline
serverless.yml
plugins:-serverless-dynamodb-local-serverless-offline

動作確認用データのseedファイルを作成

ルートディレクトリにseedsフォルダを作成してprotein.jsonを作成します。

seeds/protein.json
[{"userId":"Hanako","sentAt":1597126641204,"protein":50},{"userId":"Taro","sentAt":1597126631204,"protein":60},{"userId":"Jiro","sentAt":1597126241204,"protein":70}]

ローカル開発のための設定

下記の設定をserverless.ymlに追記し、handler.jsのSDKインポート部分を修正します。

serverless.yml
custom:serverless-offline:httpPort:8083# http://localhost:8083にAPIのエンドポイントを設定するdynamodb:stages:devstart:port:8082# http://localhost:8082でデータベースと接続するinMemory:truemigrate:trueseed:trueseed:protein:sources:# データベースと接続し、seedファイルのデータを書き込む-table:${self:provider.environment.DYNAMODB_TABLE}-proteinsources:[./seeds/protein.json]
handler.js
constoptions=process.env.LOCAL?{region:"localhost",endpoint:"http://localhost:8082"}:{};// 環境変数LOCALがtrueの場合は、ローカルにデータベースを作成してhttp://localhost:8082で接続するconstdynamo=newAWS.DynamoDB.DocumentClient(options);

その後、下記コマンドによりローカルでAPIの動作確認をすることができます。
(環境変数LOCALにtrueを代入したのち、ローカル環境を構築します。)

$ LOCAL=true sls offline start

今回、AWSのバックエンドの動作確認をローカルですることができますが、LINEのWebhookの動作確認をオフラインですることが出来ないので、handler.jsを少し書き換えます。

handler.js
"use strict";constline=require("@line/bot-sdk");constcrypto=require("crypto");constclient=newline.Client({channelAccessToken:process.env.ACCESSTOKEN,channelSecret:process.env.CHANNELSECRET,});constoptions=process.env.LOCAL?{region:"localhost",endpoint:"http://localhost:8082"}:{};constAWS=require("aws-sdk");constdynamo=newAWS.DynamoDB.DocumentClient(options);constTableName=process.env.TableName;module.exports.logProtein=async(event,context)=>{constbody=JSON.parse(event.body);constsignature=crypto.createHmac("sha256",process.env.CHANNELSECRET).update(event.body).digest("base64");constcheckHeader=(event.headers||{})["X-Line-Signature"];// if (checkHeader === signature) {if(true){// cryptの検証をしないif(body.events[0].replyToken==="00000000000000000000000000000000"){letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:'{"result":"connect check"}',};context.succeed(lambdaResponse);}else{try{constuserId=body.events[0].source.userId;constsentAt=body.events[0].timestamp;constprotein=Number(body.events[0].message.text);constyesterday=sentAt-24*3600*1000;constqueryParams={TableName,ExpressionAttributeValues:{":y":yesterday,":u":userId},KeyConditionExpression:"userId = :u and sentAt > :y",};constputParams={TableName,Item:{userId,sentAt,protein,},};constputResult=awaitdynamo.put(putParams).promise();constresult=awaitdynamo.query(queryParams).promise();consttotalProtein=result.Items.map((item)=>item.protein).reduce((a,b)=>a+b);constmessage1={type:"text",text:`この24時間で${totalProtein}gのタンパク質を摂取したぞ`,};constmessage2=totalProtein<100?{type:"text",text:`引き続き、高タンパク/低脂質/低糖質の食事を心掛けろ`,}:{type:"text",text:`タンパク質の摂りすぎも禁物だ。過剰摂取は腸内環境を悪化させる危険があるぞ`,};// ユーザーへの返信をしない// return client//   .replyMessage(body.events[0].replyToken, [message1, message2])//   .then((response) => {//     let lambdaResponse = {//       statusCode: 200,//       headers: { "X-Line-Status": "OK" },//       body: '{"result":"completed"}',//     };//     context.succeed(lambdaResponse);//   })//   .catch((err) => console.log(err));// ユーザへの返信の代わりにレスポンスを返す letlambdaResponse={statusCode:200,headers:{"X-Line-Status":"OK"},body:JSON.stringify([message1,message2]),};context.succeed(lambdaResponse);}catch(error){return{statusCode:error.statusCode,body:error.message,};}}}else{console.log("署名認証エラー");}};

エンドポイントにリクエストを送って動作確認

postmanなどを使ってhttp://localhost:8083/dev/proteinにリクエストを送ればローカルで動作確認することができます。
スクリーンショット 2020-08-15 15.22.06.png

最後に

ずいぶん長くなってしまいましたが、初学者にもやさしいServerless Flameworkを使ったAWSバックエンド開発の素晴らしさを伝えたかったです。
お読みいただきありがとうございました。

参考にさせていただいた記事

参考、というか下記の内容をそのままコピーした感じになってしまいました。
初学者にも分かりやすくまとめて頂いています。感謝です。


Viewing all articles
Browse latest Browse all 9050

Latest Images

Trending Articles