はじめに
この記事はLINE BotとAWS CDKのハンズオン資料のために作成しました。このハンズオンでは事前に以下のものの準備が必要になります。
-
AWSアカウント
- アクセスキーIDとシークレットアクセスキーが必要になります。一応この記事にも取得方法を説明します。
-
Gitpodのアカウント
- GitHubのアカウントと連携することで登録できます。
-
GitpodのChrome拡張(推奨)
- GitHubのソースコードを1クリックでオンラインIDEにコピーすることができます。
- Chrome拡張を使わない場合は
gitpod.io#
をURLの先頭に付与することでIDEを開くことができます。
-
LINE Developersのアカウント
- LINE Bot作成に必要となります。
以上で本ハンズオンのための準備は完了です。ここからはGitpodを用いたWEBのIDEでの作業となりますので、ソフトウェアのインストールなどの作業は不要です。
今回つくるもの
このような買い物リストBotを作成します。具体的には以下の機能を実装します。
- 入力した単語を「買い物リスト」に追加する
- 「リスト確認」とメッセージを送ると今まで追加した「買い物リスト」の中身を確認することができる
- 「リストをクリア」とメッセージを送ると今まで今まで追加した「買い物リスト」の中身を削除することができる
今回このLINE Botの開発にはAWSというクラウドサービスを用います。AWSには様々なサービスが提供されていますが、今回用いるサービスは以下です。
-
Lambda
- バックエンド(今回はLINE Bot)の開発に用います
-
API Gateway
- LambdaをURLから呼び出すために用います
-
DynamoDB
- 「買い物リスト」の保存に用います
AWS構成は以下のような感じです。
構成そのものとしてはLambdaとAPI Gateway、DynamoDBというよくあるサーバーレスの構成となっています。今回はLambdaをLINE Botとしての会話のやりとりとDynamoDBからのデータ操作の2つの機能に分けてLambda Invokeで呼び出す形で実装します。また、各Lambdaそれぞれが共通で用いるutil関数とnpmパッケージをLambda Layerで管理するようにします。
CDKとは(ざっくり)
AWS-CDKとは、プログラミング言語を使ってAWSのクラウド環境を構築ためのオープンソースのフレームワークです。AWS-CDKを用いることでメインコンソールを使わずに各自が使い慣れた言語(今回はTypeScript)・エディタを用いたAWS開発を行うことができます。
以前にまとめた記事も見てくれると嬉しいです。
今回は以下の用語を抑えてもらえるとAWS-CDKを問題なく使えるかと思います。
-
CloudFormation
- AWSの構築をオートメーション化させるための設定ファイルをかくためのツールです。設定ファイルはJSONかYAMLを使って書くことができます。CDKはこのCloudFormationをプログラミング言語で書くためのツールとなってます。
- スタック
- リソースの集まり(つまりはCDKで書いたAWSインフラの構成そのもの)を言います。CDKで書いたコードはスタックという単位で管理されます。
- デプロイ
- ここではスタックを実際にAWSの実環境に構築することを意味します。
ハンズオン手順
Gitpodを立ち上げる
今回ハンズオンで用いるGitHubのレポジトリのページを開きます。GitpodのChrome拡張をインストールしているとGitpod
ボタンが追加されているはずなのでそこをクリックします。
するとこんな感じ↓のvscode的なエディタが立ち上がります。これからこのGitpodを用いてCDKの開発を行ってきます。
Chrome拡張が使えない場合はこちらをクリックしてIDEを立ち上げます。
LINE Botチャンネルの作成
@sumihiro3 さんのこちらの資料にわかりやすく手順が乗っておりますのでこちらを参考に進めてください。こちらでチャネルシークレットとチャネルアクセストークンを取得します。
AWSのアクセスキーIDとシークレットアクセスキーを取得する
アクセスキーIDとシークレットアクセスキーはAWS CLIなどのコマンドラインツールでAWSを操作するのに使用します。今回はAWS-CDKを用いるのでAWS CLIのインストールは不要ですが、認証キーは当然必要なので取得しておきます。元々用意されている方はこの項目はスキップしてください。以下よりアクセスキーとシークレットアクセスキーの取得方法について説明します。
まずAWSにログインして、サービス検索欄からIAM
を探してIAMにアクセスします。
IAMにアクセスしたら、左メニューよりユーザー
を選択して、ユーザーを追加
ボタンをクリックします。
そしたらユーザー詳細の設定
に遷移するので適当なユーザー名を指定して(今回はhandson
にした)、アクセスの種類
にプログラムによるアクセス
を選択します。
次にアクセスの許可の設定
に遷移するので、既存のポリシーを直接アタッチ
を選択してAdministratorAccess
を選択します。後の設定項目(3以降)は特に操作は不要なのでそのままユーザー作成に進んで大丈夫です。
ユーザーが作成されたらこのような画面に遷移します。ここで注意したいのが、**アクセスキーIDとシークレットアクセスキーはこの画面でしか確認できないということです。**この画面上でそれぞれをメモするか、.csvのダウンロード
をクリックするなどを行ってください。
これでAWS-CDKのデプロイのための認証情報の取得は完了です。基本的にこれ以降はAWSのマネジメントコンソールを操作する作業は発生しません。
認証関係の設定
まずはAWS-CDKのための認証情報を設定します。普通はAWS CLIからprofileを登録するのですが今回は簡単のために以下のようにコマンドを下部のターミナルに打ち込みます。
export AWS_ACCESS_KEY_ID=アクセスキー
export AWS_SECRET_ACCESS_KEY=シークレットアクセスキー
export AWS_DEFAULT_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID
とAWS_SECRET_ACCESS_KEY
は2つ前の項目で取得したアクセスキーとシークレットアクセスキーそれぞれを入力してください。AWS_DEFAULT_REGION
はAWSのリージョンを指定するものですが、今回は東京(ap-northeast-1)を指定します(リージョン名についてはこちらをご参照ください)。
次にLINE Botのチャネルシークレットとチャネルアクセストークンを設定します。Gitpodの一番上の階層で.env
というファイルを作成してください。
ACCESS_TOKEN="チャネルアクセストークン"
CHANNEL_SECRET="チャネルシークレット"
ACCESS_TOKEN
にチャネルアクセストークンを、CHANNEL_SECRET
にチャネルシークレットをコピペします。以上で認証に関する設定は完了です。
CDKの実装
フォルダ構成
ここから実際にCDKの実装を行っていきます。まずは今回のハンズオン用のプロジェクトフォルダの構成の確認を行っていきます。
├── README.md
├── bin*
├── cdk.context.json
├── cdk.json
├── jest.config.js
├── lambda*
├── layer*
├── lib*
├── package-lock.json
├── package.json
├── tsconfig.json
└── yarn.lock
今回の開発に関係するものに*
をつけました。
- bin
- 実装したスタック(ざっくりというとAWSの構成のこと)を呼び出す上位レベルのコードが格納されています。今回は特に触りません。
- lambda
- lambdaのコードが格納されています(今回はLINE Botに関するコード)。本ハンズオンではあらかじめコードを用意しているのでこれをCDKを実装して呼び出します。
- layer
- lambda layerのコードが格納されています。これも実装済みですが、cdkでlambda layerを呼び出す実装を行っていきます。
- lib
- AWSの構成に関するコードが格納されています。今回はこの
lib
をメインに実装していきます(ただしlib/layerSetup.ts
については特に触りません)。
- AWSの構成に関するコードが格納されています。今回はこの
用いるコマンド
今回用いるコマンドについて簡単に説明します(詳細)。
build
TypeScriptで書いたスタックをJavaScriptのコードに変換します。これはTypeScriptに関係するコマンドですが、デプロイ前に必ず必要となるのでお忘れなく。
yarn build
別ターミナルで以下を実行すると常にビルドが走るようになるのでこちらを使うと良いかもです。
yarn watch
bootstrap
CDKで書いたスタック(正式にはCloudFormation)をデプロイするためのS3を用意する。とりあえず最初に実行するやつ。
yarn cdk bootstrap
deploy
CDKで書いたスタックをデプロイする。これが一番使うやつ。
yarn cdk deploy
destroy
CDKで書いたスタックを削除する。最後に実装するやつ。
yarn cdk destroy
他にも種類はありますが、今回用いるコマンドはこの3つです。
最初に打って欲しいコマンド
ここから本格に実装に入っていきますが、最初に2つのコマンドを実行してください。
yarn
↑でpackage.json
の中の必要パッケージをインストールします
yarn cdk bootstrap
↑でデプロイのための準備をします。ここで何かしらのエラーが発生した場合、AWSの認証情報の設定にミスがある可能性があるので確認をお願いします。
2つのLambdaを用意する
ここから実装です。まずはLINE Botのための会話用と買い物リストの操作のLambdaを2つ用意します。
export class CdkLineBotStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const dbHandler = new lambda.Function(this, 'DbHandlerFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/dbHandler'), // コードのディレクトリ
})
const linebot = new lambda.Function(this, 'LineBotFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/linebot'), // コードのディレクトリ
})
}
}
今の構成を図にすると↓こんな感じです。単純に2つのLambdaが並んでいるだけです。
DynamoDBを用意する
買い物リストを実装するためには何かしらのデータベースが必要になるので、DynamoDBを用意します。
export class CdkLineBotStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }
})
const dbHandler = new lambda.Function(this, 'DbHandlerFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/dbHandler'),
})
const linebot = new lambda.Function(this, 'LineBotFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/linebot'),
})
}
}
今の構成を図にすると↓こんな感じです。単純に3つのリソースが並んでいるだけですね。
API Gatewayを設置する
LINE Botの作成のために、LambdaをPOSTリクエストで呼び出し可能にしたいのでAPI Gatewayを用意します。
export class CdkLineBotStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }
})
const dbHandler = new lambda.Function(this, 'DbHandlerFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/dbHandler'),
})
const linebot = new lambda.Function(this, 'LineBotFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/linebot'),
})
const api = new apigateway.RestApi(this, 'Api')
api.root.addMethod('POST', new apigateway.LambdaIntegration(linebot))
}
}
今の構成を図にすると↓こんな感じです。1つの会話用のLambdaがPOSTリクエストで呼び出せるようになりました。
Lambda layerを用意する
2つのLambdaでlayer
のutil関数・npmパッケージを共有するためにLambda layerを用意します。
export class CdkLineBotStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }
})
const layer = new lambda.LayerVersion(this, 'layer', {
compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
code: lambda.Code.fromAsset('layer.out'),
})
const dbHandler = new lambda.Function(this, 'DbHandlerFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/dbHandler'),
layers: [layer], // layerの指定
})
const linebot = new lambda.Function(this, 'LineBotFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/linebot'),
layers: [layer], // layerの指定
})
const api = new apigateway.RestApi(this, 'Api')
api.root.addMethod('POST', new apigateway.LambdaIntegration(linebot))
}
}
ここからコードの差分がわかりにくくなってくるかもしれません。実装内容としてはlayer
というクラスを定義して、2つのLambdaに設定しています。ここまでですべてのリソースが揃ったので下の図ようになります。
環境変数の受け渡し
ここから各リソースのつなぎ込み作業に入ります。まず、各Lambdaにはそれぞれ知ってなければならない情報があります。
- 会話用Lambda(
linebot
)- LINE botに関する認証情報(チャネルシークレットとチャネルアクセストークン)
- 買い物リスト操作用のLambda名
- 買い物リスト操作用Lambda(
dbHnadler
)- DynamoDBのテーブル名
なのでそれぞれを環境変数で受け渡します。
export class CdkLineBotStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }
})
const layer = new lambda.LayerVersion(this, 'layer', {
compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
code: lambda.Code.fromAsset('layer.out'),
})
const dbHandler = new lambda.Function(this, 'DbHandlerFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/dbHandler'),
layers: [layer],
environment: { // 環境変数の設定
TABLE_NAME: table.tableName,
}
})
const linebot = new lambda.Function(this, 'LineBotFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/linebot'),
layers: [layer],
environment: { // 環境変数の設定
ACCESS_TOKEN: process.env.ACCESS_TOKEN!,
CHANNEL_SECRET: process.env.CHANNEL_SECRET!,
FUNCTION_NAME: dbHandler.functionName,
}
})
const api = new apigateway.RestApi(this, 'Api')
api.root.addMethod('POST', new apigateway.LambdaIntegration(linebot))
}
}
一見これですべてのリソースのつなぎ込みができたかのように見えますが、実際は↓こんな感じです。
ロールの付与
Lambdaが他のAWSリソース(ここではLambdaやDynamoDB)にアクセスするにはロールを与える必要があります。CDKではロールの受け渡しは1行で書くことができます。
export class CdkLineBotStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }
})
const layer = new lambda.LayerVersion(this, 'layer', {
compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
code: lambda.Code.fromAsset('layer.out'),
})
const dbHandler = new lambda.Function(this, 'DbHandlerFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/dbHandler'),
layers: [layer],
environment: {
TABLE_NAME: table.tableName,
}
})
const linebot = new lambda.Function(this, 'LineBotFunction', {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset('lambda/linebot'),
layers: [layer],
environment: {
ACCESS_TOKEN: process.env.ACCESS_TOKEN!,
CHANNEL_SECRET: process.env.CHANNEL_SECRET!,
FUNCTION_NAME: dbHandler.functionName,
}
})
dbHandler.grantInvoke(linebot) // Lambdaの呼び出し権限付与
table.grantFullAccess(dbHandler) // DynamoDBの操作権限付与
const api = new apigateway.RestApi(this, 'Api')
api.root.addMethod('POST', new apigateway.LambdaIntegration(linebot))
}
}
これで↓のような構成を完成させることができました。
デプロイ
後は以下のコマンドでデプロイします。デプロイ時になにかCDKに聞かれると思うのですべてy
で答えてください。
yarn build
yarn cdk deploy
API Gatewayのあるスタックをデプロイするとデプロイ成功後にURLがターミナルに表示されます。このURLがLINE BotのWebhook用のURLとしてそのまま用いるのでコピーしておきましょう。
後はこの資料を参考にしながらURLを設定すればLINE Botは完成です。認証情報関係に問題がなければ買い物リストbotが動くはずです。
おまけ:Lambdaを共通化しよう
おそらくLambdaに関するCDKのコードを見ていると共通化したくなる部分があると感じるかと思います。このときに用いるのがConstruct
と呼ばれるライブラリ化の機能です(詳細)。Constructを用いた共通化の例を以下に示します。今回はlib/lambdaUtil.ts
で新たにファイルを作成してLambdaUtil
という共通ライブラリを作成することにします。
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
interface LambdaUtilProps {
layer: lambda.ILayerVersion
path: string,
environment?: {
[key: string]: string
},
}
export class LambdaUtil extends cdk.Construct {
public readonly handler: lambda.Function
constructor(scope: cdk.Construct, id: string, props: LambdaUtilProps) {
super(scope, id)
const { layer, environment, path } = props
this.handler = new lambda.Function(this, id, {
runtime: lambda.Runtime.NODEJS_12_X,
handler: 'index.handler',
code: lambda.Code.fromAsset(path),
layers: [layer],
environment,
})
}
}
LambdaUtilProps
で定義したものがこのライブラリに与えるパラメータです。runtime
やhandler
が共通化されています。
そしてlib/cdk-line-bot-stack.ts
を以下のように書き換えます。
import * as cdk from '@aws-cdk/core'
import * as lambda from '@aws-cdk/aws-lambda'
import * as apigateway from '@aws-cdk/aws-apigateway'
import * as dynamodb from '@aws-cdk/aws-dynamodb'
import { LambdaUtil } from './lambdaUtil'
export class CdkLineBotStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }
})
const layer = new lambda.LayerVersion(this, 'layer', {
compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
code: lambda.Code.fromAsset('layer.out'),
})
const dbHandler = new LambdaUtil(this, 'DbHandlerFunction', {
path: 'lambda/dbHandler',
layer,
environment: {
TABLE_NAME: table.tableName,
},
}).handler
const linebot = new LambdaUtil(this, 'LineBotFunction', {
path: 'lambda/linebot',
layer,
environment: {
ACCESS_TOKEN: process.env.ACCESS_TOKEN!,
CHANNEL_SECRET: process.env.CHANNEL_SECRET!,
FUNCTION_NAME: dbHandler.functionName,
}
}).handler
dbHandler.grantInvoke(linebot)
table.grantFullAccess(dbHandler)
const api = new apigateway.RestApi(this, 'Api')
api.root.addMethod('POST', new apigateway.LambdaIntegration(linebot))
}
}
今回はあまり見た目に変化がありませんが、共通化の機能を用いることで実装ミスを減らすことができます。
さいごに
全体のコードはこちらで用意しています。また、デプロイしたスタックを削除したい場合は、
yarn cdk destroy
を実行すれば削除することができます。