Clovaの公式SDKがLambdaで使えるようになっていたので試してみた。

# はじめに

Clovaスキルを普段、自作のSDKを使ってLambdaで開発している私ですが、ハンズオンの講師をするにあたって公式SDKを使おうと思いコードをみているとLambdaで実行できるような[メソッド](https://github.com/line/clova-cek-sdk-nodejs/blob/master/src/client.ts#L95)が追加されていました。
公式ドキュメントなどで使い方などを書いていなかったので参考になればと思います🎵

とりあえず、コードをみたい！という方は以下。
[Lambdaのコード(検証なし、lambda()メドッド使用）](https://qiita.com/imajoriri/items/b67318cfda04998255b8#lambda%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89)
[Lambdaのコード（検証あり、lambda()メソッド使用しない）](https://qiita.com/imajoriri/items/b67318cfda04998255b8#lambda%E3%81%AE%E3%82%B3%E3%83%BC%E3%83%89-1)

# 対象者

[公式SDK](https://github.com/line/clova-cek-sdk-nodejs)をHerokuなどのサーバーで使ったことあるがLambdaで使ってみたい方を対象としています。

Clovaスキルの作成方法（インテント、スロットなどの登録）はこの記事では書いていません。

入門者向けには以前記事を書いています。
[[入門]Clovaスキル(CEK)は作りながら覚えて行く（前提知識編）](https://qiita.com/imajoriri/items/8d511cdfde8cb731ff82)

# 構成と環境

言語はNode.jsを使用しています。

LambdaをCEK（Clovaスキル）から直接叩くことはできないです。
CEKとLambdaの間にAPI Gatewayを挟むことになります。
以下がそのアーキテクチャです。

<img width="767" alt="名称未設定ファイル_xml_-_draw_io.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/23ab2d46-df49-e1dd-2a41-64e20693df76.png">



# Lambdaの作成

とりあえず、Lambdaを作成しましょう。
まあ、関数名はなんでもいいんですが適当に作成します。

<img width="1308" alt="Lambda_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/5319da50-156f-2f8d-bcc9-99ed71f8094f.png">

# API Gatewayの設定

次にAPI Gatewayの作成をしていきます。
以下のように「api」と検索するとAPI Gatewayが出てくるのでクリックします。

<img width="786" alt="AWS_マネジメントコンソール.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/6ca6adba-ec77-fa85-42fe-d48b39e22e3b.png">

API Gatewayのダッシュボードに移動するので「APIの作成」をクリックします。
これも名前は適当でいいんですが、今回は以下のように作成します。

|項目|値|
|---|---|
|API 名|cek-official-sdk-gataway|
|説明|for lambda cek-official-sdk function|

<img width="1227" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/cc18d3b7-c906-2325-7d0a-675a0c8eaf41.png">

次にリソースを作成します。
画面左上の「アクション」からリソースの作成を選択します。
リソース名には「clova」と入力して作成します。

<img width="1158" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/4a6238fd-ed39-2bed-2bd8-c2057490fa54.png">

次にメソッドを作成します。
「リソースの作成」ボタンの上の「メソッドの作成」をクリックします。
メソッドは「POST」を選択します。

<img width="1167" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/0839c29b-e0ff-c95a-56cb-622c0c467e0b.png">

先ほど作成したLambdaの関数名を入力します。

<img width="1164" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/d0b47119-5008-1d91-0ba3-86e641978888.png">

次にAPIのデプロイをします。
以下の画像のようにAPIのデプロイをクリックします。

<img width="1161" alt="Banners_and_Alerts_と_API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/9cd88b67-494a-7aa2-152e-132b669cae9c.png">

デプロイのステージの選択が出てくるので以下のように入力してデプロイします。

|項目|値|
|---|---|
|デプロイされるステージ|新しいステージ|
|ステージ名*|prod|
|ステージの説明|production|

<img width="624" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/b5ab4451-eba1-14d3-c832-eb48c87c7476.png">

URLが発行されるのでメモします。
***画面左からclova/POSTをクリックした後の画面のURLです***

<img width="1167" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/1f4f7b13-a169-07cd-c0e6-d026fa43daa7.png">

メモしたURLをClovaのサーバーの設定に入力します。

<img width="786" alt="_入門_Clovaスキル_CEK_は作りながら覚えて行く（Lambda使ってコード実装編）_-_Qiita.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/7b908aca-c2ac-2d7a-3367-d4905072c6bd.png">


# Lambdaのコード

よし、これでLambdaとCEKの紐付けはできました！！！
あとは、コードを書いて行くのみです！！（実はそんなこともないのですが、それについては後述します）

必要なモジュールをインストールしていきます。

```
$ npm init
$ npm install --save @line/clova-cek-sdk-nodejs
```

まず、Lambdaのコードを説明する前に公式のGitHubのサンプルコードをみてみます。

```js:herokuなどのサーバー
const clova = require('@line/clova-cek-sdk-nodejs');
const express = require('express');
const bodyParser = require('body-parser');

const clovaSkillHandler = clova.Client
  .configureSkill()
  .onLaunchRequest(responseHelper => {
    responseHelper.setSimpleSpeech({
      lang: 'ja',
      type: 'PlainText',
      value: 'おはよう',
    });
  })
  .onIntentRequest(async responseHelper => {
    const intent = responseHelper.getIntentName();
    const sessionId = responseHelper.getSessionId();

    switch (intent) {
      case 'Clova.YesIntent':
        // Build speechObject directly for response
        responseHelper.setSimpleSpeech({
          lang: 'ja',
          type: 'PlainText',
          value: 'はいはい',
        });
        break;
      case 'Clova.NoIntent':
        // Or build speechObject with SpeechBuilder for response
        responseHelper.setSimpleSpeech(
          clova.SpeechBuilder.createSpeechText('いえいえ')
        );
        break;
    }
  })
  .onSessionEndedRequest(responseHelper => {
    const sessionId = responseHelper.getSessionId();

    // Do something on session end
  })
  .handle();

const app = new express();
const clovaMiddleware = clova.Middleware({ applicationId: "YOUR_APPLICATION_ID" });

app.post('/clova', clovaMiddleware, clovaSkillHandler);

app.post('/clova', bodyParser.json(), clovaSkillHandler);
```

これだと、Herokuなどのサーバーで実行する用のコードなのでLambda用に変更します。
不必要なところはコメントアウトしていくつかコードを足しています。

```js:lambdaのコード
const clova = require('@line/clova-cek-sdk-nodejs');
//const express = require('express');
//const bodyParser = require('body-parser');

//const clovaSkillHandler = clova.Client
exports.handler = clova.Client
  .configureSkill()
  .onLaunchRequest(responseHelper => {
    responseHelper.setSimpleSpeech({
      lang: 'ja',
      type: 'PlainText',
      value: 'おはよう',
    });
  })
  .onIntentRequest(async responseHelper => {
    const intent = responseHelper.getIntentName();
    const sessionId = responseHelper.getSessionId();

    switch (intent) {
      case 'Clova.YesIntent':
        // Build speechObject directly for response
        responseHelper.setSimpleSpeech({
          lang: 'ja',
          type: 'PlainText',
          value: 'はいはい',
        });
        break;
      case 'Clova.NoIntent':
        // Or build speechObject with SpeechBuilder for response
        responseHelper.setSimpleSpeech(
          clova.SpeechBuilder.createSpeechText('いえいえ')
        );
        break;
    }
  })
  .onSessionEndedRequest(responseHelper => {
    const sessionId = responseHelper.getSessionId();

    // Do something on session end
  })
  .lambda()
  //.handle();

//const app = new express();
//const clovaMiddleware = clova.Middleware({ applicationId: "YOUR_APPLICATION_ID" });
//app.post('/clova', clovaMiddleware, clovaSkillHandler);
//
//app.post('/clova', bodyParser.json(), clovaSkillHandler);
```

変更した部分を少し説明します。
まずは、コードの上の方の部分です。
Lambdaは`exports.handler`に指定した関数を実行するので`clovaSkillHandler`として変数で持っていたのを`exports.handler`に変更します。

```js
... 省略 ...

//const clovaSkillHandler = clova.Client
exports.handler = clova.Client
... 省略 ...
```

次にはコードの下の方です。`handle()`関数をLambda用に`lambda()`に変えます。

```js
  .lambda()
  //.handle();
```

最後にexpress周りをコメントアウトします。

```js
//const app = new express();
//const clovaMiddleware = clova.Middleware({ applicationId: "YOUR_APPLICATION_ID" });
//app.post('/clova', clovaMiddleware, clovaSkillHandler);
//
//app.post('/clova', bodyParser.json(), clovaSkillHandler);
```

他のコードの部分の説明はここではしません。
田中みそ氏の記事が参考になります。
[Clova公式SDK(Node.js)の使い方まとめ](https://qiita.com/miso_develop/items/6b256c1e8757c0ace4a9)


# しかーし！検証をしていない

ここであることに気がつきます。
検証をしていません。
***検証？なにそれ？という方は[こちら](https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md#RequestMessageValidation)***

実際に検証をしなくても動くことは動くのでしなくてもいいやって方はこの先を読む必要はありません。（いや、本当はしたほうがいいです）

SDKの中をみなくても検証をしていないことがわかるんですが、なぜわかるかというと今まではLambdaでHeaderを取得できていないからです。
Headerの`SignatureCEK`の値を検証に使うのでしていないことがわかります。

# API Gateway再設定（Headerを取得できるように）

まずはHeaderを取得できるようにAPI Gatewayの設定をします。
先ほど作成したAPIの「結合リクエスト」をクリックします。

<img width="1160" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/c51b2a6e-1942-3935-5728-c71a2347cdca.png">

次に「Lambda プロキシ統合の使用」にチェックを入れます。
これでHeader（＋もろもろ）取得することができます。

<img width="1140" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/5f203b56-e87f-6d39-4752-b76c59cee981.png">

そして、先ほど同じようにデプロイします。
今回はすでに作成されている「pord」を選択します。

<img width="609" alt="API_Gateway.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/247abb70-8080-0509-e3de-8c830c14b819.png">


# Lambdaのコード


Lambdaで取得できるデータの形が変わるので先ほどのコードではエラーがおきます。
ですので、コードを書き換えていきたいと思います。
さらには、検証も行なっていきます。
いかがそのコードになります。
注目は最後の方の`exports.handler`の中です。

```js
const clova = require('@line/clova-cek-sdk-nodejs');

const clovaSkillHandler = clova.Client
  .configureSkill() 
  .onLaunchRequest(responseHelper => {
    responseHelper.setSimpleSpeech({
      lang: 'ja',
      type: 'PlainText',
      value: 'おはよう',
    });
  })
  .onIntentRequest(async responseHelper => {
    const intent = responseHelper.getIntentName();
    const sessionId = responseHelper.getSessionId();

    switch (intent) {
      case 'MenuIntent':
      case 'Clova.YesIntent':
        // Build speechObject directly for response
        responseHelper.setSimpleSpeech({
          lang: 'ja',
          type: 'PlainText',
          value: 'はいはい',
        });
        break;
      case 'Clova.NoIntent':
        // Or build speechObject with SpeechBuilder for response
        responseHelper.setSimpleSpeech(
          clova.SpeechBuilder.createSpeechText('いえいえ')
        );
        break;
    }
  })
  .onSessionEndedRequest(responseHelper => {
    const sessionId = responseHelper.getSessionId();

  })
  //.lambda()

exports.handler = async (event, content) => {

  
  const signature = event.headers.signaturecek || event.headers.SignatureCEK;
  const applicationId = process.env["applicationId"];
  const requestBody = event.body;
  // ヘッダーとスキルのExtensionIdとリクエストボディで検証
  await clova.verifier(signature, applicationId, requestBody);

  // 「Lambdaプロキシの結合」を有効にするとCEKからのJSONの中身は「event.body」で文字列で取得できる。
  var ctx = new clova.Context(JSON.parse(event.body));
  const requestType = ctx.requestObject.request.type;
  const requestHandler = clovaSkillHandler.config.requestHandlers[requestType];

  if (requestHandler) {
    await requestHandler.call(ctx, ctx);

    console.log("--- responseObject ---");
    console.log(ctx.responseObject);
    console.log("--- responseObject end ---");

    //　CEKに返すレスポンス
    const response =  {
        "isBase64Encoded": false,
        "statusCode": 200,
        "headers": {},
        "body": JSON.stringify(ctx.responseObject),
    }
    console.log(response);
    return response;
  } else {
    throw new Error(`Unable to find requestHandler for '${requestType}'`);
  }
}
```

次にLambdaの環境変数にExtensionIDをセットします。
キーは`applicationId`です。

<img width="1284" alt="Lambda_Management_Console.png" src="https://qiita-image-store.s3.amazonaws.com/0/195882/a3985082-2fa6-1496-4862-49d47a7064ed.png">

これで検証もしたコードになりました。（長い！！）
`exports.handler`の中身はそのままコピペでどのスキルもいけると思います。（環境変数のapplicationIdを変更する必要はあり）

注意する点は、LambdaからCEKへのレスポンスの形も変えないといけないということです。
以下のように返さないといけないみたいです。

```json
{
    "isBase64Encoded": true|false,
    "statusCode": httpStatusCode,
    "headers": { "headerName": "headerValue", ... },
    "body": "..."
}
```

以下などを参照していただけるとわかると思います。
https://www.uzumax.org/2018/07/aws-lambda-502-internal-server-error.html