#はじめに
30代未経験からエンジニア転職をめざすコーディング初学者のYNと申します。お読みいただきありがとうございます。
前に作ったLINEのオウム返しbotを少しだけ発展させた内容でbotを作りましたので学習ログとして投稿させていただきました。
#Bot内容
下記のように、ユーザーが送ったタンパク質摂取量を記録できるbotをつくりました。
↓一応友達登録もできます。(思いついたタイミングで勝手に消去する可能性があります。)
#やったこと
Serverless Flameworkを使って、ローカルPC上でサーバレスのAWSのバックエンドを構築した後デプロイし、LINEのbotをつくりました。
イメージはこんな感じです。
#手順
- 事前準備
-
Serverless Flamework
の初期設定 - DynamoDBでデータベースをつくる
- API-GatewayでLambda関数を公開する
- Lambda関数を記述する
- Serverless Frameworkを使ってローカル環境で動作確認する
#事前準備
###LINEデベロッパー登録
こちらを参照ください。
###Serverless Flameworkのインストール
- ローカルPCにグローバルインストール
$ npm i -g serverless
- 動作確認
$ sls -v
(sls
と serverless
のどちらのコマンドでも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をいじったことがある方は、すでに設定されているかもしれません。その場合は上書きすることが出来ます。)
まずはIAMコンソールでアクセスキーを作成します。
(このアクセスキーは慎重に扱い、くれぐれもGithubなどにupなさらぬよう。。)
次に、クレデンシャル情報を設定します。
$ serverless config credentials --provider aws --key aws_access_key_id --secret aws_secret_access_key
これで、クレデンシャル情報の設定は完了です。
クレデンシャル情報を変更する必要がない限り、この設定は初回のみで大丈夫です。
とりあえず試しにデプロイ
serverless.yml
にデプロイ先のリージョンの設定を追記すればとりあえずデプロイすることができます。
# 抜粋
provider:
name: aws
runtime: nodejs12.x
region: 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全体の設定についてです。
# AWS周りの設定
provider:
name: aws
runtime: nodejs12.x
region: us-east-2
stage: dev
environment: #環境変数をここに定義できます。今回は「DYNAMODB_TABLE」という環境変数を定義しています。
DYNAMODB_TABLE: ${self:service}-${self:provider.stage} #「self」とは、このファイルに記載されているクラスそのものです。
iamRoleStatements: #ここでは、Lambda関数にDynamoDBへのアクセス権限を与えています。
- Effect: Allow
Action:
- 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
の設定をします。
# DynamoDBの設定
resources:
Resources:
Protein:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.DYNAMODB_TABLE}-protein
#作成するデーブル名です。どんな名前でもいいのですが、「line-bot-dev-protein」としています。
AttributeDefinitions:
- AttributeName: userId
AttributeType: S
- AttributeName: sentAt
AttributeType: N
KeySchema:
- AttributeName: userId
KeyType: HASH
- AttributeName: sentAt
KeyType: RANGE
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
-
まずは適当なテーブル名を付けます。
今回は「line-bot-dev-protein」としました。 -
AttributeDefinitions
とKeySchema
は非常に大事です。
主キー(HASH)とソートキー(RANGE)を組み合わせて、一意に定まるように設定します。それ以外の記録情報(今回はタンパク質摂取量)は定義する必要はありません。ここら辺がSQLとの大きな違いですね。
テーブルをデプロイする
追記したserverless.yml
をデプロイするとテーブルが作成されているのが分かります。
$ sls deploy
API-GatewayでLambda関数を公開する
今回、Lambdaで実装したい関数をlogProtein
として、
- LINEでユーザーからメッセージ(タンパク質摂取量)が送られてくる
- DynamoDBのデーブルからユーザーの情報(24時間以内のタンパク質摂取量)をよみとる
- ユーザーにメッセージを返す
という内容としますが、Lambda関数を記述する前に、API-Gatewayを使ってLambda関数を使って外部から関数を呼び出せるようにします。
serverless.yml
のLambda設定部分を記述します。
# Lambdaの設定
functions:
logProtein:
handler: handler.logProtein # handler.jsにlogProteinという言う関数を定義してLambdaにデプロイします。
environment: # Lambda関数で有効な環境変数を定義。handler.jsで参照する。
TableName: ${self:provider.environment.DYNAMODB_TABLE}-protein
events:
- http:
path: protein #アクセスするときのPath
method: post #HTTPメソッドを指定
logProtein
の概形を記述します。
"use strict";
module.exports.logProtein = async (event, context) => {
};
この状態でデプロイすると、
$ sls deploy
API-Gatewayにdev-line-bot
というAPIが作成されており、/protein
というエンドポイントにlogProtein
というLambda関数と統合されていることが分かります。
#Lambda関数を記述する
前に作ったLINEのオウム返しbotを発展させて、
- LINEでユーザーからメッセージ(タンパク質摂取量)が送られてくる
- DynamoDBのデーブルからユーザーの情報(24時間以内のタンパク質摂取量)をよみとる
- ユーザーにメッセージを返す
という内容を記述します。
前回からの変更部分にコメントを加えていきます。
"use strict";
const line = require("@line/bot-sdk");
const crypto = require("crypto");
const client = new line.Client({
channelAccessToken: process.env.ACCESSTOKEN,
});
const AWS = require("aws-sdk");
// AWSのSDKをインポート
const dynamo = new AWS.DynamoDB.DocumentClient();
// DynamoDBと接続
const TableName = process.env.TableName;
// 接続先のテーブル名を設定。ymlファイルに記述された環境変数を参照している。
module.exports.logProtein = async (event, context) => {
const body = JSON.parse(event.body);
const signature = crypto
.createHmac("sha256", process.env.CHANNELSECRET)
.update(event.body)
.digest("base64");
const checkHeader = (event.headers || {})["X-Line-Signature"];
if (checkHeader === signature) {
if (body.events[0].replyToken === "00000000000000000000000000000000") {
let lambdaResponse = {
statusCode: 200,
headers: { "X-Line-Status": "OK" },
body: '{"result":"connect check"}',
};
context.succeed(lambdaResponse);
} else {
try {
const userId = body.events[0].source.userId;
const sentAt = body.events[0].timestamp;
const protein = Number(body.events[0].message.text);
// ユーザーからのリクエストに含まれるWebhookイベントオブジェクトの情報をよみこむ。
// https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objectsを参照
const yesterday = sentAt - 24 * 3600 * 1000;
// リクエストが送られてきた24時間前のタイムスタンプを指定。
const putParams = {
TableName,
Item: {
userId,
sentAt,
protein,
},
};
const putResult = await dynamo.put(putParams).promise();
// リクエストに含まれるタンパク質摂取量をDynamoDBに書き込む。詳細下記。
const queryParams = {
TableName,
ExpressionAttributeValues: { ":y": yesterday, ":u": userId },
KeyConditionExpression: "userId = :u and sentAt > :y",
};
const result = await dynamo.query(queryParams).promise();
const totalProtein = result.Items.map((item) => item.protein).reduce(
(a, b) => a + b
);
// ユーザーが24時間以内に摂取したタンパク質の量ををDynamoDBから読み込む。詳細下記。
const message1 = {
type: "text",
text: `この24時間で${totalProtein}gのタンパク質を摂取したぞ`,
};
const message2 =
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));
} catch (error) {
return {
statusCode: error.statusCode,
body: error.message,
};
}
}
} else {
console.log("署名認証エラー");
}
};
-
リクエストに含まれるタンパク質摂取量をDynamoDBに書き込む
DynamoDB
にデータを1件書き込むためには、put
メソッドを使います。
詳細は公式ドキュメントを参照ください。 -
ユーザーが24時間以内に摂取したタンパク質の量ををDynamoDBから読み込む
今回は、「条件に合致するデータすべて」を取得するために、query
メソッドを使っています。
今回の条件とは、「①任意のユーザーの」「②24時間以内の」すべてのデータとなるため、下記のようにパラメータを設定しました。
詳細は公式ドキュメントを参照ください。
const queryParams = {
TableName,
ExpressionAttributeValues: { ":y": yesterday, ":u": userId },
KeyConditionExpression: "userId = :u and sentAt > :y",
};
最後に、LINEのMessaging API
で使っているアクセストークンとチャンネルシークレットをymlファイルに追記します。
(Githubにあげる場合は、ymlファイルに書かずにLambdaコンソールで直接入力するのがいいと思います。)
provider:
name: aws
runtime: nodejs12.x
region: us-east-2
stage: dev
environment:
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
plugins:
- serverless-dynamodb-local
- serverless-offline
###動作確認用データのseedファイルを作成
ルートディレクトリに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インポート部分を修正します。
custom:
serverless-offline:
httpPort: 8083 # http://localhost:8083にAPIのエンドポイントを設定する
dynamodb:
stages: dev
start:
port: 8082 # http://localhost:8082でデータベースと接続する
inMemory: true
migrate: true
seed: true
seed:
protein:
sources: # データベースと接続し、seedファイルのデータを書き込む
- table: ${self:provider.environment.DYNAMODB_TABLE}-protein
sources: [./seeds/protein.json]
const options = process.env.LOCAL
? { region: "localhost", endpoint: "http://localhost:8082" }
: {};
// 環境変数LOCALがtrueの場合は、ローカルにデータベースを作成してhttp://localhost:8082で接続する
const dynamo = new AWS.DynamoDB.DocumentClient(options);
その後、下記コマンドによりローカルでAPIの動作確認をすることができます。
(環境変数LOCAL
にtrueを代入したのち、ローカル環境を構築します。)
$ LOCAL=true sls offline start
今回、AWSのバックエンドの動作確認をローカルですることができますが、LINEのWebhookの動作確認をオフラインですることが出来ないので、handler.js
を少し書き換えます。
"use strict";
const line = require("@line/bot-sdk");
const crypto = require("crypto");
const client = new line.Client({
channelAccessToken: process.env.ACCESSTOKEN,
channelSecret: process.env.CHANNELSECRET,
});
const options = process.env.LOCAL
? { region: "localhost", endpoint: "http://localhost:8082" }
: {};
const AWS = require("aws-sdk");
const dynamo = new AWS.DynamoDB.DocumentClient(options);
const TableName = process.env.TableName;
module.exports.logProtein = async (event, context) => {
const body = JSON.parse(event.body);
const signature = crypto
.createHmac("sha256", process.env.CHANNELSECRET)
.update(event.body)
.digest("base64");
const checkHeader = (event.headers || {})["X-Line-Signature"];
// if (checkHeader === signature) {
if (true) { // cryptの検証をしない
if (body.events[0].replyToken === "00000000000000000000000000000000") {
let lambdaResponse = {
statusCode: 200,
headers: { "X-Line-Status": "OK" },
body: '{"result":"connect check"}',
};
context.succeed(lambdaResponse);
} else {
try {
const userId = body.events[0].source.userId;
const sentAt = body.events[0].timestamp;
const protein = Number(body.events[0].message.text);
const yesterday = sentAt - 24 * 3600 * 1000;
const queryParams = {
TableName,
ExpressionAttributeValues: { ":y": yesterday, ":u": userId },
KeyConditionExpression: "userId = :u and sentAt > :y",
};
const putParams = {
TableName,
Item: {
userId,
sentAt,
protein,
},
};
const putResult = await dynamo.put(putParams).promise();
const result = await dynamo.query(queryParams).promise();
const totalProtein = result.Items.map((item) => item.protein).reduce(
(a, b) => a + b
);
const message1 = {
type: "text",
text: `この24時間で${totalProtein}gのタンパク質を摂取したぞ`,
};
const message2 =
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));
// ユーザへの返信の代わりにレスポンスを返す
let lambdaResponse = {
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
にリクエストを送ればローカルで動作確認することができます。
最後に
ずいぶん長くなってしまいましたが、初学者にもやさしいServerless Flamework
を使ったAWSバックエンド開発の素晴らしさを伝えたかったです。
お読みいただきありがとうございました。
参考にさせていただいた記事
参考、というか下記の内容をそのままコピーした感じになってしまいました。
初学者にも分かりやすくまとめて頂いています。感謝です。