こんにちは、SCSKの鈴木です。
本記事では「試験日まで毎日励ますサーバーレスLINE Botを作ろう(マネジメントコンソール編)」で作った構成をAWS CDKで構築したいと思います。(以下「マネジメントコンソール編」と呼びます)
「マネジメントコンソール編」の記事は以下から参照ください。
AWS CDKとは?
以下の記事で概要について触れられているため、本記事では割愛します。
用語
以下、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
- AWS CloudFormation => CloudFormation
前提
構成図のみ再掲しておきます。
事前準備
先程紹介した「マネジメントコンソール編」の以下の内容が完了していることが前提となります。リンクを再掲します。
- LINE Botの準備
- SSM Parameter StoreにLINEアクセスキーを保存
※ この手順のみマネジメントコンソールまたはAWS CLIから保存します。
また、CDKを利用するために、以下の準備も必要です。
- AWS CLIがインストールされた環境 または AWS Cloud9の準備
- AWS CDKのインストール(Cloud9は初期にインストールされているため不要です)
環境の注意点
- 本記事ではcdk 2.24.1を利用します。(2022/5/17 現在最新バージョン)
- 本記事ではJavaScriptを利用しています。
※ TypeScriptが主流ですが、自信がないためJavaScriptを利用しました。ただ、TypeScriptを利用する場合も npm watch をすること以外はほぼ同様に実装可能だと思っています。
作成手順
プロジェクトの雛形作成
まずは、プロジェクトの雛形を作成します。空のディレクトリ上で以下を実行してください。
$ cdk init sample-app --language javascript
主要なファイルは以下ですが、主に触るのはlib/ディレクトリ名-stack.jsファイルになります。
- package.json
- aws-cdk-libにAWSをCDKで操作する主要なモジュールが含まれる
- bin/ディレクトリ名.js
- package.jsonから呼び出される。スタックのエントリーポイント
- lib/ディレクトリ名-stack.js
- CDKで起動するリソースを定義するファイル
lib/ディレクトリ名-stack.jsを編集
以下のように定義します。
const cdk = require('aws-cdk-lib'); const lambda = require('aws-cdk-lib/aws-lambda'); const apigw = require('aws-cdk-lib/aws-apigateway'); const dynamodb = require('aws-cdk-lib/aws-dynamodb'); const iam = require('aws-cdk-lib/aws-iam'); const events = require('aws-cdk-lib/aws-events'); const targets = require('aws-cdk-lib/aws-events-targets'); class CdkWorkshopStack extends cdk.Stack { /** * @param {Construct} scope * @param {string} id * @param {StackProps=} props */ constructor(scope, id, props) { super(scope, id, props); // DynamoDBの準備 const examTable = new dynamodb.Table(this, 'Exam', { partitionKey: { name: 'test_name', type: dynamodb.AttributeType.STRING }, tableName: 'Exam' }); // Lambda実行ロールにSSMへのアクセスを許可する const executionLambdaRole = new iam.Role(this, 'MyLambdaExecutionRole', { roleName: 'my-lambda-execution-role', assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( 'service-role/AWSLambdaBasicExecutionRole' ), iam.ManagedPolicy.fromAwsManagedPolicyName( 'AmazonSSMReadOnlyAccess' ) ] } ); // 依存関係をLambda Layerとして保存 const layer = new lambda.LayerVersion(this, 'MyLayer', { code: lambda.Code.fromAsset('lambda-layer'), compatibleRuntimes: [lambda.Runtime.NODEJS_16_X] }); // 試験データ登録側のLambda関数を作成する const register = new lambda.Function(this, 'RegisterHandler', { runtime: lambda.Runtime.NODEJS_16_X, code: lambda.Code.fromAsset('lambda-register'), handler: 'register.handler', role: executionLambdaRole, layers: [layer] }); // LINEにメッセージを送るLambda関数の定義 const pushMessage = new lambda.Function(this, 'PushMessageHandler', { runtime: lambda.Runtime.NODEJS_16_X, code: lambda.Code.fromAsset('lambda-push-msg'), handler: 'pushMessage.handler', role: executionLambdaRole, layers: [layer] }); // DynamoDBへの読み書き許可 examTable.grantReadWriteData(register); examTable.grantReadData(pushMessage); // API Gatewayの定義 new apigw.LambdaRestApi(this, 'Endpoit', { handler: register }); // EventBrigeでLambdaを定期実行 new events.Rule(this, 'ScheduleRule', { schedule: events.Schedule.cron({ minute: '0', hour: '13' }), targets: [new targets.LambdaFunction(pushMessage, {})] }); } } module.exports = { CdkWorkshopStack }
CDKのAPIリファレンスにはConstruct Library(L2 Construct)が用意されています。基本的にはリファレンスから実現したいものを探してきて、引っ張ってくるといった形で実現しました。
詳細はAWS Cloud Development Kit (CDK) v2を参照ください。
ディレクトリ階層の確認
今後の作業で新規フォルダとして作成し、Lambda関数コードを実装していきます。
ディレクトリ階層を間違えないように「linebot」というプロジェクトディレクトリでの階層を例に表します。※主要フォルダ/ファイルのみ載せています。
linebot ├── bin │ └── linebot.js ├── lambda-layer │ └── nodejs │ ├── node_modules │ ├── package-lock.json │ └── package.json ├── lambda-push-msg │ └── pushMessage.js ├── lambda-register │ └── register.js ├── lib │ └── linebot-stack.js ├── node_modules ├── cdk.json ├── package.json └── package-lock.json
依存関係をLambda Layerとして保存する
続いて、Lambdaの実装に入ります。「マネジメントコンソール編」同様、Lambda内で利用する外部モジュールをLambda Layerに登録します。
先程の「lib/ディレクトリ名.js」内では以下のように定義しました。
// 依存関係をLambda Layerとして保存 const layer = new lambda.LayerVersion(this, 'MyLayer', { code: lambda.Code.fromAsset('lambda-layer'), compatibleRuntimes: [lambda.Runtime.NODEJS_16_X] });
これは、lambda-layerフォルダ内をLambda Layerとして保存します。
そこで新しくlambda-layerフォルダを作成し、外部モジュールをインストールしておきます。
インストールするものは「マネジメントコンソール編」同様です。
$ mkdir -p lambda-layer/node && cd lambda-layer/node $ npm init $ npm install @line/bot-sdk $ npm install aws-sdk $ npm install dayjs
試験データ登録側のLambda関数を作成する
続いて、Lambda関数の中身を実装します。先ほど、「lib/ディレクトリ名.js」の中で以下のように定義しました。
// 試験データ登録側のLambda関数を作成する const register = new lambda.Function(this, 'RegisterHandler', { runtime: lambda.Runtime.NODEJS_16_X, code: lambda.Code.fromAsset('lambda-register'), handler: 'register.handler', role: executionLambdaRole, layers: [layer] });
ここでは、lambda-registerフォルダの中のregister.jsのhandlerを呼び出しています。
よって、新しくlambda-registerフォルダを追加し、register.jsを作成し、以下を追加します。
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 ParameterStoreから値を取得する 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 }); };
コードに中身については「マネジメントコンソール編」と同様です。(筆者の都合で、DynamoDBのテーブル名など、一部が変わっている部分はありますが。)
LINEにメッセージを送るLambda関数を作成する
次にLINEに通知する側のLambda関数を定義します。lib/の中では以下のように定義しました。
// LINEにメッセージを送るLambda関数の定義 const pushMessage = new lambda.Function(this, 'PushMessageHandler', { runtime: lambda.Runtime.NODEJS_16_X, code: lambda.Code.fromAsset('lambda-push-msg'), handler: 'pushMessage.handler', role: executionLambdaRole, layers: [layer] });
ここでは、lambda-push-msgフォルダの中のpushMessage.jsのhandlerを呼び出しています。よって、新しくlambda-push-msgフォルダを追加し、pushMessage.jsを作成し、以下を追加します。
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); };
これで、デプロイする準備は完了です。
デプロイ手順
あとはCDKでデプロイするために以下のコマンドを実行することでデプロイできます。
$ cdk synth $ cdk bootstrap $ cdk deploy
各コマンドの説明を簡単に記載します。
- cdk synth
- ここまでに作成したものから、CloudFormationテンプレートを作成する
- cdk bootstrap
- CDKの初期設定として、リージョンごと、言語ごとに1回実行が必要
- CloudFormationにCDKToolkitというスタックが生成される
- cdk deploy
- CloudFormation経由でリソースが作成される
デプロイ完了後、CloudFormationに遷移すると新しくスタックが作られており、定義した各サービスが起動していることが確認できます。
デプロイされたURLをLINE Messaging APIと連携する
デプロイが完了すると、コンソールにURLが表示されます。またはマネジメントコンソールでAPI GatewayからでもデプロイされたURLを確認できます。
確認したURLを「マネジメントコンソール編」の「API Gatewayを設定し、LINE Messaging APIと連携する」同様にLINE Messaging APIに設定します。
これで完了です。LINEから動作確認をしてみてください。
おわりに
AWS CDKを利用することで「マネジメントコンソール編」で紹介した多くの手順が、1つのスタックファイルとLambda Layer含め3つのLambda関数ファイルで定義できました。
L2 Constructで表現できる範囲であれば、簡単に記述ができ便利に感じました。
本記事を読みながら1から同じものを構築してみると、より便利さが伝わるかもしれません。
最後まで読んでいただき、ありがとうございました。
もし、「マネジメントコンソール編」を読まれていない方、是非合わせてご覧ください。