こんにちは、SCSKの鈴木です。
本記事は「作ってみた」系の記事になります。
最近サーバーレス関連のAWSサービスを学んでいる初心者が、勉強の一環として作ったものを紹介します。
- 背景と作成物
- 用語
- 構成
- 作成手順
- LINE Botの準備
- SSM Parameter StoreにLINEアクセスキーを保存
- AWS CDKで構築したい方へ
- DynamoDBの準備
- 依存関係をLambda Layerとして保存
- 試験データ登録側のLambda関数を作成する
- LambdaからDynamoDB, SSMへのアクセスを許可する
- API Gatewayを設定し、LINE Messaging APIと連携する
- テスト実行してみる
- LINEにメッセージを送るLambda関数を作成する
- LambdaからDynamoDB, SSMへのアクセスを許可する
- テスト実行してみる
- EventBridgeでLambdaを定期実行し、毎日メッセージを送る
- 動作確認
- おまけ(自分の好きな名言などを投稿する)
背景と作成物
突然ですが、皆さんは受験生の頃、「入試まであと〇日!!」のような壁紙を目にしたことありますか?
毎日試験までの日数が減っていくことに焦りも感じ、当時の私はモチベーションを保つことができたので、同じようなものを資格勉強などでも作りたいと考えました。
そこで、モチベーションを保つために毎日自分を励まして(煽って)くれるLINE Botを作りました。作成物の操作イメージとしては次のような形です。
- LINEに試験名、試験日、受験する目的を入力する
- 毎日22時にLINEから試験まで残り〇日のメッセージと煽り文句が届く
用語
以下のAWSサービスは次のように省略して呼びます。
- AWS Lambda => Lambda
- Amazon DynamoDB => DynamoDB
- Amazon API Gateway => API Gateway
- Amazon EventBridge => EventBridge
- AWS Systems Manager => SSM
- AWS KMS => KMS
- AWS Cloud Development Kit => CDK
構成
作成手順
LINE Botの準備
実はLINE Botを作成するのも初めてでしたが、LINEのMessaging APIを使うことで簡単に実装できました。事前準備として、Messaging APIのドキュメント通りにチャネルを作成します。
次に、チャネルアクセストークンを発行するの通りに、チャネルアクセストークンを発行します。
SSM Parameter StoreにLINEアクセスキーを保存
Systems Manager のセキュリティのベストプラクティスに従って、機密データをSSM Parameter Storeにアップロードして、Lambdaから呼び出すようにします。
LambdaとLINEを連携させるために以下の情報を登録します。
- LINE_ACCESS_KEY:Messaging API設定 > チャネルアクセストークン
- LINE_USER_KEY:チャネル基本情報 > あなたのユーザーID
今回はどちらもデフォルトのKMSキーを指定しました。
AWS CDKで構築したい方へ
CDKを利用した構築は「AWS CDK編」の記事を参照ください。
- マネジメントコンソールから各サービスを作成 ← 「マネジメントコンソール編」
- AWS CDKを利用して作成 ← 「AWS CDK編」
「AWS CDK編」の記事はこちらになります。
DynamoDBの準備
テーブルを作成します。設定は基本デフォルトのまま利用しました。
依存関係をLambda Layerとして保存
Lambdaで外部モジュールを利用するために、ローカルでインストールしたnode_modulesをzip化してLayerとしてアップロードします。
LINE Botの実装にはLINE Messaging API SDK for nodejsを使います。
$ npm init $ npm install @line/bot-sdk $ npm install aws-sdk $ npm install dayjs
zipファイルで固めたら、Lambda Layerを作成します。
試験データ登録側のLambda関数を作成する
受験する試験をDynamoDBに登録するLambda関数を作成します。
Node16.Xのランタイムを利用しました。
今回は個人で利用するだけなので、入力形式も決め打ちの簡易コードです。エラーハンドリングなども行っていません。
「:」区切りで試験名、試験日、目的をLINEから入力されたものを受け付けて、保存します。(試験名:試験日:目的)
const AWS = require("aws-sdk");
const line = require('@line/bot-sdk');
const ssm = new AWS.SSM();
const dynamo = new AWS.DynamoDB.DocumentClient();
// SSM ParameterStoreから値を取得する
const getSSMParameter = async (name) => {
let result = await ssm.getParameter({
Name: name,
WithDecryption: true
}).promise();
return result.Parameter.Value;
};
// DynamoDBに登録する
const insert = async exams => {
await dynamo
.put({
TableName: "exam",
Item: {
test_name: exams[0],
test_day: exams[1],
purpose: exams[2]
}
})
.promise();
};
exports.handler = async (event, context, callback) => {
const body = JSON.parse(event.body).events[0];
const exams = body.message.text.split(":");
// SSMパラメータストアからLINEのアクセスキーを取得
let LINE_ACCESS_KEY = await getSSMParameter('LINE_ACCESS_KEY');
const client = new line.Client({
channelAccessToken: LINE_ACCESS_KEY
});
let message;
if (exams.length === 3) {
insert(exams); // DynamoDBに登録
message = '登録したよ';
} else {
message = '登録したい試験を「試験名:日付(YYYY-MM-DD):目的」の形式で入力してね!';
}
await client.replyMessage(body.replyToken, {
type: 'text',
text: message
});
};
さらに、先ほど作成したLambda Layerを適用します。
「コード」>「レイヤーの追加」>「カスタムレイヤー」から先ほど作成したものを選択し、「追加」します。
LambdaからDynamoDB, SSMへのアクセスを許可する
LambdaからSSM Parameter Store・DynamoDBにアクセスできるように、実行ロールに2つのポリシーをアタッチします。
- AmazonSSMReadOnlyAccess:SSMへのアクセスポリシー
- カスタマーインラインポリシー:DynamoDBへのアクセスポリシー(参考)
- dynamodb:PutItemが許可されていれば問題ないです。
API Gatewayを設定し、LINE Messaging APIと連携する
API GatewayでのHTTP APIの開発に従って、先程作成したLambdaに統合し、デプロイします。生成されたURLを「LINE Messaging API設定」の「Webhook URL」に登録します。
テスト実行してみる
デプロイが完了したら、LINEで「試験名:試験日(ハイフン区切り):目的」を送ってみます。 「登録したよ」と返ってきて、DynamoDBに保存されていれば登録側の処理は終了です。
LINEにメッセージを送るLambda関数を作成する
もうひとつ新しくLambda関数を作ります。
今回はこれから受験する試験のみ登録している前提で考えており、DynamoDBから全件取得しています。試験まで残り何日かを計算し、決め打ちで設定した一言を加えてLINEにメッセージ送信するようにしています。
const dayjs = require('dayjs');
const AWS = require("aws-sdk");
const line = require('@line/bot-sdk');
const dynamo = new AWS.DynamoDB.DocumentClient();
let params = {
TableName: 'exam',
};
const ssm = new AWS.SSM();
const generateText = (test_name, day, purpose) => {
const messages = [
'君の本気はそんなもんじゃないでしょ?',
'今日は何をしたんだい?内省してごらん?',
'間にあいそ?',
'明日は今日の倍がんばろうな',
`目的は ${purpose} って言ってたよね?達成できそう?`
];
return `${test_name}まで残り${day}日だよ!\\n`
+ messages[Math.floor(Math.random() * messages.length)];
};
// SSM ParameterStoreから値を取得する
const getSSMParameter = async (name) => {
let result = await ssm.getParameter({
Name: name,
WithDecryption: true
}).promise();
return result.Parameter.Value;
};
// 試験までの残り日数を計算
const calcRemainingDays = test_day => dayjs(test_day).diff(dayjs(), 'd');
const generateMessages = items => {
let messages = [];
items.forEach(function(exam, index) {
messages.push({
type: 'text',
text: generateText(exam.test_name,
calcRemainingDays(exam.test_day),
exam.purpose)
});
});
return messages;
};
exports.handler = async (event, context, callback) => {
const LINE_ACCESS_KEY = await getSSMParameter('LINE_ACCESS_KEY');
const USER_ID = await getSSMParameter('LINE_USER_ID');
const client = new line.Client({
channelAccessToken: LINE_ACCESS_KEY
});
// dynamoDBから取得
let examObj = await dynamo.scan(params).promise();
// メッセージ生成
let replyMessage = generateMessages(examObj.Items);
await client.pushMessage(USER_ID, replyMessage);
};
先ほど同様にLambda Layerを適用します。
「コード」>「レイヤーの追加」>「カスタムレイヤー」から先ほど作成したものを選択し、「追加」します。
LambdaからDynamoDB, SSMへのアクセスを許可する
先程同様にLambdaの実行ロールに以下ポリシーをアタッチします。
- AmazonSSMReadOnlyAccess:SSMへのアクセスポリシー
- AmazonDynamoDBReadOnlyAccess:DynamoDBへの読み取りアクセスポリシー
テスト実行してみる
Lambdaのテスト実行をしてみます。
LINEにメッセージが返ってくれば成功です。
EventBridgeでLambdaを定期実行し、毎日メッセージを送る
毎日煽ってもらうために、EventBridgeを利用してLambdaを定期実行します。
毎日22時にLambdaを呼び出し、LINE通知されるようにスケジューリングします。
日本標準時(JST)はUTCに比べて9時間早いため、それを考慮してCron式を登録します。
動作確認
22時にLINEから通知が来たら成功です。
おまけ(自分の好きな名言などを投稿する)
さらにやる気を上げるために、日々のメッセージに合わせて有名人の名言などを投稿させました。
※今回は著作権の関係でおまけについては非公開としますが、簡単に実装のみ紹介します。
DynamoDBにquoteテーブルを新規作成し、noを連番でパーティションキーとして登録し、名言を保存します。
「LINEにメッセージを送るLambda関数を作成する」のLambdaコードを追加することで実装可能です。
省略
// minからmaxまでのランダムな整数を返す
const getRandomNo = (min, max) =>
Math.floor( Math.random() * (max + 1 - min) ) + min;
// ランダムに名言を1件取得する
const getRandomQuote = async () => {
// quoteテーブルのitem数を取得
let itemCountObj = await dynamo.scan({
TableName: 'quote',
Select: "COUNT"}).promise();
return await dynamo.query({
TableName: 'quote',
KeyConditionExpression: '#no = :quote',
ExpressionAttributeNames: {
'#no': 'no'
},
ExpressionAttributeValues: {
':quote': getRandomNo(1, itemCountObj.Count)
}
}).promise();
};
exports.handler = async (event, context, callback) => {
省略
// dynamoDBから取得
let examObj = await dynamo.scan({ TableName: 'exam' }).promise();
let quoteObj = await getRandomQuote();
// メッセージ生成
let replyMessage = generateMessages(examObj.Items);
replyMessage.push({
type: 'text',
text: quoteObj.Items[0].quote
});
await client.pushMessage(USER_ID, replyMessage);
};
登録していた名言の中の1つがメッセージの最後に送られてきたら成功です。
おわりに(CDKを利用した作成)
本記事では手動でマネジメントコンソールを操作し、LINE Botを作成しました。
見て分かる通り、手順が多く大変です。記事も長くてわかりにくいですね。
ということでAWS CDKを使ってIaC化をしました。
ここまで読んでくださった方、ありがとうございました。
「AWS CDK編」も合わせてよろしくお願いします。