AWS Lambda上でPythonを使ってTwilioアプリケーションを書く時のハマり所と知っておくと嬉しい小ネタ

  • 2
    いいね
  • 0
    コメント

意外とハマり所が多かったので、Qiitaの記事にします。

今回Twilioアプリケーションのホスティング先として、AWS Lambda+API Gatewayを採用しました。以下のような構成図ですね。

Untitled Diagram.png

TwilioでPythonの開発を行う場合、公式サイトではFlaskを使った事例が紹介されていますが、AWS LambdaではAPI Gatewayを経由しないとHTTPのリクエストを受け付ける事が出来ません。しかも、AWS Lambdaではどんどん新しいソリューションが作られていきますので、既存の情報がすぐに古くなってしまいます。

色々試行錯誤をした結果、LambdaやAPI Gatewayへのデプロイを支援してくれるフレームワークがある事を知りました。この辺のフレームワークを上手に使わないと、API GatewayやIAM Role周りが面倒くさすぎます。

awslabs/chalice: Python Serverless Microframework for AWS
Miserlou/Zappa: Serverless Python Web Services

参考までに、Lamberyは採用しませんでした。Lamberyでは複数個の関数を書けないので、Twilioアプリケーション向きではないです。

Chaliceを使った場合

今回は支援フレームワークとしてChaliceを採用しました。所が、このChaliceにハマり所が結構あるんですね。

UnicodeDecodeError で止まってしまい、デプロイが出来ない

今回の開発はWindows上で行ったのですが、chalice deployを実行した所、必ずUnicodeDecodeErrorが発生してデプロイを進める事が出来ず、随分悩みました。

Creating Role
UnicodeDecodeError: 'cp932' codec can't decode byte 0xef in position xxx: illegal multibyte sequence

このエラー、ChaliceがIAM Roleを生成するためにソースコードを読み込んでいるのですが、その際にUnicode文字がソースコードに含まれていると適切にソースコードを扱う事が出来ずに吐かれたものです。日本人がTwilioアプリケーションを書こうとしているわけだから、Unicode文字である日本語が含まれるのはある意味当然のことです。日本語をコメント含めてソースコードから完全に削除すればdeployは成功するのですが、これでは目的を達成することが出来ません。

この'cp932'はShift-JISの事ですから、要はWindows側の文字コードと競合してるのですね。このエラーを回避するべく色々と試行錯誤して、解決方法を見つけました。IAM Roleを生成している箇所でエラーが発生していますので、chalice new-projectしてからすぐにdeployを実行してdeployed.jsonを生成してしまいます。

deployed.json
{
  "dev": {
    "api_handler_name": "twilio-hoge-dev",
    "api_handler_arn": "arn:aws:lambda:ap-northeast-1:xxxxxx01:function:twilio-hoge-dev",
    "region": "ap-northeast-1",
    "api_gateway_stage": "dev",
    "backend": "api",
    "chalice_version": "0.9.0"
  }
}

このファイルに含まれる"api_handler_arn"を"iam_role_arn"としてconfig.jsonに書き込んでしまいます。併せて"manager_iam_role"を行わないように設定します。

config.json
{
  "version": "2.0",
  "app_name": "twilio-hoge",
  "stages": {
    "dev": {
      "manage_iam_role": false,
      "iam_role_arn": "arn:aws:lambda:ap-northeast-1:xxxxxx01:function:twilio-hoge-dev",
      "api_gateway_stage": "dev"
    }
  }
}

これでエラーの原因となるIAM Roleの自動生成を回避することが出来ます。この問題、Chaliceのバグっぽいのですが、まだPython使いとしては駆け出しの僕では、原因個所を特定できませんでした。orz

正直バッドノウハウな解決方法ですが、現時点ではこの方法で乗り切ってます。あと、Windows以外の環境ではこのトラブルは発生しない可能性が高いです。

GET/POSTの併用で難儀し、GETに統一した

Twilio側ではPOSTでの利用がデフォルトになっていましたので、ChaliceでもGET/POST両方を受け入れる設定で開発を進めていました。所が、AWS Lambdaと連携するAPI Gatewayではapplication/jsonを扱うのが標準になっていまして、Twilio側からPOSTで飛んでくるapplication/x-www-form-urlencodedを適切に解釈することが出来ません。Twilio側のデバッカーで見ても415が返ってくるだけだったので、どこに原因があるのか切り分けるのに随分苦労しました…。

API Gateway側で頑張って変換マクロを書いたり、Chaliceアプリケーション側で頑張ってGET/POST別の処理を書けば解決する可能性はありますが、シンプルに記述する趣旨から離れる一方です。本来はフレームワーク側で面倒を見てほしい部分ですが、Chalice自身、そういう設計思想ではないのでしょうね。

そこで、今回はPOSTを扱う事を断念して、GETに統一しました。Twilio開発では基本HTTPリクエストに対してTwimlを返すという流れでアプリケーションが動作するのですが、バイナリデータを直接扱うわけではないのでPOSTを無理して使い続けるメリットが薄いのですね。

application/xmlを明示して返す

折角ですので、今回書いたアプリケーションの一部を掲載します。Lambda特有の事項として、Twimlを返す時にapplication/xmlだと明示して返さないと、API Gatewayがapplication/jsonと判断してTwilio側に返してしまうのですね。API Gateway側の挙動を変えようとしたのですが、問題が解決しなかったのでアプリケーション側で解決させました。

app.py
import re

from chalice import Chalice, Response
from twilio.twiml.voice_response import VoiceResponse

app = Chalice(app_name='twilio-hoge')


@app.route("/hello", methods=['GET'])
def hello():
    """Respond to incoming phone calls with a 'Hello world' message"""
    # Start our TwiML response
    resp = VoiceResponse()

    # Read a message aloud to the caller
    resp.say("こんにちは!ぼくはアリスです!", language='ja-JP', voice='alice')

    return Response(body=str(resp),
                    status_code=200,
                    headers={'Content-Type': 'application/xml'})

Chaliceの類似フレームワークとしてZappaがあるのですが、そちらを使えば解決できた可能性はあります。今回はChalice向けにコードを最適化したのですが、Zappaを試された方がいましたら是非ハマり所などを教えて頂ければ嬉しいです。

Chalice開発で知っておくと嬉しい小ネタ

新しく記事を起こすほどの内容でもないので追記します。

1)デバッグ設定

Chaliceで開発している際は、app.debugをTrueにしておくことをおススメします。

app = Chalice(app_name='hello-alice')
app.debug = True

Python側でエラーが発生した時にHTTPレスポンスでエラーを吐いてくれますので、原因の追跡がやりやすくなります。この小ネタを知ってからはデバッグが楽になりました。

Twilioアプリケーションのデバッグをする場合は、いきなりTwilio側から呼び出すのではなく、まずはブラウザかコマンドラインからAPI Gatewayを叩いて動作確認しておくのがおススメです。

Prod(本番)ステージに移行する時は、忘れずにFalseにしておくかコメントアウトしておきましょう。

2)あれ、ステージの名称はどこから取得できる?

URLを指定する時など、API Gateway側のステージ名称を取得したくなる機会が出てきます。ステージ名称はapp.current_request.context["stage"] で取得できます。

Twilio開発で一番わかりやすい事例は、redirectをさせたい時ですね。

resp.redirect("/" + app.current_request.context["stage"] + '/hello')

ChaliceのドキュメントにもWe haven't discussed those concepts yet.とか書いてあったり、見つけるのに苦労しました。Twilio開発にも必要になるTIPSなのでご活用ください。

他のクラウドサービスの状況

他のクラウドサービスの状況も気になりましたので、調べてみました。

Google

2017.06時点ではnode.jsしかサポートされていませんが、GoogleはLambda相当の機能をCloud Functionsとして用意してきました。ざっと仕様を確認しましたが、Cloud Functionsでは直接HTTPリクエストを扱えますね。小規模アプリケーションを作成するのに冗長だなと感じたAPI Gateway周りを扱わずに済む分、学習コストは低そうです。

Microsoft

MicrosoftもAzure Functionsを用意してきました。こちらでは2017.06時点でβ段階ですが、Python/PHPなどを扱う事も出来ます。Github仕様のWebhookを直接サポートしているのは興味深いですね!cronベースでTimerイベントを発生させることが出来るなど、痒い所に手が届きそうです。

GoogleやMicrosoftの動向を見る限り、これらのLambda系アプリケーション実行サービスはPaaSの後継サービスとして位置づけられて行ってますね。しかし、この「サーバーレス・アーキテクチャ」という表現に違和感が…。ユーザーがサーバサイドを意識しなくていいだけの話で、クラウド側ではおそらくコンテナベースでサーバが動作していますから。プラットフォーマー側がどのようにしてサービスを実装しているのかを考察しておけば、この手の仕組みの理解を得るのも早まるでしょうね。

今回はLambdaを採用しましたが、次回はAzure Functionsを使うかもしれません。Qiitaでも2017.06時点で9投稿しかないなど事例がほとんど転がっていませんが、ハマり所は少なそうなので、今回ほど苦労せずには済むかと踏んでます。

まとめ

  • Chaliceのドキュメントは面倒くさがらずに、しっかり読みましょう。
  • API Gatewayは学習用としてはよい題材でした。一般に公開するWeb APIを作る時には相性が良さそうな仕組みです。
  • AWSだけ触るのではなくて、他のクラウドサービスの動向も比較した方が幸せになれそう。