仕組みから理解するTwilio #3-1 - AWS API Gateway+Lambda実装Walkthrough(前編)

  • 5
    Like
  • 0
    Comment

この記事は下記記事の続きです。先にこちらを読むことを推奨いたします。
仕組みから理解するTwilio #1 - はじめに
仕組みから理解するTwilio #2 - 通信フロー・データ構造

Twilio Clientから電話をかける/Twilio Clientに電話をかけるアプリケーションを実装していきます。分量が多くなるので前後編に分割します。
以降の内容は、ある程度AWS上で開発した経験、特にAWS Lambda・API Gatewayを組み合わせてAPIを開発・公開したことがある人を想定しています。経験がない方は、一度、両者を組み合わせてAPIを作って公開して、感覚をつかむことを推奨します。

  • 1. はじめに
    • 1. Twilio利用時の基本構成・用語の定義
    • 2. 今回検証した環境
  • 2. 通信フロー・データ構造
    • 1. 認証・CapabilityToken授受
    • 2. 外部電話からTwilioクライアントへのCall(IncomingCall)
    • 3. Twilioクライアントから外部電話へのCall(OutgoingCall)
  • 3-1. AWS API Gateway+Lambda実装Walkthrough(前編) ←いまココ
    • 1. API Serverの処理の実装(Python on AWS Lambda)
    • 2. API Gatewayの設定
  • 3-2. AWS API Gateway+Lambda実装Walkthrough(後編)
    • 3. Twilio Clientの実装とデプロイ
    • 4. 動作確認!

Walkthroughの概要

今回実現するTwilio Clientの仕様を再確認しましょう。

  • 出来ること
    • Twilio Clientから、任意の番号を指定して一般の電話にかけることができる(アウトバウンド通話)
    • 一般の電話からTwilio Clientに電話をかけることができる(インバウンド通話)
  • Twilio Clientの仕様
    • 自身を識別する値として、Twilio電話番号のみ保持する。自身のClient Nameは保持しない。
    • HTML/JavaScriptで実装され、S3上に配置される。

電話番号等のパラメータは下記であると仮定します。

パラメータ 説明・備考
Twilio電話番号 050-3123-4567 Twilio Clientに割り当てた電話番号。Twilioコンソールから事前に購入しておいて下さい。
Client Name DeviceId_0001 Twilio Server内で当該Twilio Clientを識別・制御するための名前。
外部の電話 090-5987-6543 Twilio外の電話。ご自身の携帯電話等を利用してください。

今回実現する構成を再確認しましょう。
TwilioDiagrams_Fig1-1.png

AWS Lambdaを利用して各APIを実装し、これをAPI Gatewayから利用できるよう構築します。
API名・Lambda関数名・API GatewayのResource名はそれぞれ下記のとおりの対応としています。

API名 AWS Lambda関数名 Resource名
CapabilityToken取得API TwilioRetrieveCapabilityToken /twilio/retrieve_capability_token
Incoming Call用TwiML返却API TwilioRetieveTwimlCallIncoming /twilio/retrieve_twiml/call_incoming
Outgoing Call用TwiML返却API TwilioRetieveTwimlCallOutgoing /twilio/retrieve_twiml/call_outgoing

各APIは、API GatewayにてprodステージにDeployすることとします。従って、API Serverは最終的に下記のようなURLを提供することとなります。

JavaScriptで実装したTwilio Clientは下記のような画面になります。
この画面から、一般の電話に電話をかけたり、一般の電話を受信することができます。
twilio_client_webbrowser.png

Twilioに関する設定作業はTwilioアカウントの開設・準備を参照してください。

AWSに関する具体的な作業は下記のとおりとなります。AWSアカウントは開設済みであるものとします。

  • 1. API Serverの処理の実装(Python on AWS Lambda)
    • 1-1. CapabilityToken取得APIを、Lambda関数として実装・デプロイする
    • 1-2. Incoming Call用TwiML返却APIを、Lambda関数として実装・デプロイする
    • 1-3. Outgoing Call用TwiML返却APIを、Lambda関数として実装・デプロイする
  • 2. API Gatewayの設定
    • 2-1. AWS Management ConsoleのAmazon API Gatewayのメニューから、APIを作成する
    • 2-2. 各API用のResourceを定義する
    • 2-3. CORSを設定する
    • 2-4. CapabilityToken取得API(/twilio/retrieve_capability_token)用のMethodを定義する
    • 2-5. Incoming Call用TwiML返却API(/twilio/retrieve_twiml/call_incoming)用のMethodを定義する
    • 2-6. Outgoing Call用TwiML返却API(/twilio/retrieve_twiml/call_outgoing)用のMethodを定義する
    • 2-7. prodステージを作成しDeployする
    • 2-8. 各APIの動作確認を行う
  • 3. Twilio Clientの実装とデプロイ
    • 3-1. Twilio ClientをJavaScirptで実装する
    • 3-2. S3 Bucketを作成し、Static Website Hostingを有効にする
    • 3-3. S3 Bucket上に配置する
  • 4. 動作確認!
    • 4-1. WebブラウザでS3上のTwilio Clientに、HTTPSでアクセスする
    • 4-2. 検証済みの電話番号からTwilio Clientに電話をかける
    • 4-3. Twilio Clientから検証済みの電話番号に電話をかける

それでは以下から一つ一つ見ていきましょう。

1. API Serverの処理の実装(Python on AWS Lambda)

今回の検証で作成するAPIは、AWS Lambda上にPythonで実装しています。
Lambdaを利用するのが初めてと言う方は、下記ドキュメントに記載の手順を実行し、どういったものか感覚をつかむと良いでしょう。

AWS Lambda 開発者ガイド - ご利用開始にあたって
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/getting-started.html

使ってみるとわかりますが、Lambda関数のInput/Outputの形式は、JSONが一番利用しやすいです。そのため、今回実装する関数もできる限りJSONを利用します。

1-1. CapabilityToken取得APIを、Lambda関数として実装する

インターフェース(Input/Output)

まずはこのLambda関数のInputとOutputの構造を決めます。
認証・CapabilityToken授受で定義したとおりの構造としましょう。

Input
{"twilioPhoneNumber": "+81-50-1234-9876"}

Twilio Clientは、自分自身に紐付けられたTwilio電話番号をソースコード上に保持しているものとします。このAPIは、Inputに指定されたTwilio電話番号が、登録済みのものであるかどうかのみをチェックすることを以って認証することとします。

Output
{"capabilityToken": capabilityToken, "success": true}

認証が成功した場合、このAPIはCapability Tokenを生成します。また、処理の成否も併せて返すようにします。

実装

それでは実際にLambda関数を書いていきましょう。

TwilioRetrieveCapabilityToken.py
from __future__ import print_function
from twilio.util import TwilioCapability

print('Loading function')


class UnregisteredPhoneNumberException(Exception):
    def __init__(self,value):
        self.value = value


def get_client_name_by_phone_number (phone_number):
    ''' This function returns a valid clientName.
        If the parameter "phone_number" is not registered, this function raise an "UnregisteredPhoneNumberException".
    '''

    # regularize the input string "phone_number" to match registered phone numbers.
    phone_number_wo_hyphen = phone_number.replace('-','')

    # match and get the correct clientName
    if (phone_number_wo_hyphen == "+8150312345678"):
        clientName= "DeviceId_0001"

    else:
        raise UnregisteredPhoneNumberException('Unregistered phone number "' + phone_number + '"')

    return clientName


def lambda_handler(event, context):
    ''' Creates and responds a Twilio Capability Token.
    This function will be received a json formatted string like below.
    {"twilioPhoneNumber": "+81-50-1234-9876"}

    This function will return a json formatted string like below.
    If this function succeed, the parameter "success" will be set as true.
    {"capabilityToken": capabilityToken, "success": true}
    '''

    # configure parameters belong to Twilio
    twilio_account_sid = "{{twilio_accound_sid}}"
    twilio_auth_token = "{{twilio_account_auth_token}}"
    twilio_app_sid = "{{twilio_app_sid}}"
    expiration_time_for_capability_token = 3600


    # get necessary parameter from "event" and "context"
    twilio_phone_number = event.get("twilioPhoneNumber")


    # Create a Capability token with twilio_account_sid and its twilio_auth_token
    # It enables a Twilio client to receive an incoming call and to make an outgoing call.
    try:
        capability = TwilioCapability(twilio_account_sid, twilio_auth_token)
        capability.allow_client_incoming(get_client_name_by_phone_number(twilio_phone_number))
        capability.allow_client_outgoing(twilio_app_sid)
        capabilityToken = capability.generate(expiration_time_for_capability_token)
        res = {"capabilityToken": capabilityToken, "success": True}

    except UnregisteredPhoneNumberException:
        res = {"capabilityToken": None, "success": False}

    return res

作成したLambda関数を見ていきましょう。

まず、環境に依存する値として以下の項目があります。ご利用の環境に応じて読み替えてください。

  • lambda_handler関数内
    # configure parameters belong to Twilio
    twilio_account_sid = "{{twilio_accound_sid}}"
    twilio_auth_token = "{{twilio_account_auth_token}}"
    twilio_app_sid = "{{twilio_app_sid}}"
    expiration_time_for_capability_token = 3600

当該関数の冒頭にて、TwilioアカウントSID及びこれに紐づくAuth Token、事前に作成したApp SIDを指定していますので、環境に合わせて変更してください。
Capability Tokenの有効期限はここでは3600秒としていますが、これも適宜変更してください。

  • get_client_name_by_phone_number関数内
    # match and get the correct clientName
    if (phone_number_wo_hyphen == "+815012349876"):
        clientName = "DeviceId_0001"

Twilioアカウントセットアップ時に取得したTwilio電話番号を指定してください。clientNameは好みに応じて変更してください。
なおこの関数は、現在はTwilio電話番号とClient Nameの紐付けをソースコード内に持っていますが、将来的にはDB接続を行いDBから紐付け情報を取り出すことを想定しています。

処理の流れは以下のようになります。

  • Lambda関数へのInputパラメータから「twilioPhoneNumber」を取得する。
  • TwilioCapabilityオブジェクトを作成する。(※これはまだCapability Tokenではありません。)
  • 「twilioPhoneNumber」から、これに紐付けられたClient Nameを取得する。(get_client_name_by_phone_number関数を実行する)
  • TwilioCapabilityオブジェクトに、インバウンド通話を許可する。引数には、上記で取得したClient Nameを指定する。
  • TwilioCapabilityオブジェクトに、アウトバウンド通話を許可する。引数には、事前に作成したTwiML AppのSIDを指定する。
  • TwilioCapability#generate関数を実行し、Capability Tokenを生成する。引数には有効期限を指定する。
  • Outputとして返すデータをdictionary型として生成する。
  • 生成したデータを返却する。(※Lambda関数からの戻り値としてdictionary型を返すと、自動的にJSONに変換されて返却されます。)

デプロイ

このLambda関数はtwilio.utilパッケージを必要とします。そのため、AWS Management Consoleのインラインエディタから上記ソースコードを貼り付けただけでは動きません。
デプロイパッケージを作成し、これをアップロードする必要があります。下記の手順に従い、デプロイパッケージ(zipファイル)を作成します。

デプロイパッケージの作成 (Python)
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python-how-to-create-deployment-package.html

適当なLinuxサーバにログインし下記のように実行します。ソースコードと必要なライブラリを全てカレントディレクトリ配下に配置し、このディレクトリをzip圧縮します。

# Twilioライブラリの依存関係の情報を取得するために、Twilio Client Quickstart for PythonをCloneする
# 必要なのはrequirements.txtだけなので、これを手動でもってきても良い
$ git clone https://github.com/TwilioDevEd/client-quickstart-python

# 作業用ディレクトリを作成する
$ mkdir ./twilio_on_aws
$ cd ./twilio_on_aws

# ソースコードを作成する。先のソースをコピー&ペーストする。
$ vi TwilioRetrieveCapabilityToken.py

# 関連ライブラリを作業用ディレクトリにインストールする
$ pip install -r ../client-quickstart-python/requirements.txt -t ./

# ソースコード・関連ライブラリをzipファイルに圧縮、デプロイパッケージを作成する
$ zip -r ../TwilioRetrieveCapabilityToken.zip ./

上記のデプロイパッケージができたら、AWS Management Consoleからアップロードします。
LambdaFunction_TwilioRetrieveCapabilityToken_1.png

Configurationは、Handlerの設定さえ間違えなければあとはデフォルトで問題ないと思います。このLambda関数はAWSの他のサービスを利用しないので、IAM Roleはlambda_basic_executionでかまいません。
LambdaFunction_TwilioRetrieveCapabilityToken_2.png

以上で、CapabilityToken取得APIの実装とデプロイは完了です。

1-2. Incoming Call用TwiML返却APIを、Lambda関数として実装する

インターフェース(Input/Output)

Twilio ServerとAPI Serverのインターフェースの詳細は下記にまとめました。
外部電話からTwilioクライアントへのCall(IncomingCall)

InputとしてTwilio Serverから渡されるパラメータはHTTP GETパラメータの構造に近く、またOutputはXMLを返す必要があるため、いずれもJSONとの親和性は悪そうです。
Lambda関数内でデータ構造の変換処理を行うことも可能ですが、本質的ではない処理を記述するのはアーキテクチャ的にあまり好ましくありません。しかし、JSONに拘泥するのも時間を浪費します。
そこで、Inputについてのみ変換処理をAPI Gatewayに任せます。InputはAPI GatewayでJSON形式に変換するものとします。OutputはLambda関数でXMLを生成してそのまま返すこととします。

Twilio ServerからのHTTP POSTリクエストをAPI Gatewayで変換してもらい、このAPIは下記のようなJSONを期待するものとします。

Input
{
    "AccountSid": "AC3exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "ApiVersion": "2010-04-01",
    "Called": "+815031234567",
    "CalledCountry": "JP",
    "CalledVia": "815031234567",
    "Caller": "+819059876543",
    "CallerCountry": "JP",
    "CallSid": "CA1fnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn",
    "CallStatus": "ringing",
    "Direction": "inbound",
    "ForwardedFrom": "815031234567",
    "From": "+819059876543",
    "FromCountry": "JP",
    "To": "+815031234567",
    "ToCountry": "JP"
}

TwiMLをそのまま返却することとします。

Output
<?xml version="1.0\" encoding="UTF-8"?><Response><Dial timeout="60"><Client>DeviceId_0001</Client></Dial></Response>

実装

それでは実際にLambda関数を書いていきましょう。

TwilioRetrieveTwimlCallIncoming.py
from __future__ import print_function
import json

print('Loading function')

def get_client_name_by_phone_number(phone_number):
    # regularize the input string "phone_number" to match registered phone numbers.
    phone_number_wo_hyphen = phone_number.replace('-','')

    # match and get the correct clientName
    if (phone_number_wo_hyphen == "+815031234567"):
        clientName = "DeviceId_0001"

    else:
        clientName = None

    return clientName

def lambda_handler(event, context):
    ''' Returns a TwiML to enable a real phone to call a Twilio device.
    This function will be received the following parameters.

    {
        "Caller": "+819012345678",
        "To": "+815098765432"
    }
    '''

    call_incoming_phone_number = event.get("To")
    client_name = get_client_name_by_phone_number(call_incoming_phone_number)

    if (client_name is None):
        res = '<?xml version="1.0" encoding="UTF-8"?><Response><Say language="en-US" loop="0">An error occurred. Your Twilio phone number {CallIncomingPhoneNumber} is invalid.</Say></Response>'
    else:
        res = '<?xml version="1.0\" encoding="UTF-8"?><Response><Dial timeout="60"><Client>{ClientName}</Client></Dial></Response>'


    strfmt = {"ClientName": client_name, "CallIncomingPhoneNumber": call_incoming_phone_number}

    return res.format(**strfmt)

作成したLambda関数を見ていきましょう。

まず、環境に依存する値として以下の項目がありますので、書き換えましょう。

def get_client_name_by_phone_number(phone_number):
    # 中略

    if (phone_number_wo_hyphen == "+815031234567"):
        clientName = "DeviceId_0001"

Twilioアカウントセットアップ時に取得したTwilio電話番号を指定してください。clientNameは好みに応じて変更してください。
このget_client_name_by_phone_number関数は、将来的にはDB接続を行いDBから紐付け情報を取り出すことを想定しています。便宜上、現時点ではTwilio電話番号とClient Nameの紐付けをソースコード内に持つこととします。

処理の流れは以下のようになります。

  • Lambda関数へのInputパラメータから「To」を取得する。これは受信先のTwilio電話番号となる
  • 受信先のTwilio電話番号に対応するClient Nameを取得する
  • 取得したClient Nameを元に、これに電話をかけるためのTwiMLを生成する。受信先のTwilio電話番号が事前に登録したものでなければ、エラーメッセージを読み上げるためのTwiMLを生成する。
  • 生成したTwiMLをそのまま返す。

デプロイ

このLambda関数は外部のライブラリを必要としないため、AWS Management Consoleのインラインエディタからデプロイすることができます。
LambdaFunction_TwilioRetrieveTwimlCallIncoming_1.png

Configurationは、Handlerの設定さえ間違えなければあとはデフォルトで問題ないと思います。このLambda関数はAWSの他のサービスを利用しないので、IAM Roleはlambda_basic_executionでかまいません。
LambdaFunction_TwilioRetrieveTwimlCallIncoming_2.png

1-3. Outgoing Call用TwiML返却APIを、Lambda関数として実装する

インターフェース(Input/Output)

Twilio ServerとAPI Serverのインターフェースの詳細は下記にまとめました。
Twilioクライアントから外部電話へのCall(OutgoingCall)

これもIncoming Call用TwiML返却APIと同様、InputをAPI Gatewayに変換してLambda関数に入力、Lambda関数からTwiMLを直接返却する方式とします。

Twilio ServerからのHTTP POSTリクエストをAPI Gatewayで変換してもらい、このAPIは下記のようなJSONを期待するものとします。

Input
{
    "Direction": "inbound",
    "CallSid": "CA48nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn",
    "From": "client:DeviceId_0001",
    "Caller": "client:DeviceId_0001",
    "ApiVersion": "2010-04-01",
    "ApplicationSid": "APf7xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "AccountSid": "AC3exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "CallStatus": "ringing",
    "callerPhoneNumber": "+81-50-3123-4567",
    "callOutgoingPhoneNumber": "+81-90-5987-6543"
}

TwiMLをそのまま返却することとします。

Output
<?xml version="1.0" encoding="UTF-8"?><Response><Dial timeout="60" callerId="+81-50-3123-4567"><Number>+81-90-5987-6543</Number></Dial></Response>

実装

それでは実際にLambda関数を書いていきましょう。

TwilioRetrieveTwimlCallOutgoing.py
from __future__ import print_function
import json

print('Loading function')

def lambda_handler(event, context):
    ''' Returns a TwiML to enable a Twilio device to call a real phone.
    This function will be received the following parameters.

    {
        "callerPhoneNumber": "+81-50-3123-4567",
        "callOutgoingPhoneNumber": "+81-90-59876543"
    }
    '''

    print ("event: " + json.dumps(event))
    caller_phone_number = event.get("callerPhoneNumber")
    call_outgoing_phone_number = event.get("callOutgoingPhoneNumber")


    res = '<?xml version="1.0" encoding="UTF-8"?><Response><Dial timeout="60" callerId="{callerId}"><Number>{callOutgoingPhoneNumber}</Number></Dial></Response>'
    strfmt = {"callerId": caller_phone_number, "callOutgoingPhoneNumber": call_outgoing_phone_number}

    return res.format(**strfmt)

このLambda関数は、Inputとして渡されたパラメータを元にTwiMLを生成するだけの処理となります。従って、環境に依存する箇所はありません。

処理の流れは以下のようになります。

  • Lambda関数へのInputパラメータから「callerPhoneNumber」(発信元Twilio電話番号)「callOutgoingPhoneNumber」(発信先の通常電話番号)を取得する。これはTwilio Clientから渡されるカスタムパラメータとなる。
  • 発信者番号に「callerPhoneNumber」を、発信先に「callOutgoingPhoneNumber」を指定したTwiMLを生成する。
  • 生成したTwiMLをそのまま返す。

デプロイ

このLambda関数は外部のライブラリを必要としないため、AWS Management Consoleのインラインエディタからデプロイすることができます。
LambdaFunction_TwilioRetrieveTwimlCallOutgoing_1.png

Configurationは、Handlerの設定さえ間違えなければあとはデフォルトで問題ないと思います。このLambda関数はAWSの他のサービスを利用しないので、IAM Roleはlambda_basic_executionでかまいません。
LambdaFunction_TwilioRetrieveTwimlCallOutgoing_2.png

まとめ

上記で必要なAPIの実装は完了です。必要なパラメータをJSONで入力し、意図した結果が返ることを確認しましょう。
AWS Management Consoleから簡単に動作確認ができます。
Input/Outputの形式をまとめておきます。

API名 Input Output
CapabilityToken取得API JSON JSON
Incoming Call用TwiML返却API JSON XML
Outgoing Call用TwiML返却API JSON XML

2. API Gatewayの設定

2-1. API Gatewayの利用にあたって

AWSのAPI Gatewayは、RESTfulなAPIを実現するにあたり便利な機能が用意されています。独特な概念があるので、あらかじめこれを抑えておきましょう。

Amazon API Gateway の概念
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-basic-concept.html

このWalkthroughにおいて重要な概念は以下となりますのでおさえておきましょう。

  • API Gateway API
    • APIを管理する単位。
  • Resource
    • APIのURLを構成する要素(≒パス)。Resourceには後述するMethodを定義することができる。
  • Method
    • いわゆるHTTPメソッド。各ResourceにGETやPOST、PUTなどのMethodを定義し、Lambda関数などの処理を紐付ける。
  • Method Request
    • 外部からのHTTPリクエストを受け取る際のインターフェースを定義する。URLクエリ文字列に必要な文字列を追加する、HTTPヘッダ任意の値を追加する、特定のデータ構造(Model)を定義することでValidationを容易にする、といったことができる。
  • Integration Request
    • Method Requestで加工されたデータをバックエンドでどのように処理するかを定義する。バックエンド処理として、Lambda関数に紐付ける、外部のRESTful APIを実行する、他のAWS APIを呼び出す、といった定義ができる。
    • Body Mapping Templateを定義・適用することで、Method Requestから渡されたデータを加工し、バックエンドに渡すことができる。※このWalkthroughでは重要な役割を担います。
  • Intagration Response
    • バックエンドから返却されたデータをどのように返すか定義する。バックエンドから返却されたデータの内容を判別し任意のHTTPステータスコードを設定する、任意のHTTPレスポンスヘッダを付与する、HTTPレスポンスボディの内容を変換する、といったことができる。
    • Body Mappiing Templateを定義・適用することで、バックエンドからの返却データを変換することができる。※このWalkthroughでは重要な役割を担います。
  • Method Response
    • Integration Responseから返却されたデータをクライアントに返す際に、HTTPステータスコード・HTTPレスポンスヘッダを加工、定義することができる。
    • クライアントに返却するデータ構造をModelとして定義することができる。APIの実装上あまりメリットはないが、API Gateway SDKをクライアントアプリに組み込む際に、強い型付けの実装とすることができる。
  • API デプロイとStage
    • API Gateway APIにResourceやMethodを定義しただけでは、外部から呼び出せる状態にはならない。Stageにデプロイすることで初めて呼び出し可能となる。
    • Stageとは、実行環境切り替えのための仕組み。devやprodなどを定義して運用することが多い。
    • デプロイとは、ある時点のResource・Methodの状態(≒スナップショット)をStageに適用するもの。

上記概念はドキュメントを読んだだけではピンとこないと思います。下記のチュートリアルを試して、API Gateway独自の感覚をつかみ、概念を理解しておきましょう。

Amazon API Gateway の使用開始
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/getting-started-intro.html

2-1. AWS Management ConsoleのAmazon API Gatewayのメニューから、APIを作成する

AWS Management ConsoleからAmazon API Gatewayのコンソールを開きます。

「Create APIs」ボタンを押し、新たなAPI Gateway APIを作成します。ここでは「twilio_on_aws」という名称で作成します。作成すると「APIs」一覧の中にAPI Gateway APIが追加されます。
APIGateway_Create_4.png

2-2. 各API用のResourceを定義する

API Gateway APIができたら必要なResourceを追加していきます。Resource≒APIのパスの構成要素のようなものと思えば良いでしょう。

まずは「/twilio」というResourceを作成します。この下に必要なResourceを追加していき、最終的に下記のパスを構成できるようにします。
なお、Resource NameとResource Pathはそれぞれ別に指定しますが、別に指定するメリットはあまりないと思いますので同一にします。デフォルトでResource Pathの区切り文字が「-」になりますが、これは「_」にします。

/twilio/retrieve_capability_token
/twilio/retrieve_twiml/call_incoming
/twilio/retrieve_twiml/call_outgoing

APIGateway_Resources_1.png

2-3. CapabilityToken取得API(/twilio/retrieve_capability_token)用のMethodを定義する

/twilio/retrieve_capability_tokenを選択し、「Actions」から「Create Method」を選択します。Methodは「POST」を選択します。
このMethodをどのような実装に結びつけるか選択する画面に遷移するので、下記のように値を設定し、先に作成したLambda関数に紐付けます。

項目名 設定値
Integration Type Lambda Function
Lambda Region ap-northeast-1
Lambda Function TwilioRetrieveCapabilityToken

APIGateway_retrieve_capability_token_1.png

「Save」ボタンを押すと「Add Permission to Lambda Function」というポップアップが表示されるので「OK」を押します。
APIGateway_retrieve_capability_token_2.png

これはLambda関数ごとに設定される、「Lambda関数ポリシー」を変更したことを意味します。Resource「/twilio/retrieve_capability_token」から、Lambda関数「TwilioRetrieveCapabilityToken」を実行する権限を付与しています。
Lambda関数ポリシーの詳細については下記ドキュメントを参照してください。

AWS Lambda でリソースベースのポリシーを使用する (Lambda 関数ポリシー)
https://docs.aws.amazon.com/{ja_jp/lambda/latest/dg/access-control-resource-based.html

以上で、CapabilityToken取得APIの実装は完了です。

「TEST」から、作成したAPI Gateway及びLambda関数のテストができますので、既定のInputを指定してAPIを実行し、意図した結果が返ってくることを確認しましょう。

RequestBody
{
  "twilioPhoneNumber": "+81-50-1234-9876"
}
ResponseBody
{
  "success": true,
  "capabilityToken": "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ...."
}

なお、この時点ではまだRESTful APIとして、他のホストから呼び出すことはできません。後述するDeploy作業が必要になります。

2-4. Incoming Call用TwiML返却API(/twilio/retrieve_twiml/call_incoming)用のMethodを定義する

/twilio/retrieve_twiml/call_incomingを選択し、「Actions」から「Create Method」を選択します。Methodは「POST」を選択します。
このMethodをどのような実装に結びつけるか選択する画面に遷移するので、下記のように値を設定し、先に作成したLambda関数に紐付けます。

項目名 設定値
Integration Type Lambda Function
Lambda Region ap-northeast-1
Lambda Function TwilioRetrieveTwimlCallIncoming

APIGateway_retrieve_twiml_call_incoming_1.png

「Save」ボタンを押すと「Add Permission to Lambda Function」というポップアップが表示されるので「OK」を押します。これも同様に「Lambda関数ポリシー」を変更します
APIGateway_retrieve_twiml_call_incoming_2.png

Resource/Methodの設定ができたら、いったんテストをしましょう。「TEST」からテストができます。Request Bodyに下記のようなJSONを指定し、意図したTwiMLが返却されるかテストします。

RequestBody
{
    "Caller": "+819059876543",
    "From": "+819059876543",
    "Called": "+815031234567",
    "To": "+815031234567"
}
ResponseBody
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Dial timeout=\"60\"><Client>DeviceId_0001</Client></Dial></Response>"

今回構築したAPIは、上記までの作業に加え、次の2つのことを実現する必要があります。

  • APIへの入力を変換する(「&」で区切られたKey=ValueペアからJSONへの変換)
    • Twilio ServerからのリクエストはJSONではなく、GETパラメータのようなKey=Valueペアとなりますので、これを変換する必要があります。(Twilio Serverからのリクエストは、application/jsonではなく、application/x-www-form-urlencodedとなります)
  • レスポンスの自動エスケープ処理の停止
    • レスポンスのTwiMLを確認すると、「"」を含んでいた場合、その直前にエスケープ記号「\」が自動的に挿入されていることがわかります。このままではWell-formedなXMLになりませんので、LambdaからのOutputを何も加工せず返却できるように設定する必要があります。

レスポンスの自動エスケープ処理の停止

まずは、レスポンスの自動エスケープ処理を停止させます。これは「Integration Response」で設定します。
「Integration Response」を開き、「Method response status」が200の行を展開したのち、「Body Mapping Templates」を展開します。
デフォルトで「application/json」が指定されているので、これを「-」ボタンを押して外します。
APIGateway_retrieve_twiml_call_incoming_3.png

その後「Add mapping template」の「+」ボタンを押し、Content-Typeとして「application/xml」を追加します。併せて「Body Mapping Templates」の内容を指定できるので、下記を1行指定し、「Save」ボタンを押します。

$input.path('$')

APIGateway_retrieve_twiml_call_incoming_4.png

先ほどと同様のRequestでMethodのTESTを行い、レスポンス内容を確認しましょう。「"」が除去され、Well-formedなXMLになっています。

ResponseBody
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Dial timeout="60"><Client>DeviceId_0001</Client></Dial>
</Response>

APIへの入力を変換する(「&」で区切られたKey=ValueペアからJSONへの変換)

Twilio Serverから渡されるパラメータはJSONではなく、GETパラメータのようなKey=Valueペアです。これを変換するためには、「Integration Request」で「Body Mapping Template」を定義する必要があります。

「Integration Request」を開き、「Body Mapping Templates」を展開します。
「Request body passthrough」から「Wehen there are no templates defined (recommended)」をチェックします。
「Add mapping template」の「+」ボタンを押し、「application/x-www-form-urlencoded」を追加します。
APIGateway_retrieve_twiml_call_incoming_6.png

「Body Mapping Template」として、下記の内容を貼り付けし「Save」を押します。

#set($raw_input = $input.path("$"))

{
  #foreach( $kvpairs in $raw_input.split( '&' ) )
    #set( $kvpair = $kvpairs.split( '=' ) )
      #if($kvpair.length == 2)
        "$kvpair[0]" : "$kvpair[1]"#if( $foreach.hasNext ),#end
      #end
  #end
}

APIGateway_retrieve_twiml_call_incoming_7.png

Mapping Templateの詳細は下記記事にまとめました。必要に応じて参照してください。

API GatewayのMapping Templateで、GETクエリパラメータのような文字列をJSONに変換する

設定後、テストをしたいところですが、リクエストパラメータを指定できず、うまくテストができないようです。
APIGateway_retrieve_twiml_call_incoming_8.png

そのため、Deploy後に動作確認を行います。
Outgoing Call用TwiML返却APIを作成する前に、2-7及び2-8の作業を実施して、動作確認しておきましょう。

2-5. Outgoing Call用TwiML返却API(/twilio/retrieve_twiml/call_outgoing)用のMethodを定義する

twilio/retrieve_twiml/call_outgoingを選択し、「Actions」から「Create Method」を選択します。Methodは「POST」を選択します。
このMethodをどのような実装に結びつけるか選択する画面に遷移するので、下記のように値を設定し、先に作成したLambda関数に紐付けます。
APIGateway_retrieve_twiml_call_outgoing_2.png

項目名 設定値
Integration Type Lambda Function
Lambda Region ap-northeast-1
Lambda Function TwilioRetrieveTwimlCallOutgoing

「Save」ボタンを押すと「Add Permission to Lambda Function」というポップアップが表示されるので「OK」を押します。これも同様に「Lambda関数ポリシー」を変更します
APIGateway_retrieve_twiml_call_outgoing_2.png

Resource/Methodの設定ができたら、いったんテストをしましょう。「TEST」からテストができます。Request Bodyに下記のようなJSONを指定し、意図したTwiMLが返却されるかテストします。

RequestBody
{
    "callerPhoneNumber": "+81-50-3123-4567",
    "callOutgoingPhoneNumber": "+81-90-5987-6543"
}
ResponseBody
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response><Dial timeout=\"60\" callerId=\"+81-50-3123-4567\"><Number>+81-90-5987-6543</Number></Dial></Response>"

このAPIもIncoming Call用TwiML返却APIと同様、上記までの作業に加え、次の2つのことを実現する必要があります。

  • APIへの入力を変換する(「&」で区切られたKey=ValueペアからJSONへの変換)
    • Twilio ServerからのリクエストはJSONではなく、GETパラメータのようなKey=Valueペアとなりますので、これを変換する必要があります。
  • レスポンスの自動エスケープ処理の停止
    • レスポンスのTwiMLを確認すると、「"」を含んでいた場合、その直前にエスケープ記号「\」が自動的に挿入されていることがわかります。このままではWell-formedなXMLになりませんので、LambdaからのOutputを何も加工せず返却できるように設定する必要があります。

レスポンスの自動エスケープ処理の停止

まずは、レスポンスの自動エスケープ処理を停止させます。これは「Integration Response」で設定します。
「Integration Response」を開き、「Method response status」が200の行を展開したのち、「Body Mapping Templates」を展開します。
デフォルトで「application/json」が指定されているので、これを「-」ボタンを押して外します。
APIGateway_retrieve_twiml_call_outgoing_3.png

その後「Add mapping template」の「+」ボタンを押し、Content-Typeとして「application/xml」を追加します。併せて「Body Mapping Templates」の内容を指定できるので、下記を1行指定し、「Save」ボタンを押します。

$input.path('$')

APIGateway_retrieve_twiml_call_outgoing_4.png

先ほどと同様のRequestでMethodのTESTを行い、レスポンス内容を確認しましょう。「"」が除去され、Well-formedなXMLになっています。

ResponseBody
<?xml version="1.0" encoding="UTF-8"?><Response><Dial timeout="60" callerId="+81-50-3123-4567"><Number>+81-90-5987-6543</Number></Dial></Response>

APIへの入力を変換する(「&」で区切られたKey=ValueペアからJSONへの変換)

Twilio Serverから渡されるパラメータはJSONではなく、GETパラメータのようなKey=Valueペアです。これを変換するためには、「Integration Request」で「Body Mapping Template」を定義する必要があります。

「Integration Request」を開き、「Body Mapping Templates」を展開します。
「Request body passthrough」から「Wehen there are no templates defined (recommended)」をチェックします。
「Add mapping template」の「+」ボタンを押し、「application/x-www-form-urlencoded」を追加します。
APIGateway_retrieve_twiml_call_outgoing_6.png

「Body Mapping Template」として、下記の内容を貼り付けし「Save」を押します。

#set($raw_input = $input.path("$"))

{
  #foreach( $kvpairs in $raw_input.split( '&' ) )
    #set( $kvpair = $kvpairs.split( '=' ) )
      #if($kvpair.length == 2)
        "$kvpair[0]" : "$kvpair[1]"#if( $foreach.hasNext ),#end
      #end
  #end
}

APIGateway_retrieve_twiml_call_outgoing_7.png

Mapping Templateの詳細は下記記事にまとめました。必要に応じて参照してください。

API GatewayのMapping Templateで、GETクエリパラメータのような文字列をJSONに変換する

設定後、テストをしたいところですが、リクエストパラメータを指定できず、うまくテストができないようです。
APIGateway_retrieve_twiml_call_outgoing_8.png

そのため、Deploy後に動作確認を行います。
先に2-7及び2-8の作業を実施して、動作確認しておきましょう。

2-6. CORSを設定する

Methodを作成したら、各ResourceにCORS(Cross Origin Resource Sharing)を許容する設定をしておきましょう。
これは、JavaScriptで実装されるWebブラウザアプリに適用される仕様で、ドメインをまたいだAPIの呼び出しを許容/拒否する設定になります。
今回実装するTwilio Clientは、HTML自体はS3のドメインである https://twilio-on-aws-us.s3.amazonaws.com で提供されます。しかしながら、APIは https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/ といったAPI Gateway用のドメインで提供されます。
従って、現在作成しているAPIは、S3ドメインのコンテンツからのAPI呼び出しを許容するよう、CORSを設定する必要があります。

CORSの詳しい説明は下記のサイト等を参照してください。

HTTP アクセス制御 (CORS)
https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control

本来は、どのドメインのコンテンツからAPIを呼び出せるか、個別のドメイン名を指定する必要がありますが、このWalkthroughではそこまでの制御を行わず、全てのドメインからAPI呼び出しを許容することとします。

この場合の設定は簡単です。対象Resourceを選択した状態で「Actions」から「Enable CORS」を選択します。
付与するヘッダをカスタマイズできますが、そのまま「Enable CORS and replace existing CORS headers」ボタンを押します。
APIGateway_CORS_1.png

確認のウィンドウが表示されるのでそのまま「Yes, replace existing values」ボタンを押します。
APIGateway_CORS_2.png

Methodに「OPTIONS」が追加されています。「Integration Response」及び「Method Response」の設定を確認すると、CORSの設定が追加されていることがわかります。

APIGateway_CORS_3.png
APIGateway_CORS_4.png

APIを提供するResource全てに同様の設定を行いましょう。

2-7. prodステージを作成しDeployする

必要なResource・Methodの定義が完了したら、これを外部から呼び出せるようにDeployします。
まず、「Actions」から「Deploy API」を選択します。
最初はStageが定義されておりません。「Deployment stage」から「[New Stage]」を選択します。
各値を下記のように設定し、「Deploy」ボタンを押します。

項目名 設定値
Stage name* prod
Stage description ※任意。ここではProduction Environmentとしています。
Deployment description ※任意。ここではFirst releaseとしています。

APIGateway_Deploy_1.png

Deploy後、左ペインの「APIs」→「API名」→「Stages」から、作成されたStageを確認することができます。
「Invoke URL」が、外部から利用できるURLとなります。
APIGateway_Deploy_2.png

Stage名を展開すると、これまでに作成したResource・Methodがそのままマッピングされていることが分かります。
動作確認に必要ですので、各APIの具体的なInvoke URLを確認しておきましょう。
APIGateway_Deploy_3.png

2-8. 各APIの動作確認を行う

動作確認はcurlで行います。事前にLinuxインスタンスを作っておきましょう。
なお、curlは「-v」オプションを指定することで、詳細な情報を出力することができます。うまく動かなかった場合には「-v」オプションを活用し、トラブルシュートしましょう。

CapabilityToken取得API

Linux上で下記のコマンドを実行します。

$ curl -v -H "Content-type: application/json" -X POST -d '{"twilioPhoneNumber": "+81-50-3123-4567"}' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/twilio/retrieve_capability_token

# 既定のレスポンスが返却されることを確認する。
{"success": true, "capabilityToken": "eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9....."}

Incoming Call用TwiML返却API

Linux上で下記のコマンドを実行します。
POSTリクエストのBodyは、インバウンド通話時のTwilio ServerからのHTTP POSTリクエストと同様の内容を指定します。

curl -v -H "Content-type: application/x-www-form-urlencoded; charset=UTF-8" -X POST -d 'ApplicationSid=AP75zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz&ApiVersion=2010-04-01&Called=&Caller=%2B819059876543&CallStatus=ringing&CallSid=CCA86nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn&To=%2B815031234567&From=%2B819059876543&Direction=inbound&AccountSid=AC3exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/twilio/retrieve_twiml/call_incoming

# 既定のレスポンスが返却されることを確認する。
<?xml version="1.0" encoding="UTF-8"?><Response><Dial timeout="60"><Client>DeviceId_0001</Client></Dial></Response>

Outgoing Call用TwiML返却API

Linux上で下記のコマンドを実行します。
POSTリクエストのBodyは、アウトバウンド通話時のTwilio ServerからのHTTP POSTリクエストと同様の内容を指定します。

curl -v -H "Content-type: application/x-www-form-urlencoded; charset=UTF-8" -X POST -d 'AccountSid=AC3exxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&ApiVersion=2010-04-01&ApplicationSid=AP75zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz&Called=&Caller=client%3ADeviceId_0001&CallSid=CA06nnnnnnnnnnnnnnnnnnnnnnnnnnnnnn&CallStatus=ringing&Direction=inbound&From=client%3ADeviceId_0001&To=&callerPhoneNumber=%2B81-50-3123-4567&callOutgoingPhoneNumber=%2B81-90-5987-6543' https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/twilio/retrieve_twiml/call_outgoing

# 既定のレスポンスが返却されることを確認する。
<?xml version="1.0" encoding="UTF-8"?><Response><Dial timeout="60" callerId="+81-50-3123-4567"><Number>+81-90-5987-6543</Number></Dial></Response>

以上で、API Server側の実装は完了です。
次からTwilio Clientを実装していきます。