Twilio FlexとAPI Gateway、Lambda、DynamoDBを連携させて架電してきたユーザがVIPユーザかFlex画面に表示させてみる


## はじめに

この記事は、[Twilio Flex Advent Calendar 2021](https://qiita.com/advent-calendar/2021/twilio-flex)の22日目の記事です。

普段、AWS周りをよくさわることが多いのですが、今回はTwilioに挑戦してみます。そして、Twilio Flexはじめて知りました。

とても面白そうなサービスだと思い、また簡単に無料トライアルを開始することできたので、今回、Twilio FlexとAWSのサービスを連携する系のブログを書きたいと思います。

## やりたいこと

電話をかけてきたユーザがVIPユーザなのか、そうでないのかDynamoDBのテーブルに保存してあるユーザ情報をもとに判定して、Twilio Flexの着信画面に表示したい

着信時にVIPユーザからの着信であればvipをつけて表示
![2021-12-22_1_07_38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/723fa443-1b1f-2a75-a105-3c16cb9004aa.png)

## 概要図

今回、構築するシステムの全体像はこんなかんじです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/bc91fc39-5094-c956-e591-8fa6a2b03a13.png)



TwilioとAWSとの連携はAPI Gatewayを使って連携します。

Twilio FunctionsからAPI Gateway + Lambda + DynamoDBで構築したAPIを実行し架電したユーザ情報を取得、Functionsで受け取ったレスポンス値からVIPか判定し、Flex画面に表示する流れとなります。

（フロー作成するStudioから直接APIを実行できそうではあったのですが、今回、柔軟性が高いTwilio Functionsを利用しています。）


## 前提

- AWSアカウントは発行済みで利用できる状態になっている

- IAMユーザの設定がされている状態になっている

- AWS CLIなど利用できるようにアクセスキー、クレデンシャルの設定がされている状態になっている

- Serverless FrameworkがインストールできるNode.js環境がある




## Twilio Flexをセットアップ

下記ブログを参考にセットアップしていきます。

https://qiita.com/mobilebiz/items/d06a29214b437339d226

ログイン後、「**Explore Products**」 => 「**All Products**」 => Solutionsの**Flex**を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/ba1e8876-f2ca-5c26-ff59-cbf318231cda.png)


「Create my Flex account」をクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/3802c5fd-7735-a024-d5ad-8a37984db415.png)


アカウント名を入力し、Verifyをクリック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/062e4110-edb4-2d0d-1cc2-26a9e93542d3.png)


数分でFlex環境が作成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/637af212-b567-5c04-2373-c285bc55439f.png)

Flexの画面が表示されたら、右上にある「**COMPLETE**」をクリックしていきタスクを完了しておきます。

![2021-12-16_17_37_54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/64ad2bb6-5c61-9114-f5e1-e359d6c3cdfb.png)


また、右上を確認すると、自分自身の状態が**Available**になっていることがわかります。

これは自分自身が有効なワーカーとしてログインしているということになります。

**Available**の状態だと課金されて続けるので、状態をオフラインにしておきます。そうすれば、Flexの課金が止まります。

利用していないときは適宜オフラインにしておきましょう。


オフラインの方法は右上の自分自身のアイコン付近をクリックして、プルダウンメニューから「Offline」を選択することでオフラインにできます。

### 着信のテスト

下記ブログを参考に着信のテストを行っておきましょう。より理解が深まるとかと思います。

https://qiita.com/mobilebiz/items/d06a29214b437339d226#flex%E3%81%AE%E8%B5%B7%E5%8B%95

## APIGateway + Lambda + DynamoDBを使って簡易APIを実装

Twilio Flexのセットアップが完了したので、次はAPIの実装を行います。


DynamoDBに保存されているユーザ情報が取得する簡易なAPIを実装します。

サンプル実装なので認証はAPIキーで行います。

例としては、下記のようにAPIを実行するとvipかどうか情報が取得できるAPIを実装する想定です。

```
$ curl -X GET "<endpoint>/dev/users/<id>" -H "x-api-key: XXXXXXXXXXXXXXX"
{"ID": "<id>", "vip": true}
```

また、今回は手順を簡単にするためServerless Frameworkを使いデプロイします。

### 環境

- Node.js: v16.13.0

- Lambdaのランタイム： python 3.8

```
$ serverless --version
Framework Core: 2.69.1
Plugin: 5.5.1
SDK: 4.3.0
Components: 3.18.1
```

### Serverless Framework をインストール

```
$ npm install serverless -g
```

### プロジェクトを作成

```
$ serverless create --template aws-python3
```

雛形作成されたら、ファイルを編集していきます。


handler.pyを下記のように編集します。

```python
import json
import boto3
from botocore.exceptions import ClientError

def hello(event, context):
    print(event)
    id = event["pathParameters"]["id"]
    body = {
        "ID": id,
        "vip": False
    }
    dynamodb = boto3.resource("dynamodb")
    table = dynamodb.Table("test-table")
    response = {
        "statusCode": 200,
        "body": json.dumps(body)
    }
    try:
        res = table.get_item(Key={"ID": str(id)})
        print(res["Item"])
        response["body"] = json.dumps(res["Item"])
        print(response)
        return response
    except KeyError:
        return response
    except ClientError as e:
        print(e.response["Error"]["Message"])
```

DynamoDBにデータがなければ、vipの値がfalseが、データがあれば上書きして返すようにしています。

「test-table」というテーブル名でDynamoDBのテーブルを作成する予定なので、そのテーブル名を指定しています。

サンプル実装ですので本番環境での利用はお控えください。


serverless.ymlを下記のように編集します。

```yml
service: twilio-sample
frameworkVersion: "2"

provider:
  name: aws
  runtime: python3.8
  lambdaHashingVersion: 20201221
  apiGateway:
    apiKeys:
      - testKey
    usagePlan:
      quota:
        limit: 1000
        offset: 2
        period: MONTH
      throttle:
        burstLimit: 200
        rateLimit: 100
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:GetItem
          Resource:
            - "*"
functions:
  hello:
    handler: handler.hello
    events:
      - http:
          private: true
          path: users/{id}
          method: get
          request:
              parameters:
                  paths:
                      id: true

# DynamoDBのテーブルを作成
resources:
  Resources:
    StateTable:
      Type: "AWS::DynamoDB::Table"
      Properties:
        AttributeDefinitions:
          - AttributeName: ID
            AttributeType: S
        KeySchema:
          - AttributeName: ID
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST
        TableName: test-table
```

サンプル実装なのでLambda関数は「hello」というデフォルトの関数名のままにしています。適宜変更ください。

「test-table」というテーブル名でDynamoDBのテーブルが作成されます。



### デプロイ

AWSのプロファイルをデプロイ先アカウントに向け、デプロイします。

下記コマンドを実行し、リソースの情報など出力されたら成功です！

```
$ sls deploy
Serverless: Stack update finished...
Service Information
service: twilio-sample
stage: dev
region: us-east-1
stack: twilio-sample-dev
resources: 17
api keys:
  testKey: xxxxxxxxxxxxxxxxx
endpoints:
  GET - https://xxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/users/{id}
functions:
  hello: twilio-sample-dev-hello
layers:
  None
```

出力された**api key**と**endpoint**を控えておきます。

### DynamoDBにデータ保存

次は、バージニアリージョンに作成されたDynamoDBのテーブルにデータを追加します。

AWSのマネージメントコンソールからDynamoDBのサービスページに移動し、作成されたテーブル「test-table」を選択します。

追加するデータのIDは今回、電話番号になります。「+81」を先頭につけて、電話番号の先頭0を削った形で値を設定してください。

例： **090xxxxyyyy** のとき **+8190xxxxyyyy** を値に設定

次に「**新しい属性の追加**」でboolを選択し、属性名を「**vip**」とし、値を「**True**」にして保存してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/b41e401d-54a7-a24c-e8a8-8d99dc2b3a72.png)

### APIの動作確認

curlで動作確認してみます。

想定通りのレスポンスが返ってこればOKです。

DynamoDBに登録していないIDでリクエストすると**vip**が**false**になってかえってくることも確認しておきましょう。

```
$ curl -X GET "https://xxxxxxxxxxxx.execute-api.us-east-1.amazonaws.com/dev/users/+8190xxxxyyyy" -H "x-api-key: XXXXXXX"
{"ID": "+8190xxxxyyyy", "vip": true}%   
```

## Twilio Functionsの設定

API側の準備もできたので、次はTwilio Flexと連携するためのTwilio Functionsの設定を行っていきます。



フロー作成するStudioにHTTPをコールする[Make HTTP Request](https://jp.twilio.com/docs/studio/widget-library/http-request)がありますが、ヘッダーの設定ができない？ようだったので、より柔軟なTwilio Functionsを今回利用します。


Twilio Functions は
Twilio ログイン後、「**Explore Products**」 => 「**All Products**」 => Developer toolsの**Functions**を選択すると表示されます。

ピン留めされていなければしておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/217899e9-ac7f-7b2f-9ba8-dfe575c515d8.png)


Servicesは今回「test」として作成していきます。適宜変更ください。

### Functionsから外部APIを実行する

今回、手順簡略化するために、はじめからある```/welcome```を編集する形で進めます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/3534703d-869f-3cc8-f639-d3ba31d30025.png)

下記のように編集します。

```js
const axios = require('axios');
exports.handler = async (context, event, callback) => {
  const apiKey = context.API_KEY;
  const endpoint = context.ENDPOINT;

  const tel = event['data'];
  const { data } = await axios.request({
    url: `${endpoint}/users/${tel}`,
    method: 'GET',
    headers: {
      'X-Api-Key': apiKey
    }
  });

  let message = '';

  if(data.vip) {
    message = 'vip'
  }

  return callback(null, {
    message: message
  });
};
```

処理の流れを簡単に解説すると

- Functionに入力される**event**に含まれる、電話番号情報を取得
- 先程実装した、APIを実行
- APIのレスポンスに含まれるvipがTrueならmessageに**vip**をいれる
- callbackでmessageを返す

という処理を行っています。

### 環境変数を設定する

API Keyやエンドポイントはコードに埋め込みはせずに環境変数から取得するようにするため、**Environment Variables**に設定します。

今回、`API_KEY`と`ENDPOINT`を設定しています。

`ENDOPOINT`には`https://xxxxxxx.execute-api.us-east-1.amazonaws.com/dev`という形で値を登録しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/990a6c31-4d4d-ee29-6316-531121f90d81.png)



ここで設定した値は**context**から値を取得できます。

contextのサンプル

```json
{
  PATH: '/welcome',
  AUTH_TOKEN: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
  SERVICE_SID: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
  API_KEY: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
  ENVIRONMENT_SID: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
  ENDPOINT: 'https://xxxxxxx.execute-api.us-east-1.amazonaws.com/dev',
  ACCOUNT_SID: 'xxxxxxxxxxxxxxxxxxxxxxxxxx',
  DOMAIN_NAME: 'xxxxxx.twil.io',
  getTwilioClient: [
    Function
  ]
}
```

### Twilio Functionsで利用するライブラリを追加

functions内で利用するライブラリは**Dependencies**から追加することで利用できるようになります。

今回、APIをリクエストするために**axios**を追加しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/321f1b64-6000-4114-b7e3-84687b3c3b42.png)


### デプロイ

**save**を押して、**Deploy All**ボタンを押すことで反映されます。


## Twilio Studio でフローを編集する

Twilio Studioが左ペインになければ、
ログイン後、「**Explore Products**」 => 「**All Products**」 => Developer toolsの**Studio**を選択するとアクセスできます。

ピン留めされていなければしておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/217899e9-ac7f-7b2f-9ba8-dfe575c515d8.png)

### Voice IVRを編集

デフォルトで用意されているものがあるので、それを編集していきます。

着信したら実行されるフローが「**Vocie IVR**」になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/db7aebf6-3878-9c40-ec0f-ce2f13942226.png)


Twilio Functionsをフローに挟むために、**Run Function**を追加します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/89a9715f-e44a-f501-1ed9-944e4c7406dd.png)

下記のようにフローを編集します。
![2021-12-22_1_15_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/2ba129d9-29e2-e5a0-6ec3-531ffe0ee641.png)

**function_1**の設定は下記のようにします。

![2021-12-22_1_17_25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/a20d0fc0-3303-2366-50a4-e9704886cf1b.png)

**Function Parameters**に**data**,**{{trigger.call.From}}**として追加しておきます。

**{{trigger.call.From}}**は着信があった電話番号になります。

[triggerで取得できる値のドキュメント](https://jp.twilio.com/docs/studio/widget-library/trigger-start)

ここで設定した値が**events**に入ってきます。

設定できたら、Saveボタンを押します。


## Flexの着信画面にVIPかどうか表示させる

FlexのUIはカスタマイズすることが可能です。

ですが、プラグイン開発が必要になったりするため、今回は既存仕組みを編集する形で対応します。

### 着信画面にデータを渡しているのはSendCallToAgentの「Send To Flex」Widgetを編集して対応

StudioからTaskRouter(着信画面など)にコールを引き渡す役目をするのが、「Send To Flex」Widgetになります。

ここでは、すでに**SendCallToAgent**として実装されていますので、こちらを少し修正して着信画面にVIPかどうか表示させたいと思います。

つまり、**SendCallToAgent**のATTRIBUTESを下記のように修正します。

```json
{ "type": "inbound", "name": "({{widgets.function_1.parsed.message}})  {{trigger.call.From}}" }
```

![2021-12-22_1_32_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/77e8bf09-aad4-fe23-12f1-d87619b7800e.png)


現在、nameパラメータを使うことで、Flexの着信画面に電話番号が表示されるようになっています。

そこにFunctionから受け取った値（**{{widgets.function_1.parsed.message}}**）を追加しています。

### 設定を反映

修正したフローをTwilio Studioに反映させるためには、最後にパブリッシュする必要があります。

画面上部にあるPublishボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/5b283688-89de-8839-dd14-6969b6b5950f.png)



## 動作確認

DynamoDBに登録されている電話番号からFlexに電話をかけてみます。

すると、想定通りVIPと表示されました！

![2021-12-22_1_07_38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/723fa443-1b1f-2a75-a105-3c16cb9004aa.png)

DynamoDBのデータを消して、再度着信してみると、今度はVIPが表示されなくなりました！

![2021-12-22_1_09_29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155802/1a30bdc1-4043-f26d-5fd7-add405088eca.png)


もし、うまくいかなければAWS側のCloudWatch Logsに出力されるLambdaのログを確認するとよいかと思います。

## AWS環境の片付け

検証後のお片付け

フルマネージドサービスを利用しているので、時間で課金されることはないですが削除するには

下記コマンドを実行することでAWSのリソースを削除することができます。

```
$ sls remove
```

## 参考サイト

https://qiita.com/mobilebiz/items/4906a2d37607e1082808

https://qiita.com/mobilebiz/items/55c9a4d7009d1ba2a3ec