
この記事はBASE Advent Calendar 2020の5日目の記事です。
SRE Groupのngswです。 Eコマースプラットフォーム「BASE」における障害発生時に、社内関係者に連絡網に基づいて電話発信するシステムを構築しました。 このエントリでは、その導入までの経緯と具体的な当該システムの説明をします。
TL;DR
- 「BASE」で問題が発生した際に意思決定者に電話発信する周知システムを構築した
- 「導入前に考えたこと」をまず主題として書いた
- 参考URL記事のまま手順であるが、それでも導入時に詰まった事柄など落ち穂拾い的に追記した
謝辞
- Twilio FunctionsとStudioを使って連続架電を行う - Qiita
- 大変わかりやすい記事であり、ほぼすべてを参考にさせていただいた。このQiita記事がなければ短期間で実現することは不可能であったと考える
導入に至る経緯
- 07月某日 : サイト閲覧遅延障害が起きたことで「発生とあわせて社内関係者に機械的に一斉周知する方法が必要なのではないか」という議論があった
- 09月某日 : 07月と別起因ではあるが同様のサイト閲覧遅延が発生し、当時の議論が再度浮上した
- 個人的な感想 : 二度議論されたことは対応する価値があるのではないか。少なくとも検証する価値はあるのではないかと考えた
導入前に考えたこと
まずはじめに「解決すべき課題がなにか」を考えなくてはならない。 「あわせて社内関係者に機械的に一斉周知する方法が必要なのではないか」という議論から考えられるのは、議論発案側の役割(とそこに課せられた責務)が関係してくる。プロダクトが正常動作していないことを利用者であるショップオーナー様に通知する責任があり、またはその状況を認識して次なる意思決定に備える必要があるという、それぞれの立場からくる「わたしに然るべき通知をできるだけ早くください」という表明にほかならない。
その一方でこれらがなぜ求められる状態になったのか。これは「システム障害対応時に対応エンジニアが周知する時間をうまく取れない場合がある」ということの証左でもある。システムに対する止血対応と、プロダクトに対するインパクト度合いを含めた状況説明。これがアラートに機敏に反応できた対応エンジニアに一手にかかってきてしまっていて、結局その対応エンジニアが障害対応全般を含めたボトルネックになっているということでもある。この点を機械的にスムーズに解決する方法が求められているのだろう。
なるほど、裏を返せばWebエンジニアは意思決定者を含めた社内関係者に、関係者はユーザであるショップオーナー様に対して「説明責任がある」ということである。今思えば仕事というもののほとんどはこの「説明責任を果たすことに終始するのではないか」とまで考えるようになった。
であればこれは単なる障害通知システムの補助機能ではない。わたしは大義を手に入れた。それでははじめよう。
仕様
- BASEショップページの応答速度低下の検知は、既存のmackerel監視設定を利用する
- 発報はmackerel にてTwilio架電通知を利用すればよさそうである
- ただし上記機能だけでは架電先が1つの番号に限られてしまうという制約がある
- そのためmackerel上の設定では dummy call とする(この成否はどうでもよいので ngsw の番号を設定した)
- mackerelは上記処理の成否をStatus Callback URL(Twilio) にリクエストする
- 上記のリクエストを契機に後述する Twilio Functions に設定したjsが電話番号のDictを持つのでfor文で一人ずつ電話発信
- 音声「対応可能なら1、無理なら0」に対して、キー1押下ならそこでループ終了、キー0なら次の人(無視、通話中、即切りも同じ)
- 配列最後までいったら指定最大回数までループする
構成要素 / 登場人物
全体
| Name | Role | Memo |
|---|---|---|
| 「BASE」 | 監視対象 | 主に閲覧に関する応答速度に注目 |
| Mackerel | 監視システム | 「BASE」システムを監視してTwilioに通知 |
| Twilio | 電話発信システムとして利用 |
Twilio内の細分化
| Name | Role | Memo |
|---|---|---|
| Twilio Functions | Flowを呼び出すためのスクリプト | URLをもつ |
| Twilio Studio | Flowを定義する | 音声認識やプッシュ番号認識ができ、条件分岐ができる |
| 購入番号 | 発信番号 | 購入しないと発信行為に制約がある |
参考URL
- Twilio FunctionsとStudioを使って連続架電を行う - Qiita を参考にした
- 大事なことなので何度でも
Twilio Functions
- Functionsのコードは一度デプロイして画面を閉じた場合、改めてFunctions画面からスクリプト内容を再取得しようとすると失敗が繰り返され編集できない事象を確認している(運良く取れるときがある / Rate Limit ?)
- 以下でURLが確定する
https://${DOMAIN}/$(パス名)- Functions作成時に
- Environments DOMAIN が払い出される
- パス名を自分で決める
- Functions作成時に
| Functions | /function-name |
| Assets | (無視) |
| Settings | - |
| Environment Variables | - |
| FLOW_SID | https://jp.twilio.com/console/studio/dashboard で閲覧可能な FW から始まるSIDのこと |
| FROM_NUMBER | +{購入番号} |
// https://qiita.com/mobilebiz/items/8757eec854f37ce0cb2d
/**
* Studioを連携させた連続架電 - serialCallStudio
*
* @param idx リストインデックス
* @param loop ループ回数
*/
// 架電先リスト(発信したい電話番号のリストをE.164形式で記述します)
const callList = {
"携帯1": "+8150yyyyyyy0",
"携帯2": "+8180yyyyyyy1",
};
// ループ回数(1以上の整数、1ならループしない)
const maxLoop = 2;
exports.handler = function(context, event, callback) {
// カウンター関連
let idx = event.idx || 0; // インデックスパラメータを取得
let loop = event.loop || 1; // ループパラメータを取得
if (idx >= Object.keys(callList).length) { // リストの最後まで到達
idx = 0; // インデックスは0に戻す
if (loop >= maxLoop) { // ループ回数が最大値を超えたので終了
callback(null, "Call count was expired.");
} else {
loop++; // ループ回数をインクリメント
}
}
// 架電先電話番号
let number = callList[Object.keys(callList)[idx]];
idx++; // インデックスをインクリメント
// 架電するStudioフローを呼び出す
const client = context.getTwilioClient();
client.studio.v1.flows(context.FLOW_SID).executions.create({
to: number,
from: context.FROM_NUMBER,
parameters: JSON.stringify({
idx: idx,
loop: loop,
})
})
.then((call) => {
callback(null, "OK");
})
.catch((error) => {
callback(error);
});
};
Twilio Studio
- 手順的なデッドロック
- Functions が先にできないと Twilio Studio で Flow が完成しない
- しかしFunctions完成には FLOW_SID が必要
- 解決策は以下の手順の中で
Run Functionだけをとりあえず置いといてPublishなりして Flow SID を確定しておくこと

FLOW CONFIGURATION
- Flow 実行のトリガー



Config
| FLOW NAME | SerialCallStudio |
| REST API URL | https://studio.twilio.com/v1/Flows/FWxxxxxxxxxxxxxxxxxxx/Executions |
| WEBHOOK URL | https://webhooks.twilio.com/v1/Accounts/ACxxxxxxxxxxxxxxxx/Flows/FWxxxxxxxxxxxxxxxxxxx |
| TEST USERS | (空) |
Transitions
| IF INCOMING MESSAGE | (空) |
| IF INCOMING CALL | (空) |
| IF REST API | call |
MAKE OUTGOING CALL V2
- 架電時に受電側のステータスで分岐



Config
| WIDGET NAME | call |
| NUMBER TO CALL | {{contact.channel.address}} |
| RECORD CALL | OFF |
| DETECT ANSWERING MACHINE | OFF |
| SEND DIGITS | (空) |
| TIMEOUT | 60 SECONDS |
| SIP USER NAME | (空) |
| SIP PASSWORD | (空) |
Transitions
| IF ANSWERD | Gather |
| IF BUSY | LoopFunction |
| IF NO ANSWER | LoopFunction |
| IF CALL FAILED | LoopFunction |
GATHER INPUT ON CALL
- 通話中のメッセージと、そのメッセージに対する回答の保持
- メッセージはテキストだけでなく設定次第で音声ファイルでも可能
- 受電者の以下のアクションを理解できる
- どのプッシュボタンを押下したか
- 音声認識による回答内容(はい or いいえみたいなもの)
- これはうまく動かなかった



Config
| WIDGET NAME | Gather |
| SAY OR PLAY MESSAGE | |
| TEXT TO SAY | {ここに発音させたい1 or 0で分岐するようなテキスト文を書く} |
| LANGUAGE | Japanese |
| MESSAGE VOICE | Alice |
| NUMBER OF LOOPS | 1 |
| STOP GATHERING AFTER | 5 SECONDS |
| STOP GTHERING ON KEY PRESS? | YES / # |
| STOP GATHERING AFTER | (空)DIGITS |
| SPEECH RECOGNITION LANGUAGE | Default |
| SPEECH RECOGNITION HINTS | (空) |
| PROFANITY FILTER | True |
| ADVANCED SPEECH SETTINGS | - |
| SPEECH TIMEOUT (IN SECONDS) | auto |
| SPEECH MODEL | Numbers & Commands |
Transitions
| IF USER PRESSED KEYS | YesOrNo |
| IF USER SAID SOMETHING | (空) |
| IF NO INPUT | LoopFunction |
Split Based On…
- 条件分岐とその判定



Config
| WIDGET NAME | YesOrNo |
| VARIABLE TO TEST | widgests.Gather.Digits |
Transitions
| COMPARING WITH | {{Gather.Digits}} |
| IF NO CONDITION MATCHES | LoopFunction |
| YES == 1 | Equal To / 1 / SayYes |
| NO == 0 | Equal To / 0 / LoopFunction |
Say/Play
- Push 1 だった際の後処理



Config
| WIDGET NAME | SayYes |
| SAY OR PLAY MESSAGE OR DIGITS | Say a Message |
| TEXT TO SAY | {ここに発音させたいテキスト文を書く} |
| LANGUAGE | Japanese |
| MESSAGE VOICE | Alice |
| NUMBER OF LOOPS | 1 |
Transitions
| IF AUDIO COMPLETE | (空) |
Run Function
- Push 0 ないしは 1 以外だった際の再実行
- または通話中やなんらかの理由で通話不可だった場合



Config
| WIDGET NAME | LoopFunction |
| SERVICE | SerialCallStudio |
| ENVIRONMENT | ui |
| FUNCTION | /function-name 1 |
| FUNCTION URL | (自動付与) |
| Function Parameters | これらはjs内で引き回して利用される |
| idx | {{flow.data.idx}} |
| loop | {{flow.data.loop}} |
Transitions
| IF SUCCESS | (空) |
| IF FAIL | (空) |
結び
前半では導入の経緯を、後半ではシステムの詳細を書いた。繰り返しになるが概ねの主題は前半部である。そこが語りたいことのすべてであった。後半部の成果物解説はその残滓でしかないが、それでも同様に導入を考えた方がいた場合に、何かしらの参考になればと大元のQiita記事をさらに補完できるような記述を心がけた。 導入後はすこぶる順調で特にクレームもなく、期待通りの稼働をしており役割は果たせたと考えている。このようなシステムやツールを用いて説明責任を果たしていくのがSRE(だけでなくエンジニア)なのだと感じたという点を強く強調し、結びとしたい。
明日は BASE BANK 株式会社の松雪さん (@applepine1125) の記事です。 引き続きよろしくお願いいたします。