Help us understand the problem. What is going on with this article?

LINE Bot×AWS CDKハンズオン

はじめに

この記事はLINE BotAWS 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構成は以下のような感じです。

Untitled.png

構成そのものとしてはLambdaAPI GatewayDynamoDBというよくあるサーバーレスの構成となっています。今回は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ボタンが追加されているはずなのでそこをクリックします。

handson1.png

するとこんな感じ↓のvscode的なエディタが立ち上がります。これからこのGitpodを用いてCDKの開発を行ってきます。

handson7.png

Chrome拡張が使えない場合はこちらをクリックしてIDEを立ち上げます。

LINE Botチャンネルの作成

@sumihiro3 さんのこちらの資料にわかりやすく手順が乗っておりますのでこちらを参考に進めてください。こちらでチャネルシークレットチャネルアクセストークンを取得します。

AWSのアクセスキーIDとシークレットアクセスキーを取得する

アクセスキーIDとシークレットアクセスキーはAWS CLIなどのコマンドラインツールでAWSを操作するのに使用します。今回はAWS-CDKを用いるのでAWS CLIのインストールは不要ですが、認証キーは当然必要なので取得しておきます。元々用意されている方はこの項目はスキップしてください。以下よりアクセスキーとシークレットアクセスキーの取得方法について説明します。

まずAWSにログインして、サービス検索欄からIAMを探してIAMにアクセスします。

dandson6.png

IAMにアクセスしたら、左メニューよりユーザーを選択して、ユーザーを追加ボタンをクリックします。

handson2.png

そしたらユーザー詳細の設定に遷移するので適当なユーザー名を指定して(今回はhandsonにした)、アクセスの種類プログラムによるアクセスを選択します。

handson3.png

次にアクセスの許可の設定に遷移するので、既存のポリシーを直接アタッチを選択してAdministratorAccessを選択します。後の設定項目(3以降)は特に操作は不要なのでそのままユーザー作成に進んで大丈夫です。

handson4.png

ユーザーが作成されたらこのような画面に遷移します。ここで注意したいのが、アクセスキーIDとシークレットアクセスキーはこの画面でしか確認できないということです。この画面上でそれぞれをメモするか、.csvのダウンロードをクリックするなどを行ってください。

handson5.png

これで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_IDAWS_SECRET_ACCESS_KEYは2つ前の項目で取得したアクセスキーシークレットアクセスキーそれぞれを入力してください。AWS_DEFAULT_REGIONはAWSのリージョンを指定するものですが、今回は東京(ap-northeast-1)を指定します(リージョン名についてはこちらをご参照ください)。
次にLINE Botのチャネルシークレットチャネルアクセストークンを設定します。Gitpodの一番上の階層で.envというファイルを作成してください。

.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については特に触りません)。

用いるコマンド

今回用いるコマンドについて簡単に説明します(詳細)。

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つ用意します。

lib/cdk-line-bot-stack.ts
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が並んでいるだけです。

Untitled(1).png

DynamoDBを用意する

買い物リストを実装するためには何かしらのデータベースが必要になるので、DynamoDBを用意します。

lib/cdk-line-bot-stack.ts
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つのリソースが並んでいるだけですね。

Untitled(2).png

API Gatewayを設置する

LINE Botの作成のために、LambdaをPOSTリクエストで呼び出し可能にしたいのでAPI Gatewayを用意します。

lib/cdk-line-bot-stack.ts
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リクエストで呼び出せるようになりました。

Untitled(3).png

Lambda layerを用意する

2つのLambdaでlayerのutil関数・npmパッケージを共有するためにLambda layerを用意します。

lib/cdk-line-bot-stack.ts
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に設定しています。ここまでですべてのリソースが揃ったので下の図ようになります。

Untitled.png

環境変数の受け渡し

ここから各リソースのつなぎ込み作業に入ります。まず、各Lambdaにはそれぞれ知ってなければならない情報があります。

  • 会話用Lambda(linebot
    • LINE botに関する認証情報(チャネルシークレットとチャネルアクセストークン)
    • 買い物リスト操作用のLambda名
  • 買い物リスト操作用Lambda(dbHnadler
    • DynamoDBのテーブル名

なのでそれぞれを環境変数で受け渡します。

lib/cdk-line-bot-stack.ts
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))

  }
}

一見これですべてのリソースのつなぎ込みができたかのように見えますが、実際は↓こんな感じです。

Untitled.png

ロールの付与

Lambdaが他のAWSリソース(ここではLambdaやDynamoDB)にアクセスするにはロールを与える必要があります。CDKではロールの受け渡しは1行で書くことができます。

lib/cdk-line-bot-stack.ts
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))

  }
}

これで↓のような構成を完成させることができました。

Untitled.png

デプロイ

後は以下のコマンドでデプロイします。デプロイ時になにか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という共通ライブラリを作成することにします。

lib/lambdaUtil.ts
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で定義したものがこのライブラリに与えるパラメータです。runtimehandlerが共通化されています。
そしてlib/cdk-line-bot-stack.tsを以下のように書き換えます。

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

を実行すれば削除することができます。

ufoo68
IoT関係の会社で働いています。学生のときは画像認識を使った研究をしていました。最近はAWSかLINEを使った開発に興味があります。
access
SDNからセンサ、家電、電子書籍まで。ACCESSはあらゆるレイヤのデバイス、サービスを「繋げて」いきます。
http://jp.access-company.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした