Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

UIテストの自動化にMagic Podを導入した話

こんにちは。インキュベーション本部のQAエンジニアの米山です。主にCLINICSアプリのQAを担当しています。メドレーには2020年8月に入社しました。

今回は入社してまず行ったことの一つ、リグレッションテストの自動化と、そのために導入したMagic Podというツールについて、経緯や導入してみた結果をご紹介したいと思います。

CLINICSとは

私の所属するチームで開発しているCLINICSというプロダクトはアプリでオンライン診療や、クリニック・病院から処方箋を発行してもらうことができ、オンライン上で診察からお薬の受け取りまで完結できるサービスです。 プラットフォームはiOSAndroidのネイティブアプリ、それから同様のサービスをWebブラウザからも利用することが出来ます。

QA/リリース周りの状況

CLINICSの開発組織にQAエンジニアがジョインしたのは昨年(2020年)ですが、サービス自体は2016年にローンチされています。

本組織ではリリース前に行うリグレッションテストについては、開発メンバを中心にチーム全体で行う文化があります。 アプリのリリースは隔週で行っており、その都度開発メンバ自身によってテストが行われていましたが、自動化されたUIテストは存在していませんでした。

メドレーではQAエンジニアがジョインして間もないため、やりたいこと・やるべきことは多岐にわたる中でまず何から着手するべきか検討しました。

QAプロセスの策定・改善から、新機能をリリースまで推進するためのQA活動もあり、並行して幾つか動いている中でテスト自動化をどのタイミングで、どうやってスタートするか悩みました。

バリューから考える

f:id:medley_inc:20210115142126p:plain

メドレーのバリューはこの三つです。これらのバリュー視点で考えてみました。

「凡事徹底」として、リリース前のリグレッションテストをしっかり行うことは当然のこととして考えられます。

「中央突破」の視点ではどうかと考えると、やはりテストプロセスにおいて、特にリリース毎に繰り返し作業となるリグレッションテストを自動化することは王道であり、ベストプラクティスの一つだと考えられます。 そのため自動化は優先度高く進めるべきではあります。

残る一つ「未来志向」については、例えば1~2年後やその先を考えて、リグレッションテストが自動化されているべきか否かで言うとやはりYesです。

また、別の観点として、現在はわずか2人のQAエンジニアに対して、複数のプロダクトが存在している状況で、QAエンジニアがアサインされていないプロダクトも多くあります。

私自身も昨年10月からアプリ・基盤チームに異動したこともあり、今後についてもまた体制が変わっていくことは十分に考えられました。

そんな状況下では、仮にUIテストを自動化した環境を用意できたとして、その後に担当者が不在になった場合も考慮しておく必要があります(自動テストにおいて、担当が不在になったことでメンテされなくなり形骸化するケースはよくある話です)。

そのため、仮に実装者が不在となった後でも誰かに引き継ぎやすく、またエンジニア以外でも運用できる環境が望ましいと考えました。そういった観点でツールとしては基本ノーコードでもメンテできるMagic Podは有力な候補となりました。

これらをまとめると、以下のような結論に至りました。

  • テストの自動化は推進した方が良い
  • ただし、他のメンバでもメンテしやすい環境を選定する

ただしQAとしてやるべき事が沢山ある中で、テスト自動化だけに専念できる状況ではありません。 そのためなるべく他タスクと並行して小コストで進められる事も重要な要素でした。

自動化されたUIテストは全くない事や、他のテストの密度も鑑みると、なるべく早い段階で一定の自動テスト環境は用意したいという想いもありました。

これらの状況も踏まえ、ツールを選定・トライアルしてみた結果、Magic Podを導入することに決めました。

Magic Podの紹介

Magic Podについて、サービス自体の詳細は割愛しますが、端的にいうとクラウド環境かつGUIからUIテストの実装及び実行を行うことができるツールです。

GUIで自動テストが実装できるツールだと、Autifyなども有名です。 Autifyはブラウザ向けのツールですが、実装方法はMagic Podとは少し異なり、操作をレコーディングしてテストシナリオが自動で生成される形が基本です。

一方、Magic Podは以下のようにアプリの画面をまずキャプチャで取り込み、そこからテストで使いたい項目を選択し、シナリオにドラッグアンドドロップしていくことでテストシナリオを生成することができます。

f:id:medley_inc:20210115141554p:plain

ログインなど、複数のテストで使う部分は共通化しておきます。

テスト対象がiOSアプリであろうと、Androidアプリやブラウザであろうと基本的に同じI/Fからテストの生成・メンテが出来ることは大きな強みの一つです。

また、テストで使用するフィールドの要素を選択可能なことも、状態変化に強いテストとする上での強みとなります。

例えば「調剤薬局名でさがす」というテキストフィールドに対して、そのテキストを使うのか、IDなのか、テキストフィールドなのかxpathなのかといった所です。

そのため、

  • テキストが頻繁に変わるような場所(例えば日付など)ではテキストを使わない
  • アプリ内部でリファクタリングなどが動いている場合であれば逆にIDは変わる可能性が高いため、テキストで指定する

UIテストを作り込む上では当然のことではありますが、上記のような工夫によりテストの成功率を上げることができます。

導入してみて

トライアル中は探りながらの部分はあったものの、慣れると実装工数は非常に短期間で実装でき、トータルでもiOSで2~3週間(オンボーディング含む)、AndroidのUIテストについては実質2~3日で基本的なテストシナリオの自動化を行う事ができました。

その後、運用しながら落ちやすいテストの改修を行ったり、運用が安定してからはCIにも連携しています。

UIテストの運用においては定期的に実行することは非常に重要なことですが、Magic Podの場合、BitriseではUI上から設定でき、Circle CIに対してもドキュメントを参照しながら比較的容易に設定できます。

実際、昨年1クォーター運用してみて、幾つかのクラッシュをリリース前に検知してくれました。

また、私自身、過去にはXCTestにおけるUITest(Testing Your Apps in Xcode)やAppiumを使ってUIテストを運用していたため、以下ではそれら他ツールとの比較も含めて紹介してみたいと思います。

実装コスト

実装コストにも初期構築と、その後のメンテコストで分かれますが、他のツールと比較して、大きく異なるのは初期構築コストだと思います。

Magic Podについては環境構築コストは非常に低コストで行うことができます(基本的な部分は1日あれば十分だと思います)。 またテストのレポーティングやキャプチャ機能なども標準で付いていますので、この辺りも自前で頑張る必要はありません。

次にメンテコストですが、例えばXCUITestではまずビルドを行い、debugして各ボタンなどの要素のIDなどを確認し、それらを用いてコーディングしていました。 Magic Podでは一度アプリをアップロードして、スキャンすることで画面の要素を一括で取得でき、その中から操作したい要素を選択することができます。

そのためこちらもコストはだいぶ下がります。ただ、この部分については他のツールや言語でも慣れればそう時間はかからないのでもしかしたら大差ないかもしれません。

あえて言うとdebugでIDを確認する手間が楽になる、実装したテストを試して実行するのが容易(ビルド待ちの時間がない)といった辺りでしょうか。

運用コスト

UIテストといえばFlakyなテスト(落ちたり落ちなかったりするテスト)に悩まされることは多いですが、運用してみると最初の内はそういったこともありましたが、現状ではほぼ起きていません。

これはMagic Podに限った話ではありませんが、

  • クラウド上で実行されることで環境要因で落ちることは稀
  • 落ちた時には自動でリトライされる
  • ビルドもCI上で実行している
  • 実行はメンバが活動していない時間帯に行っている

といった辺りが要因かと思います。

またMagic Podのようなツールを使っている場合に助かる部分としては、Xcodeなど、UIテストに必要なツールのアップデートに対するメンテが不要ということも挙げられます。

逆に少し辛い所

ここまでMagic Podの良い部分を多く書きましたが、逆にこのようなGUIでのテストツールを使うことで少しやり辛い点も紹介しておきたいと思います。

1. テストコードのレビュー

テストコード(ケース)はMagic Pod上で管理されているため、PRレビューなどのプロセスを行うことができません。 そのため、ケースの修正に対して、反映させる前にレビューしてもらいたい場合は、テストケースをコピーしてから編集するなど少し工夫が必要になるかと思います。

現状では困ることはありませんが、複数人で同一のプロジェクトに対して運用したい場合は少し煩雑になりそうです。

2. テストコードの管理

自動テストにおいて、テスト結果に影響が出る仕様変更が入るような場合、仕様変更に対するテストコードの修正は開発と並行して用意しておき、プロダクトへの変更がマージされるタイミングで同時にテストコードの修正もマージしたいケースがあります。

Magic PodではGitHub上でテストコードを管理していないため、このようなケースへの対応を自動で行うことが難しく、予めテストケースを分けて用意しておき、実装がマージされた後に手動で置き換えるか、マージされた後に影響のあるテストケースを修正するといった手動でのプロセスが必要になります。

現時点で気になったのは上記の2点ですが、これらも今後改善されていく可能性は大いにありますし、プロセスの中での工夫次第で対処も可能かと思います。

その他

基本的にUIテストを自動化する上で気をつけるべきことやアンチパターンはどんなツールを使っても同じです。 他のツールでは難しいことが、このツールでは実現出来るということも稀で、時にはプロダクト側で手を入れる必要もあります。 どんなツールであれ、何かしら工夫すれば達成出来ることが多いため、違いが出るのは実装や運用、オンボーディング等のコスト部分が最も大きいのではないかと感じています。

周囲のサポート

テスト自動化を行う場合(だけではないですが)、周囲の理解を得ることは大事な部分ですが、チームメンバは皆前向きで興味を持ってくれて進めやすい環境でした。

特にCI連携の部分ではiOS/Androidの開発の方にもサポートしていただき大変助かりました。

そしてMagic Podについては、数年前から運用している株式会社ノハナの武田さんにも事前に話を伺ったり、オンボーディング中は質問させていただいたりしました(ありがとうございました!)。

またMagic Podの伊藤様には導入時からトラブルシューティングに多大なサポートをいただいています。

Circle CIに入れ込む際には、ちょっと詰まった点があり伊藤様とメールでやり取りしていたのですが、その日のうちにドキュメントがアップされたり、 とある環境下で不明なエラーが出ていて相談した際には、ストアからCLINICSアプリをダウンロードして試していただいたり、とにかくいつも迅速かつご丁寧な対応が印象的でした。

まだQAチームもないような少人数の状況では、こういったトラブルに対して相談でき、共に解決法を探れる方がいるという意味でも非常に心強いです。

今後について

アプリのUIテストについて、改善していきたいことはまだまだ沢山あるのですが、現状でも基本的なテストは用意できているため、じっくり腰を据えて改善していきたいと考えています。

また現在はブラウザのテスト自動化を進めています。メドレーのCLINICS以外のプロダクトの多くはWebブラウザをプラットフォームとしているため、Webについてはプロダクトを跨いだ活動も行っていければと考えています。

長くなりましたが、最後までお読みいただきありがとうございました。

www.medley.jp

ChatOpsな稟議ワークフローシステムを開発しました

はじめに

こんにちは。コーポレートエンジニアの溝口です。
メドレーでは、今年7月に稟議ワークフローシステムを導入しました。
詳細は「システム概要」の章でご紹介しますが、システムの全体像としては以下のようになっております。

f:id:medley_inc:20201225155753p:plain

ワークフローシステムと聞かれたら、どんなシステムを思い浮かべますか?
申請者がシステムで申請すると、予め定められた承認者へ承認依頼がメールで通知され、申請内容の確認及び承認のためにシステムへログインする、という流れがよくあるワークフローシステムではないかなと思います。

我々はコーポレート部門として「徹底的に合理性を追求した組織基盤や、仕掛けづくりを行っていく」ことを目指しています。故に、メドレーの稟議ワークフローシステムにおいても便利合理的なシステムを目指して開発を行いました。今回ChatOpsの概念を取り入れることで、一般的なワークフローシステムよりも洗練されたシステムを構築できたかなと思います。
本稿ではシステム概要及び、裏側の仕組みをご紹介していきます。
最後までお付き合いいただければ幸いです。

ChatOpsとは

ChatOpsとは「チャットサービス(Chat)をベースとして、システム運用(Ops)を行う」という意味です。ざっくり書くと「システムからChatへメッセージを飛ばし、次のアクションが同じChat画面で開始できる」というものとなります。(下記フローはあくまで一例です)

f:id:medley_inc:20201225155817p:plain

ChatOpsには以下のメリットがあると考えています。

  1. 常に立ち上げているツールという共通インターフェースである

  2. インタラクティブなコミュニケーションにつながり、スピーディである

  3. 共有しやすく、記録に残しやすい

本稿では、詳しく説明はしませんが、興味がある方は事例等を解説しているサイトもあるので、是非探してみてください。

なぜChatOpsなのか

稟議申請においては
承認者「これ値引きしてもらって××円になったはずだけど、金額間違ってない?」
申請者「すいません、変更し忘れました。差戻しお願いします」
などのコミュニケーションが度々発生します。
通常のシステムであれば、確認事項がある際はシステム内のコミュニケーション機能を使う、もしくは、ChatにURLや稟議番号を転記して確認のためのコミュニケーションを取ることが想定されます。

メドレー内の業務コミュニケーションはSlack上で殆ど完結しています。
Slackではない他の場所で会話が発生すると情報が分散しますし、SlackにURLを転記するといった行為や、別システムへのログインなども非効率です。
そこで、共通インターフェースのChatを中心にシステム構築する=ChatOpsを採用し、稟議ワークフローを構築してみようと考えました。結果、稟議ワークフローシステムの情報をSlackへ連携し、稟議におけるコミュニケーションはSlackに集約、承認行為もSlack上で可能、というシステムを構築することができました。

システム概要

申請

申請者はTeamSpirit上で稟議内容を記入し、稟議申請を行います。 TeamSpiritとは、勤怠管理や工数管理、経費精算などを管理できるクラウドサービスです。Salesforceをプラットフォームとして採用しており、アイデア次第でいろいろなカスタマイズが可能です。

f:id:medley_inc:20201225155854p:plain

Slackから申請できるようにするのがChatOpsのあるべき姿かもしれませんが、過去の申請からコピーしたい、申請種別ごとに入力する項目が異なる等の要件を考慮し、TeamSpiritから申請するように設計しました。申請の導線については、今後もよりよい仕組みに磨き上げていきたいと考えています。

承認

申請者が「稟議申請」ボタンを押下すると、Slackの稟議チャンネルに申請内容及び添付ファイルが自動投稿されます。
承認者は申請内容に問題がなければ、投稿に配置されているボタンを利用して承認・差戻しが行えます。

f:id:medley_inc:20201225155917p:plain

承認者は稟議ワークフローシステムへアクセスすることなく、Slackで承認行為が完結できます。稟議内容において確認事項がある場合にはSlackの投稿スレッドで申請者と質疑応答のやり取りができ、承認・差戻しの判断に必要なコミュニケーションが行えます。

後続のアクション

承認後には、
・申請者に承認or差戻し結果をSlackのDM(ダイレクトメッセージ)で通知する


・後続の担当者へSlackで通知する
・(法務押印などの)承認後タスクを作成し担当者に通知する
等、後続のアクションへつながっていく仕組みも用意しました。

システムの裏側

入力インターフェース

入力画面は、TeamSpiritで標準提供されている稟議オブジェクトを利用しました。入力項目は標準で用意されているコンポーネントを利用し、メドレー独自で定義しています。承認プロセスを定義すれば、Slackを使わずにTeamSpiritのみでも運用は可能です。

Slack通知

Salesforceの標準機能とApex を用いたScript処理を使ってSlack通知をしています。

Apexとは、Salesforce内で利用するビジネスロジック用のオブジェクト指向プログラミング言語(ほぼJava)のことです。

Slack通知までの大きな流れは以下です。
1. 稟議申請ボタンを押したタイミングでステータス項目を「未申請」から「申請中」へ変更
2. プロセスビルダーにてステータス項目が「申請中」になったことを検知してApexをコール
3. Apex内で申請情報や承認者情報の取得
4. Slack APIをコールし、Slackへ投稿

1~4のプロセスを詳しく見ていきます。

1. 稟議申請ボタンを押したタイミングでステータス項目を「未申請」から「申請中」へ変更

申請者が「稟議申請」ボタンを押したタイミングで承認プロセスを走らせます。
申請時のアクションとして、 ステータス「申請中」とします。ステータスが変わる毎に処理を走らせているので、ステータス定義は一つ肝になります。

2. プロセスビルダーにてステータス項目が「申請中」になったことを検知してApexをコール

プロセスビルダーを利用することで「稟議レコードを作成または編集したとき」に何らかの処理を実施することが可能です。今回は、ステータスが「申請中」になった場合にApexをコールする、という処理にしています。

3. Apex内で申請情報や承認者情報の取得

通知に必要な情報を揃えるため、Apexの処理では稟議オブジェクトの申請情報と合わせて次の承認者情報も取得しています。

String ownerId = p.OwnerId;
//申請者のユーザ名を取得
String applicant = [SELECT Username FROM User WHERE Id = : ownerId].Username;
//承認プロセスのレコード取得
String processInstanceId = [SELECT Id FROM ProcessInstance WHERE TargetObjectId = : p.Id ORDER BY CreatedDate DESC limit 1].Id;
//承認者のIDを取得
String approveId = [SELECT OriginalActorId FROM ProcessInstanceWorkitem WHERE ProcessInstanceId = : processInstanceId].OriginalActorId;

4. Slack APIをコールし、Slackへ投稿

Apexが取得した情報をもとに、Slackに投稿します。
稟議内容を記載し、申請者・承認者に対してメンションされるようにユーザ名も記載します。
また、今回は承認者用にインタラクティブボタンを配置する必要があったので、Block Kitを利用し、ボタン付きメッセージを作成しました。

{
  "text": "hoge",
  "blocks": [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "fuga"
      }
    },
    ...
    {
      "type": "actions",
      "elements": [
        {
          "type": "button",
          "text": {
            "type": "plain_text",
            "text": "承認"
          },
          "value": "Approve",
          "style": "primary"
        },
        {
          "type": "button",
          "text": {
            "type": "plain_text",
            "text": "差戻し"
          },
          "value": "Reject",
          "style": "danger"
        }
      ]
    }
  ]
}

TeamSpirit(Salesforce)→Slackへの投稿は開発において苦労したポイントの一つです。

Slackからのアクション

Slackの投稿に埋め込んでいるボタンがクリックされた際は、Lambdaを経由してTeamSpirit(Salesforce)のRestAPIをコールし、承認処理を実行しています。
また承認後は、ボタンを「承認」スタンプに置き換えています。

開発を終えて

稟議ワークフローシステムを導入するにあたり、ChatOpsの概念を取り入れSlackに連携する業務システムを構築しました。
承認者からは「Slackで承認やコメントができ、社外からでもすぐに対応できるので便利」「Salesforce-Slack連携は他にも活用できるので是非やっていこう」などのコメントをいただきました。また、承認後にもスレッドにて、「振込お願いします」「物品届きました」等のやりとりも行っており、情報がSlackに集約されていく狙い通りの運用になったかと思っています。

Chatサービスを利用している会社では、今回ご紹介したChatOpsは業務効率化するにあたり、有効な手法になるのではないでしょうか。もちろん、すべてChatに連携すればよいというものでもなく、しっかり設計や運用検討を行う必要があります。
今後はChatOpsに限らず業務効率化につながるものはどんどんやっていきたいと考えています。

さいごに

メドレーのコーポレート部門では「徹底的に合理性を追求した組織基盤や、仕掛けづくりを行っていく」ことを目指して、業務効率改善のための開発を推進しています。面白そう!と感じた方、メドレーでどんどん改善してみたい!と思っていただけた方は、ぜひ弊社採用ページからご応募お願いします!

www.medley.jp

最後まで読んでいただきありがとうございました。

CloudFront のエラー監視精度を上げた話

はじめに

はじめまして、メドレー新卒入社2年目の森川です。

インフラ経験がまだ4ヶ月ほどの未熟者ですが、AWS認定資格クラウドプラクティショナー の試験に合格することができました。上位の資格取得に向けて今後も勉強していきます。

先日私が担当させていただいた CloudFront のアラート改善について、問題の原因と対応方法を本記事で書かせていただきます。

よろしければお付き合いください。

背景と問題

弊社が運営しているプロダクトの一つ ジョブメドレー ではインフラ環境に AWS を利用しています。

監視には CloudWatch や Datadog などを使用しています。サービスの異常を検知するための設定のひとつに、CloudFront のエラーレスポンス増加を検知するためのアラート通知があります。

CloudFront が返すレスポンスのうち、特定の時間範囲の中で4xx, 5xx系のエラーを返した割合が閾値を超過したことを検知して、CloudWatch アラームから Lambda を通して Slack に通知を行っています。

ところが、ある頃を境に CloudFront での4xx系エラーレスポンスの発生割合が増加し、アラートの通知頻度が想定以上に高くなってしまいました。

f:id:medley_inc:20201221172048p:plain

原因

調査を行ったところ、刷新した社内システムにて以下2つの原因でアラートが発生していることが分かりました。

f:id:medley_inc:20201221172100p:plain

原因1. 社外サービスからのアクセスでアラートが発生

CloudFront のログを確認したところ、社外サービス(Slack, Google スプレッドシートなど)からのアクセスに対してステータスコード 403 を返しているレスポンスログが数多く記録されていました。

これらのサービスに弊社の社内管理システムの URL がポストされると、プレビューを表示するためのリクエストが送信されますが、この時のリクエストが社外からのアクセスとして WAF で制限されていました。

f:id:medley_inc:20201221172124p:plain

インフラ刷新前から現在まで稼働している CloudFront のログも確認したところ、こちらでも同様のエラーレスポンスが発生していることが分かりました。しかし、エラー割合増加のアラートが頻発することは現在でもほとんどありません。

以前はジョブメドレーが持つシステム全体へのアクセスをひとつの CloudFront で処理していたため、アラート通知の割合として計算する際の母数が大きく、社外からのアクセスによるエラーが発生していても、その割合が閾値を超過することが少なかったからだと考えられます。

インフラ構成を刷新したことをきっかけに、これまで目立っていなかった社外からのアクセスという問題が表面化してきたのです。

原因2. 利用者が少ない時間にエラーレートが高くなりアラートが発生

CloudWatch アラームでは、一定期間内でのレスポンスのうち、4xx, 5xx系のエラーごとにその割合が閾値を超過したことを検知してアラートを発生させる設定としていました。

しかし、深夜など利用者が少ない時間に一度でもエラーが発生すると、その割合が跳ね上がってしまうことでアラート発生頻度が増加し、誤検知と言える状態になっていました。

以下の画像では、4xx系エラーの割合が夜間に100%となっている箇所が確認できます。(表示時間は UTC です)

f:id:medley_inc:20201221172140p:plain

対応方法

2つの原因に対し、それぞれ対応を行いました。

対応1. 特定の社外サービスからのアクセスをエラー検知の対象外とする

各サービスの設定により、プレビュー表示によるアクセスを停止させる選択肢が考えられます。しかし、該当するサービスすべてに設定を行うのは難しく、管理も複雑になりそうです。

そこで、特定の社外サービスからのアクセスを エラー検知の対象外とする 方針で対応を行いました。

ログのすべてを CloudWatch アラームの評価対象としていたために、誤検知と言えるアラートが発生しているのが現状です。したがって、評価させたいログだけに絞り CloudWatch で評価させることができれば解決が図れます。今回であれば、特定のユーザーエージェントや IP アドレスなどを除外して CloudWatch に渡すという処理が求められます。

その実現のため、今回新たに作成したのが Lambda の関数です。

f:id:medley_inc:20201221172156p:plain

S3 に CloudFront のログが保存されたことをトリガーに Lambda を起動させるように設定しました。

ログごとに記録されているリクエスト元のユーザーエージェントや IP アドレスなどを確認し、除外対象かどうかを判定します。

そうして選別を通過したログを今度はステータスコードの5つのクラス(1xx, 2xx, 3xx, 4xx, 5xx系)ごとに振り分けます。

ただし、CloudFront ではステータスコード000 が入ることがあります。

f:id:medley_inc:20201221172213p:plain https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/AccessLogs.html

ステータスコード 000 はアラートで検知したところで対応できることが特にないため、検知対象から除外する方針としました。

(S3 のログを直接確認すると 000 なのですが、Athena でログを確認すると 0 で表示されるため、少しハマりました)

こういった意図しない値がステータスコードに含まれていた場合などを検知できるようにするため、5つのクラス以外の値が含まれていた場合に UNKNOWN_STATUS_CODE なクラスとして分類するようにしました。

必要なものに絞ったログを6つのステータスパターンに分け、それぞれの件数を CloudWatch メトリクスへ PUT させます。

f:id:medley_inc:20201221172229p:plain

ここまでが Lambda の仕事となります。

各ステータスのログの件数を CloudWatch メトリクスで確認できるようになったので、レスポンス全体における4xx, 5xx系エラーの割合が算出できます。これを元に閾値を設定し、以前のようなアラートを作成することができました。

対応2. CloudWatch アラームの検知ルールを調整する

利用者が少ない時間にエラーレートが高くなりアラートが発生する件については、 CloudWatch アラームの検知ルールを調整 することによって対応しました。

一定期間でのエラー数に閾値を定め、超過した際にアラートを通知するように変更しました。つまり、割合ではなく絶対数で判断させるようにしています。

以下の画像の緑色のグラフが新たな検知ルールで参照するものとなります。橙色で示しているのが4xx系エラーの割合ですが、これが100%となっている箇所においても新たな検知ルールには反応していないことが分かります。

f:id:medley_inc:20201221172242p:plain

対応を終えて

Lambda を用いた集計処理の作成と、アラートの検知ルールの調整を行うことで、CloudFront のエラー監視精度を向上させることができました。

以前は頻繁にアラートがあがっていましたが、対応後はすっかり落ち着きを見せています。

システムの安定稼働を実現するためにも、適切にアラートを検知できるように今後も改善を図っていきたいと思います。

今回の課題に対する解決手段としてはシンプルな対応であったかとは思いますが、私には実りの多い紆余曲折な経験となりました。

AWS の基本的なサービスの連携を学ぶことができたことに加え、新たに作成する AWS のサービスの課金額の試算や、実行計画を定めてからの実装など事前準備を意識して取り組むことができました。恵まれた環境の中、日々学ばせていただいております。

さいごに

メドレーでは「医療ヘルスケアの未来をつくる」というミッションを掲げ、各プロダクトの開発・運営が進められています。

エンジニア・デザイナーをはじめ多くのポジションで新たなメンバーを募集しています。ご興味をお持ちいただけた方は、ぜひお気軽にお話しさせていただければと思います!

ここまでお付き合いいただき、ありがとうございました。

www.medley.jp

AWS MediaConvert と hls.js で動画配信サービスを構築しました

こんにちは、第一開発グループの矢野です。ジョブメドレー開発エンジニアとして、主にバックエンドを担当しています。

直近では、ジョブメドレーが先月リリースした 「動画選考」 機能の開発プロジェクトに携わっており、動画ファイルのアップロード/配信環境の設計・実装を行っていました。

今回のブログでは、この「動画選考」機能の開発に利用した AWS Elemental MediaConvert サービスと、hls.js という OSS ライブラリについて紹介したいと思います。

ジョブメドレーの「動画選考」機能

はじめに、今回リリースした「動画選考」機能について概要を紹介します。

新型コロナウイルス感染拡大によって、対面での面接に不安を感じたり、公共交通機関の利用が難しくなったりすることにより、満足な転職活動ができなくなっている方もいらっしゃるかと思います。 このような課題を解決するために、ジョブメドレーではリアルタイムにオンラインで面接を行う「WEB面接」と、事業者があらかじめ設定した質問に対して応募者が動画で回答を送る「動画選考」の2つの機能を提供開始いたしました。

ref. WEB面接・動画選考機能のリリースのお知らせ

動画選考(動画面接)は、近年増加傾向にあるオンライン選考の一種です。一般的に、求職者 / 就活生が PC ・スマートフォン等のカメラで、予め用意された設問に応じて動画を撮影し、企業に送ることで選考を行います。

ref. WEB面接・動画選考とは? 実施の流れ、使用ツール、マナー、注意点などを徹底解説!

私たちジョブメドレーの動画選考では、事業所があらかじめ設定した質問に対して、求職者が回答動画を提出することができます。事業所も求職者も、動画で質問・回答を送ることで、書類だけでは伝わらない雰囲気や強みを相手に伝えることができます。

f:id:medley_inc:20201126150024p:plain

WEB面接・動画選考機能のリリースのお知らせ

動画配信サービスの設計ポイント

Web アプリでこのような動画配信サービスを開発する場合、「ユーザによる動画アップロード環境」と「ユーザへの動画の配信・再生環境」を提供する必要があります。

ジョブメドレーで扱う動画は一般公開されるものではなく、公開条件も複雑です。

よって今回は、この「動画アップロード/配信環境」を自サービス内に構築する方針をとり、以下のような動画まわりの設計ポイントについて検討・技術選定を行うことにしました。

(もちろん、要件によっては YouTube や、法人向け動画配信プラットフォームを契約した方が手軽な場合もあるかと思います)

  • 動画の録画・撮影
    • サポートしたい動画ファイルのフォーマットをどうするか
    • Web アプリ内に録画機能を設けるか
  • 動画のアップロード(ストレージ)
    • 動画ファイルのバリデーションで「動画ファイルの解析」を行うか
    • 動画ファイルのアップロード先(ストレージ)をどこにするか
  • 動画のエンコード
    • 動画ファイルのエンコード形式(H.264、HLS 等)をどうするか
    • 非同期エンコードの場合、ステータス検知・エラーハンドリングをどうするか
  • 動画の配信(ダウンロード)
    • 配信形式(ダウンロード/ストリーミング)をどうするか
    • 暗号化をする場合、復号をどのように行うか
    • 動画ファイルの公開方法(アクセス制限)をどうするか
  • 動画の再生
    • Web ページ上で再生させるのか、その場合の表示・再生制御をどうするか
    • ブラウザサポートをどこまでにするか、非対応・エラー時の制御をどうするか

今回は、上記の太字で記載した 「動画のエンコード」に MediaConvert を、「動画の再生」に hls.js をそれぞれ採用しています。

各項の詳細は省きますが、全体を通して大まかに、以下のフローで「動画アップロード→エンコード(変換)→配信・再生」を実現することにしました。

  1. ブラウザから Ajax で動画を S3 へアップロードする
  2. MediaConvert が動画を HLS 形式にエンコード(変換)する
  3. ブラウザで hls.js を使い動画を CloudFront からストリーミング形式で受信、再生する

今回はこの「動画アップロード→エンコード(変換)→配信・再生」に焦点を絞り、MediaConvert と hls.js をどのように使ったのかを紹介します。

MediaConvert による HLS エンコード

AWS Elemental MediaConvert は、S3 との親和性が高いファイルベースの動画変換サービスです。自前で ffmpeg などを使って動画エンコードサーバを構築・管理することなく、スケーラブルな動画変換処理を手軽にシステムに組み込むことができます。

f:id:medley_inc:20201126150358p:plain

ref. AWS Elemental MediaConvert

料金は出力する動画の再生時間に応じた従量課金です。AWS コンソールから GUI ベースでエンコード設定を作成したり、ジョブ(エンコード処理)を登録することができます。

また、他 AWS サービス同様に API が提供されており、AWS CLI や各言語の SDK を使ってプログラムからエンコード処理を登録することができ、システム連携も容易です。

# CLI でエンコードジョブを登録する例
$ aws --endpoint-url https://abcd1234.mediaconvert.region-name-1.amazonaws.com --region region-name-1 mediaconvert create-job --cli-input-json file://~/job.json

上記 CLI コマンドで下のようなエンコード設定を記載した JSON を使いジョブを作成すると、S3 上の動画ファイルをサクッとエンコードしてくれます。ジョブはキューイングされ、内部で並列処理されるため、大量のエンコード要求にも簡単に応じることができます。

{
  ...
  "Settings": {
    "Inputs": [
      {
        # 入力元の S3 バケット上の動画ファイル key を指定
        "FileInput": "s3://testcontent/720/example_input_720p.mov"
      }
    ],
    "OutputGroups": [
      {
        "OutputGroupSettings": {
          "FileGroupSettings": {
            # 出力先の S3 バケット key を指定
            "Destination": "s3://testbucket/output"
          }
        },
        # 動画・音声のエンコード設定を指定
        # ここで品質レベル毎に振り分けた複数のファイルを出力したり
        # サムネイル jpg を作成したりすることも可能
        "Outputs": [
          {
            "VideoDescription": {},
            "AudioDescriptions": {}
          }
        ]
      }
    ]
  }
}

ref. AWSCLIを使用したAWSElemental MediaConvertCreateJobの例

エンコードが完了したジョブは、cron + SDK などで API を介して定期チェックする他に、CloudWatch Events によるイベント監視 → Lambda で処理するようなこともできます。

ref. AWS Elemental MediaConvert による CloudWatch イベント の使用

なぜ動画を再エンコードするのか

通常、ユーザからアップロードされる動画ファイルは、既に何らかのコーデックで圧縮され .mp4.mov などのコンテナフォーマットに変換されていることが殆どです。

しかし Web ページで <video> タグを使いこれら動画ファイルを再生しようとした場合、 「動画フォーマットにブラウザが非対応だと再生できない」 という環境依存問題があります。

ブラウザと動画フォーマットのサポート表

f:id:medley_inc:20201126150540p:plain

ref. HTML5 video > Browser support

この問題に対応するため、多くの動画配信サービスでは、ユーザの動画を多くの環境で再生可能な MP4 コンテナフォーマット(H.264 + AAC コーデック)などの形式へ「再エンコード」しています。

ジョブメドレーの動画選考では上記目的に加えて、動画閲覧時の回線・端末負荷を抑える 「HTTP ストリーミング形式」 で動画を配信するために、アップロードされた動画を全て HLS 形式エンコードしています。

HLS - HTTP Live Streaming 形式

HLS は HTTP Live Streaming の略で、Apple 社の開発した規格です。HTTP ベースのストリーミング通信プロトコルで、細切れにした MP4 動画ファイルを分割ダウンロードさせることで動画のストリーミング配信を実現しています。

HLS 形式にエンコードされた動画は .ts という分割されたメディアファイル群と、 .m3u8 という、メディアファイルの取得先や秒数などを記載したテキストファイルで構成されます。

.m3u8 ファイルの例(マニフェストファイル、プレイリストファイルとも)

#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:9.97663,
media-0.ts
#EXTINF:9.97663,
media-1.ts
#EXTINF:7.10710,
media-2.ts
#EXT-X-ENDLIST

ref. RFC 8216: HTTP Live Streaming

HLS は他のストリーミング形式と比較して、ライブ配信 / VOD どちらにも対応可能なこと、対応ブラウザが多いこと、専用の配信サーバを使わずに配信可能なことなどから、近年の動画配信サービスで広く利用されています。

Web エンジニアの視点から見ても、 HTTP ベースなためキャッシュや HTTPS 暗号化など、既存 Web 技術と掛け合わせることが想像しやすく、扱いやすい印象でした。

MediaConvert の HLS エンコードジョブ設定

実際にプログラムから API 経由で HLS エンコードジョブを登録する際の設定 JSON は、以下のように GUI でジョブテンプレートを作成して確認することができます。

f:id:medley_inc:20201126150659p:plain f:id:medley_inc:20201126150726p:plain f:id:medley_inc:20201126150804p:plain f:id:medley_inc:20201126150827p:plain

この「 JSON を表示」で、前述した CLI コマンド mediaconvert create-job --cli-input-json に渡せる JSON が表示されます。実装の際にはこちらを参考にしながら、ユーザーガイド を参照して利用したい機能にあわせた設定を追加していくことをおすすめします。

注意点・つまづいたポイント

  • 利用前に IAM で MediaConvert 用ロールの設定が必要です
  • AWS コンソールの Service Quotas > AWS サービス > AWS Elemental MediaConvert から確認できますが、エンコード並行処理の同時実行数上限は 20 になっています
    • AWS ルートアカウント 1 つにつき 1 サービスが割当てられるので、これを増やしたい場合は申請が必要です
  • エンコードジョブをキューイングする「キュー」を作成して、ジョブの登録時に選べるのですが、上記した「並行処理の同時実行数上限」はこの「キュー」毎に均等に振り分けられます
    • 例えば「本番キュー」と「検証キュー」の 2 つのキューを作成した場合、それぞれの並行処理の同時実行数上限は 10 ずつになるので注意してください
  • マニフェスト期間形式(Manifest duration format)に整数(INTEGER)を指定していると、iOS Safari で「動画の実際の再生時間と、再生プレイヤーのシークバーに表示される合計時間にズレが生じる」問題がありました
    • 浮動小数点(FLOATING POINT)に変更することで対応しました、マニフェストファイルに出力される各 .ts ファイルの長さが、浮動小数点 → 整数に変換され切り上げられることでズレが生じているようでした

hls.js による HLS 動画の再生制御

MediaConvert により HLS 形式にエンコードされた動画を、Web ブラウザで再生するために必要なのが、hls.js です。

f:id:medley_inc:20201126151007p:plain

ref. video-dev/hls.js

実は HLS によるストリーミング配信は、現状 Safari など限られたブラウザでしかネイティブでサポートされていません。

ref. https://caniuse.com/http-live-streaming

この HLS 動画を Safari 以外の Google Chrome や IE11 などの主要ブラウザで再生可能にするため、hls.js が利用されています。内部的には、非対応ブラウザ環境において、ブラウザの MediaSource 拡張 を使って HLS 動画を再生する仕様になっています。

Video.js との比較

似たようなライブラリに Video.js というものもあり、導入を迷ったのですが …

  • Video.js は UI もセットになった「 HLS に対応した再生プレイヤー」ライブラリ
    • HLS 対応以外にも、字幕や章分けなど機能が豊富
  • hls.js はブラウザ標準の <video> タグで HLS に対応することだけを目的にした「 HLS クライアント」ライブラリ
    • UI などはなく、動画再生プレイヤーはブラウザ標準のまま

…と、上記のように hls.js の方がシンプルにやりたいことを実現できるため、今回は hls.js を採用しました。

GitHub のスター数は先発の Video.js の方が多いのですが、hls.js も開発は活発で、日本では グノシー さん、世界的には TEDTwitter でも採用されており、十分実績があるかと思います。

hls.js による実装

基本的には README の Getting Started の通りで実装できます。一部 README のサンプルコードから抜粋して解説すると...

  var video = document.getElementById('video');
  var videoSrc = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';

  if (Hls.isSupported()) {
    var hls = new Hls();
    hls.loadSource(videoSrc);
    hls.attachMedia(video);
    hls.on(Hls.Events.MANIFEST_PARSED, function() {
      video.play();
    });
  }

上記 Hls.isSupported() の分岐で、HLS をネイティブサポートしていないブラウザの処理を実装しています。 本来 <video>src 属性にセットするべき .m3u8 ファイルの URL へ hls.loadSource() でアクセスさせ、クライアントから XHR リクエストを飛ばします。その後 hls.attachMedia()インスタンスを DOM 上の <video> タグに紐づけています。

  else if (video.canPlayType('application/vnd.apple.mpegurl')) {
    video.src = videoSrc;
    video.addEventListener('loadedmetadata', function() {
      video.play();
    });
  }

上記の分岐が iOS Safari など、HLS 動画をネイティブサポートしているブラウザ向けの処理です。単純に .m3u8 への URL を <video> タグの src へ付与しているだけですね。

(サンプルコードでは、マニフェストファイルのロード後に自動再生させるようになっているようです)

注意点・つまづいたポイント

  • hls.js クライアントが取得する HLS 動画ファイル群は、CORS ヘッダで GET リクエストを許可された環境に設置する必要があります
  • .m3u8 マニフェストファイルをアプリの API などから返却する場合、Content-Type を application/x-mpegURL にして渡す必要があります
  • iOS Safari などの hls.js 非対応ブラウザ向けの実装を意識する必要があります
    • hls.js による制御が複雑になるケースでは、同じような制御を hls.js 非対応ブラウザ向けに実装できるか?をイメージできないと手戻りが発生しそうです

この他、フロントエンドでは <video> タグのブラウザ毎の挙動や、表示の違いに時間がかかりました。(ある程度予想はしていましたが、やはりメディアの取り扱いは難しい…)

hls.js 自体は導入も手軽で、サクッと HLS 動画のマルチブラウザ対応が実現でき、とても使いやすかったです。@types も存在するので、TypeScript 環境でも難なく実装できました。

SSR や HLS + AES-128 の再生にも対応しているので、興味のある方は一度 公式ドキュメント を確認してみてください。

おわりに

従来、動画配信サービスを構築する場合、ffmpeg を載せたエンコードサーバや、ストリーミング配信サーバを別建てして、負荷に応じてスケールさせて…のような設計が必要だったかと思います。

今回、MediaConvert をはじめとした AWS サービスと hls.js を利用することで、手軽に、スケーラブルな動画エンコード/HTTP ストリーミング配信環境を構築することができました。

ジョブメドレーの動画選考はまだリリースしたばかりですので、今後反響を見ながら、さらなる改善を重ねていけたらと思います。最後までお読みいただきありがとうございました。

www.medley.jp

WEB 面接の裏側

株式会社メドレーのエンジニアの笹塚です。
私が開発を担当しているジョブメドレーで、先月10月23日に WEB 面接・動画選考をリリースしました。

job-medley.com
WEB 面接、動画選考ともに、昨今の非対面での就職活動ニーズに応えるべく開発しました。

リリースは2つの機能を同時ですが、今回は WEB 面接の裏側に絞ってご紹介します。

WEB 面接概要

WEB 面接とは、リアルタイムで事業者様と求職者様が、オンライン面接を行うことができる機能です。
専用のアプリケーションは必要なく、PC、スマートフォンのブラウザから利用できます。

f:id:medley_inc:20201126143843p:plain


サービス選定

開発にあたり、いくつかの候補があがりましたが、最終的には自社内でも導入実績のある SkyWay を使用することにしました。

SkyWay とは

WebRTC(Web Real Time Communication)を使用したオンラインのビデオ通話を、サービスに導入できるマルチプラットフォーム SDK です。

2020年11月時点で、JavaScript SDKiOS SDKAndroid SDK が提供されています。

  • シグナリングサーバなどの WebRTC に必要となるインフラ構築が不要です
  • 使用上限つきの無料プランもあります
  • NTT コミュニケーションズが開発しています

webrtc.ecl.ntt.com

WEB 面接の対応ブラウザバージョン(2020年11月時点)

PC: Google Chrome

バージョン84以上

PC: Microsoft Edge

バージョン84以上

iOS: Safari

iOS12 以上

Android: Google Chrome

バージョン85以上
Andoid9 以上

 SkyWay  JavaScript SDK の動作確認ブラウザ、WebRTC の対応状況、利用者の利用傾向から対応ブラウザのバージョンを上記のように設定しました。

webrtc.ecl.ntt.com

SkyWay の接続モデル

SkyWay でビデオ通話を実装する場合、2種類の接続モデルから選びます。

SkyWay 電話モデル

  • 電話のように1対1でのビデオ通話を想定したモデルです。
  • Peer インスタンス(シグナリングサーバによって発行された一意の PeerID を持つ)同士で接続します。
  • 接続するためには、相手の PeerID が必要になります。

SkyWay ルームモデル

  • 同一ルーム内の全ての Peer でビデオ通話するモデルです。
  • ルーム名を使用して参加します。相手の PeerID を知る必要はありません。
  • ルーム名は API キー毎に独立しています。
  • ルームの接続タイプはフルメッシュか SFU の2種類から選べます。
  • 参加者全員へのチャットなどのデータ送信もできます。

ジョブメドレーの WEB 面接では、 今後の機能拡張を想定して、こちらのルームモデルを採用しました。

ルームの通信タイプ

ルームに複数人が参加している場合、それぞれの通信をどう行うかを、メッシュと SFU から選ぶことができます。

フルメッシュ

f:id:medley_inc:20201126180010p:plain

画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90

全員が相互に通信を行います。人数が増えると、人数分端末のエンコード負荷と通信量が増加します。

SFU

SFU の場合、上りの接続は1本になるので、メッシュよりも端末のエンコード負荷や、通信量の軽減が期待できます。

f:id:medley_inc:20201126180029p:plain

画像引用元: https://webrtc.ecl.ntt.com/skyway/overview.html#_4-sfu%E3%82%B5%E3%83%BC%E3%83%90

通信方式の違いは SkyWay が隠蔽してくれるので、joinRoom 時の mode を mesh から sfu に変更するだけで切り替わります。

peer.joinRoom(roomName, { mode: 'mesh か sfu を指定', stream: mediaStream });

webrtc.ecl.ntt.com
ジョブメドレーの WEB 面接では、面接参加人数を考慮して mesh を使用しています。

実装イメージ

const peer = new Peer(peer_id, {

  key: api_key

});

peer.once('open', () => {

  room.once('open', () => {

    // ルーム参加後に発生するイベント

  });

  room.on('peerJoin', (peerId: string) => {

    // ルームに誰か参加した場合に発生するイベント

  });

  room.on('stream', (stream: RoomStream) => {

    // stream を受けた場合に発生するイベント

  });

  room.on('data', ({ src, data }) => {

   // data を受けた場合に発生するイベント

  });

});

peer.on('error', (error: Error) => {

 // エラー発生時に発生するイベント

});

peer.joinRoom(roomName, { mode: 'mesh', stream: mediaStream });


room.close();

peer.disconnect();


Peer を作成し

  1. peer.joinRoom() でルームに参加

  2. room.stream イベントで他の参加者の stream を受け取る

  3. room.data() でチャットなど、データ送信もできる

  4. room.close() でルームから退出
が SkyWay の JavaScript SDK を使用した基本的な実装になります。

この実装に

  • navigator.mediaDevices.getUserMedia() で取得した stream を joinRoom で渡す
  • steam イベントで受け取った stream を video で再生する

を追加すれば、オンラインでのビデオ通話が可能になります。

スマートフォン対応

PC と同様のコードでほぼ動作しましたが、iOSAndroid 端末で、機種依存と思われる挙動の調査と対応に時間がかかりました。その一部を紹介します。

タブを移動すると映像が映らない

発生した問題
iOS12、iOS13 などで

  • 複数のタブをひらいた場合に、映像の取得ができなくなる
  • スクリーンロックからの復帰時に、映像が取得できなくなる

という問題が起きました。iOS14 ではタブ切り替え時に映像を取得できるようになっていましたが、スクリーンロックからの復帰時は映像を取得できないままでした。

対応

この対応は visibilitychange イベントで、タブの切り替えと、スクリーンロックからの復帰時のイベントを拾い

  1. 取得済の stream の track を stopする
  2. stream を取り直す
  3. SkyWay の room.replaceStream() で、WebRTC で使用している stream を差し替える

以上の実装により、対応しました。

document.addEventListener('visibilitychange', async () => {

  if (document.visibilityState !== 'visible') {

    return;

   }

   localMedia.getTracks().forEach((track: MediaStreamTrack) => {

     track.stop();

   });

   const replaceStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })

     room.replaceStream(replaceStream);

   }

});

イヤホンの操作で映像が止まる

発生した問題 

iOS12、iOS13 では起こりませんでしたが、iOS14 でオンライン面接途中にイヤホンをスマートフォンから外すと、相手側の映像が止まるようになりました。

対応

それまでは、相手の映像と音声を再生するために、video タグに stream を渡して映像と音声を再生していましたが


video : mute にして映像を再生
audio : 音声を再生


と、音声と再生を分けたところ、イヤホンを外しても停止することはなくなりました。

制約設定

getUserMediaで取得する MediaStream は、制約を設定することでデバイスの消費リソースを抑えることができます。

設定例:
navigator.mediaDevices.getUserMedia({ audio: true, video: true });

navigator.mediaDevices.getUserMedia({ audio: true, video: { frameRate: 15 } }); 
制約が設定可能かどうかの確認:
const supportedConstraints = navigator.mediaDevices.getSupportedConstraints();

getSupportedConstraints() で、対応している制約名を取得することができます。

使用しているブラウザがその制約に対応しているかを確認し、対応している場合のみ設定を有効にします。

例えば、スマートフォンの場合に以下の設定をすれば、インカメラを使用し、フレームレートを20に抑え、320x320の解像度に制限することができます。

{ facingMode: 'user', frameRate: 20, width: 320, height: 320 }

指定した制限が必ず使用される保証はなく、機種依存の影響を受ける設定でもあるので、対象としている環境にあわせて検証と調整をする必要がある点には注意が必要です。

動作検証中に、機種依存と思われる挙動をした例を紹介します。

frameRate

指定すると一部 Android 端末で以下の挙動をした。
1. Android 端末で面接に参加する
2. iOS Safari で参加する
3. Android 側の映像と音声が Safari に送られない
Safari で先に参加する場合には問題がない。

解像度指定

例: width: 320, height: 320
frameRate だけ指定していたときに上記の挙動をした Android、iOS Safari の組み合わせで問題が起こらなくなった。

解像度を指定するとインカメラではなく、リアカメラを使う Android 端末があった。

制約設定については、現在も調整中です。

参考情報

SkyWay Conference

conf.webrtc.ecl.ntt.com

github.com
SkyWay のルーム機能を使用したデモ環境です。
GitHub にコードも公開されているので、ルーム を使用した場合の挙動と実装方法を確認できます。
開発をしていると、実装の問題なのか、SkyWay の SDK の仕様なのか、特定のデバイスで起こる問題なのかの切り分けに時間がかかるため、こちらの環境が参考になりました。

まとめ

SkyWay が WebRTC とグループでのビデオ通話の実装を統合して提供してくれるため、開発時は、自社サービスとしての WEB 面接の機能に集中することができました。

スマートフォンのブラウザ対応と調査に時間がかかることもありますが、今後も利用者からのフィードバックを得ながら改善していきたいと思います。

メドレーでは、ニーズにあわせた新機能の開発にも力を入れています。多くの利用者に実際に使われるサービスの開発をしてみたいと思った方、ぜひお気軽にお話しましょう!

www.medley.jp

GraphQL, TypeScript, Reactを用いて型安全に社内システムをリニューアルした話

こんにちは。メドレーのエンジニアの山田です。現在、医療介護求人サイト「ジョブメドレー」のチームで開発を担当しています。

今回、ジョブメドレーの社内スタッフが利用する社内システムをリニューアルした事例をご紹介します。

リニューアル対象はバックエンド領域も含まれますが、本記事ではフロントエンドの話を中心にご紹介します。

また、弊社デザイナー酒井が以前投稿した デザイナーがデザインツールを使わずに、Reactを使ってデザインした話 も関連しているので、よろしければあわせてご覧ください。

リニューアルの背景

社内システムでは、求人サイト「ジョブメドレー」を利用する求職者に関する情報や求職者の応募状況を管理しています。

前回のリニューアルから時間が経ち、複雑性が高くなってきました。その複雑性に比例して、新機能の追加や改修するためのコストも高くなっていました。

そこで上記の課題を解決するため、状態管理がしやすく、テストコードも書きやすい、メンテナブルなアーキテクチャにすべくリニューアルを実施することにしました。

検証期間も経て、今回のリニューアルにあわせて新規に作成するAPIは、GraphQLによって実装することを決めました。

型システムを持つため画面に必要なデータを柔軟に過不足なく取得できる、手動でドキュメントに落とし込まなくてもスキーマが定義されていればAPIの仕様を簡単に把握できる、等がメリットとして感じられました。

特に、GraphQLが持つ型システムが、TypeScript、Apollo、GraphQL Code Generatorのライブラリを組み合わせることで、APIに渡すパラメータや、レスポンスにも型が適用され、GraphQLスキーマの変更にクライアントの実装が比較的容易に追従できることが、大きなポイントでした。

フロントエンドの技術的なリニューアル内容

今回は特に、リニューアルに用いられたフレームワークやライブラリ、Apollo Clientを用いた状態管理、テストコード実装におけるTips等をそれぞれ部分的にご紹介します。

採用したフレームワークと主要ライブラリ

採用ライブラリ 説明
Next.js React用のフレームワーク(ボイラープレート)
TypeScript JavaScriptのスーパーセットで、静的型付け言語
React UIを構築するためのライブラリ(バージョン16.8.0でリリースされたhooksを全面的に使用)
Apollo Client GraphQL APIのクライアントで、アプリケーション全体の状態管理を実施
GraphQL Code Generator GraphQLスキーマから定義ファイル(型、カスタムhooks等)を生成
emotion + Styled System CSS in JSとして利用
formik + yup フォームのビルダー + バリデーター
Jest + React Testing Library テストコード実装用のツール群
ESLint + Prettier ルールに基づいたコードの静的解析 + スタイリング

TypeScript

今回のリニューアルで求められたことの一つとして、さらなる改善・新規機能追加などをしていく上で、ソフトウェア品質を担保するための、アプリケーションの堅牢さがありました。

そこで、フロントエンド側の開発言語としては、プログラムコード内で宣言された型によって、エラーを未然に防ぎつつ、VSCodeをはじめとするエディタのコード補完の恩恵を受けられるメリット等を考慮してTypeScriptの採用を決めました。また、他のプロジェクトでも既にTypeScriptは部分的に利用し始めていた事情もあり、逆にTypeScriptを採用しない、という選択肢はあまり考えられませんでした。

React

UIを構築するためのライブラリ/フレームワークはReactを採用しました。こちらも、弊社では別プロジェクトでReactを既に利用し始めていたこともあり、学習コストの観点から、新たに他のフレームワークを選択するメリットはほぼ無かったためです。しかし、その事を差し引いたとしてもTypeScriptとGraphQLとの相性の良さで、Reactが優勢でした。

特に、Reactの場合は、GraphQLスキーマをベースに、GraphQL Code Generatorによって型定義ファイルだけではなく、GraphQL APIとのやり取りに使えるカスタムhooksも生成して利用できるという点が、大きな利点として考えられました。

Next.js

フロントエンド開発環境を素早く構築するため、ボイラープレートとしてNext.jsを採用しました。

Next.jsの具体的な採用ポイントとしては、主に次の3点です。

  1. webpackにおける、バンドルやコンパイル、ホットリロード等の設定に時間を費やすことなく、ビジネスロジックの実装に集中できる
    • 必要があれば、next.config.js で設定を拡張できる
    • CRA(Create React App)とは異なり、拡張性に優れている
  2. pages 配下に置くReact Componentのディレクトリ構成が、自動的にルーティングとして定義される
    • ルーティングに関する設計作業が不要になる
  3. 自動コード分割等によるパフォーマンス最適化をよしなに行ってくれる

React Componentの分類

componentは大きく2つに分類し、src/components/app/src/components/ui/ それぞれのディレクトリにcomponentを置いています。分類は以下の基準で行ないました。

  • app: 本アプリケーション固有で使用される想定のもので、再利用性が低く、具体的なcomponent

  • ui: 本アプリケーション外でも使用可能な、再利用性が高く、抽象的なcomponent

社内向けシステムではあるものの、Material-UIやAnt Design等をはじめとする、外部のUIライブラリは使用せず、カスタマイズがしやすいように、全て自前で作成しました。

app配下とui配下、どちらのcomponentも基本的には コロケーション の考え方でファイルを構成しています。

一般的には、よく一緒に変更するファイルを近くに置いておくのは良いアイディアです。 この原則は、「コロケーション」と呼ばれます。

この考え方でファイルを構成することで、関連するファイルがまとまっていて、作業がしやすくなります。

src/
  components/
    app/
      partials/
        ${component}/
          apollo.cache.ts
          apollo.query.graphql
          index.tsx
          index.test.tsx
        ...

      screens/
        ${component}/
          apollo.cache.ts
          apollo.query.graphql
          index.tsx
          index.test.tsx

          ${子component名}/
            apollo.cache.ts
            apollo.query.graphql
            index.tsx
            index.test.tsx
            validation.ts

src/components/app ディレクトリ配下でさらに、partialsscreensディレクトリでcomponentを分けています。

screensには、Next.jsでrouteとして扱われる src/pages 配下のcomponent からimportされるcomponentが配置されています。

画面のバリエーションが増える度に、このscreensにファイルが追加されていきます。

partialsには、app配下で複数のcomponentから利用されるcomponent(画面をまたいで共有されるもの等)を配置しています。

screenspartialsそれぞれ直下のcomponentで、必要であれば適宜、componentを分割して子componentを持つ構成にしています。

apollo.cache.tsapollo.query.graphqlについては後述の状態管理の話でご紹介します。

状態管理

アプリケーションの状態管理については、グローバルにアクセスできる状態の管理にはApollo Clientの InMemoryCache によるcache機構で行い、特定のcomponent内に閉じている局所的な状態の管理にはuseState等のReact Hooksを使って行っています。

状態管理の必要性が生じた際、アプリケーションの複雑性を上げないように、なるべくuseState等のhooksを用いたlocal stateだけで済ませられないかどうかを検討します。

例えば、クリックするとドロップダウンリストが表示されるセレクトボックスのcomponentで、ドロップダウンリストの表示状態をそのcomponent内だけで扱いたいのであればuseStateを用いたlocal stateで十分であると考えられます。

親子関係ではないcomponent同士でのやりとりが必要になった時や、サーバのデータと関連する場合等で、ローカルのデータを一元管理しておいた方が良さそうなケースでは、Apollo Clientのcacheを利用します。

Apollo Client

Apolloに関連するファイルの構成については以下の通りです。

src/
  apollo/
    cache.ts
    client.ts
    types.ts
    withApollo.ts
  • cache.ts: Apolloにおけるlocal stateのinitialStateresolverを全画面分このファイルでまとめて、最終的にNext.jsのsrc/pages/_app.tsxに渡るようにする

    • component固有のlocal stateに関するinitialStateおよびstateのupdaterとなるresolverはcomponent毎のapollo.cache.tsにて、別途定義
  • client.ts: Apollo Clientのインスタンスを生成するファイル

  • types.ts: Apollo関連の型定義ファイル

  • withApollo.ts: Apllo Clientの &lt;ApolloProvider /> でラップして返すHigher-Order Compoents(HOC)

実装については割愛しますが、client.tsとwithApollo.ts に関しては、Next.jsの example(with-apollo)等を参考にしました。

画面固有のApolloの状態管理に関わるファイルはsrc/components/**/${component名}/配下に置いています。

こちらもコロケーションの考え方で、componentに関わる状態管理は該当のcomponentと同じ場所に置くことを意識しています。

src/
  components/
    app/
      ${component}/
        apollo.cache.ts
        apollo.query.graphql
        apollo.schema.graphql
  • apollo.cache.ts: component固有のApolloにおけるlocal stateのinitialStateおよびresolverを定義するファイル

  • apollo.query.graphql: クエリを定義するファイル

  • apollo.schema.graphql: local stateのGraphQLスキーマを定義ファイル

ファイルの命名について、ディレクトリ階層をできるだけ深くしたくない ので、apollo等によるディレクトリは設けていませんが、Apollo関連のファイル群として認識できるよう、ファイル名にapollo.プレフィックスをつけて命名しています。

QueryとMutationの実行について

GraphQL Code Generatorのプラグイン TypeScript React Apollo をインストールして、hooksを生成する設定にした上で、component毎にそれぞれGraphQLのスキーマとクエリが記述された.graphqlファイルをもとに、GraphQL Code Generatorが生成するカスタムhooksを利用します。

こちらのカスタムhooksをReact Componentで利用することで、Apollo Client経由でGraphQL APIとローカルのApollo cacheに接続して、データのやり取りを行うことができます。

Query

Queryのhooksは2種類あり、実行するタイミングによっていずれか適切な方を選んで実行しています。

API 実行タイミング
useQuery Componentがrenderされたらクエリ実行
useLazyQuery 任意のイベントをトリガーにしてクエリ実行

use***Query

通常であればuseQueryでクエリの結果をrenderしますが、GraphQL Code Generatorを利用する場合は、それぞれのクエリをラップしたカスタムhooksが生成されるので、useQuery,useLazyQueryをそのまま使うことはありません。

query AllPosts {
  allPosts {
    id
    title
    rating
  }
}

↑のようなクエリを用意するとsrc/__generated__/graphql.tsxに対して、次のようなカスタムhooksが型と一緒に生成される設定にしています。

// Apollo Client: 2.6.9、GraphQL Code Generator: 1.15.0の場合の例
export function useAllPostsQuery(baseOptions?: ApolloReactHooks.QueryHookOptions&lt;AllPostsQuery, AllPostsQueryVariables>) {
  return ApolloReactHooks.useQuery&lt;AllPostsQuery, AllPostsQueryVariables>(AllPostsDocument, baseOptions);
}

export function useAllPostsLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions&lt;AllPostsQuery, AllPostsQueryVariables>) {
  return ApolloReactHooks.useLazyQuery&lt;AllPostsQuery, AllPostsQueryVariables>(AllPostsDocument, baseOptions);
}

React Componentでは生成されたカスタムhooksを次のように呼び出してサーバーから返ってくる結果を受け取って、データ出力、ローディング状態のチェック、エラーハンドリング等を行います。

const { data, loading, error } = useAllPostsQuery();

Mutation

データの書き込みはuseMutationで行います。

Query同様、GraphQL Code Generator)によって生成されたカスタムhooks use***Mutation を使っています。

cacheの更新

Mutationが複数エンティティの更新、エンティティの新規作成または削除の場合、Apollo Clientのcacheは自動更新されず、Mutationの結果が自動的にrenderされません。

このような場合でも、useMutationupdate optionを使えば、cacheオブジェクトを引数に取れる関数を設定できるので、この関数内で直接cacheを更新できます。

また、updateの代わりに refetchQueries のoptionを使って、任意のQueryを実行して、シンプルにcacheを更新することもできます。

但し、この方法だとNetwork通信によるオーバーヘッドが発生します。

このオーバーヘッドを犠牲にしてでも、サーバーからデータ取得したいQueryがあるような場合には、このrefetchQueriesが有効です。

local state の管理

ここからは特定のcomponentの状態管理をlocal stateを使ってどのように管理しているかを、ご説明していきます。

@client を使ったQuery

Next.jsのプロジェクトで、local state の管理を Apollo Client で行う場合の例としては、次の通りです。

スキーマ

# src/components/app/Home/apollo.schema.graphql

type Home {
  currentPostId: Int!
}

extend type Query {
  home: Home
}

クエリ:

# src/components/app/Home/apollo.query.graphql

query HomeCurrentPostId {
  home @client {
    currentPostId
  }
}

キャッシュの初期値:

// src/components/app/Home/apollo.cache.ts

export const cache = {
  __typename: 'Home',
  currentPostId: 0,
  ...,
};
// src/apollo/cache.ts

const caches = {
  ...,
  home: home.cache,
};

export {
  ...,
  caches,
};
// src/pages/_app.tsx

export const cache = new InMemoryCache();

  ...

const client = new ApolloClient({
  link,
  cache: cache.restore(initialState || {}),
  resolvers,
  connectToDevTools: true,
});

cache.writeData({ data: caches });

GraphQLクエリとスキーマが定義されていればGraphQL Code Generatorがuse***Queryのコードを生成する設定にしています。

ローカルデータの場合、クエリで@clientディレクティブをつけてローカルデータであることを明示します。

@client を使ったMutation

local stateの更新をGraphQLのMutationとして行う場合の例としては、次の通りです。

スキーマ

# src/components/app/Home/apollo.schema.graphql

type UpdateCurrentPostId {
  currentPostId: Int!
}

extend type Mutation {
  updateCurrentPostId(id: Int!): UpdateCurrentPostId
}

クエリ:

# src/components/app/Home/apollo.query.graphql

mutation UpdateCurrentPostId($id: Int!) {
  updateCurrentPostId(id: $id) @client {
    currentPostId @client
  }
}

resolver:

// src/components/app/Home/apollo.cache.ts

const updateCurrentPostId: MutationResolvers['updateCurrentPostId'] = (_, args, { cache }) => {
  cache.writeData({
    data: {
      home: {
        __typename: 'Home',
        currentPostId: args.id,
      },
    },
  });

  return null;
};

export const Mutation = {
  updateCurrentPostId,
};

Query同様に@clientディレクティブをつけてローカルデータであることを明示します。

実際のMutationの処理自体はresolverの中にcache.writeData()を使って記述します。

Mutationの命名は、動詞+名詞の形式で可能な限り意味のある具体的な名前をつけることを意識しています。

Apolloを使った開発を便利にしてくれるツール

Apollo Clientを使って開発する際は、ローカルのApollo cacheの状態や、クエリを試しに実行するためのツールとして、Google Chrome拡張機能 Apollo Client Developer Tools が非常に便利です。

こちらの拡張機能Chromeにインストールすると、Apollo Clientを使ってGraphQL APIにアクセスするサイトに遷移した状態でChrome Dev Toolsを開くと Apollo のタブが表示されます。そこでクエリの実行や、API仕様の確認、ローカルのApollo cacheの確認等を行うことができます。

GraphQL関連のテストコードについて

Apollo Clientを使ったReact Componentの開発で、QueryおよびMutation実行のテストを実施するには、テストフレームワークのJest、react-testing-libraryとあわせて、Apollo公式でも紹介されている MockedProvider を用いる方法が一般的かと思います。

クエリとクエリに対するレスポンスを組み合わせたモックデータを用意しておき、ApolloProviderの代わりにMockedProviderでテスト対象のcomponentをラップすることで、APIサーバーやNetwork環境に依存せず、モックで指定したクエリがリクエストされると、モックでそれに対応するように用意したレスポンスデータが確実に取得できる仕組みを作れます。

その仕組みとreact-testing-libraryを使って、componentでrenderされるUI上の操作をトリガーにして実行される、クエリのテストを行うことができます。

QueryだけではなくMutationもモックすることができて、便利なツールではありますが、テストケース毎にモックデータは手動で作成しなければならない点が、なかなか骨が折れる作業です。

実際にアプリケーションを動かして、テスト対象のcomponentをrenderし、Queryに渡されるvariablesやレスポンスの値をConsoleに出力し、ブラウザのDev Tools上で一個一個オブジェクトをコピーして、エディタに貼り付けしたりする作業が発生します。

AutoMockedProviderの作成

そこで、わざわざテスト作成やスキーマ変更の度に、手動でモックデータを用意しなくても、GraphQLスキーマで定義されている型を見て、自動でクエリに対するレスポンスをモックしてくれるAutoMockedProviderを、 こちらの記事 を参考にして作成しました。

MockedProviderの代わりに、AutoMockedProviderを用いてテスト対象のComponentをラップすることで、MockedProviderを使ってテストしていた内容と同じテストが実施できます。

MockedProviderを使って毎回モックデータを用意し、テストを実施することに疲れている方は是非、お試しください。

(紹介先の記事では、graphql-toolsmakeExecutableSchema()に渡すschemaSDLjsonファイルで定義されていますが、graphql-tagのライブラリを併用すれば、graphqlファイルでも同様にschemaSDLとして適用することも可能です)

リニューアルを振り返って

今回のリニューアルでは、GraphQL、TypeScript、Reactをセットで採用したことにより、フロント側ではGraphQL Code Generatorを使って、あらかじめ用意しておいたGraphQLスキーマから、TypeScriptの型だけではなく、ReactのHooks関数まで生成して利用できたことが、開発効率の向上に非常に影響を与えたと思います。

GraphQL APIのクライアントで、アプリケーション全体の状態管理を行うApollo Clientのcache機構の使い方等を体得するまでに、学習コストは決してゼロではありませんでしたが、TypeScriptとGraphQLの型システムの恩恵をフルに受け、Next.jsのレールにのっかり、型安全な開発環境を手に入れることができました。

我々、開発者の体験だけではなく、今後のプロダクト全体への生産性にも良い影響を及ぼしてくれると確信しています。

さいごに

メドレーではエンジニア・デザイナーを積極募集しています。

「テクノロジーを活用して医療ヘルスケアの未来をつくる」というミッションに共感し、課題解決を行いたい方は是非、ご応募ください。

www.medley.jp

技術を使うための技術も大切というお話

初めまして。CLINICS の開発を担当しているエンジニアの平山です。 (同姓ですが CTO ではございません)

CLINICS の開発は「スモールチーム制」をとっておりまして、現在そのうちの1つをチームリードしています。

前職は長らく SIer に勤めていました。去年の12月にメドレーに JOIN して、間も無く1年経とうとしている。。と思うと、あっという間だったなぁという印象です。

さて、本日はメドレーで隔週開催している社内勉強会(TechLunch)において発表した内容についてご紹介させて頂ければと思います。

はじめに

「技術を使うための技術」というテーマとなりますが、プロダクト開発をしていく上では欠かせない素養と考えています。メドレーに所属しているエンジニアの1人として、どのように日々課題と向き合っているのか。当テーマを通してお伝えできればと思います。

また、この考え方自体は「医療というテーマ」や「事業の背景(ベンチャーSIer)」を問わず必要とされる場面があるかもしれません。(自身も前職では様々な場面でお世話になりました...)

即効性のあるものではありませんが、じわじわ効いてくる内容ではないかと思います。よろしければお付き合いください。

本題

「技術を使うための技術」

みなさんはこの言葉から、何を思い浮かべるでしょうか。筆者が試しに Google で検索した際上位にヒットしたのは

AIエンジニア
IoT

といった結果でした。なるほど。少し雑に解釈すると「技術(アルゴリズム等)を使うための技術(機械学習、家電等)」といった感じなのでしょうか。(この結果を拾ってきたというのも、いい意味で Google すごいなって感じました)

筆者が今回のテーマとして指しているのは、下記となります。

ロジカルシンキング
推論

これらの 思考を整理する「手段」 とエンジニアの武器である 技術という「手段」 をかけ合わせることで、大きなテーマである「医療」に向き合っています。

手段を目的にしてはならない

先に挙げたとおり「思考の整理」も「技術」も 「手段」 に過ぎません。これらを用いて、適切な一手を指していく為には「目的」に対する解像度を高く描く必要があります。

筆者の発表から抜粋した「技術を使うための技術」の要素をイメージした図

f:id:medley_inc:20201023135338p:plain

整理力

目的を達成する為に必要な「情報」を取捨選択するための要素。

SaaS ✖️ toB の世界においては「プロダクトが解決すべき課題か否かを業務の本質を踏まえて取捨選択する」と言い換えられるかもしれません。

業務知識

目的の「解像度」を高めるための要素。

CLINICS においては、医療情報を扱う上での規制(3省2ガイドライン)や、医療機関(医師・医事・診療科の特性)の業務、診療報酬についての知識、法改正、レセコン(ORCA)... と様々あります。

技術知識

目的を達成する為に必要な「指し手」を選択するための要素。

(エンジニアにとっては説明するまでもない内容であると思いますが) ここが欠けてしまうと絵に描いた餅で終わってしまいます。メドレーのエンジニアにおいても日々研鑽し、プロダクトに対してコミットを続けています。

行動力

目的を達成するための「推進力」を高めるための要素。

各種知識と整理した情報を推進力に変えていく為には、その時の状況に応じた動きをする必要があります。ステークホルダーとの調整は当然ですが、組織内連携といった「横の動き」も必要です。

想像力

これまで挙げたそれぞれの手段を適切に利用していくための要素としての土台が「想像力」であると考えています。

課題(Issue)への取り組み例

f:id:medley_inc:20201023135418p:plain

番号 概要
Issue に取り組む際に「本当に解決すべきこと」についての想像を働かせます。Issue に記載されていることが 本当にプロダクトとして解決すべきことなのか を含めて考えます。これまでの運用が必ずしも正しいわけではない。という点がポイントです。
① について「想像のまま」で終わらせてはいけないので、業務知識と照らし合わせて確度を高めていきます。常勤医師やカスタマーサポートにヒアリングしながら、医療業務としてのあるべき形の解像度を上げていく プロセスです。
① 及び ② で高めた解像度は言葉の延長線上なので、関係者間の認識のギャップが発生しやすいです。プロトタイプを作成して、視覚・触感レベルでギャップを埋めていくことで、あるべき形に向けて洗練させていきます。
① 〜 ③ のタイミングを問わず、必要に応じて関係者と相談しながら進めていきます。エンジニアが立てた仮説をデザイナーの目線で評価・UI/UX 最適化をして頂いたり、大きめの機能については、医療機関パイロット運用のご協力を仰いだりすることもあります。

① 及び ② の項に作業のウェイトが偏っているように見えるかと思います。実際、課題を解決する為の半分以上をここに割いています。

理由は「1度作って公開した機能」は、その後1人歩きをして作成者の意図しない方向で利用をされることがあるからです。

そして、利用者がその運用を定着させてしまうと 誤った機能においても「削ぎ落とすことが困難」 です。これは「使われていない機能」よりも直接的な負債といった形でボディブローのように効いてきます。

  • 「どのような使われ方をするか」について想像すること
  • その使われ方が、プロダクトの目指す世界と合っていること

エンジニアは技術を形にする上で、常に想像力を働かせて取り組む必要がある。 というのが筆者の持論です。

さいごに

執筆の締めにあたって CTO ブログを見返してみると、大事なことはここに詰まっていました。筆者は前職の SIer 時代に読んだのですが、この記事にすごく共感したのを覚えています。

toppa.medley.jp

メドレーでは 医療ヘルスケアの未来を作る という大きな目標、そしてその未来を作る為に解決すべき課題に向かって、今回ご紹介したプロセスや考え方も含め試行錯誤しながら、事業部一丸でプロダクト開発を推進しています。

エンジニアの総合力を発揮して医療ヘルスケアの未来を一緒に作り上げていきたい!という方にお会い出来ることを楽しみにしております。

www.medley.jp