障害アラート同時通話という仕組みを思いついたのでサーバレスで実現した

この記事はSpeee Advent Calendar 2018かつTwilio Advent Calendar 2018の5日目の記事です。一石二鳥という魂胆です。

エンジニア界隈で定番のAdventCalendarですが、私はAdventCalendarの由来がお菓子であることを知らず、アンジャッシュ的なことをかましてしまいました。

妻:アドベントカレンダーあげたら〇〇ちゃん(友人のお子さん)喜ぶかな〜
自分:え、AdventCalendar知ってるの?てか流石に〇〇ちゃんには難しすぎない?
妻:もう少し子供向けのがいいかな?
自分:いや子供向けっていうか、そもそもどんな記事書くつもりなの?
妻:え?
自分:え?

はじめに

Webサービスは突然死にます。いかなる努力を費やしたとしても死ぬときは死にます。

昨今のWebサービスでは、死活監視ツールを入れてサーバの生存状況やら異常やらを検知できるようにしているケースも多いと思います。

しかし異常を検知できたとしても、すぐ人が気づいて対処できるかどうかは別問題です。例えば深夜の時間帯や外出中など、そもそも気づくことが困難なシチュエーションがあります。

ある程度の規模の開発チームであれば、必ず誰か待機者を立てたりエスカレーションフローを組むなどしてカバーできるかもしれません。しかし、せいぜい2~3人の小規模チームだとそれは難しいです。

個人的にありそうなネックを列挙していきます。

  • 障害に気づきにくい
    • そもそも万全な待機ができないので、生半可な通知では反応が遅れることも多いです
  • 作業が競合して二次災害が起こる恐怖
    • 自分が気づいた頃には他の誰かがすでに対応開始しているかもしれない
    • うっかりクリティカルな操作が競合して二次災害が起こったり
    • かといってエスカレフロー作るのはちょっとやりすぎ感がある
  • 個人での対処の判断が難しいケースもある
    • 相談したいんだけど相手が気づいてくれなくてヤキモキしたり
  • コストかけたくない
    • PagerDutyとかあるけどそこまでお金払いたくない
    • OSSでもwakerとかあるけど監視サーバの保守もあまりしたくない
    • 固定費はかけたくない、でも障害には気づきたいんや。。!(ジレンマ)

ということで、すごくお手軽にこれらの問題を解決できそうな仕組みを考えました。

で、思いついたのが 障害アラートベル×同時通話×サーバレス という組み合わせです。

障害アラートベル×同時通話×サーバレス

アラートベルによる通知

f:id:kohtaro24:20181206013850p:plain 電話発信で障害を伝える仕組みは近年よく採用されてるように思えます。プッシュ通知よりは気づけますからね。

同時通話で障害対応をサポート

f:id:kohtaro24:20181206020443p:plain せっかく電話で受けるなら、電話をもっと有効活用したいじゃないですか。 電話に出た人同士の通話をそのまま繋げてしまえば、コミュニケーション取りながら対応できるので作業の競合が防げたり、相談も都度行えますね。

サーバレスでコストカット

これらの仕組みをサーバレスで実現できれば、固定費がまるまる浮くわけですね。 起きるかどうか分からない障害の通知のために常時サーバを動かしておくのは、可能なら避けたい心理があります。

作ってみた

f:id:kohtaro24:20181206024214p:plain さまざまな監視クライアントと連携できるように、このようなWebhook URLをトリガーにするようにしました。 GETパラメータで発信先の番号と着信応答時のメッセージ内容を指定することで、アラートイベントに合わせて柔軟に内容を変更できます。

実際に動いている様子がこちらです。

いかがでしょう?ちょっと使ってみたくなりませんか!?

作り方

これらの仕組みは全てTwilio上で構築されており、サーバレスの実現にあたりTwilio Functionsを使用し、同時通話にはConferenceを使用しています。着信からの同時通話の仕組みについては去年作ったこれが大体ベースです。

ものの30分であっという間に実現できてしまうのでぜひ試してみてください。

発信用電話番号を購入する

Twilioコンソールにログインし、電話番号の購入ページから適当に050番号を探して購入します。

050番号は毎月108円の維持費がかかります(これだけは固定費としてかかってしまう)。 f:id:kohtaro24:20181206031223p:plain

Functionsを作成する

Functionsを使ってサーバレスアプリケーションを作っていきます。

  • Functionsについて
    • 実態はAWSAPI GatewayとLambdaのラッパー
    • Twilioコンソール上でjavascriptを書いてボタンをポチるだけで特定のエンドポイントにデプロイできる
    • 静的ファイルのホスティングを提供するTwilio Assetsと組み合わせれば割と高い自由度が得られる

API Gateway×Lambdaを使った場合と比較すると、このあたりが嬉しいところですかね。

  • デプロイが楽
  • TwilioのNode.jsライブラリがデフォルトで使える
  • Twilioに関するクレデンシャル情報を省略できる
  • Twilioからのリクエストのみを受け付けるように制限かけたりが簡単にできる

ではまずTwilio Functionsの設定画面を開きます。

Enable ACCOUNT_SID and AUTH_TOKEN がONになってることを確認し、なってなければONにしてSaveします。 f:id:kohtaro24:20181206043020p:plain

次にTwilio Functionsの作成画面を開きます。 f:id:kohtaro24:20181206035113p:plain

今回は二つのFunctionを用意します。

  • Function(1): 任意のクライアントからリクエストを受けて各電話番号に発信する
  • Function(2): 発信の応答者を電話会議につなぐ

テンプレートを聞かれるのでBlankを選択して作成。 f:id:kohtaro24:20181206035215p:plain

作成されたら、このように書き換えます。 f:id:kohtaro24:20181206040730p:plain

以下がFunction(1)の処理内容です。

GETパラメータとして二つのパラメータを受け取ります。

  • telsパラメータ:+81から始まる発信対象の番号をカンマ区切りで入れる
  • messageパラメータ:応答後に読み上げる音声メッセージのテキストを入れる
exports.handler = function(context, event, callback) {
    const twilioClient = context.getTwilioClient();
    // telsパラメータ内の電話番号に対して順番に発信
    event.tels.split(',').forEach(function(tel){
        twilioClient.calls.create({
            // 応答があったらFunction(2)のURLに処理を渡す。
            url: 'https://' + context.DOMAIN_NAME +'/join_conference?message=' + encodeURIComponent(event.message),
            from: '+815012345678', // 購入済みのTwilio番号(発信番号として通知される)。自分が購入した電話番号に書き換えること
            to: tel,
        }, function(err, result) {
            console.log('Created message using callback');
            console.log(result.sid);
            callback();
        });
    });
};

Saveを押したら即デプロイされます。 f:id:kohtaro24:20181206042551p:plain

これで任意のクライアントからWebhookリクエストを受け、各電話番号に発信する仕組みが出来ました。 f:id:kohtaro24:20181206050046p:plain

次にFunction(2)を作成します。 (1)と同様にBlankを選択して作成したら、このように書き換えます。 f:id:kohtaro24:20181206044901p:plain

以下がFunction(2)の処理内容です。

GETパラメータとして一つのパラメータを受け取ります。

  • messageパラメータ:応答後に読み上げる音声メッセージのテキストを入れる(Function(1)から渡される)
exports.handler = function(context, event, callback) {
    let twiml = new Twilio.twiml.VoiceResponse();
    let conferenceName = 'AlertRoom'  
    twiml.say({ language: 'ja-JP' }, "エマージェンシー、エマージェンシー、" + event.message) // アラートメッセージを自動読み上げ
    twiml.dial().conference(conferenceName); // AlertRoomという電話会議室に招待。初めの一人は保留音が鳴った状態で待機になる。
    // twiml.dial().conference({ waitUrl: 'https://hogehoge/assets/保留BGM.mp3' }, conferenceName); // 保留音として任意のBGMを流したい場合はこうなる
    callback(null, twiml);
};

Saveをお忘れなく。

これで発信の応答者を電話会議につなぐ仕組みができました。 f:id:kohtaro24:20181206050510p:plain

なんとこれで完成です! Function(1)のエンドポイントにGETパラメータをつけてアクセスしてみましょう。

https://#{自分に割り当てられたサブドメ}.twil.io/alert?tels=#{発信したい電話番号(+81が必要)をカンマ区切りで}&message=hoge

あとは任意の監視ツールにて特定のアラートを拾った際に、このURLにWebhookリクエストを飛ばすようにしてしまえばOKです。

ちなみにTwilioでもWebhookアラートは設定できます。 f:id:kohtaro24:20181206052204p:plain

Webhookじゃなくてメールしかないよって場合は、Sendgridとかとうまく連携する感じですかね〜…

まとめ

  • 障害に気づきにくい

    → 電話鳴らして気づかせる

  • 作業が競合して二次災害が起こる恐怖

    → 電話で状況やりとりすればいいから大丈夫!

  • 個人での対処の判断が難しいケースもある

    → 電話で相談すればいいから大丈夫!

  • コストかけたくない

    → サーバレス化で固定費は毎月108円(番号維持費)。障害発生時に通話料が発生

    → ちなみにFunctions自体は毎月1万アクセスまで無料

    → Webhook作っちゃえば導入側はURL指定するだけなので片手間にできる

電話ってすげえ。

いかがでしたでしょうか。お金と人がいればこんな問題はあまり気にならないかもしれませんが、小さいチームには需要あるんじゃないかということで記事にしてみました。

もしよかったら試してみてください。ご覧頂きありがとうございました。

TwilioでAmazon Pollyが使えるようになったので早速試した

  • TwilioではTwiMLのSay動詞を使って通話相手に対するTTS(Text To Speech)を手軽に実行できる
  • 日本語も読み上げてくれるがかなり棒読みなので実運用で使うには厳しさがある
  • そのため読み上げ品質を妥協できない場合は他サービスの音声合成APIなどを使って独自に音声読み上げを実現するケースも多かった
  • 先日のTwilioBlogによるとSay動詞が利用するTTSプロバイダとしてAmazon Pollyが選択可能になったらしい

やってみよう

TTSプロバイダをAmazon Pollyに変更する

YOUR DEFAULT PROVIDORをAmazon Pollyに設定

f:id:kohtaro24:20180809134751p:plain

間もなく変更が完了する

f:id:kohtaro24:20180809134724p:plain

なお、Amazon Pollyにプロバイダを変更すると100文字読み上げあたり0.0008ドルのコストが発生することには注意(デフォルトは無料)。

Sayで音声を流してみる

こんな感じの音声を流してみた

<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Say language="ja-JP">お電話ありがとうございます。現在、電話に対応できるオペレーターがおりません。恐れ入りますが、改めてお掛け直しください。</Say>
</Response>

Before(Pollyじゃない方)

おかけなお⤴しください

after(Pollyの方)

棒読み感はまだありますが明らかにPollyの方がいいですね。

個人的にはとりあえずPollyに変更しとこうかなと思いました。 記事によるとPollyのSSMLもSay動詞内に書けるようなので、色々調整はできそうです。  

朝起きれない問題に本気で向き合う目覚ましサービスを作った

この記事はTwilio AdventCalendar 2017かつSpeee AdventCalendar 2017の21日目の記事です。

昨日の記事はこちら

朝起きれていますか?

朝早く起きれると、自由な時間が増えて色々ハッピーになります。

朝起きれない問題

早起きが得なことは分かっているのですが、早起きは大体いつも失敗してしまいます。 その原因を考えてみます。

目覚ましアラームは次第に効能が悪くなる

朝起きるためにスマホ等でアラームをセットするわけですが、セットしたアラーム音も毎日聞いてると慣れてきて、次第に聞き流せるようになってしまいます。 そうならないように ものすごく嫌な音 をセットしてみたりするのですが、あまりにも嫌すぎて無意識の間にアラームを止めてしまうことが多々ありました。

逆説的には、 日々新鮮で楽しい出来事 が朝の目覚めには必要なのかもしれません。

「全部自己責任だから」という甘え

早起きはいつも失敗するものの、 出社が間に合わなくなるまで寝てしまうケース はほぼありません。 つまり、 自分以外の存在に対して影響が及ぶ状況下 では起きれるのです。

結局のところ、「早く起きれなかった分には誰も困らない」という甘えが裏にあるのではないでしょうか。

どうすれば朝起きれるのか

問題点の逆説から、以下の二つを抑えれば早起きの成功率が上がりそうです。

  1. 起きる度に新鮮で楽しい出来事が発生すること
  2. 自分以外の存在に対して影響が及ぶ状況下であること

上記を実現する最もシンプルな方法は 人に起こしてもらう ことです。

コミュニケーションを取ることで意識が覚醒する

起こしてくれる人がいるということはつまり、対話が発生します。対話は更新され続けるものであるため、常に新鮮な刺激とストレスの解放を得ることができます。

起こしてくれる誰かがいるという状況が気持ちを動かす

起こしてくれる人がいる手前、流石に無視するわけにはいきません。自分が起きずにいることがその人の損害になってしまいます。

起こす側の問題

上記から、起こしてくれる人の存在が非常に重要だということが分かりました。

しかし、起こす側の人にも解消すべき問題があります。

それは 起こす人が先に起きていないといけない という前提条件です。 すなわち、この条件を満たす上では 起こす人を起こす人 が必要であり、以下この構造は無限に続きます。

f:id:kohtaro24:20171221025628p:plain

この無限階層問題を脱出する上では 互いが互いを起こし合う という仕掛けを実現する必要があります。 f:id:kohtaro24:20171221031021p:plain

遠隔地にいる人を起こさなければならない時、最もよく使われるであろうツールはなんでしょう?

そう、 電話 です。

つまり お互いの電話に自動発信できる仕組み があればいいんです。 f:id:kohtaro24:20171222013832p:plain

お互いの発信によってお互いが目覚め、更に会話することによって意識が覚醒する。あわよくばそのまま二人の予定も立てられる。これは起きれそうです。 f:id:kohtaro24:20171222014717p:plain

作ってみた

  • 目覚ましの予約

目覚ましを利用するために予約を行います。 予約は以下のようなフローで行えます。

  1. 指定の電話番号に発信する
  2. 音声案内に従い、起こして欲しい日時と一緒に起きたい人の電話番号を伝える
  3. 寝る
  • 予約時刻のコール

予約した日時になると2人の電話が着信し、お互いが着信に応答した時点で通話がはじまります。

仕組みについて

この機能の大半はTwilioを用いることで実現しています。 バックエンドのアプリケーションとしてRuby on Rails、スケジューラとしてSidekiqを利用しました。

目覚まし予約の仕組み

Twilioの電話番号に着信が来るとRailsがイベントを受け取り、命令をTwiMLで返しています。 f:id:kohtaro24:20171222023201p:plain

このケースでは、Sayによる音声案内、Gatherによる音声テキスト化をTwiMLから利用しています。

Gatherの結果で得られたテキストから、rails側で正規表現なり駆使して必要なパラメータを抽出し、Sidekiqにスケジュール登録します。

f:id:kohtaro24:20171222023223p:plain

同時発信の仕組み

予約された日時になると、SidekiqがTwilio REST APIに電話番号への発信をリクエストすることで、Twilio経由で対象者への発信が行われます。

f:id:kohtaro24:20171222023750p:plain

Twilioの発信に対して応答があった際、Railsがイベントを受け取り、命令をTwiMLで返します。

f:id:kohtaro24:20171222024200p:plain

このケースでは、Dial, Conferenceによる電話会議の機能を利用しており、発信への応答者を随時Conference Room(会議室)に招待します。

f:id:kohtaro24:20171222024411p:plain

これにより、 2人が着信に応答したタイミングで通話が開始される 仕組みを実現できます。

だいぶざっくりした説明になってしまったのですが、詳細が気になる方はソースコードこちらにあるので御覧ください。

この目覚ましの特徴

① アプリもサイトも不要

予約機能も電話一つで利用できるようにしたので、ぶっちゃけ家電だったりガラケーでも問題なく利用できます。 そしてhtmlもcssjavascriptも書かないで済むので開発者が楽です。

② 予約者以外は朝起きて電話に出るだけでいい

親しい人と一緒のサービスを利用したいとなった場合、多くのSNSなどではアプリを入れる必要があったりアカウントを作ったり友達申請したりと、利用を開始できるまでの障壁がいくつもあります。この目覚ましにおいては、予約者が相手の番号を知ってるだけで利用条件が満たされます。

(全然知らない人の電話番号を登録するようなイタズラもできてしまうので、もし公に運営するならSMS認証挟んだりなどの対策が必要だと思います)

③ 最大250人で一緒に目覚ましできる

Conferenceの最大キャパシティが250人なので、その気になれば250人で同時に目覚ましすることも可能です。

(とても通話なんかできたもんじゃないと思いますが)

いかがでしょうか

この目覚まし、めざましくないですか?

正直、ConferenceやGatherを使ってみたいという気持ちだけが先行して、テーマ自体は完全にネタのつもりだったのですが、中々面白いものが形になったなと思います。

Twilioはまだまだ面白い使い方ができそうなので、引き続き試していこう。

おまけ

某モンスターの鳴き声で音当てゲーム的なものも作ってみました

libphonenumberをrailsに導入して電話番号を自在に扱う

libphonenumberについて

電話番号のバリデーション関連ライブラリをGoogleが公開したもの。Googleの2段階認証プロセスなどで導入されているらしい。

番号のパターンからフリーダイヤルか携帯かなど種別も判定することができる。 実態はPhoneNumberMetadata.xmlを始めとする正規表現メタデータを元に判定している様子。

libphonenumber関連記事

ruby on railsでlibphonenumberを使うにあたって

今のところ公式にサポートされている言語はJava, C++, JavaScriptのみだが、libphonenumberベースのruby gemは非公式ながらも存在する。

どちらのGemもPhoneNumberMetadata.xmlをMarshal.dumpしたdatファイルをgem内に有している。

で、どっちを使えばいいの?

libphonenumberの基本的な機能はどちらのGemでも大体サポートしている

ロケーションや番号種別ごとのバリデーション

irb(main):020:0> TelephoneNumber.valid?('03-1234-5678', :jp)
=> true

# Phonelib.parse('03-1234-5678', :jp).valid? でもいける
irb(main):018:0> Phonelib.valid_for_country?('03-1234-5678', :jp)
=> true

任意の番号ルールの拡張

標準の番号ルールを拡張して、独自のブラックリストなりを作ることも可能そう。

TelephoneNumber.override_file = '/path/to/override_phone_data.dat'

Phonelib.override_phone_data = '/path/to/override_phone_data.dat'

固定電話番号からのジオコーディング

irb(main):008:0> TelephoneNumber.parse('03-1234-5678', :jp).location
=> "Tokyo"

irb(main):009:0> Phonelib.parse('03-1234-5678', :jp).geo_name
=> "Tokyo"

電話番号からの種別判定

irb(main):051:0> TelephoneNumber.parse('0120123456', :jp).valid_types
=> [:toll_free]

irb(main):029:0> Phonelib.parse('0120123456', :jp).types
=> [:toll_free]

電話番号のE164フォーマット化

irb(main):054:0> TelephoneNumber.parse('0120123456', :jp).e164_number
=> "+81120123456"
irb(main):055:0> Phonelib.parse('0120123456', :jp).e164
=> "+81120123456"

Phonelibでのみサポートされている機能

ActiveRecord Integration

modelのvalidatesにもこんな感じでそのまま書ける。validatorクラスとか作りたくない人用かな。

validates :attribute, phone: { possible: true, allow_blank: true, types: [:voip, :mobile], country_specifier: -> phone { phone.country.try(:upcase) } }

Phonelibを使うことにした

  • telephone_numberと比較して提供されてる機能が豊富
  • libphonenumberの最新のメタデータに追従できている
  • commit頻度が高くcontributorも多い安心感

railsのmodelにバリデーションを適用する

やりたいこと

  • 電話番号のフォーマット違反の検知
    • これは Phonelib.parse(value, :jp).valid? などでいける
  • 受け入れたくない電話番号種別の検知
    • Phonelib.parse(value, :jp).types でvalidした電話番号種別が取れるので、良しなに弾く

受け入れる番号種別はホワイトリストではなくブラックリストで運用した方が良さそう

電話番号の種別はこれからも増えることが予想され、そういった場合ホワイトリストだと意図しないものまで弾いてしまいかねない。

よって確実に通したくないものを追記する運用で考える。

今回は明らかに個人ではないと思われる電話番号種別を弾いてみる。

実装

# frozen_string_literal: true
class TelephoneValidator < ActiveModel::EachValidator
  REJECTABLE_TYPES = %i(toll_free uan premium_rate pager).freeze

  def validate_each(record, attribute, value)
    parsed_tel = Phonelib.parse(value, :jp)
    return unless parsed_tel.invalid? || parsed_tel.types.any? { |type| REJECTABLE_TYPES.include?(type) }

    record.errors[attribute] << '無効な電話番号です'
  end
end

あとはmodelに

# frozen_string_literal: true
class User < ApplicationRecord
  validates :tel, telephone: true
end

みたいな感じで適用して終わり!

[49] pry(main)> User.new(tel: '08012345678').valid?
=> true

# 080は11桁であるべきなのでinvalid
[50] pry(main)> User.new(tel: '080123456789').valid?
=> false

# 0120は許可してない種別(フリーダイヤル)なのでinvalid
[51] pry(main)> User.new(tel: '0120123456').valid?
=> false

CoffeeScriptをやめてES6を使うためにやったことまとめ

この記事は Speee Advent Calendar 2017 11日目の記事です。

10日目は @iida-hayato による 5000兆円トークンに見るスマートコントラクトの設計と実装。 でした。

記事について

Rails製の自プロダクトからCoffeeScriptを廃止してES6を使えるようにした話です。

CoffeeScriptやめたいけど、色々やること多そう。。」みたいなところで悩まれている方々の参考になれば幸いです。

作業ログをベースに書いてるので、自プロダクト環境固有の話を含むことをご了承ください。

背景

  • オワコン化が叫ばれるCoffeeScript
    • ES6のリリースにより、クラス構文などが標準JSで利用できるようになった
    • 今となっては学習コストが高いわりに強い恩恵がない
      • 普段Rubyを書かないフロントエンジニアにとっては特に
    • TypeScriptの登場
  • フロントエンジニアの採用リスク
    • 上記要因などあって、CoffeeScriptを書ける/書きたがるフロントエンジニアが市場から減っている
  • TypeScript採用したい
    • そのためにもまずはCoffeeScriptを標準JSに置き換える

移行前のフロントエンド状況

ビルド方法

  • coffeeify + browserify + gulpで frontend/配下の .coffee をトランスパイル+バンドル
  • バンドルした .jsapp/assets/javascripts 配下に配置
const gulp = require('gulp');
const browserify = require('browserify');
const coffeeify = require('coffeeify');

const dir = {
    srcJS: "./frontend/js/",
    distJS: "./app/assets/javascripts/"
};

const coffeeFiles = [
    'hoge.coffee',
    'fuga.cofffe',
    ...
]

gulp.task('build', () => {
    jsFiles.forEach(function (file) {
        var srcfile = coffee2jsExtension(file);
        browserify({
            entries: [dir.srcJS + file],
            extensions: ['.coffee'],
            transform:  [coffeeify]
        })
        .bundle()
        .pipe(source(srcfile))
        .pipe(gulp.dest(dir.distJS));
    }
});

function coffee2jsExtension(file){
    return file.replace(".coffee", ".js");
}

やったこと

  1. decaffeinateで既存の.coffeeを.jsに全て置き換え
  2. トランスパイル周辺の設定
  3. コケるfeature specをなんとかする
  4. JSエラー検知の有効化

今回のスコープではやらないことにしたもの

使用言語やフレームワークのリプレイスは遅れるほど負債が溜まっていく上、ステークホルダーがいつまでも笑顔でいるとは限りません。

とにかくCoffeeScriptをやめることを第一優先に、下記に挙げられるものは別タイミングでやることに決めました。

  • ESLint導入
  • browserify => webpackへの移行
  • ES6風のコードにする

1. decaffeinateで .coffee.js

coffeeファイルをjsに置き換える上で、decaffeinateを利用します。

ちなみにdecaffeinateという言葉には「カフェインを取り除く」という意味があるようです。おしゃれ。

$ npm install -g decaffeinate
$ find ./ | grep .coffee | grep -v node_modules | xargs -I{} decaffeinate {} 
.//app/assets/javascripts/hogehoge.coffee → app/assets/javascripts/hogehoge.js
.//app/views/hoge/histories/edit.coffee → app/views/hoge/histories/edit.js
.//frontend/js/hoge/hige.coffee → frontend/js/hoge/hige.js
...

suggestionsが付与されたコードを確認する

decaffeinateの思想は 生成したコードの動作の正確性 に重きを置いており、そのため生成されたコードが既存コードの意図よりも 過剰に防御的 になってることがあります。

そういった可能性があるjsファイルに対して、decaffeinateはsuggestionsを残します。

decaffeinateの実行結果の例

-  $('.hogeButton').on 'click', (event) ->     
-   button  = $(event.target)      
-   {from, to}= button.data()      
-  
-   $.ajax(        
-     url: "/hoge/hige"      
-     data: { from: from, to: to }     
-     dataType: 'json'     
-     method: 'POST'       
-   )

+ /*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */

+ $('.hogeButton').on('click', function(event) {
+   const button  = $(event.target);
+   const {from, to}= button.data();
+
+   return $.ajax({
+     url: "/hoge/hige",
+     data: { from, to },
+     dataType: 'json',
+     method: 'POST'
+   });
+ })

自プロダクト内で散見されたsuggestionsをいくつか紹介します。

DS102: Remove unnecessary code created because of implicit returns

  • CoffeeScriptは関数の終わりに暗黙的に最後のステートメントをreturnしている
  • JavaScriptにはこの動作はない
  • decafeinateはCoffeeScriptと同一の挙動を模倣するので、必ず最後にreturnが生成される
  • よって実装上不要なreturnは確認した上で消しましょう

DS104: Avoid inline assignments

  • 関数の結果を再利用するためにdecaffeinateが変数を自動生成する
  • 評価順序を模倣するためインラインで代入を行っており、コードが見辛くなることがある

CoffeeScript

accounts[getAccountId()] //= splitFactor

decaffeinate後のJavaScript

let name;
accounts[name = getAccountId()] = Math.floor(accounts[name] / splitFactor);

修正例

const accountId = getAccountId();
accounts[accountId] = Math.floor(accounts[accountId] / splitFactor);

DS207: Consider shorter variations of null checks

  • CoffeeScriptにはnullやundefinedを評価する ? 演算子がある
  • JavaScriptでは != nullに置き換えられるが、単にboolとして扱うほうが望ましいケースもある

CoffeeScript

if currentUser()?
  sendMessage(customMessage() ? 'Hello')

decaffeinate後のJavaScript

if (currentUser() != null) {
  let left;
  sendMessage((left = customMessage()) != null ? left : 'Hello');
}

修正例

if (currentUser()) {
  sendMessage(customMessage() || 'Hello');
}

その他のsuggestionsのパターンと解消例はsuggestions.mdを参照。

2. トランスパイル周辺の設定

  • coffeefy関連の処理を削除
  • babelifyを導入
    • ES6を既存ブラウザで実行できる形式(ES5)にトランスパイルする
    • IEなどES5のサポートが限定的なブラウザに向けてbabel-polyfillも併用

package.json

  "dependencies": {
+     "babel-plugin-espower": "2.1.2",
+     "babel-polyfill": "6.26.0",
+     "babel-preset-es2015": "6.6.0",
+     "babelify": "7.2.0",
    ...
  },
+   "babel": {
+     "presets": [
+       "es2015"
+     ]
+   }

gulpfile.js

const gulp = require('gulp');
const browserify = require('browserify');
- const coffeeify = require('coffeeify');
+ const babelify = require('babelify');
+ const fs = require('fs');

const dir = {
    srcJS: "./frontend/js/",
    distJS: "./app/assets/javascripts/"
};

- const coffeeFiles = [
-     'hoge.coffee',
-     'fuga.cofffe',
-     ...
+ const jsFiles = [
+     'hoge.js',
+     'fuga.js',
]

gulp.task('build', () => {
    jsFiles.forEach(function (file) {
-         var srcfile = coffee2jsExtension(file);
-         browserify({
-             entries: [dir.srcJS + file],
-             extensions: ['.coffee'],
-             transform:  [coffeeify]
-         })
-         .bundle()
-         .pipe(source(srcfile))
-         .pipe(gulp.dest(dir.distJS));
+         browserify(dir.srcJS + file, { debug: true } )
+             .transform(babelify)
+             .bundle()
+             .pipe(fs.createWriteStream(dir.distJS + file));
    }
});

- function coffee2jsExtension(file){
-     return file.replace(".coffee", ".js");
- }

各エントリポイントのJSにbabel-polyfillを適用

+ import 'babel-polyfill'

3. コケるfeature specをなんとかする

  • Poltergist + PhantomJSで書いてたfeature specがコケる
    • buttonのclickイベントが発火しない, etc
  • クロスブラウザチェック等では問題なく動作できている
  • PhantomJSでしか発現しない問題に対処するのは辛い

というような経緯で、これを期にHeadless Chromeに乗り換える決断をしました。Headless Chrome乗り換えに関する記事は後日書きます。

4. JSエラー検知の導入

クロスブラウザチェックとテストコードが通ることを確認したものの、 何か漏れてるリスクも踏まえてSentryによるJSのエラー通知を導入しました。

package.json

  "dependencies": {
+     "raven-js": "3.0.4",
  ...

各エントリポイントに追加

var Raven;

if (typeof history.pushState === "function") {
    Raven = require('raven-js');
    Raven.config(sentry_dsn).install();
}

5.リリース後

リリース後1ヶ月程度が経ちましたが、DS102の対応(不要なreturnの削除)で、消してはいけないreturnを消してた箇所が一つあったという人為的な凡ミス以外に、今のところ問題は起きていません。 工数的に大きかったのはクロスブラウザチェックやfeature spec回りで、decafeそのものは比較的スムーズに行うことができたという所感です。

とはいえフロントエンドも定期的に負債解消しないとツケが大きくなってくるのは当然で、 バックエンドだけでなくフロントエンドも継続的にキャッチアップしていかなければなぁ。。という気持ちになりました。

次回は @yt-tanabeより「SpeeeKaigiのつくりかた」です。

プロダクト開発プロセスを考えるようになってから一年が経った

Speee Advent Calendar 2016」の25日目です。

世間ではクリスマスとかいう流行り病がキテるようですが、この記事でクリスマスの話は一切しません。

クリスマスっぽい記事を読みたかった方は、@pataiji氏が書いた超絶リア充記事を御覧になれば満足頂けるかと思います。

 

自己紹介

株式会社Speeeに2015年新卒エンジニアとして入社しました。

2015年10月末頃まではウェブマーケティング事業に関連する社内ツールの開発を主務とし、

2015年11月頃からはとある新規事業の立ち上げにリードエンジニアポジションとして携わることになり、以来その事業にてプロダクトの機能開発と運用を約1年間続けてきました。

 

初めてのプロダクト開発

とある新規事業にアサインされるまで、プロダクト開発(=一つのプロダクトを長期的に開発/運用していくこと)というものをきちんと経験したことはありませんでした。

そんな中での開発チーム立ち上げとなったのですが、初期リリースの期限が早速迫っているという事情もあり、非常に大まかなフローのみを決めて開発を進めていくこととなります。

f:id:kohtaro24:20161225184644p:plain

一般的な開発プロセスといえばこうだよね、みたいな。

開発プロセスの大きな段階を一つずつ切り出して並べ、あとは見積もった工数に基いてざっくりとしたスケジュールに落とし込む、という感じでした。

 

結果どうなったか

リリース目標日にはなんとか間に合わせることができました。

ただ、計画通りに事が運んだというわけではなく、最後の最後に予実を合わせにいった形です。

f:id:kohtaro24:20161225213838p:plain

 何が起きてたのかというと、こんな感じ。実装中に仕様や優先度の変更が行われたり、設計や見積もりのやり直しが発生しています。

仕様や優先度の前提が覆る

事業の立ち上げ初期の現場において、数日の間に仮説が変化する場面は珍しくなく、「初期リリースの時点でどんな機能がどんな状態で必要なんだっけ?」という前提が実装期間の中で右往左往し、リリースのスコープから外れる機能が出てきたり、逆もまた然りであったり。

工数見積もりも覆る

「この開発はリリース日までに間に合うのか?」を明確にする上で見積もりは重要です。しかし、長期的な機能開発に費やされる工数を正確に見積もるのは大変で、仕様や設計が変化すると見積もりに関してもやり直す必要が出てきます。見積もった結果がリリース日までに間に合わないと判断された場合は、仕様上簡素化できそうな部分を探したり、代替案を考えて調整したりします。そして再度見積り。

 上記のような要因が重なった結果、初期リリースに含まれた機能群は当初見込んでいた数の半分以下になり、テストや修正に費やされる期間が縮まったことで、リリースのタイミングを予定と合わせることができた、という形です。

 

開発プロセス検討

なんとか初期リリースを乗り切った後に、これからの開発プロセスをどうしていこうか、という話し合いをチームで実施しました。

アジャイルスクラムといったキーワードがそこで登場するようになり、「アジャイルかぁ。。聞いたことはあるけどどういうものなんだろう」とか思いながら、アジャイルサムライSCRUM BOOT CAMP のような書籍を読み漁りつつ、自分たちのリソースで運用していくことができそうな開発プロセスを思案しました。

大きく変えていったことを以下に列挙していきます。

開発のリリース頻度について

プロダクトの初期リリースというのは、開発計画における分かりやすい一つのマイルストーンでした。一方で、これから進めていく開発については二つの案が考えられました。

  1. 何月何日にどんな機能をリリースをするかを事前に決めてから開発する
  2. 優先度が高いとされる機能から開発し、開発完了次第即リリースする

自分たちのチームにおいて、1のような開発が求められるケースは例外的であり、基本的には目の前の価値を早く届けることに注力したいという観点から2の案を選択しました。 

開発の周期について

先の項で書いたように、立ち上げフェーズの事業においては著しく状況が変わるため、開発チームが事業課題に追従しやすい動き方を取る必要があります。

優先度や計画を調整したり、振り返りを行い易くするために、開発プロセスに一定の周期を設けることが必要だと考えられました。

f:id:kohtaro24:20161226012607p:plain

ということで、一回あたりの開発周期(スクラムでいうところのスプリント)を1週間と置き、毎週の金曜日を締め日として、振り返り・新規開発要望及び優先度の確認・計画引き直しを毎週実施するようにしました。

工数見積もりについて

この件については、工数見積をどのくらいまで厳密にやるのか、ということが争点でした。「この機能を開発するのに〇〇時間かかります!」ということを伝えたとして、実際には開発する人によって結果が違ったり、コードレビューやリリース作業などが入ったりもするので、〇〇時間という数字を強く信用されると色々困ることもあり、これを伝えるには結構な勇気がいります。それ故に見積もり自体も非常に神経をすり減らす作業になります。

とはいえ、この機能開発は今週の計画内に組み込めそうかであったり、チームがどのくらいの開発を今週捌こうとしてるのかくらいは把握可能にしておきたいという事情はあります。

そこで自分たちのチームではストーリーポイント見積もりを採用しました。

時間ではなく作業の大きさを相対的に表したポイントを使うことで、時間を扱う際には密に考慮する必要のあった要素を、可能な限り見積もり作業から排除できます。

また、週ごとに消化できたポイントの数を計測していくことで、この機能開発は今週の計画内に組み込めそうかなどの判断もできるようになっていきます。

要件定義について

ぽっと出の開発要望の中には、「この要件、本当に適切?」って思うようなものも少なからず含まれます。

何を叶えるべきかが明確であれば、満たさなければならない要件はもっとシンプルにできるんじゃないか?ということで、開発要望において主要な項目をまとめたものをテンプレート化しました。

f:id:kohtaro24:20161226003513p:plain

githubからIssueを作ろうとすると、このようなテンプレートが表示されます。

各項目に関しては以下のような内容を記載します。

  • (Who) として
    • 要望の出処を明らかにする。
    • 要望の持ち主が誰なのかを把握することで、仕様に関するヒアリング等を誰に行うのが妥当なのかを判断できる。
  • (What) がしたい
    • どのような要望なのかを明らかにする。
    • 本質的なゴールは何なのかを把握することで、「どうなるのが理想なのか」を基準に仕様を検討できる。
  • なぜなら (Why) だからだ
    • その要望が発生した背景、実現することの妥当性について明らかにする。
  • 本Issueは (Acceptance Criteria) を以って完了とする
    • その要望を実現する上で、何がなされている必要があるかを明らかにする。

項目を共通化することで内容を把握しやすくするとともに、主要な項目に絞ることで論点が明らかになるようにしています。

そして1年が経った

上記にあげたような開発プロセスをおよそ1年間の間、少しずつチューニングを繰り返しながら継続してきました。

この開発プロセスを導入して以降、前より良くなった面は沢山あると思っているものの、1年も続けていると形骸化してきている部分がチラホラあったり、事業としてのフェーズも大きく変わっていたりするので、

  • 本来やりたかったことが今も守れているか?
  • そもそも今となっては不要(ないしは形を変えるべき)なことはないか?

など、もう一度見つめ直さないといけないなぁという気持ちでこの記事を書きました。

開発プロセスは、エンジニアが価値を提供し続ける上で絶対に欠かすことのできないもので、かつBiz側の方々の協力無しでは成立し得ないものだと思います。

日頃からの協力者である様々な方々に感謝の念を送りつつ、今年のクリスマスを終えます。良いお年を。