Twilioを使ってWeb API経由で電話をかける(前編)

こんにちは、AWS担当のwakです。

f:id:nurenezumi:20160127154326j:plain

【インデックス(予定)】

  1. CloudWatchの監視結果をSlackに流す(AWS Lambdaバージョン)
  2. AWS LambdaでWebサイトの死活監視を行ってSlackに結果を流す
  3. DynamoDBにパラメーターを入れてハードコードをなくす
  4. CloudWatchの監視結果をTwilioに流して電話をかける(前編)(今回)
  5. CloudWatchの監視結果をTwilioに流して電話をかける(後編)
  6. SlackからEC2インスタンスを起動・停止するコマンドを作る

APIで電話をかけよう

Twilioというものがあります。これは電話に関する諸々をWeb APIでコントロールできるサービスで、電話を受ける・かける、録音する、SMSを送信するといった面倒そうな処理の面倒をまとめて見てくれる優れものです。

今回はこのサービスの機能のうち、

  • 電話をかける
    • 指定した番号に電話をかけることができます。
  • 日本語読み上げ
    • 指定した文章を、電話口で(そこそこ自然な)日本語で読み上げてくれます。
  • 音声ファイル再生
    • 読み上げの前後に、予め準備した音声ファイル(mp3など)を再生してくれます。

を使って、《CloudWatchでアラートが発生した際、電話でアラートを通知する》仕組みをサーバーなしで構築することにしました。前編ではこの手順のための下準備としてTwilioの使い方を説明します。

概要

  1. まずいつもの通りCloudWatchで監視内容を設定します。
  2. 監視でエラーが発生すると、AWS SNSにアラートが飛ぶようにします。
  3. そのSNSをトリガーとしてAWS Lambdaが起動するようにします。
  4. LambdaはDynamoDBを参照し、エラー通知先の電話番号と通知内容を取得し、Twilio APIを叩いて電話をかけて担当者を叩き起こします。
  5. 担当者は頑張って対応します。

なお、Twilioの全ての機能は有料です。まずクレジットカード(Visa/Masterのみ)を使って事前にいくらか*1料金をチャージしておき、サービスを利用するたびにそこから残高が引き落とされていく仕組みになっています。残高がゼロになるとサービスが利用できなくなりますので適宜追加してください。自動チャージも可能です。

Twilioの準備

アカウント作成

まずはTwilioのトップページより「サインアップ」へ移動してアカウントを作成します。

twilio.kddi-web.com

注意点としては、ここで作ったアカウントは米Twilioのアカウントとは別になるため、サインインする際は https://jp.twilio.com/login/kddi-web (末尾に"kddi-web"がある)を経由する必要があるということです。サインアウトした状態でサービスを使おうとすると https://jp.twilio.com/login にリダイレクトされるのですが、こちらではサインインができません。パスワードのリセットを行おうとしても「このメールアドレスに紐付いたアカウントは存在しない」と言われてしまって混乱することになります。というか、実際に混乱して1時間無駄にしました。

チャージする

アカウントができたら、クレジットカードで適当にチャージします(どうせ使うので)。お試しでもサービスは利用できますが、任意の番号に任意の内容で電話をかけることはできません。

APIクレデンシャルを取得

アカウント取得後、 アカウントページ を見ると、APIを叩くのに必要なキーが表示されています。キーには2種類あり、右側の「テスト」はAPIのテストのために利用できます(これを使ってAPIを叩いても実際に電話をかけるなどの操作は行われません。料金もかかりません)。

f:id:nurenezumi:20160107180340p:plain

発信元電話番号を確認する

電話番号画面を見ると、電話番号が一つ表示されているはずです。この電話番号は自分だけのもので、電話をかける・受けるときに使われます。(有料で)別に新規取得することもできます。最初に確認しておきましょう。

テストしてみる

この情報をもとにエンドポイントにHTTP POSTを送ると電話をかけられます。

ドキュメントの REST API: 通話を開始する を見ると、curlでテスト実行を行うためのサンプルコードが見つかるはずです(「XML」に切り替えるのを忘れないでください)。

f:id:nurenezumi:20160107181519p:plain

既にAPIキーは埋め込まれていますので、{AuthToken}を本物の値に書き換え、Fromを自分の電話番号(上記)に書き換え、さらにToを自分の好きな番号に変更して実行すればいいです。

curl -XPOST https://api.twilio.com/2010-04-01/Accounts/AC92555d**************************/Calls \
    -d "Url=http://demo.twilio.com/docs/voice.xml" \
    -d "To=%2B81311112222" \
    -d "From=%2B8199998888" \
    -u 'AC92555d**************************:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

発信先電話番号は国番号(日本なら"+81")から指定します。たとえば03-1111-2222にかけるのであれば、先頭のゼロを取り、"+81"を付加して"+81311112222"になります。さらにcurlの仕様により+%2Bエスケープする必要があるので、パラメーターはTo=%2B81311112222と書くことになります。FromはTwilioから発行された電話番号を指定しますが、同様に修正してください。

--data-urlencodeを使えばこの手間は省けますし見やすくなります。

curl -XPOST https://api.twilio.com/2010-04-01/Accounts/AC92555d**************************/Calls \
    --data-urlencode "Url=http://demo.twilio.com/docs/voice.xml" \
    --data-urlencode "To=+81311112222" \
    --data-urlencode "From=+8199998888" \
    -u 'AC92555d**************************:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

ただのBASIC認証つきのHTTP(S) POSTなので、もちろんPowerShellでも同じように書けます(若干面倒です)。こちらでもエスケープは必要ありません。

$key = "AC92555d**************************"
$secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$uri = "https://api.twilio.com/2010-04-01/Accounts/$key/Calls"
$xml = "http://demo.twilio.com/docs/voice.xml"
$parameters = @{Url=$xml; To="+81311112222"; From="+8199998888"; }

# BASIC認証
$auth = "${key}:${secret}"
$base64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($auth))
$header = @{Authorization = "Basic $base64"}

Invoke-WebRequest -Uri $uri -Method POST -Body $parameters -Headers $header

うまく行けば電話がかかってきて「Enjoy!」と言ってくれるはずです。

喋る内容をカスタマイズする

日本語で喋らせる

電話で喋る内容は、上記のパラメーターの中で指定した http://demo.twilio.com/docs/voice.xml の中で指定しています。

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say voice="alice">Thanks for trying our documentation. Enjoy!</Say>
    <Play>http://demo.twilio.com/docs/classic.mp3</Play>
</Response>

これはTwiMLと呼ばれるXMLで、文法は TwiMLTM: Twilio マークアップ言語 で解説されています。これと同じようなXMLファイルを自分で用意してWebサーバーでホストし、そのURLを指定すれば任意の文章を読み上げさせることができます。日本語にも対応していて、単にlanguage属性で言語に日本語を指定すればいいだけです。

<?xml version="1.0" encoding="UTF-8"?>
<Response>
    <Say voice="alice" language="ja-JP" loop="3">大切なサーバーが停止しました。</Say>
    <Play>http://s3-ap-northeast-1.amazonaws.com/YOUR-BUCKET-NAME/very-very-horrible-music.mp3</Play>
</Response>

思わず心臓が躍り出すような通告が3回繰り返された後、S3に置いた*2mp3ファイルが再生されます。

TwiMLをS3に配置するときの注意点

TwiMLをS3にホストすればWebサーバーは必要ありませんが、TwilioはデフォルトでこのTwiMLにPOSTでアクセスしに行くため、S3ではエラーになってしまいます*3。パラメーターに Method=GET を追加してこの動作をオーバーライドできます。curlの例であれば--data-urlencode "Method=GET" \を、PowerShellであれば

$xml = "https://s3-ap-northeast-1.amazonaws.com/YOUR-BUCKET-NAME/voice.xml"
$parameters = @{Url=$xml; To="+814155551212"; From="+81012345678"; Method="GET"}

のように書いてください。

Twimletsを使う

いくらサーバーが必要ないとはいえ、あらかじめファイルを作成してアップロードしておくのは面倒なものです。Twilioから提供されているWebサービス「Twimlets」を使うと、上述のTwiMLを動的に生成することができます。

Twilio Labs

パラメーターにXMLの中身を丸ごと渡してあげると、そのままのXMLを出力してくれます。*4

http://twimlets.com/echo?Twiml=%3CResponse%3E%0A%20%20%20%20...

このURLを生成するためのPowerShellのコードを示します。

Add-Type -AssemblyName System.Web
$raw = '<Response><Say voice="alice" language="ja-JP" loop="3">大切なサーバーが停止しました。</Say><Play>http://s3-ap-northeast-1.amazonaws.com/YOUR-BUCKET-NAME/very-very-horrible-sound.mp3</Play></Response>'
$encoded = [System.Web.HttpUtility]::UrlEncode($raw, [System.Text.Encoding]::UTF8)
$xml = "http://twimlets.com/echo?Twiml=" + $encoded

Linuxであればnkfか何かを使ってください。あらかじめ用意することができる固定メッセージであれば上述のURLからブラウザで作ることもできます。このURLさえ準備できれば、Twilioにリクエストを送る際にUrlオプションにこのURLを渡してあげればいいわけです。なお、curlでリクエストを送る場合は-dではなく--data-urlencodeオプションを使ってください。

Lambda(Node.js)から呼ぶ

ライブラリも提供されているのですが、この程度であれば直接リクエストを送ってしまった方が早いでしょう。次のコードはLambdaで実行すると電話をかけて処理を終了します。

var https = require('https');

exports.handler = function(event, context) {
    var key = "AC92555d**************************"
    var secret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    var options = {
        hostname: "api.twilio.com",
        port: 443,
        path: "/2010-04-01/Accounts/" + key + "/Calls",
        method: 'POST',
        auth: key + ":" + secret,
        headers : { "Content-Type" : "application/x-www-form-urlencoded" }
    };

    var req = https.request(options, function(res) {
        if (200 <= res.statusCode && res.statusCode <= 204) {
            console.log("OK!")
            context.succeed();
        } else {
            console.log("HTTP status code is NOT 2xx but " + res.statusCode);
            res.setEncoding("utf8");
            res.on('data', function(data) {
                console.error(data);    
                context.fail(res);
            });
        }
    });
    
    req.on('error', function(e) { context.fail(e.message); });

    var rawXml =
        '<?xml version="1.0" encoding="UTF-8"?>' +
        '<Response>' +
        '  <Say voice="alice" language="ja-JP" loop="3">大切なサーバーが停止しました。</Say>' +
        '  <Play>http://s3-ap-northeast-1.amazonaws.com/YOUR-BUCKET-NAME/very-very-horrible-music.mp3</Play>' +
        '</Response>';
    var urlParam = encodeURIComponent('http://twimlets.com/echo?Twiml=' + encodeURIComponent(rawXml));

    req.write("To=+81311112222&");
    req.write("From=+8199998888&");
    req.write("Method=GET&");
    req.write("Url=" + urlParam);
    req.end();
};

注意点としては、

  • HTTP POSTでパラメーターを送信するので、ヘッダにContent-Type : application/x-www-form-urlencodedを指定する
  • XMLの中身はencodeURIComponentでURLエンコードしてTwimletsに渡す
  • そのURL自体をまとめて再度encodeURIComponentでURLエンコードしてTwilioに渡す

といったあたりです。

まとめ

bashPowerShellのコンソールと、電話という子供の頃から慣れ親しんできたデバイスとが繋がるのはなんだか不思議な感じがします。次回はこちらを使い、CloudWatch+Lambdaから呼び出して実際に電話をかけることにします。

*1:2,000円以上、1,000円単位になります

*2:Twilioのサーバーが取得できるように外部からアクセス可能でないといけません

*3:S3はHTTP POSTを受け取ると無条件でエラーを返す仕様となっています

*4:メッセージ内容だけを渡すバージョンもあるのですが、language属性の指定ができないため日本語では使えませんでした