Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

アラート対応で気をつけていること

こんにちは!開発本部の id:lime1024 です。

アラート対応について社内で esa にまとめていたところ、参考になると社内でフィードバックをいただいたので開発者ブログでも紹介します。

前提として、平日日勤帯でのアラート発生を想定しています。

対応するときに気を付けていること

初動は早くする

アラートが発生しているという状況を把握して報告するのは、一分一秒でも早くするように心がけています。

Classi では Datadog の Monitor を使っており、その通知先が Slack になっています。基本的には各アプリケーションのオーナーであるチームにメンションが飛ぶようになっているので、メンションが飛んできたらまずは該当のアラートを「見ること」を、アラート対応のためのチャンネルに書きます。 「見ること」を書くことで、アラートが出ても誰も見ていないという状態を防ぐことや、一人で見る自信は無いけれど誰かと一緒であれば見てみたいという人も反応しやすくなります。

なにはともあれ反応する

自分にメンションが飛んできているということは、対応する必要があるからです。もし、メンションが来ているけれど反応しない・または反応する必要がないときは、メンションを投げる先が間違えているか、しきい値が間違っているのでアラートがオオカミ少年にならないように見直しを行います。

もしも、通話中だったり忙しいときは今やっていることが障害対応より優先されるものかどうかをまず考えます。とは言え、本当に忙しくて対応が難しいときはあるので、そのときは "誰か見れませんか?" と書きます。

何を見ているか・何を対応しているかを外からわかるようにする

アラートに対していま何をしているかは、アラート対応のためのチャンネルでオープンにやるようにしています。オープンにやるというのは以下の 2 点です。

  • 基本的には Slack のスレッドには書かない・書いてもスレッドの外に出す (Also sent to #channel)
  • アラート対応のためのチャンネル以外で対応しない

アラート対応の様子が気になって後から覗きに来た人の目線で考えると、Slack のスレッド機能では状況が見えにくいです。ただし、すでに収束していて調査内容をまとめるときや状況整理を行うとき等はスレッドに書くこともあります。同様にアラート対応のためのチャンネル以外に状況が記載されていると後から来た人がキャッチアップをしづらくなります。

また、どんなに対応が進んでいても、状況が書かれていないと外から見ても分からないため、定期的に対応状況のサマリを書くようにしています。特に複数人で通話しながら対応しているときに通話内で情報共有が進み、通話外の人への共有漏れが発生しがちになるため、気を付けるようにしています。

アラートの内容がわからないとき

わからないことを表明する

わからないからアラート対応をしなくてもいいわけではないので、わからないときはわからないことを表明するようにしています。すると、人がわらわら集まってきます。

もし誰も集まらなかったときは、ちょっとドキドキしますが Slack で @channel します。

何がわからないか深堀りする

いま出ているアラートに対して、自分がわからないのはアラートの内容なのか、それともアラートが発生しているアプリケーションのドメイン知識が無いのかを深堀りします。

アラート発生時の初動の時点では、アプリケーションのことはわからなくてもある程度はなんとかなりますし、アラート内容のこともアプリケーションのこともわからなくても見れるものはあります。自分がドメイン知識を持っていないアプリケーションでアラートが発生したときは以下のことを確認しています。

  • 該当のアプリケーションがいま動いているのか・体感で遅くなっていないかをブラウザから実際にアクセスして確認する
  • レスポンスタイムが遅くなっているのであれば、それはどのエンドポイントなのかを Datadog から特定する
  • 5xx エラーが出ているのであれば、それは ALB からなのか ECS からなのかを確認する
  • 該当のアプリケーションの ECS コンテナは生きているか・再起動を繰り返していないか
  • 該当のアプリケーションで直近でリリースは無かったか

以上のことを確認している間に、アラートに気付いた他の人達が集まってくれるはずです。

解決しようとしない

わからないものはわからないので、アラートに反応して対応もしたけど解決できなかった...と思わないようにしています。実際に解決する人・解決できる人の一助になれたら良いくらいの温度感でいるようにしています。

さいごに

アラートが発生してどうしよう!?となったときにこの記事を思い出して頂けたら嬉しいです。

社内向けのデータ基盤から集計結果をReverse ETLしてサービスに組み込んだ話

こんにちは、データエンジニアの滑川(@tomoyanamekawa)です。
Classiでは2022年5月に学校内のユーザー利用状況を集計し可視化したダッシュボード機能をリリースしました。
この機能のデータ集計は既存の社内用データ基盤からのReverse ETLで実現しました。
そのアーキテクチャの説明と「社内用データ基盤」から「ユーザー影響あるシステムの一部」になったことによる変化について紹介します。

ダッシュボード機能とは

  • 概要
    • 先生のみが利用可能な機能
    • 先生と学年・クラスごとの生徒の利用状況を可視化したダッシュボードを提供する機能
  • 要件・制約
    • アプリケーションはAWS上で動かす
    • 前日までの利用状況がアプリケーション上で朝8時までに閲覧可能になっていること
    • 学校/学年/クラスごとで集計する
    • 学校を横断した集計はしない

既存の社内用データ基盤とは

社内でのデータ分析を主な用途としているBigQueryを中心にした基盤のことです。
この記事の範囲では「AWS上で動いているアプリケーションのデータを日次でBigQueryに集約し、集計している」ということがわかれば十分です。
もし詳しく知りたい場合はこちらの記事で紹介しています。

Reverse ETLとは

各アプリケーション等からデータ基盤に集約・集計(ETL処理)したデータを、アプリケーションで二次利用するためにアプリケーション側の何らかのデータベースに書き戻す処理のことです。

もっと適切な定義があるかもしれませんが、当記事ではこの認識で進めます。
もしReverse ETLについて知りたい場合はこのあたりの記事がいいと思います。

この記事で話さないこと

  • アプリケーションの実装などのReverse ETLより後段のシステムについて
  • ダッシュボード機能のUIや集計内容含めた詳細
  • 既存のデータ基盤の詳細

Reverse ETLのアーキテクチャ

データ基盤のアーキテクチャ図

処理の説明

上記のアーキテクチャで日次のバッチ処理でデータを連携しています。
赤枠部分がReverse ETLに関連する部分です。

全体のワークフローはCloud Composerで管理しています。
DataLake側からDAGをトリガーして、下記の処理を順番に行うようになっています。

  • BigQuery上でqueryを実行しテーブルを作成
  • 集計結果のvalidation
  • AWS Step Functionsの実行をトリガーする

トリガーされたAWS Step Functionsが集計結果の書き込みを担っていて下記のような処理をしています。

  • 書き込み先の新しいテーブルをRDSに作成
  • 学校ごとにLambda jobを作成し、各LambdaがBigQueryからのデータ取得・RDSへの書き込みを行う
  • 全学校分の処理が正常に終わった場合にアプリケーションから参照するテーブルを新しいテーブルに切り替える

このアーキテクチャになるまでの背景

なぜアプリケーション上で直接集計せずに既存のデータ基盤を経由する選択をしたのか?

  • 複数のDBをまたぐ必要があり、1つのアプリケーションから横断的にアクセスするには既存の全体インフラ構成から変更が必要だった
  • 今後ログデータも集計に使う可能性があり、集約場所・データ量の観点からBigQueryが必要になりそうだった
  • 既存のデータ基盤でデータ集約や管理体制などがすでにある程度整っていて流用可能そうだった

データ基盤からのReverse ETL以外で検討した(が選ばなかった)選択肢

  • SaaSのBIツールを用いてデータ基盤上のデータから直接ダッシュボードを作成して、アプリケーションにembeddedする

    • そもそもアプリケーション側での可視化の実装がいらなくなり、ユーザー側で任意の集計が可能になるなどのメリットがあったが費用面から断念した
  • アプリケーションから直接BigQueryに接続する

    • BigQueryからのレスポンスが遅く、アプリケーションとして許容できなかった
    • BigQuery上での計算時間に関してはBI Engineやアプリケーション側でのキャッシュ等での高速化は可能だが下記の観点で不十分だった
      • この方法では初回の計算時間がかかってしまうことは避けられず、機能の特性上「1日1回見る」というユースケースが想定されるため、ユーザーの初回アクセス時に時間がかかることを許容できなかった
      • そもそもBigQuery上での計算時間以外の接続等でも時間がかかっていて、高速化の限界があった

Reverse ETLする上で他に検討した(が選ばなかった)選択肢

  • 書き戻し先をRDB以外にする

    • 集計の軸やデータの集計期間から今後のデータ量を考えるともRDBMS(postgreSQL)で対応できそうだった
    • 一般的なRDBMSで対応できそうであればその他のデータベースを優先する大きな理由がなかった
  • 書き戻し先をCloud SQLにする

    • Cloud SQLも選択肢に入ったが下記の理由からRDSを選択した
      • BigQueryからのwriteは1回/日なのに対してアプリケーションからのreadはもっと多いので、RDBはBigQueryよりアプリケーションに近い位置にあるほうが良かった
      • 社内的にCloud SQLよりRDSのほうが運用ノウハウがあった

社内用データ基盤からユーザー影響のあるシステムになったことによる変化

当たり前ですが、障害が起きた場合にユーザーにご迷惑をおかけしてしまうのでシステムを安定稼働させることの重要性が上がりました。
そのため社内用データ基盤であった時に設けていた基準よりも一段厳しくする必要がありました。
その対応内容を下記に示しますが、振り返ってみると新規の大きな問題は発生しておらず、元々それなりのレベルでデータ基盤を運用できていたという自画自賛をチームでしました。

気にしたこと・対応したこと

  • 社内用の分析のための処理とユーザー影響のある処理の分離

    • Google Cloudのリソースの観点では元々datalake用のprojectと社内ユーザーが利用するprojectは分けていたため問題なかった
    • Cloud Composer上のワークフロー(DAG)では処理が混ざっていたので、DAGの分離とコード整理をして気軽にデプロイしていい範囲とそうでない範囲をコードベースで分けた
  • データ基盤のSLO計測

    • もともと取り組んでいたので、計測・可視化自体は問題なかった
    • ダッシュボード機能の要件を元に基準にしている時刻・稼働率を引き上げるのみで済んだ
  • alert基準・通知の見直し

    • 何らかの想定外があった時は後続の処理でalertが飛べばいいという前提でalertを仕込めていなかった部分があったが、各処理ごとにalertが飛ぶようにした
    • errorは起きていないが、処理が遅れている時に気づくために遅延通知を導入した
  • デプロイフローの強化

    • CIでのチェックの強化
    • devからprodに上げる際の運用の明文化

残課題

  • 処理時間が長いため、障害時の復旧に時間がかかる
    • 現状では数時間かかる処理が含まれており、大幅短縮するためにはアーキテクチャレベルでの変更が必要だが着手できていない
  • このデータ基盤での障害発生時に対応できる人が少ない
    • 普段からデータ基盤を触っているデータエンジニアは障害から復旧させられるが、他のエンジニアでは権限や前提知識など様々な理由で状況を把握することができない
    • 以前はデータエンジニア内だけで十分であったが、データエンジニアも人数が少ないので安定稼働の観点では心許ない
  • データ品質の担保
    • データマネジメント文脈で「データ品質」と言うと様々な観点が出てくるのでここだけでは書ききれませんが、ユーザー影響のあるシステムになったことでデータ品質の要素の網羅性や各要素で求める基準が上がったがまだ改善途中という点で課題が残っている
    • 例えば有効性の観点では、集計後の一番最後の段階でユニーク性や値の範囲を確認して一定の担保はしているが、データが変換される各フェーズごとでチェックするほうが望ましい

データ基盤としての今後

このリリースによってデータの価値をユーザーに直接届けられるようになりました。
以前は社内の意思決定など間接的な形でしかユーザーに価値を届けることが出来なかったので、これはデータ基盤にとって大きな一歩です。
今後も機械学習などを含めた様々な形でデータからユーザーに価値提供することを考えています。

データ基盤に閉じた範囲では、肥大化したデータ基盤をデータメッシュをベースにした形でのリアーキテクチャを検討しています。
現状のデータ基盤はdatalakeにデータを集約して1つのデータパイプラインに載せるというモノリシックな構成になっています。
また、各層でデータを実体化させて保持しており、データの二重持ちによる無駄が発生してしまっています。
この状態では、今後データの入力・出力ともに増えたきた時に不要な依存関係や権限の管理コストが大きくなってしまうことが想定されるので、この問題に向き合います。

上記のようにまだまだやることがあるので、一緒に進めてくれる方を募集しています!
興味を持った方はこちらからどうぞ!

新サービスのブランド「tetoru」策定に至るまで

こんにちは。UXデザイン部の原田です。小中学校向けに「tetoru」というプロダクト開発に携わっています。 2022年4月に「tetoru」は正式リリースをいたしました。

corp.classi.jp

今回はこの生まれたてのサービスがどのようなプロセスを経て「tetoru」というネーミングやロゴになったのか、新サービスのブランド策定/開発プロジェクトの経緯を含めてお話をしたいと思います。

そもそも「tetoru」とはどんなサービス?

箇条書きでまとめてみました。

  • tetoruは小中学校の先生と保護者向けの連絡サービスです
  • 弊社 Classiと校務支援システムのEDUCOM社の2社で共同開発しています
  • 学校の先生にはWebサービス、保護者の方にはスマホアプリを提供しています
  • 「学校連絡」「欠席連絡」といった機能があり先生-保護者間の連絡をデジタル上で行うことができます
  • 先生は業務効率の向上、保護者は学校連絡をアプリに集約することができます
  • 煩雑なやりとりの負荷を軽減することができ、子どもの成長を見守る時間と機会の増加につながります

図にするとこのような形です。

tetoru_service

サービスブランドをちゃんとしたいという危機感

そのようなサービスの「tetoru」ですが、約2年前のチーム立ち上げ当初はサービス名称を考える暇もなくプロダクト開発をしてました。 リリース時期が2022年春ということに決まった頃、サービス名称が決まらないまま開発を優先するのは「やばい」と感じました。新しいサービスを世に出すには「なぜこのサービスが必要なのか」「それに賭ける想いや熱意」また「そのサービスが広く浸透した世界観がどうあるべきか」など、新サービスをひとことで表現できるブランドアイデンティ(BI)とビジュアルアイデンティティ(VI)が絶対必要です。

当時のメンバーはプロダクト開発で手一杯な状況で、サービスブランドを策定するリソースは社内にはとてもありませんでした。プロダクトとブランドの両輪がしっかりした状態でリリースするには、リリース時期から逆算で考えて十分に余裕を持ったマイルストーンを引く必要があります。プロダクトが良くても、ブランドが中途半端だと多くの人に知っていただけず機会損失にもつながるし、かといってリソースをブランド策定に割いてプロダクトが疎かになってしまったら本末転倒です。

また、このtetoruというプロダクトは弊社だけではなく、子会社のEDUCOM社との共同開発によるサービスです。ブランドは2社にとって愛着があるものにならなければ本当の意味で強いサービスにはなりません。

とてもじゃないが片手間にやるようなことではない、だって世に放たれたら一生続くものになるんだから! そういった危機感があり、今回は企業や商品などのブランディングのプランニング&クリエイティブを主に手がけるハイライツ株式会社というデザイン会社さまと一緒に新サービスのブランド策定/開発プロジェクトを立ち上げました。

www.highlights.jp

新サービスのブランド策定/開発プロジェクトで創意工夫した点

プロジェクトを立ち上げるにあたり、大きく4つの点を意識しました。

  1. RFPを元にコンペを開催し、お願いしたい会社の選定
  2. 経営層を巻き込んだ関係者インタビューの実施
  3. ネーミングおよびロゴを客観的かつ多角的に評価するシートを作成
  4. 会議中は「全員発話」で相互理解を促す

ここからはそれぞれの項目ごとにご紹介できればと思います。

1.RFPを元にコンペを開催し、お願いしたい会社の選定

これは最初から決めていたのですが、このプロジェクトはコンペを開催し複数の会社の中からパートナー会社を選定しました。私自身ブランド策定に関するプロジェクト知見を持っていなかったため、このブランドを託し、一緒に考えることのできる仲間を探す必要性を感じたからです。

そこで2〜3ヶ月程度かけて調査をしてパートナー候補会社に対し問い合わせを実施。オンラインMTGにて会社のことや事業理解、ケーパビリティや体制面などざっくばらんに対話を行い、一緒にこのプロジェクトをやっていきたいかどうかを判断した上でコンペ参加依頼をしました。 またコンペ開催に際しRFP(提案依頼書)もイチから作成しました。

前職はクライアントワークを主体としてましたので、しっかりと練られたRFPがあるだけでもパートナー会社さまの提案がよりシャープになるという経験則があったからです。書面として文書化することによって、メンバー内でも共通見解を整理することもでき、発注-請負間で言った言わないの水掛け論にもつながりづらくなるのでこの作業はプロジェクト開始前からしっかり準備をしててよかったなぁと思います。

提案内容のプレゼンテーションにはプロジェクトメンバー全員が参加、採点とコメントを記入し「どの会社、どの人と一緒に新サービスのブランドを考えたいか」といったところを基準に最終的には全員が納得できる会社を選定していました。

2.経営層を巻き込んだ関係者インタビューの実施

社内には定性面/定量面のユーザーリサーチ資料がありましたが、それらを踏まえつつさまざまな役職/職能のメンバー12名の関係者にデプスインタビューを実施しました。インタビュー対象者は弊社とEDUCOM社の2社からそれぞれ選出。経営層/事業責任者から始まり、営業やCS、PdM、ディレクター、UXデザイナー、エンジニアなど全方位をカバー。インタビュアーも教育関係に強い方に担当いただき、業界特有の事情も話しやすいように配慮いただきました。

インタビュー内容に関してもそれぞれの会社のことからサービスのこと、関わるヒトのことなど網羅的に話をしました。役職や立場、人によっては同じ新サービスについて話しても見えている景色や世界観のイメージは異なります。それぞれの気持ちをインタビューを通して発散していただき、最終的に印象的だった80程度のキーワードを抽出し集約。このシートは各社の経営層含む12名の気持ちがひとつにまとまっているものであり、この要素をもとに「tetoru」で実現したい世界やコンセプトを立案、方向性を整理してBIおよびVIの策定につなげていきました。

要所要所で進捗を共有していたこともあり、ちゃぶ台返しのような波乱は起こることもなかったです。日々のコミュニケーションはとても大事だなと感じます。

3.ネーミングおよびロゴを客観的かつ多角的に評価するシートを作成

サービスを表すネーミングやロゴって評価するのがすごく難しいです。基準を設けなければ主観的な感覚の評価になりがちで、ある特定の人の意見に流されて全員が納得できずに決まってしまう可能性もあります。そういった事態は絶対に避けたく、客観的に評価できる採点シートを作成しました。

採点シートを作成する際はこちらの記事を参考にしてカスタマイズしました。

takejune.com

参考にネーミングの採点シートを掲載しますが、今回は「単体評価」と「外部要因評価」の2項目に大きく分けて採点をしています。

tetoru_review

単体評価とはそのネーミングそのものの評価、外部要因評価はネーミングにまつわる外部影響になります。ネーミングそのものが良くても、似たような名称が世の中から見てすでにどこかにあると、浸透するのに時間がかかる可能性もあります。そういった異なる視点を持った上で◯なら1点、△なら0.5点、×なら0点の採点を行いスコアリングしました。特に単体評価は人によって意見が分かれることもあるので評価する際も誰かの独断で行うのではなく、各社で分かれて採点を行い、その内容を内部定例にてシェア。対話を行った上で、ひとつの意見としてまとめて最終評価をしています。

このシートがあることでその字面や第一印象だけではなく、音の響きや発音のしやすさ、印象の残りやすさなど五感を意識した評価を行うことができました。また単体評価が良くても外部要因評価で阻害要因があるかどうか(ドメイン名や類似サービスの有無)を事前に確認することができました。

結果として多角的にかつ公平に見極めることができ「tetoru」というネーミングが確定。ロゴも同様の手順で進めました(せっかくなので最終案に決定する前の検証バリエーションをお見せします)。

tetoru_variation
tetoruのロゴバリエーション
tetoru_variation2
アクセントカラーの比較

4.会議中は「全員発話」で相互理解を促す

コロナ禍ということもあり、プロジェクトは完全オンラインで行いました。ここでよく陥りがちなことは主要メンバーしか喋る機会がなく、会議中ひとことも発言しないメンバーがいることです。

このプロジェクトに関しては毎回の定例会議では「顔出し&必ず発言する」ことをマストにしました。とはいえ、発言を強要する訳ではなく、シンプルに思ったことをシェアしてもらうといった側面が大きく、相互理解を促す役割が大きいです。流れを簡単にご紹介します。

  1. パートナー会社から会議の主題の内容を共有いただく(方向性案やロゴ案など)
  2. いただいた内容に対し、PMである自分がまず最初に感想やコメントをシェア
  3. 終わったら、自分以外のプロジェクトメンバーを指名する
  4. 指名された人が同様に感想やコメントをシェア
  5. 以降、全員が喋り終わるまで繰り返し

ここでいう「感想やコメント」は文字通りそのまま感じたことを共有してもらっています。私自身、tetoruのネーミング案の提案をいただいた回では提案の数やバリエーション、ひとつひとつの案に込める想いを聞いて「プロの仕事って本当に凄くて泣きそうになりました」という子どものような感想も会議の場で伝えています(笑)。でもそれでいいのです、だって全員発話の目的は相互理解することなので。

言葉にならないモヤモヤでも、話を聞いて感じたことを素直に言葉にして共有するだけでも誰かの感性を刺激することにもなります。実際、まとまらない状態のまま吐き出すことによって議論が大きく盛り上がることもよくありました。本当の意味での言いたいことを言い合える関係性を構築し、誰かの不満が蓄積しないよう全員の気持ちを尊重した上で和気藹々と進めていました。

対話と検討を重ねて、ついに「tetoru」誕生

こうしたやりとりを経て、新サービスのブランド「tetoru」は誕生しました。 タグラインやステートメントまで私たちの想いが織り込まれ、そのDNAが随所に刻まれております。

tetoru_logo tetoru_statement

「tetoru」という言葉には学校の先生と保護者、また社内の職種や領域を超えて互いが手と手を取り合い、子どもたちの未来につながるサービスになってほしいという願いが込められてます。ロゴの印象的な赤・緑・黄色の3つのアクセントは子ども・先生・保護者を表してます。それらがしなやかにつながり、成長と発見をかなえ、人肌のある、あたたかなサービスを目指しています。

今回のご担当いただいたハイライツ株式会社のみなさまには感謝の気持ちでいっぱいで、このプロジェクトを通して日々の業務に邁進するための「核」が生まれたような気がしてます。それぞれの職能は異なれど、この核のために仕事に打ち込み、何か迷ったり困ったりしたらこのステートメントに立ち返ることのできる指針のようなものになりました。

※ハイライツ株式会社さまのWebサイトでもプロジェクト事例としてご紹介いただいておりますのでぜひご覧ください。

www.highlights.jp

その後のリアクション

プロジェクト関係者は自分たちで検討を重ねたため馴染んでいるのが当たり前のことですが、それ以外のメンバーもこの想いを汲み取ってくれているのでブランドの力ってすごいなぁって感じています。

また、リリース後にとある学校さまにインタビューを行った際には「プロダクトのあたたかみを感じる」と答えてくださった方もいて、感謝の気持ちでいっぱいです。 今後もこれに満足することなくtetoruで実現したい世界に向けてプロダクトとコミュニケーションの拡充を、この核を中心に展開していければと思います。

人材募集中です

今回はtetoruが誕生するまでの経緯と工夫したポイントについてご紹介しました。 最後に採用の紹介だけさせてください。 弊社の小中学校事業領域は実現したい未来に対しまだまだ人材が足りておらず、絶賛メンバー募集中です! カジュアル面談から受け付けておりますのでご興味のある方はぜひお問い合わせください。

hrmos.co

Datadogで深夜バッチの失敗アラートを営業時間に受け取る方法

深夜の定期バッチの監視

Webサービスのオフピーク時に重たい処理を実行させるというのは一般的なプラクティスといえます。

特に深夜〜早朝は多くのサービスでバッチ処理を実行させているのではないでしょうか。

Webサービスだけではなく、当然バッチ処理も監視して失敗したらそれを発見し対処したいです。

しかし、失敗を発見しても即座にユーザ影響がないので対応は後でも良いという場合、素朴に監視ルールを作るとバッチが失敗した深夜・早朝にアラートが発報されることになります。

発報されたアラートを見て「これは今すぐに対応してなくても良いな」と判断するのであれば、それは狼少年アラートといえるのではないでしょうか。

悪貨が良貨を駆逐すると言われるように、狼少年アラートがはびこれば良貨のアラートもいずれ無視されるようになってしまうことは容易に想像できます。

Datadogの timeshift 関数でアラートの発報タイミングを変える

この狼少年アラート問題を解決するためには 事象が起きたタイミングとアラート発報のタイミングを変える ことができたらよさそうです。

まず最初の案として、当社は監視サービスとしてDatadogを利用しているので、定期的にCustom Checkを投稿・実行することを考えました。

しかし対象のバッチ処理はAWS Step FunctionsAWS Lambdaを組み合わせたシステムで、DatadogのAWS連携でStep Functionsの失敗数は既にメトリクスとしてDatadogに収集されており、これと重複する処理を作るのはナンセンスです。

通常のメトリクス監視で通知だけずらせたらシンプルで合目的なのですが、そういった解決策はないでしょうか?

ところでモニターの設定画面をあれこれ見ていると hour_before() というある時点より1時間前の値をその時点のメトリクス値とする関数が存在することを知りました。

これを使えば、過去のメトリクス値をもとにアラートを発報することもできるはずです。しかし、1時間前あるいは1日前では間隔が短すぎ・長すぎます。 任意の時間だけずらす関数はないのでしょうか?

実はtimeshiftというまさしくそのものの関数があります。というか hour_before() などの XXX_before() 関数はすべて timeshift() 関数へのエイリアスになっています。

我々のバッチシステムは毎朝4時に実行されるので、コアタイムに近い午前10時に通知できれば良いと考え、6時間シフトさせた値を監視することにしました。

実際のクエリは以下のようになります:

timeshift(sum:aws.states.executions_failed{/* snip */} by {stage}.as_count(), -21600)
# 60 * 60 * 6 = 21,600

むすび

Datadogの timeshift() 関数を使い、深夜早朝に実行されるバッチのアラートを実際に対処すべき時間に通知する方法について紹介しました。

今回紹介した方法は「次の営業時間内に対応すればよい」という温度感のアラートを運用の実際に近付けるためのものであって、当然ながら濫用して即時対応すべきアラートを遅延させることを推奨するものではありません。

重要なことは 本当に対応すべきタイミングに必要なアラートを受け取る 仕組み作りであることは疑いようがありません。

当社ではWebサービスの監視について継続的に改善を進めており、過去にはこのブログでも紹介しました。

AWS ECS監視のオオカミ少年化を防ぐために考えたちょっとしたこと Amazon EventBridge(CloudWatch Events)で動かしているバッチをDatadogで監視する仕組みを構築した話

今後もアラートルールを見直しシステムの監視を洗練させ続けたいと思います。

DatadogでECS Fargate TaskのCPU利用率が100%を超えて表示されていたので調べてみた

こんにちは。開発本部の遠藤です。

ClassiではAmazon ECSをアプリケーション実行環境として利用しています。

ECSの各種メトリクスをDatadogを使ってモニタリングしながら、日々安定稼働しているかどうかをチェックしています。

そのうちの一つの重要なメトリクスとして、ECSのFargate TaskのCPU利用率が過度に高まっていないか、があるのですが、ある時期、CPU利用率が100%を超えてしまっていて「一体なにが起きてるんだ??」と疑問を持ちました。

今回はそれについて深堀りしてみたので、ニッチなトピックですが紹介したいと思います。

ECS Fargate TaskのCPU利用率が100%を超えて表示されている

こちらが実際にCPU利用率が100%を超えてしまったときのグラフです。

Datadogのメトリクスは ecs.fargate.cpu.percent です。なお、container_id でコンテナごとにグルーピングして表示しています。

下記図で示すように、AWSの CloudWatch Logs Insights を使用して、上記コンテナが稼働しているECS Fargate Taskの CpuReservedCpuUtilized をもとにCPU利用率を計算してみると、およそ50%ということがわかります。

この違いはどこから来ているのか、あるいはDatadogにCPU利用率が100%を超えうるというバグが存在しているのか、Datadogのメトリクスの算出方法を追って探ってみたいと思います。

DatadogはCPU利用率をどう算出しているのか

Datadogで表示されているこの ecs.fargate.cpu.percent というメトリクスはどのような計算式で算出されているのでしょうか?

Datadogのドキュメント には Percentage of CPU used per container (Linux only) Shown as percent と記載されているのみでした。

ということでソースコードを読んで確認していきます。(以後参照するソースコードについては2022/05/23時点の最新の master/main ブランチのものとなります)

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L220-L222

  cpu_percent = (cpu_delta / system_delta) * active_cpus * 100.0
  cpu_percent = round_value(cpu_percent, 2)
  self.gauge('ecs.fargate.cpu.percent', cpu_percent, tags)

このあたりで算出しているようです。

cpu_deltasystem_delta について見ていきます。

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L141

  request = self.http.get(stats_endpoint)

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L161

  stats = request.json()

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L167-L169

        for container_id, container_stats in iteritems(stats):
            if container_id not in exlcuded_cid:
                self.submit_perf_metrics(container_tags, container_id, container_stats)

submit_perf_metrics の中では以下のような処理を行っています。

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L181-L212

           # CPU metrics
            cpu_stats = container_stats.get('cpu_stats', {})
            prev_cpu_stats = container_stats.get('precpu_stats', {})

            value_system = cpu_stats.get('cpu_usage', {}).get('usage_in_kernelmode')
            if value_system is not None:
                self.rate('ecs.fargate.cpu.system', value_system, tags)

            value_user = cpu_stats.get('cpu_usage', {}).get('usage_in_usermode')
            if value_user is not None:
                self.rate('ecs.fargate.cpu.user', value_user, tags)

            value_total = cpu_stats.get('cpu_usage', {}).get('total_usage')
            if value_total is not None:
                self.rate('ecs.fargate.cpu.usage', value_total, tags)

            available_cpu = cpu_stats.get('system_cpu_usage')
            preavailable_cpu = prev_cpu_stats.get('system_cpu_usage')
            prevalue_total = prev_cpu_stats.get('cpu_usage', {}).get('total_usage')

            # This is always false on Windows because the available cpu is not exposed
            if (
                available_cpu is not None
                and preavailable_cpu is not None
                and value_total is not None
                and prevalue_total is not None
            ):
                cpu_delta = float(value_total) - float(prevalue_total)
                system_delta = float(available_cpu) - float(preavailable_cpu)
            else:
                cpu_delta = 0.0
                system_delta = 0.0

request.json で取得したレスポンスデータを使って計算していて cpu_stats.system_cpu_usageprecpu_stats.system_cpu_usage の差分が system_delta であり、 cpu_stats.total_usageprecpu_stats.total_usage の差分が cpu_delta です。

では request.json はどのendpointにリクエストを投げて取得したデータなのでしょうか?

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L141

  request = self.http.get(stats_endpoint)

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L76

  stats_endpoint = API_ENDPOINT + STATS_ROUTE

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L40-L42

API_ENDPOINT = 'http://169.254.170.2/v2'
METADATA_ROUTE = '/metadata'
STATS_ROUTE = '/stats'

http://169.254.170.2/v2/stats これがendpointのようです。

タスクメタデータエンドポイントバージョン 2

169.254.170.2 v2 でググってみるとAWSのデベロッパーガイドがすぐに出てきました。

タスクメタデータエンドポイントバージョン 2 - Amazon Elastic Container Service

Amazon ECS コンテナエージェントのバージョン 1.17.0 から、さまざまなタスクメタデータおよび [Docker 統計]を、Amazon ECS コンテナエージェントによって指定される HTTP エンドポイントで awsvpc ネットワークモードを使用するタスクで利用できます。

169.254.170.2/v2/stats

このエンドポイントはタスクに関連付けられたすべてのコンテナの Docker 統計 JSON を返します。返される各統計の詳細については、Docker API ドキュメントの「ContainerStats」を参照してください。

つまりDatadogはこのAWSのタスクメタデータエンドポイントを叩いてデータを取得していて、返ってきているのはDocker 統計 JSONということです。

Docker Containers Stats

Docker API ドキュメントの ”ContainerStats” の箇所を見てみます。

Docker Engine API v1.41 Reference

詳細なレスポンスはドキュメントに記載されているので、ここでは気になるものをピックアップします。

先に出てきた cpu_stats です。

"cpu_stats": {
    "cpu_usage": {
        "percpu_usage": [
            8646879,
            24472255,
            36438778,
            30657443
        ],
    "usage_in_usermode": 50000000,
    "total_usage": 100215355,
    "usage_in_kernelmode": 30000000
    },
    "system_cpu_usage": 739306590000000,
    "online_cpus": 4,
    "throttling_data": {
        "periods": 0,
        "throttled_periods": 0,
        "throttled_time": 0
    }
},

疑問

DatadogはこのAWSのタスクメタデータエンドポイントを叩いてデータを取得していることがわかりました。 ではもともとの疑問であった100%を超えるCPU利用率の表示というのはDatadogのバグではないということになるのでしょうか?

もう一度Datadogの計算式を見てみます。

https://github.com/DataDog/integrations-core/blob/4cf0f7dc759683b454cf46b9ff5e2de561a58339/ecs_fargate/datadog_checks/ecs_fargate/ecs_fargate.py#L220

  cpu_percent = (cpu_delta / system_delta) * active_cpus * 100.0

これをタスクメタデータエンドポイントのレスポンスのkeyを使用して表現すると以下のようになります。

((cpu_stats.total_usage - precpu_stats.total_usage)
/ (cpu_stats.system_cpu_usage - precpu_stats.system_cpu_usage))
* cpu_stats.online_cpus
* 100.0

response sample の percpu_usage のarrayのvalueの合計が total_usage のvalueと一致することから total_usage はその名の通りコンテナ全体のCPU消費時間を指しているはずです。

そうであるなら、 稼働しているCPUコア数を指す cpu_stats.online_cpus を乗算する必要性がどうしても見いだせず、以下の計算式で十分に思えました。

cpu_percent = (cpu_delta / system_delta) * 100.0

なぜ cpu_stats.online_cpus を乗算しているのでしょうか?

そう思ってる人が他にもいた

そもそもDatadogがなぜこの計算式を採用しているのかというと、docker stats のそれに準じているからだと推測されます。

https://github.com/docker/cli/blob/14962747e4af2c992bca55008c27387cd2268620/cli/command/container/stats_helpers.go#L180

  cpuPercent = (cpuDelta / systemDelta) * onlineCPUs * 100.0

そしてこの計算式について異議を唱えるissueがありました。

https://github.com/docker/cli/issues/2134

docker stats CPU shows values above 100%

I don't think there is a system monitor tool (linux or windows) that shows CPU usage above 100%. Conceptually, 100 percent means maximum value.

So what is the reason to multiply by the number of cpus? Seem that is not required because total_usage already accounts for them.

なおこのissueに対して明確な回答はなさそうでしたが、以下のようなコメントがあります。

This is not an issue. If you have N CPU cores, the CPU usage can be up to N * 100%.

これだけだと納得がいかなかったのですが、mobyのissueにそれらしい回答がありました。

https://github.com/moby/moby/issues/29306#issuecomment-405293005

That's not really something that can be changed easily, as it will break many users; this output was modelled after how top works on Linux; https://unix.stackexchange.com/questions/34435/top-output-cpu-usage-100

top コマンドをモデルにしているようです。

top command の Irix mode

言及されているリンクを見てみます。

linux - top output: cpu usage > 100% - Unix & Linux Stack Exchange

You are in a multi-core/multi-CPU environment and top is working in Irix mode. That means that your process (vlc) is performing a computation that keeps 1.2 CPUs/cores busy. That could mean 100%+20%, 60%+60%, etc.

Irix mode を知らなかったので調べてみます。

https://man7.org/linux/man-pages/man1/top.1.html

I :Irix/Solaris-Mode toggle When operating in Solaris mode ('I' toggled Off), a task's cpu usage will be divided by the total number of CPUs. After issuing this command, you'll be told the new state of this toggle.

Irix mode と Solaris mode があり、Solaris modeはcpu usageをCPUコア数で割った数が表示されるようです。 つまりdefaultのIrix mode はcpu usageをCPUコア数で割らないということを意味することがわかります。

これが docker stats のCPU使用率算出の計算式のモデルになっているようです。

まとめ

Irix mode ではcpu usageをCPUコア数で割らない値をCPU使用率として表示しています。 たとえばCPUコア数が4のサーバであれば、最大400%として表示され得るということです。

docker stats も Datadog の ecs.fargate.cpu.percent のメトリクスもこの表示方式を踏襲し、以下の計算式を採用していることがわかりました。

  cpu_percent = (cpu_delta / system_delta) * active_cpus * 100.0

つまりわざわざCPUコア数を乗算することで、top コマンドの Irix mode でのCPU利用率の値を表現していたのです。

冒頭の疑問にやっとここで返ってこれるのですが、該当のECS Fargate Task はcpuを4096 = 4vCPU(CPUコア数:4) で設定し稼働させているので、100%を超える値が表示されていたということです。

なのでこのコンテナは実質的には4で割った値である 50 ~ 60% あたりで稼働していると考えてよさそうですし、CloudWatchで取得した値と近しくて納得感があります。

ただし、どのCPUコアがそれぞれ何%なのかまではわからず、いずれかが100%近くまでCPUを利用している可能性は大いにあり得るので、注意は必要です。

Datadogの ecs.fargate.cpu.percent で検索してもなかなか今回の事象について情報が得られなかったのですが、コードリーディングすることで納得感が得られ、とてもスッキリしました。

加えて、Datadogが実際なにをしているかを垣間見れたことでDatadogへの理解が深まり、芋づる式にECS、Docker、topコマンドについても知見を得られたので学びが多かったです。

同じような事象に遭遇し、疑問を持った方の参考になれば幸いです。

monorepoでのWeb Components配信アーキテクチャ

コミュニケーションチームのid:NozomuMiyamotoです。

Classiでは、2021年12月のAngularJSのEOLに向けて、Web Componentsを利用した段階的AngularJS脱出作戦を進めてきました。これはAngular Elementsと呼ばれるAngularで作成したコンポーネントをWebComponentsに変換する技術を用い、部分的にシステムリプレースを行う作戦です。 今回は、そこでぶつかった課題と解決に用いたWebComponentsの配信アーキテクチャについて紹介します。

背景

Classiにはplatformと呼ばれる最初期から存在する巨大なリポジトリがあります。このリポジトリには様々な言語で書かれた複数のサービスが共存しており、全体像を把握することが非常に困難です。これを管理可能なレベルまで解体するとともに、EOLを迎えるシステムの置き換えを行うことが、コミュニケーションチームのミッションの1つです。前述の動画で紹介したAngular Elementsを用いた脱出作戦は、フロントエンドのスコープで、それを果たすための施策でした。

当初、AngularElementsのリポジトリは機能ごとに作られており、成果物を手動でplatformに移植する運用がなされていました。勿論、こうした運用は望ましくなく、オペミスによる障害も引き起こしかねません。ただ、こうした問題の改善をしようにも、リポジトリが分かれていては、その分だけメンテナンスに力がいります。こうした事情から、我々はNxを用いて機能別にリポジトリが散らばったAngular Elementsのモノレポ化を行い、アジリティと安全性を高めるためにデプロイフローの改善に踏み出しました。

問題

モノレポ化により開発体験の向上という恩恵が得られましたが、また別の課題に直面することにもなりました。それはバージョン管理と切り戻しに関する問題です。 全ての機能のAngularElementsを単一のパッケージでバージョン管理した場合、過去のリリースで障害が発生した際に関係ない機能まで切り戻す必要が生じてしまいます。

例えば、アンケート機能とカレンダー機能の2種類の機能がAngular Elementsで提供されており、同一バージョンのパッケージしか利用できないとしましょう。ここで以下のようなケースが考えられます。

  1. アンケートへの機能追加のためにAngular Elementsのパッケージをv1.1.0にアップデートする。
  2. カレンダー機能の修正のためにAngular Elementsのパッケージをv1.2.0にアップデートする。
  3. 1週間後、1. のリリースが原因で致命的な欠陥が見つかる。
  4. Angular Elementsのバージョンをv1.0.0まで切り戻す必要が生じる。

このケースでは、障害の原因はアンケートへの機能追加であるにも関わらず、原因と無関係なカレンダー機能の修正まで切り戻されてしまいます。

目的

この問題は、配信パッケージを分けるなどの方法で機能ごとにバージョンを選択できる状態にしてやれば簡単に解決します。ただ、背景で述べた通り、platformレポジトリはすでに複数のコードにより煩雑になっているため、これ以上、依存関係や開発環境を複雑にするのは望ましくありません。そこで、ここではplatformレポジトリにできる限り変更を加えず、機能ごとのバージョン指定ができるようにすることを目指します。

採用したアプローチ

結論から言えば、我々はURLベースでWebComponentsのバージョン管理を行う手法を採用しました。これは、次のようなscriptタグを用いて、指定したバージョンのWebComponentsを読み込む手法です。

<script src="https://cdn.classi.jp/elements/v1.0.0/questionnaire.js"></script>

(古くからあるCDN経由でJavaScriptのライブラリを読み込み方法ですね)

非常にシンプルな手法ですが、移植先の環境に殆ど変更を加える必要がないですし、npmなど、JSのパッケージマネージャーがない環境でも容易に扱えます。唯一、CDNの環境を整えることだけが少々手間ですが、この作戦の最後にはAngularアプリケーションとして独立したSPAをCDN経由で配信するためこれは将来への投資になると考えられます。

アーキテクチャ

具体的には、Conventional Commit + semantic-release + GitHub Actions +S3 + CloudFrontの構成で下記の流れで、機能ごとに指定したWebComponentsのパッケージを読み込みます。

  1. 開発者がConventional Commitに従ってコミットしてoriginにpushする。
  2. GitHub ActionsがSemantic Releaseを実行し自動バージョンニングを行う。
  3. GitHubActionsがビルドされた成果物をS3 Bucketにアップロードする。
  4. 開発者がplatformのscriptタグの向き先を任意のバージョンのURLに変更する。(※)

※ ここで次のようにバージョンを指定できるため、機能ごとのバージョンアップ(もしくは切り戻し)が可能となる。

<script src="https://cdn.classi.jp/elements/v1.1.0/questionnaire/main.js"></script>
<script src="https://cdn.classi.jp/elements/v1.0.0/calendar/main.js"></script>

採用しなかったアプローチ

もちろん、他にもいくつかのアプローチを検討しましたが、それらについて採用に至らなかった理由を以下で述べます。

npm + Nx Multi Packaging

Nxのマルチパッケージング機能を用い、機能ごとにAngularElmentsをバージョニング・パッケージングし、npmのprivate repositoryで配信する手法です。npm linkが利用できることは開発においても魅力的ですが、フロントエンジニア以外もplatformレポジトリを頻繁に触るため、アクセスキーの取得などが開発環境を複雑にすると考え採用に至りませんでした。

GitHub Release

GitHubのRelease機能を用いて機能ごとに各バージョンのビルドされたWebComponentsを保管し、GitHub Actionsからそれらを取得する手法です。GitHub Actions内で取得する各機能のバージョンを指定するために、何らかのバージョン管理手段を別途用意する必要があるため採用しませんでした。

Git Submodule

AngularElementsのモノレポをplatformのsubmoduleにし、サブモジュールごとビルドする手法。AngularElementsのモノレポ化の構想段階ではこの手法を用いる予定でしたが、今回の切り戻しの問題が懸念されたため採用しませんでした。

むすび

WebComponentsはその性質上、一般公開を前提とするため、URLに基づくバージョン管理で十分に要件を満たせると考えます。また、シンプルな構成で利用先への要求がscriptタグの利用以外ないことは大きなメリットです。Railsなどのnpmを用いていない環境下でのシステムリプレイスにも役に立つでしょう。

残された課題として、コンポーネントの粒度でも同じ問題が起こり得ることが挙げられます。これを防ぐためには、同様にコンポーネントの粒度でバージョン指定を可能にする必要がありますが、その場合、各コンポーネントに@angular/coreの一部がバンドルされることになるので全体のファイルサイズが膨れ上がります。それを避けるためには、共通モジュールのみを初期でロードし、各コンポーネントは必要に応じてLazyLoadできるような構成を取る必要があるでしょう。

WebComponentsを用いた取り組みはまだ事例が少なく、我々も手探りで進めている状態ではありますが、その分、この技術には大きな可能性を感じています。Classiでは現在も開発者を募集しておりますので、こうした取り組みにご興味のある方は、ぜひ一度ご連絡ください!

それではまた次回の投稿をお楽しみに!

Mock Service WorkerでAPIをモックして開発をスムーズに進められた話

こんにちは。開発本部 認証連携チームでエンジニアをしている id:ruru8net です。前回はこちらの記事を書かせていただきました。
tech.classi.jp

現在は認証基盤再建というプロジェクトの中で、主にフロントエンド開発を担当しています。この記事ではフロントエンド開発においてAPIのモックのために「Mock Service Worker」を使ったところスムーズに開発を進めることができたので、使い方を紹介したいと思います。
mswjs.io

ツールの導入

弊社ではフロントエンドのフレームワークにAngularを採用しているのでAngularでの導入手順を記します。
基本的にはドキュメントの手順通りです。

1. インストール

$ npm install msw --save-dev
# or
$ yarn add msw --dev

2. モックを定義

src/mocks/handlers.tsを作成し、ファイル内にモックしたいAPIを定義します。

$ mkdir src/mocks
$ touch src/mocks/handlers.ts

今回はREST APIをモックするようにするので以下のように定義します。
src/mocks/handler.ts

import { rest } from 'msw';
    
export const handlers = [
  rest.get('/status', (req, res, ctx) => {
    return res(
      ctx.status(200),
      ctx.json({
        success: true
      })
    );
  }),
];

3. Mock ServiceWorker CLIのinitコマンドを実行

以下のコマンドを実行します。
(使うフレームワークによってディレクトリのパスが異なります。他のフレームワークの場合はこちら https://mswjs.io/docs/getting-started/integrate/browser#where-is-my-public-directory )

$ npx msw init ./src --save

コマンドを実行するとsrc配下に mockServiceWorker.jsが生成されます。
またAngularの場合はドキュメントに、

and add it to the assets of the angular.json file

とあるので、その通りにangular.jsonのassetsにmockServiceWorker.jsを追加します。

angular.json

{
  --- 省略 ---
     "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
           ...,
            "assets": [
              "src/favicon.ico",
              "src/assets",
+             "src/mockServiceWorker.js",
              "src/robots.txt"
            ],

4. workerを定義

src/mocks/browser.ts を作成し、workerを構成します。
https://mswjs.io/docs/getting-started/integrate/browser#configure-worker

$ touch src/mocks/browser.ts

src/mocks/browser.ts

// src/mocks/browser.js
import { setupWorker } from 'msw';
import { handlers } from './handlers';

 // This configures a Service Worker with the given request handlers.
export const worker = setupWorker(...handlers);

5. workerを起動

https://mswjs.io/docs/getting-started/integrate/browser#start-worker
src/main.ts内にて開発環境でのみworkerを起動するよう定義します。
弊社ではAPI用のURL(apiBaseUrl)がenvironmentに定義されていない場合にworkerを起動するようにしました。
またplatformBrowserDynamic().bootstrapModule(AppModule);よりも前にworkerが起動されていないと、ngOnInit()などで行われるAPI呼び出しがworker起動よりも先に行われNot Foundとなってしまったので、workerの起動が完了してからplatformBrowserDynamic().bootstrapModule(AppModule);を実行させるようにしています。

src/main.ts

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
    
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
    
if (environment.production) {
  enableProdMode();
}

async function prepare() {
  if (environment.apiBaseUrl === '') {
    const { worker } = await import('./mocks/browser');
    return await worker.start();
  }
  return;
}

// woker起動であるprepare()が完了してからplatformBrowserDynamic().bootstrapModule(AppModule);を実行する
prepare()
  .then(() => {
    return platformBrowserDynamic().bootstrapModule(AppModule);
  })
  .catch((err) => console.error(err));

6. モックが正しくできているかの確認

  • yarn startなどでサーバーを起動して開発者コンソールを確認します。workerが正しく起動されている場合、以下が表示されます。

    f:id:ruru8net:20220329122725p:plain
    workerが正しく起動した場合のコンソール表示

  • モックしているAPIが呼ばれる度にコンソールに表示されるので、モックの内容が正しいかどうかもコンソールにて確認ができます。

    f:id:ruru8net:20220329122914p:plain
    モックが正しく呼ばれた場合のコンソール表示

  • 404が出る場合は、正しくモックできていない可能性が高いので、src/mocks/handler.tsを見直したり、APIのパスが誤っていないかを確認します。

    f:id:ruru8net:20220329122924p:plain
    モックが呼び出せず404エラーになった場合のコンソール表示

便利だったオプション

Mock Service Workerには様々なオプションがあります。
ドキュメントが丁寧なのでそちらに記載されているのですが、その中でも自分が実際に使い、便利だなと思ったものを紹介します。

request.bodyでの分岐

以下のようにrequest内のbodyを見て、処理を分岐することができます。
これによって正常系や異常系の分岐を簡単におこなうことができました。

src/mocks/handler.ts

rest.post('/login', (req, res, ctx) => {
  const { username } = req.body as { username: string };

  if (username === 'unknown') {
    return res(
      ctx.status(404),
      ctx.json({
        success: false,
        errors: {
          message: 'このユーザは存在しません。',
        },
      })
    );
  }
    
  return res(
    ctx.status(200),
    ctx.json({
      success: true
    })
  );
}),

応答時間を遅らせる

delay() を使うことで応答までの時間を指定秒遅らせることができます。
https://mswjs.io/docs/api/context/delay
レスポンスを受け取ってから処理をするといったような同期的な実装が正しく動作しているかを確認するのに便利でした。

src/mocks/handler.ts

rest.post('/status', (req, res, ctx) => {
  return res(
    // 1000ms = 1s 遅らせる.
    ctx.delay(1000),
    ctx.status(200),
    ctx.json({
      success: true
    })
  )
})

ネットワークエラーを返す

以下のように書くことで、ネットワークエラーとしてレスポンスを返すことができます。
https://mswjs.io/docs/api/response/network-error

src/mocks/handler.ts

rest.get('/status', (req, res, ctx) => {
  return res.networkError('Failed to connect')
})

コンソールではこのように表示されます。

f:id:ruru8net:20220329121210p:plain
MSWでnetworkError()をreturnした場合のコンソール表示

ネットワークエラーのハンドリングを実装する際にとても便利でした。

Mock Service Workerを使った開発でのメリット

サーバサイドに依存しない

今回携わっていたプロジェクトではサーバサイドとフロントエンドでリポジトリも異なり、それぞれ別の人が開発をしていました。 また開発の進め方として、サーバサイドの開発者と一緒にAPIを決定し、定義書にまとめ、その後の開発はそれぞれで進めていくという流れでした。
フロントエンド開発ではMock Service Workerを使えばいくらでもAPIを検証することができ、サーバサイドの開発進捗に依存することなく進められたことが一番良かったです。
開発が進む中で何度かAPIの仕様が変わることもありましたが、src/mocks/handler.ts を修正すれば済むので、APIの追加や修正に負担もなくとても開発が進めやすかったです。

ユーザーエラーの再現のしやすさ

ネットワークエラーはユーザーによく起こりうるものでありながら、開発環境ではなかなか再現がしづらいと感じていました。しかし先程紹介したようにオプションで簡単に定義することができます。その他のエラーも開発環境での再現ができるのでハンドリングのしやすさも大きなメリットの1つだと思います。

型定義を通して実際のレスポンス形式と同期が取れる

handler.tsにモックを定義する際のレスポンスにも、実際のAPIのレスポンスで使用している型をimportすることで、APIの変更があった場合にモックの修正にも気がつくことができます。同期が取れるように型を意識しておくことも大事だと思います。

api.service.ts

type GetStatusResponse = {
  success: true;
  status: string;
};

async getStatus(): Promise<LoginMethod[]> {
  return await this.http
    .get<GetStatusResponse>(`/status`)
    .toPromise()
    .catch(error => console.log(error));
}

src/mocks/handler.ts

import { rest } from 'msw';
import { GetStatusResponse } from 'api.service.ts'

export const handlers = [
  rest.get('/status', (req, res, ctx) => {
    const response: GetStatusResponse = {
      success: true,
      status: 'hoge',
    }
    return res(
      ctx.status(200),
      ctx.json(response)
    );
  }),
];

Mock Service Workerを使った開発でのデメリット

他のモックの手法を使ったことがなく、他との比較はできないのですが困ったところは今のところ全くないです。
もし今後新たな開発をすることになっても Mock Service Workerを採用したいと思っています。

今後やっていきたいこと

モックAPIの定義ファイルのリファクタ

src/mocks/handler.ts に全てのAPIを羅列してしまうとファイルの行数が膨大になり、可読性が下がります。 ファイルをいくつかに切り分け、APIを種類ごとに分類するといったようなリファクタが必要であると思い、現在試行錯誤しています。
できる限りAPI定義書と合わせて見た際に理解がしやすい構成にしたいです。

まとめ

弊社では毎週「フロントエンド互助会」といった各プロジェクトのフロントエンド開発をしているエンジニアが集まり、進捗共有や相談を行う時間があります。
今回、そこでMock Service Workerについて共有したところ、「便利そう」「使って見たい」と言った声や、実は他のプロジェクトでも使っていて、そこではこんなファイル構成をしているよ、といったお話を聞くことができました。またこのブログを執筆する機会もこちらで提案してもらい、書かせていただいてます。
まだまだ社内でのMock Service Workerの浸透率は高くないので、次の一歩として布教活動と今後Mock Service Worker使っていくというプロジェクトがあれば積極的にサポートをしていきたいです。

© 2020 Classi Corp.