試験日まで毎日励ますサーバーレスLINE Botを作ろう(マネジメントコンソール編)

こんにちは、SCSKの鈴木です。

本記事は「作ってみた」系の記事になります。
最近サーバーレス関連のAWSサービスを学んでいる初心者が、勉強の一環として作ったものを紹介します。

背景と作成物

突然ですが、皆さんは受験生の頃、「入試まであと〇日!!」のような壁紙を目にしたことありますか?

毎日試験までの日数が減っていくことに焦りも感じ、当時の私はモチベーションを保つことができたので、同じようなものを資格勉強などでも作りたいと考えました。

そこで、モチベーションを保つために毎日自分を励まして(煽って)くれるLINE Botを作りました。作成物の操作イメージとしては次のような形です。

  1. LINEに試験名、試験日、受験する目的を入力する
  2. 毎日22時にLINEから試験まで残り〇日のメッセージと煽り文句が届く

demo画像

用語

以下の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

構成

今回作成したアーキテクチャ図は以下のようなものです。architecture

作成手順

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

SSM Parameter Store

今回はどちらもデフォルトのKMSキーを指定しました。

SSM Parameter Store Setting

AWS CDKで構築したい方へ

ここからは2通りの方法で作成しました。本記事は1の方法を紹介します。
CDKを利用した構築は「AWS CDK編」の記事を参照ください。                     

  1. マネジメントコンソールから各サービスを作成 ← 「マネジメントコンソール編」
  2. AWS CDKを利用して作成 ← 「AWS CDK編」

「AWS CDK編」の記事はこちらになります。

DynamoDBの準備

テーブルを作成します。設定は基本デフォルトのまま利用しました。

DynamoDB Setting

依存関係を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 Layer Setting

試験データ登録側の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」に登録します。

2022年4月のアップデートでLambda Function URLsが利用できるようになりました。これにより、今回のような簡易実装であればAPI Gatewayを利用せずに実装することも可能です。
API GatewayのAPIタイプの選択として、個人用途なのでREST APIではなく、低レイテンシ・低コストで実現できるHTTP APIを選択しました。

Webhook URL Setting

テスト実行してみる

デプロイが完了したら、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を定期実行します。

EventBridge Setting

毎日22時にLambdaを呼び出し、LINE通知されるようにスケジューリングします。
日本標準時(JST)はUTCに比べて9時間早いため、それを考慮してCron式を登録します。

EventBridge Cron

EventBridge Target

動作確認

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編」も合わせてよろしくお願いします。

タイトルとURLをコピーしました