BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

Twilioを利用した障害時の自動連絡網システムについて

f:id:ngsw:20201202174543p:plain

この記事はBASE Advent Calendar 2020の5日目の記事です。

SRE Groupのngswです。 Eコマースプラットフォーム「BASE」における障害発生時に、社内関係者に連絡網に基づいて電話発信するシステムを構築しました。 このエントリでは、その導入までの経緯と具体的な当該システムの説明をします。

TL;DR

  • 「BASE」で問題が発生した際に意思決定者に電話発信する周知システムを構築した
  • 「導入前に考えたこと」をまず主題として書いた
  • 参考URL記事のまま手順であるが、それでも導入時に詰まった事柄など落ち穂拾い的に追記した

謝辞

導入に至る経緯

  • 07月某日 : サイト閲覧遅延障害が起きたことで「発生とあわせて社内関係者に機械的に一斉周知する方法が必要なのではないか」という議論があった
  • 09月某日 : 07月と別起因ではあるが同様のサイト閲覧遅延が発生し、当時の議論が再度浮上した
  • 個人的な感想 : 二度議論されたことは対応する価値があるのではないか。少なくとも検証する価値はあるのではないかと考えた

導入前に考えたこと

まずはじめに「解決すべき課題がなにか」を考えなくてはならない。 「あわせて社内関係者に機械的に一斉周知する方法が必要なのではないか」という議論から考えられるのは、議論発案側の役割(とそこに課せられた責務)が関係してくる。プロダクトが正常動作していないことを利用者であるショップオーナー様に通知する責任があり、またはその状況を認識して次なる意思決定に備える必要があるという、それぞれの立場からくる「わたしに然るべき通知をできるだけ早くください」という表明にほかならない。

その一方でこれらがなぜ求められる状態になったのか。これは「システム障害対応時に対応エンジニアが周知する時間をうまく取れない場合がある」ということの証左でもある。システムに対する止血対応と、プロダクトに対するインパクト度合いを含めた状況説明。これがアラートに機敏に反応できた対応エンジニアに一手にかかってきてしまっていて、結局その対応エンジニアが障害対応全般を含めたボトルネックになっているということでもある。この点を機械的にスムーズに解決する方法が求められているのだろう。

なるほど、裏を返せばWebエンジニアは意思決定者を含めた社内関係者に、関係者はユーザであるショップオーナー様に対して「説明責任がある」ということである。今思えば仕事というもののほとんどはこの「説明責任を果たすことに終始するのではないか」とまで考えるようになった。

であればこれは単なる障害通知システムの補助機能ではない。わたしは大義を手に入れた。それでははじめよう。

仕様

  1. BASEショップページの応答速度低下の検知は、既存のmackerel監視設定を利用する
  2. 発報はmackerel にてTwilio架電通知を利用すればよさそうである
  3. ただし上記機能だけでは架電先が1つの番号に限られてしまうという制約がある
  4. そのためmackerel上の設定では dummy call とする(この成否はどうでもよいので ngsw の番号を設定した)
  5. mackerelは上記処理の成否をStatus Callback URL(Twilio) にリクエストする
  6. 上記のリクエストを契機に後述する Twilio Functions に設定したjsが電話番号のDictを持つのでfor文で一人ずつ電話発信
  7. 音声「対応可能なら1、無理なら0」に対して、キー1押下ならそこでループ終了、キー0なら次の人(無視、通話中、即切りも同じ)
  8. 配列最後までいったら指定最大回数までループする

構成要素 / 登場人物

全体

Name Role Memo
「BASE」 監視対象 主に閲覧に関する応答速度に注目
Mackerel 監視システム 「BASE」システムを監視してTwilioに通知
Twilio 電話発信システムとして利用

Twilio内の細分化

Name Role Memo
Twilio Functions Flowを呼び出すためのスクリプト URLをもつ
Twilio Studio Flowを定義する 音声認識やプッシュ番号認識ができ、条件分岐ができる
購入番号 発信番号 購入しないと発信行為に制約がある

参考URL

Twilio Functions

  • Functionsのコードは一度デプロイして画面を閉じた場合、改めてFunctions画面からスクリプト内容を再取得しようとすると失敗が繰り返され編集できない事象を確認している(運良く取れるときがある / Rate Limit ?)
  • 以下でURLが確定する https://${DOMAIN}/$(パス名)
    • Functions作成時に
      • Environments DOMAIN が払い出される
      • パス名を自分で決める
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 を確定しておくこと

f:id:ngsw:20201201203516p:plain

FLOW CONFIGURATION

  • Flow 実行のトリガー

f:id:ngsw:20201201203614p:plain

f:id:ngsw:20201201203647p:plainf:id:ngsw:20201201203704p:plain

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

  • 架電時に受電側のステータスで分岐

f:id:ngsw:20201201203742p:plain

f:id:ngsw:20201201203755p:plainf:id:ngsw:20201201203812p:plain

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 いいえみたいなもの)
      • これはうまく動かなかった

f:id:ngsw:20201201203853p:plain

f:id:ngsw:20201201203906p:plainf:id:ngsw:20201201203916p:plain

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…

  • 条件分岐とその判定

f:id:ngsw:20201201203950p:plain

f:id:ngsw:20201201204005p:plainf:id:ngsw:20201201204016p:plain

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 だった際の後処理

f:id:ngsw:20201201204036p:plain

f:id:ngsw:20201201204056p:plainf:id:ngsw:20201201204107p:plain

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 以外だった際の再実行
  • または通話中やなんらかの理由で通話不可だった場合

f:id:ngsw:20201201204134p:plain

f:id:ngsw:20201201204148p:plainf:id:ngsw:20201201204157p:plain

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) の記事です。 引き続きよろしくお願いいたします。