Twilioを使った自動電話架電の仕組みを作った話

こんにちは。AiRecoのふっきーです。

本日は『New RelicとSlackBotを使った障害監視の仕組みを作った話』の第二弾をお届けします!
前回の記事では、障害監視においてSlackBotを活用する方法について詳しく解説しましたが、今回はその進化版として 「自動電話架電機能」 を組み込む方法をご紹介します。
特に、緊急時に電話連絡が必要なシチュエーションで、自動化による効率化を実現したいと考えている方には参考になると思います!
ぜひ最後までご覧いただき、皆さんのプロジェクトにも役立てていただければ幸いです。

おさらい

まずは、前回の記事を簡単に振り返りましょう。
前回はNew RelicとSlackBotを活用して、障害監視の効率化を図る仕組みを紹介しました。
SlackBotを作った目的は、大きく以下の2つでした。

  1. 特定のアラートに対してチームに連絡を行う機能を実現すること
  2. 特定のアラートにおいては電話をかける仕組みを導入すること

前回の記事では、1つ目の「SlackBotを用いたチーム連絡」に焦点を当てて説明しました。
今回は、2つ目の 「電話をかける仕組み」 にフォーカスして、その具体的な実現方法について詳しくご紹介していきます。

Twilioについて

今回の自動電話架電機能には、『Twilio』 というサービスを利用しました。
ではまず、Twilioがどのようなサービスなのか、その概要をご紹介します。
www.twilio.com
Twilioは、クラウドベースの通信プラットフォームを提供するサービスです。プログラミングを活用して電話、SMS、チャット、ビデオ通話、Eメールなど、多様な通信機能をアプリやシステムに簡単に組み込むことができます。

Twilioの主な特徴

  • 柔軟性
    APIを使用して、必要な通信機能を自由にカスタマイズできます。
  • グローバル対応
    世界中で利用可能で、多言語対応や複数の通信チャネルをサポートしています。
  • スケーラブル
    小規模なプロジェクトから大規模なシステムまで、幅広いニーズに対応可能です。

Twilioで実現できること

Twilioを使えば、以下のような通信機能を簡単に構築できます。

  1. 電話機能
  2. アプリやウェブサイトに通話機能を実装できます。
    例)コールセンター、カスタマーサポートの自動化
  3. SMS機能
  4. 自動でSMSを送受信したり、受信したメッセージを処理できます。
    例)予約確認、二段階認証コードの配信
  5. チャット機能
  6. チャットボットやカスタマーサポート向けのチャットを簡単に構築できます。
    例)LINEやWhatAppを使った顧客対応
  7. ビデオ通話機能
  8. アプリ内でビデオ会議や一対一のビデオチャットを利用可能です。
    例)オンライン診療、リモート面談
  9. Eメール機能
  10. 大量のメール配信やメールマーケティングを効率的に行えます。
    例)ニュースレター、購入確認メールの送信

Twilioの料金体系について

Twilioは従量課金制を採用しており、利用した分だけ料金が発生する仕組みです。以下に具体的な料金ルールをまとめます。

  1. 電話番号の課金ルール
    Twilioの電話番号は、取得した時点から毎月の利用料が発生します。
    料金は以下の要素によって異なります。
    • 番号の種類:携帯番号、国内番号、着信課金番号など
    • 電話番号の機能:音声通話、SMSメッセージ、SIPトランクなど
    • 電話番号の国:電話番号が所在する国
  2. 利用機能の課金ルール
    電話番号の利用料に加え、以下のような機能を使用する際に料金が発生します。
    • 音声通話:通話の発信・受信に応じた料金が発生
    • SMS送信:SMSを送信した分だけ料金が発生
    たとえ通話やSMSを一切使用しなくても、毎月の番号利用料は発生する点に注意してください。
  3. 最新の料金情報について 最新の料金は、Twilioの料金ページやPricing APIを利用することで確認できます。
    詳しくは以下のページをご確認ください。
    help.twilio.com www.twilio.com www.twilio.com

導入の流れ

ここからは、アラートBotにTwilioを導入して自動電話架電機能を実現するまでの流れを、具体的な手順に分けてご紹介します。

  1. Twilio契約
    Twilioを利用するためのバンドル申請と電話番号購入を行います。
  2. 電話をかける仕組みを作る
    TwilioのAPIを利用し、アラート発生時に電話をかける機能を実装します。
  3. 連続架電の仕組みを作る
    万が一電話に応答がなかった場合に備えて、複数の連絡先に連続で電話をかける仕組みを構築します。

1. Twilio契約

Twilioを利用するためには、まず契約を行い、電話番号を購入する必要があります。
なお、契約から電話番号購入手順については、こちらの記事を参考に進めました。
dev.classmethod.jp
こちらの記事では、電話番号を購入するために必要なバンドル申請の方法や提出書類について非常に詳しくまとめられており、初めての私でもスムーズに手続きを進めることができました。実際の手順が具体的に解説されているので、Twilioの利用を検討している方には大変参考になる内容だと思います。

問題が発生した場合の対処法

手続きの途中で進め方分からなくなった場合やレビューが進まない場合は、Help Centerを利用して問い合わせると良いです。
私の場合もレビューが全く進まなかったためチケットを起票して問い合わせたところ、すぐに対応してくれました。

  • 問い合わせ方法
    Twilioの管理画面右上のメニューから「Help Center」を選択し、チケットを起票
  • 言語
    やり取りは英語で行いますが、Google翻訳などを活用して頑張りましょう

通常、レビューは通常2~3営業日ほどで完了するとのことですが、進捗が遅い場合は早めに問い合わせると対応してくれる可能性があります。

電話番号の購入

バンドル申請のレビューが完了したら、いよいよ電話番号を購入できます。利用目的に応じた電話番号を選択してください。

トライアルプランの活用

まだ有料プランに進むのが不安な方は、まずはトライアルプランを契約することをおすすめします。
トライアルプランでは一定の額まで無料でTwilioの機能を試すことが可能です。

2. 電話をかける仕組みを作る

Twilioを利用して電話をかける仕組みを実装していきます。前回と同様に、Node.js を使用して開発を進めます。

基本的な実装

単純に電話をかけるだけであれば、以下のコードで実行できます。

require('dotenv').config();

const express = require('express');
const bodyParser = require('body-parser');
const twilio = require('twilio');

const app = express();
app.use(bodyParser.urlencoded({ extended: false }));

const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const client = twilio(accountSid, authToken);
var twiml = '<Response><Say voice="woman" language="ja-jp">エラーが頻発しています。</Say></Response>' // 読ませたいメッセージ

async function makeCall(to, from) {
    try {
        const call = await client.calls.create({
            to: to,
            from: from,
            url: 'http://twimlets.com/echo?Twiml=' + encodeURIComponent(twiml),
        });
        console.log('Call initiated.');
    } catch (err) {
        console.error(err);
    }
}

// 通話ステータスを受け取るWebhookエンドポイント
app.post('/call-status', (req, res) => {
    res.sendStatus(200); // Twilioに成功を通知
});

// サーバーを起動
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server is running on port ${PORT}`);
});

// 通話を実行
makeCall(process.env.TO, process.env.FROM);


環境変数として以下の定義が必要となります。

TWILIO_ACCOUNT_SID=TwilioのAccount SID
TWILIO_AUTH_TOKEN=TwilioのAuth Token
TO=電話をかける先。日本(国番号81)の電話番号090-1234-5678であれば、+819012345678と入力
FROM=TwilioのMy Twilio phone number


TO以外の3つの値は、Twilioホーム画面のAccount Infoより確認可能なので、そのままコピペしてください。

Twilioのアカウント情報

実行結果

コードを実行すると、設定したTOに対して電話がかかり、応答すると設定したメッセージが読まれます。
※トライアルプランを利用している場合、最初に英語の広告音声が流れるので、ガイダンスに従って適当なキーをプッシュしてください。

遭遇した課題

以下のように、設定したメッセージの最初の部分が途切れて聞こえる場合があります。

「(ププ)...が頻発しています。」


こちらの事象についてはTwilioの公式からも説明されています。
help.twilio.com
公式では回避方法として2つの方法が提案されていますが、私の場合どちらもなぜか機能しませんでした。

ではどうしたか?簡単です。力技で回避します。

<Response><Say voice="woman" language="ja-jp">こんにちはこんにちは。エラーが頻発しています。</Say></Response>


メッセージの最初に不要な言葉を付け加えることで、途切れる部分を無理やり吸収させました。
これで実行した結果、メッセージが途切れることなく聞き取ることができました。

「(ププ)エラーが頻発しています。」

メッセージの文字数は何度か試行錯誤して調整しましたが、この方法で問題なく動作しました。
簡単に対処できるのでぜひ参考にしてみてください。

3. 連続架電の仕組みを作る

さて、ここまでで電話をかける仕組みはできました。あとは、ここに何かしらの条件を付与し、条件を満たした場合に電話をかける仕組みにするだけです。
自動電話で私たちが実現したい要件を整理すると、以下の2点があります。

  1. 特定の障害が発生してから1時間経過してもIssueがACTIVATEDの場合のみ電話をかける
  2. 連続架電の仕組みを作る

順番に実現していきます。

IssueのStateをチェックする

NerdGraph APIを使用することで、アラートのIssueやinsidentの詳細情報を取得することが可能です。
詳しくは公式のドキュメントをご確認ください。
docs.newrelic.com
以下は、指定したIssueのState(ACTIVATED または CLOSED)を取得するクエリをアラートBotのコードに埋め込んだ例です。

const postData = JSON.stringify({
    query: `
      {
          actor {
              account(id: ${accountId}) {
                  aiIssues {
                      issues(filter: {contains: "${issueId}"}) {
                          issues {
                              issueId
                              state
                          }
                      }
                  }
              }
          }
      }
  `,
});

const options = {
    hostname: "api.newrelic.com",
    port: 443,
    path: "/graphql",
    method: "POST",
    headers: {
        "Content-Type": "application/json",
        "API-Key": apikey,
    },
};


指定したIssueの、と書きましたが、filterでIssueIdを指定することで、1つのIssueに絞り込んで取得しています。
IssueIdは、Issueの識別子となります。
アラートBotでIssueIdを取得するために、Slackに連携している通知にはあらかじめIssueIdが含まれるように設定しました。


ここまでで、電話をかける仕組みとIssueのStateを取得する仕組みがそろいましたので、アラートBotに組み込むことで、以下のような処理が実現できるはずです。

ざっくりフロー図

特定条件下において、1人に電話をかけるところまでできました!あと少しです。

連続架電の仕組みを作る

さて、何度も出てきている電話をかける条件である
特定のアラートが発生してから1時間経過してもACTIVATEDの場合」というのは、すぐさまサービス復旧のためのアクションを起こさなければいけないような重大な障害が発生していると判断しています。
1人に電話をかけて、その人が電話に出られない / 気付かない場合、何もアクションを起こせないという状況は、可能な限り避けたいものです。
そのため、1人だけに電話をかけるのではなく、出なかった場合は次の人に電話をかけるといった、連続架電の仕組みを作りました。
具体的には、以下のようなロジックです。

  • 1人目に電話をかけて応答がなければ2人目へ、2人目も応答しなければ3人目へ。3人目も応答しなければ、再び1人目に戻る
  • この流れを3ループ繰り返しても誰も応答しなかった場合、または誰かが電話に応答した時点で処理を終了する


この仕組みを作るうえで最も重要なのは、「電話に応答したかどうか」を正しく判断することです。
ここでは、TwilioのStatus Callbacks機能を利用しました。Status Callbacksを使うと、通話の状態を簡単に確認できます。
詳しくは公式のドキュメントをご確認ください。 help.twilio.com
Completedという状態が通話の終了を表しているため、これを利用して電話の応答状況を判断しています。

また、コール数が多すぎると煩わしいので、タイムアウト秒数も設けました。私たちのチームでは、およそ10コール程度を目安に、50秒で設定しています。必要に応じて、さらに短くするのも良いでしょう。

const callList = [
    process.env.TO1,
    process.env.TO2,
    process.env.TO3,
];
const maxLoop = 3;
const timeout = 50;
const twiml = '<Response><Say voice="woman" language="ja-jp">こんにちはこんにちは。エラーが頻発しています。</Say></Response>';

…(省略)...
// 自動電話をかける関数
async function makeCall(idx, loop) {
    if (loop >= maxLoop) {
        console.log('最大試行回数に達しました。処理を終了します。');
        return;
    }

    const to = callList[idx];
    const from = process.env.FROM;

    try {
        console.log(`${to}に電話をかけます。:(ループ: ${loop + 1}/${maxLoop}, インデックス: ${idx + 1}/${callList.length})`);
        await client.calls.create({
            to: to,
            from: from,
            url: 'http://twimlets.com/echo?Twiml=' + encodeURIComponent(twiml),
            statusCallback: `https://airec-alert-monitor.xxxxxxxx.azurecontainerapps.io/call-status?idx=${idx}&loop=${loop}`, // コールバックを受け付けるためのURL
            statusCallbackEvent: ['completed'], // 取得するCall Eventを設定
            timeout: timeout, // タイムアウト秒数を設定
        });
    } catch (err) {
        console.error(`電話の発信中にエラーが発生しました。(電話番号: ${to}, ループ: ${loop}, インデックス: ${idx}):`, err);
        return;
    }
}

expressApp.post('/call-status', async (req, res) => {
    const callStatus = req.body.CallStatus;
    const idx = parseInt(req.query.idx, 10);
    const loop = parseInt(req.query.loop, 10);

    if (callStatus === 'completed') {
        console.log('電話に応答しました。処理を終了します。');
        res.sendStatus(200);
        return;
    } else {
        console.log('電話に応答がありませんでした。次の番号に進みます。');

        let nextIdx = idx + 1;
        let nextLoop = loop;
        if (nextIdx >= callList.length) {
            nextIdx = 0;
            nextLoop += 1;
        }

        await makeCall(nextIdx, nextLoop); 
    }
    res.sendStatus(200);
});
...(省略)...
if (text.includes('特定のエラー')) {
    // Slackの通知からIssueIdを取得する
    let issueIdRegex = extractIssueIdUsingRegex(text);
    
    const timerId = setTimeout(async () => {
        try {
            // New Relic APIを使用してStateを取得
            const status = await getIssueStatusFromNewRelic(NEWRELIC_APIKEY, NEWRELIC_ACCOUNTID, issueIdRegex);
            
            // 状態が ACTIVATED の場合、Twilio を使って電話をかける
            if (status === "ACTIVATED") {
                await makeCall(0, 0);
            } else if (status === null) {
                console.error("Failed to fetch issue status from New Relic. Skipping further actions.");
                return;
            } else {
                console.log(`Status is not ACTIVATED. Current status: ${status}`);
            }
        } catch (error) {
            console.error("Error during follow-up process:", error);
        } finally {
            messageTimers.delete(message.ts);
        }
    }, 60 * 60 * 1000);

    messageTimers.set(message.ts, timerId);
}
...(省略)...

ただし、注意点があります。
たとえば、電車に乗っているときに電話がかかってきた場合、気付いても一時的に応答を拒否することがありますよね。
この「応答を拒否」した場合も、Twilioの状態としてはCompletedとして取得されてしまうのです。
つまり、上記のコードでは実際には電話に出ていない場合でも、連続架電の処理が終了してしまうのです。少し紛らわしいですよね。
とはいえ、現状このアラートBotでは、Twilioから電話がかかってくるパターンが特定のアラート1つのみです。
そのため、「Twilioから電話がかかった」という時点で状況を把握できると考え、電話を拒否した場合でも処理が終了する仕様を一旦は許容しています。
今後、電話発火の条件が増えた場合には、新たな方法を検討したいと思います。

これで電話機能を組み込んだ新・アラートBotの完成です。

最終的なフロー図

動作確認

電話処理を発火させ、色々なパターンを試してみました。

  • 電話に応答したパターン
  • 電話を拒否したパターン(応答と同じ扱いとなる)
  • 誰も電話に応答しなかったパターン

まとめ

いかがでしたでしょうか?
今回は、Twilioというサービスを使った連続架電の仕組みをご紹介しました。
Twilioは高い柔軟性と拡張性を持ち、電話をはじめとする多様なコミュニケーション手段を簡単にシステムへ組み込めることが大きな強みです。今回は特に、障害発生時の迅速な対応を可能にするための連続架電機能を中心にご紹介しましたが、工夫次第でさらに多様な運用が可能です。

今後もシステムの安定運用や通知の最適化に向けて、さらなる改善を進めていきたいと思います。
この記事が、Twilio導入や自動電話通知システムの構築を検討している方の参考になれば幸いです。

お知らせ

ecbeingでは新進気鋭なエンジニアを募集しております!
careers.ecbeing.tech