スタディサプリ Product Team Blog 株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです 2024-03-27T08:00:00+09:00 quipper-ja Hatena::Blog hatenablog://blog/10328749687186366342 Engineering Manager のオンボーディング hatenablog://entry/6801883189091941361 2024-03-27T08:00:00+09:00 2024-03-27T08:00:05+09:00 こんにちは、@chaspyです。プロダクト開発部の部長をしています。 スタディサプリ小中高の開発組織では、Engineering Manager (以降 EM と記す) という役割があります。*1 その役割は、エンジニアリングマネージャ/プロダクトマネージャのための知識体系と読書ガイド を引用させてもらうと、People Management + Technology Management を主に担ってもらっています。*2 ありがたいことに、ここ数年で新たに EM にチャレンジしてもらえる機会が増えました。本稿ではそんな EM の活躍をサポートするオンボーディングの仕組みについて説明します。 … <p>こんにちは、<a href="https://github.com/chaspy">@chaspy</a>です。プロダクト開発部の部長をしています。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小中高の開発組織では、Engineering Manager (以降 EM と記す) という役割があります。<a href="#f-c0d0e30c" id="fn-c0d0e30c" name="fn-c0d0e30c" title="リクルートの正式な役割名称は GM: Group Manager">*1</a> その役割は、<a href="https://qiita.com/hirokidaichi/items/95678bb1cef32629c317">エンジニアリングマネージャ/プロダクトマネージャのための知識体系と読書ガイド</a> を引用させてもらうと、People Management + Technology Management を主に担ってもらっています。<a href="#f-c1311512" id="fn-c1311512" name="fn-c1311512" title="Product Management / Project Management は Technical Product Manager というロールが大半を担っている。もちろん EM も関与することはある">*2</a></p> <p>ありがたいことに、ここ数年で新たに EM にチャレンジしてもらえる機会が増えました。本稿ではそんな EM の活躍をサポートするオンボーディングの仕組みについて説明します。</p> <ul class="table-of-contents"> <li><a href="#メンバーのオンボーディングとの違い">メンバーのオンボーディングとの違い</a><ul> <li><a href="#任用直後にグレード設定という重要な仕事がある">任用直後にグレード設定という重要な仕事がある</a></li> <li><a href="#主に人事の内容は秘匿された情報が多く引き継ぎの重要性が高い">(主に人事の内容は)秘匿された情報が多く、引き継ぎの重要性が高い</a></li> <li><a href="#新任-EM-を迎える絶対数が相対的に少ない">新任 EM を迎える絶対数が(相対的に)少ない</a></li> <li><a href="#EM-業務の失敗は大事になる可能性が高い">EM 業務の失敗は大事になる可能性が高い</a></li> </ul> </li> <li><a href="#オンボーディングの内容">オンボーディングの内容</a><ul> <li><a href="#Issue-Template">Issue Template</a></li> <li><a href="#メンターの存在">メンターの存在</a></li> <li><a href="#引き継ぎの内容">引き継ぎの内容</a></li> </ul> </li> <li><a href="#その他の新任-EM-をサポートする仕組み">その他の新任 EM をサポートする仕組み</a><ul> <li><a href="#全社エンジニア組織独自の研修">全社/エンジニア組織独自の研修</a></li> <li><a href="#領域専任人事によるサポート">領域専任人事によるサポート</a></li> <li><a href="#その他チーム運営メンバー育成の支援">その他チーム運営・メンバー育成の支援</a></li> </ul> </li> <li><a href="#おわりに---新任-EM-からの声">おわりに - 新任 EM からの声</a></li> </ul> <h1 id="メンバーのオンボーディングとの違い">メンバーのオンボーディングとの違い</h1> <p>さて、新しく入ったメンバーの活躍をサポートする仕組みは EM であれメンバーであれ重要です。<a href="#f-44102355" id="fn-44102355" name="fn-44102355" title="だいぶ昔に オンボーディングのはじめかた - スタディサプリ Product Team Blog や SRE Team のオンボーディングのいま - スタディサプリ Product Team Blog という記事を書いていたことを思い出しました。同じことをしていますね。">*3</a></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発組織ではオンボーディングは当たり前に定着しています。ブログ記事もたくさん書かれています。<a href="#f-10d58769" id="fn-10d58769" name="fn-10d58769" title="https://blog.studysapuri.jp/search?q=%E3%82%AA%E3%83%B3%E3%83%9C%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0">*4</a></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.studysapuri.jp%2Fentry%2F2020%2F08%2F03%2Fmob-programming-and-onboarding-in-coaching-team" title="リモート環境でも同じように楽しくやりたい!(後編) 〜2020年度 Coaching チームのモブプログラミング、オンボーディング事情 〜 - スタディサプリ Product Team Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.studysapuri.jp/entry/2020/08/03/mob-programming-and-onboarding-in-coaching-team">blog.studysapuri.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.studysapuri.jp%2Fentry%2F2023%2F06%2F12%2F080000" title="新しいチームメンバーとしてオンボーディング体制について語りたい - スタディサプリ Product Team Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.studysapuri.jp/entry/2023/06/12/080000">blog.studysapuri.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.studysapuri.jp%2Fentry%2F2022%2F11%2F22%2Fwhat-surprised-me" title="チームにジョインして驚いた3つのこと - スタディサプリ Product Team Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.studysapuri.jp/entry/2022/11/22/what-surprised-me">blog.studysapuri.jp</a></cite></p> <p>そんなわけなので、EM のオンボーディングがあるのは当たり前な気もしますが、EM ならではの特殊性や個別な事情もあるかと思います。いくつか挙げてみます。</p> <h2 id="任用直後にグレード設定という重要な仕事がある">任用直後にグレード設定という重要な仕事がある</h2> <p>弊社は 4月~ と 10月~ の半年単位で大きな人事変更があり、基本的<a href="#f-3cb864ac" id="fn-3cb864ac" name="fn-3cb864ac" title="何らかの事情で途中で交代することもある">*5</a>に EM 任用はこのタイミングになります。そしてこの切り替えのタイミングでメンバーの評価と次の期のグレードを決める会議が期初に行われます。</p> <p>評価とグレード設定は分離されており、EM が交代する場合は。評価は前任の責任、グレード設定は新任の責任で行われます。</p> <p>つまり、新任 EM のかたは最初の大仕事がグレード設定という重要な仕事になります。これはなかなか大変なのは想像に難くありません。<a href="#f-d971bfa4" id="fn-d971bfa4" name="fn-d971bfa4" title="もちろん自分が任用時もかなりプレッシャーを感じ、苦労した覚えがあります">*6</a></p> <h2 id="主に人事の内容は秘匿された情報が多く引き継ぎの重要性が高い">(主に人事の内容は)秘匿された情報が多く、引き継ぎの重要性が高い</h2> <p>業務情報であればオープンにナレッジシェアをすればいいですし、EM がよく行う作業のほとんどはドキュメント化されています。</p> <p>しかし、People Management という業務の特性上、秘匿すべきであり、かつ担当 Manager しか知らない情報・見えてない情報は少なくない量存在しています。</p> <p>この引き継ぎがうまくされるかどうかは新任 EM の今後の活躍に大きく影響すると考えています。</p> <h2 id="新任-EM-を迎える絶対数が相対的に少ない">新任 EM を迎える絶対数が(相対的に)少ない</h2> <p>毎期必ず任用者が現れるものではないため、オンボーディングプロセスそのものを整備しても役に立つ回数が少なく見えるかもしれませんし、あったとしても改善サイクルが回りにくい構造にあると思います。</p> <h2 id="EM-業務の失敗は大事になる可能性が高い">EM 業務の失敗は大事になる可能性が高い</h2> <p>特に組織や人事に関することは取り返しのつかないことになる可能性もあるため、手厚くサポートしてしすぎることはないのではと思います。</p> <hr /> <p>上記のような性質を踏まえ、EM のオンボーディングの重要性は高いと考え、2年ほど前からプロセスを作って少しずつ改善を繰り返しています。</p> <h1 id="オンボーディングの内容">オンボーディングの内容</h1> <h2 id="Issue-Template">Issue Template</h2> <p>みんな大好き Issue Template。PR で改善も可能ですし、チェックリストで状況も可視化できて便利ですよね。EM のオンボーディングでも利用しています。初版は <a href="https://github.com/yskttm">@yskttm</a> がシュッと作ってくれました。大感謝。</p> <p>長いので全文の紹介は割愛しますが、以下の要素が含まれています</p> <ul> <li>オンボーディングの目的、手段、ゴール</li> <li>Input: EM として知っておくべき情報がリンクされている</li> <li>実践 <ul> <li>前任 EM がやること <ul> <li>主に引き継ぎについて</li> </ul> </li> <li>メンター EM がやること <ul> <li>権限変更や Slack チャンネル招待など事務手続きのサポート</li> <li>立ち上がり時のサポート内容</li> </ul> </li> <li>新任 EM がやること <ul> <li>グレード設定会議への準備</li> <li>ミッションの設定の準備</li> </ul> </li> </ul> </li> </ul> <h2 id="メンターの存在">メンターの存在</h2> <p>通常のオンボーディングでもメンターはいると思います。しかし、EM オンボーディングの場合、引き継ぎ元である「前任 EM」というロールが存在します。一般的な話ですが、情報には非対称性があり、見えてない方は見えてないものを要求することはできません。そこで、引き継ぎが十分に行われるよう、前任・新任ではない第<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BB%B0%BC%D4">三者</a>のメンターが引き継ぎをファシリテートしてもらえるようにしています。</p> <p>また、メンターの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンはランダムというわけでなく、なるべく日頃働く領域が近い人<a href="#f-982f448e" id="fn-982f448e" name="fn-982f448e" title="考課・グレード設定会議も同じブロックで行われる">*7</a>を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンするようにしています。これにより、より日頃の業務上の悩みもより深く理解し、サポートできることを狙いとしています。</p> <h2 id="引き継ぎの内容">引き継ぎの内容</h2> <p>引き継ぎ内容も標準化されています。範囲を限定して <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%20Docs">Google Docs</a> で行われます。これによって引き継ぐ個人の差が出づらいようにしています。</p> <ul> <li>契約関連: 引き継ぎ元 EM が契約オーナー<a href="#f-e8eea90e" id="fn-e8eea90e" name="fn-e8eea90e" title="契約SaaSごとにオーナーをアサインし、費用の余日管理やアカウント管理に責任を持ってもらっている">*8</a>になっているものがあれば</li> <li>チーム運営について</li> <li>グループのマネジメントについて</li> <li>People Management <ul> <li>Name</li> <li>過去のグレード推移</li> <li>次回想定提案グレード</li> <li>強み</li> <li>課題</li> <li>次のグレードに上げるために必要なこと</li> <li>性格・気質</li> </ul> </li> </ul> <h1 id="その他の新任-EM-をサポートする仕組み">その他の新任 EM をサポートする仕組み</h1> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>全社共通のマネージャ研修も用意されています。</p> <h2 id="全社エンジニア組織独自の研修">全社/エンジニア組織独自の研修</h2> <p>EM 知識の定着のための研修<a href="#f-525aab54" id="fn-525aab54" name="fn-525aab54" title="マネージャーに求められる役割や概念の理解推進のため">*9</a>や、EM スキル習得のサポートのための様々な施策があります。具体的には、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A1%BC%A5%C1%A5%F3">コーチン</a>グを受けられる機会であったり、他領域合同のマネジメント相談会が行われています。知識やスキルの定着だけでなく、縦横以外のななめのつながりを作る機会にもなっています。</p> <h2 id="領域専任人事によるサポート">領域専任人事によるサポート</h2> <p>人事と EM がいる専用チャンネルが用意されており、誰でもなんでも聞くことができます。これは本当に助かっています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>という大きな企業でも誰に何を聞けばわからないという状況を解決してもらえていると感じます。@ai-muramatsu さん本当にいつもありがとうございます...!</p> <h2 id="その他チーム運営メンバー育成の支援">その他チーム運営・メンバー育成の支援</h2> <p>それ以外にも、各グループの状態の診断に役立てることができる組織<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%D9%A5%A4">サーベイ</a><a href="#f-b9d4a205" id="fn-b9d4a205" name="fn-b9d4a205" title="組織の状態を診断するエンゲージメントサーベイと、上長へのフィードバックを行うアップワードサーベイがある">*10</a>の実施や、メンバーの育成サポートを行う会議の存在、メンバー個人のキャリア実現を支援する WCM (Will/Can/Must)という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>があるなど、標準化された仕組みが多く用意されています。</p> <p>もちろんこれらは必ず活用しなければならない、というものではないですが、僕含め有効に使っているマネージャは多いと感じます。</p> <p>人事制度について興味がある方は以下のサイトもご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.recruit.co.jp%2Fpeople%2Fcareer%2F" title="キャリア - リクルートの人材マネジメントの仕組み|株式会社リクルート" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.recruit.co.jp/people/career/">www.recruit.co.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.recruit.co.jp%2Femployment%2Fmid-career%2Fhuman-resources%2F" title="人事制度・仕組み | 株式会社リクルート 中途採用サイト" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.recruit.co.jp/employment/mid-career/human-resources/">www.recruit.co.jp</a></cite></p> <h1 id="おわりに---新任-EM-からの声">おわりに - 新任 EM からの声</h1> <p>EM のオンボーディングプロセスについて紹介しました。</p> <p>最後に、現場の声を紹介したいと思います。</p> <p>半年前にオンボーディングを受けた新任 EM からは、以下のコメントをいただきました。(とっても嬉しいです...!)</p> <blockquote><ul> <li>オンボーディングがなければ、EMという役割の難しさをより強く感じ、ネガティブな気持ちで今期を終えていたかもしれません。</li> <li>メンバーのミッション設定やチームの戦略策定などEM固有の仕事に関して、メンターEMと意見を交換しアド<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>をいただけたことは、私のEMとしてのパフォーマンスを引き上げる重要な要素だったと思います。</li> </ul> </blockquote> <p>また、ちょうどこの4月から新任 EM をされる方々には、以下のコメントをいただきました。(こちらも聞けて安心です)</p> <blockquote><ul> <li>EM用のオンボーディングプロセスがあると聞いていたので、EMになることに対するハードルがやや軽減された</li> <li>誰かに相談したい類の仕事が一気に増えるので、メンターEMに相談できる環境は貴重でありがたい</li> <li>前担当者とメンターが分かれているので、より多くのEM視点を知るきっかけになるのでありがたい</li> </ul> </blockquote> <p>これからも EM を組織として活躍をサポートし、開発組織全体をより良くしていくことに貢献したいと思います。</p> <p>EM の活躍・支援に興味がある人は <a href="https://twitter.com/chaspy_">@chaspy</a> まで連絡ください。話しましょう!</p> <div class="footnote"> <p class="footnote"><a href="#fn-c0d0e30c" id="f-c0d0e30c" name="f-c0d0e30c" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>の正式な役割名称は <a class="keyword" href="https://d.hatena.ne.jp/keyword/GM">GM</a>: Group Manager</span></p> <p class="footnote"><a href="#fn-c1311512" id="f-c1311512" name="f-c1311512" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">Product Management / Project Management は Technical Product Manager というロールが大半を担っている。もちろん EM も関与することはある</span></p> <p class="footnote"><a href="#fn-44102355" id="f-44102355" name="f-44102355" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">だいぶ昔に <a href="https://blog.studysapuri.jp/entry/2019/03/25/sre-onboarding">オンボーディングのはじめかた - スタディサプリ Product Team Blog</a> や <a href="https://blog.studysapuri.jp/entry/2021/03/16/sre-onboarding-2020">SRE Team のオンボーディングのいま - スタディサプリ Product Team Blog</a> という記事を書いていたことを思い出しました。同じことをしていますね。</span></p> <p class="footnote"><a href="#fn-10d58769" id="f-10d58769" name="f-10d58769" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://blog.studysapuri.jp/search?q=%E3%82%AA%E3%83%B3%E3%83%9C%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0">https://blog.studysapuri.jp/search?q=%E3%82%AA%E3%83%B3%E3%83%9C%E3%83%BC%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0</a></span></p> <p class="footnote"><a href="#fn-3cb864ac" id="f-3cb864ac" name="f-3cb864ac" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">何らかの事情で途中で交代することもある</span></p> <p class="footnote"><a href="#fn-d971bfa4" id="f-d971bfa4" name="f-d971bfa4" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">もちろん自分が任用時もかなりプレッシャーを感じ、苦労した覚えがあります</span></p> <p class="footnote"><a href="#fn-982f448e" id="f-982f448e" name="f-982f448e" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text">考課・グレード設定会議も同じブロックで行われる</span></p> <p class="footnote"><a href="#fn-e8eea90e" id="f-e8eea90e" name="f-e8eea90e" class="footnote-number">*8</a><span class="footnote-delimiter">:</span><span class="footnote-text">契約<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>ごとにオーナーを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンし、費用の余日管理やアカウント管理に責任を持ってもらっている</span></p> <p class="footnote"><a href="#fn-525aab54" id="f-525aab54" name="f-525aab54" class="footnote-number">*9</a><span class="footnote-delimiter">:</span><span class="footnote-text">マネージャーに求められる役割や概念の理解推進のため</span></p> <p class="footnote"><a href="#fn-b9d4a205" id="f-b9d4a205" name="f-b9d4a205" class="footnote-number">*10</a><span class="footnote-delimiter">:</span><span class="footnote-text">組織の状態を診断するエンゲージメント<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%D9%A5%A4">サーベイ</a>と、上長へのフィードバックを行うアップワード<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%D9%A5%A4">サーベイ</a>がある</span></p> </div> quipper-ja スタディサプリ小学・中学講座にRoborazziを導入して半年が経過しました hatenablog://entry/6801883189090966981 2024-03-26T09:00:00+09:00 2024-03-26T09:00:00+09:00 こんにちは、Androidエンジニアの@morux2です。 スタディサプリ小学・中学講座では、Visual Regression Test (以下 VRT)を実施しています。VRTは画像比較によるUIの回帰テストです。変更前後のコードそれぞれに対する画面のスクリーンショットを比較し、意図しない差分を検知することができます。*1 今回はスクリーンショットの撮影にRoborazziを導入して半年が経過したので、現状の運用やTipsを共有できればと思います。なお、執筆当時のRoborazziのバージョンは1.9.0です。 VRTの運用 構成 実行タイミング 撮影内容 実行時間 RoborazziのT… <p>こんにちは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>エンジニアの@morux2です。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小学・中学講座では、Visual Regression Test (以下 VRT)を実施しています。VRTは画像比較によるUIの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%F3%B5%A2%A5%C6%A5%B9%A5%C8">回帰テスト</a>です。変更前後のコードそれぞれに対する画面の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を比較し、意図しない差分を検知することができます。<a href="#f-0e7f3b77" id="fn-0e7f3b77" name="fn-0e7f3b77" title="https://blog.studysapuri.jp/entry/2021-08-23/android-vrt-tips-1">*1</a></p> <p>今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>の撮影に<a href="https://github.com/takahirom/roborazzi">Roborazzi</a>を導入して半年が経過したので、現状の運用やTipsを共有できればと思います。なお、執筆当時のRoborazziのバージョンは<a href="https://github.com/takahirom/roborazzi/releases/tag/1.9.0">1.9.0</a>です。</p> <ul class="table-of-contents"> <li><a href="#VRTの運用">VRTの運用</a><ul> <li><a href="#構成">構成</a></li> <li><a href="#実行タイミング">実行タイミング</a></li> <li><a href="#撮影内容">撮影内容</a></li> <li><a href="#実行時間">実行時間</a></li> </ul> </li> <li><a href="#RoborazziのTips集">RoborazziのTips集</a><ul> <li><a href="#RoborazziとUnitTestを別々に実行する">RoborazziとUnitTestを別々に実行する</a></li> <li><a href="#複数デバイスで撮影を行う">複数デバイスで撮影を行う</a></li> <li><a href="#Lottieが含まれるテストを安定させる">Lottieが含まれるテストを安定させる</a></li> <li><a href="#ダイアログを撮影する">ダイアログを撮影する</a></li> <li><a href="#フォントスケールを変更する">フォントスケールを変更する</a></li> </ul> </li> <li><a href="#さいごに">さいごに</a></li> </ul> <h3 id="VRTの運用">VRTの運用</h3> <p><a href="https://blog.studysapuri.jp/entry/2023/10/05/introduce-roborazzi">Roborazzi導入時のブログ</a>や<a href="https://speakerdeck.com/morux2/fu-shu-duan-mo-de-visual-regression-test-wo-shi-xing-surushang-denogong-fu-toke-ti">登壇資料</a>も併せてご覧ください。</p> <p><iframe id="talk_frame_1104724" class="speakerdeck-iframe" src="//speakerdeck.com/player/e97989b13017485aac18110681662f91" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/morux2/fu-shu-duan-mo-de-visual-regression-test-wo-shi-xing-surushang-denogong-fu-toke-ti">speakerdeck.com</a></cite></p> <h5 id="構成">構成</h5> <p>VRTには大きく3つのステップがあります。半年前に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>の撮影を<a href="https://firebase.google.com/docs/test-lab?hl=ja">Firebase Test Lab</a>からRoborazziに移行しました。これによって撮影環境が実デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>から<a class="keyword" href="https://d.hatena.ne.jp/keyword/JVM">JVM</a>に変わりました。</p> <ul> <li>画面の用意 (<a href="https://github.com/airbnb/Showkase">Showkase</a>) <ul> <li>@<a class="keyword" href="https://d.hatena.ne.jp/keyword/Preview">Preview</a>のついたComposableをリスト形式で一括で取得</li> </ul> </li> <li>撮影 (Roborazzi) <ul> <li>ComposableのリストをParameterizedテストに渡してcaptureRoboImage()メソッドで撮影</li> </ul> </li> <li>比較 (<a href="https://github.com/reg-viz/reg-suit">reg-suit</a>) <ul> <li>変更前後の画像を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ストレージで保存し、比較した結果(差分)をPRにコメント・Slackに通知</li> </ul> </li> </ul> <p><figure class="figure-image figure-image-fotolife" title="生成された差分レポート"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20240318/20240318143435.png" width="1200" height="532" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>生成された差分レポート</figcaption></figure> <figure class="figure-image figure-image-fotolife" title="Slack・PR上での通知"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20240318/20240318143454.png" width="1200" height="632" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Slack・PR上での通知</figcaption></figure></p> <h5 id="実行タイミング">実行タイミング</h5> <p>masterブランチへのコミットとPRに対して実行しています。</p> <ul> <li>masterブランチ</li> </ul> <p>全てのmergeコミットに対してVRTを実行することで、 UIの差分の原因となるコミットを確実に特定できるようにしています。比較対象は直前のmergeコミットです。</p> <ul> <li>PR</li> </ul> <p> Run VRT ラベルを付与した場合に実行します。PR作成者の判断で、UIに関係のないモジュールやCI等の変更の際は実行しなくても良いことにしています。比較対象はmasterの最新のコミットです。 <figure class="figure-image figure-image-fotolife" title="Run VRTラベルをPRに付与"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20240315/20240315163724.png" width="1200" height="550" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Run VRTラベルをPRに付与</figcaption></figure></p> <h5 id="撮影内容">撮影内容</h5> <ul> <li>UI<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>は単一のデ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>、スクリーンは複数デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>で撮影をしています。</li> <li>小学講座は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>専用アプリなので、8インチ(小さめ)と10インチ(推奨サイズ)で撮影をしています。一方中学講座は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A1%BC%A5%C8%A5%D5%A5%A9%A5%F3">スマートフォン</a>と<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>で撮影をしています。</li> <li>撮影枚数は小学・中学講座合わせて650枚ほどになります。そのうち約600枚がRoborazziを用いて<a class="keyword" href="https://d.hatena.ne.jp/keyword/JVM">JVM</a>上で撮影をしています。</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="小学講座のVRT"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20240318/20240318143915.png" width="1200" height="668" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>小学講座のVRT</figcaption></figure> <figure class="figure-image figure-image-fotolife" title="中学講座のVRT"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20240318/20240318143931.png" width="1200" height="818" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>中学講座のVRT</figcaption></figure></p> <h5 id="実行時間">実行時間</h5> <p>VRTの実行時間は30分程度になります。CI環境は<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsです。内訳は以下の通りです。</p> <ul> <li>assemble-for-<a class="keyword" href="https://d.hatena.ne.jp/keyword/android">android</a>-test : 7分</li> </ul> <p>Roborazziに移行できていない撮影をFirebase Test Labで実行するために、テストアプリをビルドするステップです。<code>./gradlew assembleDebug</code>と<code>./gradlew assembleDebugAndroidTest</code>を実行し、<a href="https://github.com/actions/upload-artifact">upload-artifact</a>アクションでapkファイルをアップロードします。</p> <ul> <li>run-roborazzi : 8分</li> </ul> <p><code>./gradlew recordRoborazziDebug</code>で約600枚の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を撮影し、upload-artifactアクションで撮影した画像をアップロードします。</p> <ul> <li>run-fastlane : 5分</li> </ul> <p>Roborazziに移行できていない約50枚の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を実デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>で撮影します。<a href="https://github.com/cats-oss/fastlane-plugin-firebase_test_lab_android">Firebase Test Lab plugin for fastlane</a>を用いています。</p> <ul> <li>run-reg-suit : 15秒</li> </ul> <p>reg-suitを実行し、RoborazziとFirebase Test Labで撮影した画像に対してVRTのレポートを作成します。</p> <p><figure class="figure-image figure-image-fotolife" title="VRTの実行時間"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20240318/20240318155740.png" width="832" height="340" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>VRTの実行時間</figcaption></figure></p> <p>Roborazziを導入するまでは全ての撮影をFirebase Test Labで行っており、半数の約300枚の撮影に4,50分かかっていました。撮影枚数やCI環境が変わっているため厳密な比較はできませんが、実デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>を用いないことによる実行時間の短縮が実感できています。</p> <h3 id="RoborazziのTips集">RoborazziのTips集</h3> <p>ここからはTipsを紹介します。</p> <h5 id="RoborazziとUnitTestを別々に実行する">RoborazziとUnitTestを別々に実行する</h5> <p>参考 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftakahirom%2Froborazzi%2Fissues%2F36%23issuecomment-1517364855" title="Provide an effective method for filtering between Roborazzi tests and non-Roborazzi tests · Issue #36 · takahirom/roborazzi" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/takahirom/roborazzi/issues/36#issuecomment-1517364855">github.com</a></cite></p> <p>Roborazziの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を適用しているモジュールに、VRT以外のUnitTestが存在する場合、<code>./gradlew recordRoborazziDebug</code>を実行すると両者が走ってしまいます。そこで、gradleプロパティを独自に定義しました。<code>./gradlew recordRoborazziDebug -PexcludeNonRoborazziTests</code> と呼び出すと、VRTのみを実行することができます。</p> <p><details><summary>実装イメージ</summary></p> <pre class="code" data-lang="" data-unlink> testOptions { unitTests { all { filter { if (project.hasProperty(&#34;excludeNonRoborazziTests&#34;)) includeTestsMatching &#34;jp.studysapuri.tara.launch.vrt.*&#34; } } } }</pre> <p></details></p> <h5 id="複数デバイスで撮影を行う">複数デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>で撮影を行う</h5> <p>参考 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fandroid%2Fnowinandroid%2Fpull%2F876" title="Adds Screenshot testing with Roborazzi by JoseAlcerreca · Pull Request #876 · android/nowinandroid" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/android/nowinandroid/pull/876">github.com</a></cite></p> <p>複数デ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>で撮影を行う場合は独自の拡張関数を生やすと便利です。呼び出し元では、Roborazziに用意されている<a href="https://github.com/takahirom/roborazzi/issues/47">デバイスのConfig</a>を用いることができます。</p> <p><details><summary>実装イメージ</summary></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synType">fun</span> &lt;A : ComponentActivity&gt; AndroidComposeTestRule&lt;ActivityScenarioRule&lt;A&gt;, A&gt;.captureMultiDevice( screenshotName: <span class="synType">String</span>, devices: <span class="synType">List</span>&lt;<span class="synType">Pair</span>&lt;<span class="synType">String</span>, <span class="synType">String</span>&gt;&gt;, body: <span class="synIdentifier">@Composable</span> () <span class="synType">-&gt;</span> <span class="synType">Unit</span>, roborazziOptions: RoborazziOptions = DefaultRoborazziOptions, ) { devices.forEach { device <span class="synType">-&gt;</span> captureComponent( screenshotName = screenshotName, device = device, body = body, roborazziOptions = roborazziOptions, ) } } <span class="synType">private</span> <span class="synType">fun</span> &lt;A : ComponentActivity&gt; AndroidComposeTestRule&lt;ActivityScenarioRule&lt;A&gt;, A&gt;.captureComponent( screenshotName: <span class="synType">String</span>, device: <span class="synType">Pair</span>&lt;<span class="synType">String</span>, <span class="synType">String</span>&gt;, body: <span class="synIdentifier">@Composable</span> () <span class="synType">-&gt;</span> <span class="synType">Unit</span>, roborazziOptions: RoborazziOptions = DefaultRoborazziOptions, ) { RuntimeEnvironment.setQualifiers(device.second) <span class="synStatement">this</span>.activity.setContent { CompositionLocalProvider( LocalInspectionMode provides <span class="synConstant">true</span>, ) { body() } } <span class="synType">val</span> filePath = <span class="synConstant">&quot;</span><span class="synIdentifier">$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH</span><span class="synConstant">/</span><span class="synIdentifier">$screenshotName</span><span class="synConstant">-</span><span class="synIdentifier">${</span>device.first<span class="synIdentifier">}</span><span class="synConstant">.png&quot;</span> <span class="synStatement">this</span>.onRoot().captureRoboImage( filePath = filePath, roborazziOptions = roborazziOptions, ) } </pre> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@RunWith</span>(AndroidJUnit4<span class="synStatement">::</span><span class="synType">class</span>) <span class="synIdentifier">@GraphicsMode</span>(GraphicsMode.Mode.NATIVE) <span class="synIdentifier">@Config</span>(application = TestApplication<span class="synStatement">::</span><span class="synType">class</span>) <span class="synType">class</span> HomeFragmentTest { <span class="synIdentifier">@get:Rule</span> <span class="synType">val</span> composeTestRule = createAndroidComposeRule&lt;DummyActivityForRoborazzi&gt;() <span class="synIdentifier">@get:Rule</span> <span class="synType">val</span> testName = TestName() <span class="synIdentifier">@Test</span> <span class="synType">fun</span> takeHomeScreen() { composeTestRule.captureMultiDevice( screenshotName = testName.methodName, body = { HomeContent() }, devices = listOf( <span class="synConstant">&quot;phone&quot;</span> to RobolectricDeviceQualifiers.Pixel5, <span class="synConstant">&quot;tablet&quot;</span> to RobolectricDeviceQualifiers.MediumTablet, ) ) } } </pre> <p></details></p> <h5 id="Lottieが含まれるテストを安定させる">Lottieが含まれるテストを安定させる</h5> <p>参考 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2FDroidKaigi%2Fconference-app-2023%2Fpull%2F1265" title="Add achivement click animation #1235 by Aniokrait · Pull Request #1265 · DroidKaigi/conference-app-2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/DroidKaigi/conference-app-2023/pull/1265">github.com</a></cite></p> <p><a href="https://github.com/airbnb/lottie-android">Lottie</a>でアニメーションしている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の撮影がflakyになってしまう問題がありました。Lottieがバックグラウンドスレッドで実行されるのを防ぐことで、VRTを安定させています。</p> <p><details><summary>実装イメージ</summary></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink> <span class="synIdentifier">@Before</span> <span class="synType">fun</span> setup() { LottieTask.EXECUTOR = Executor(Runnable<span class="synStatement">::</span>run) } <span class="synIdentifier">@After</span> <span class="synType">fun</span> finished() { <span class="synComment">// 実行後はデフォルトの設定に戻す</span> LottieTask.EXECUTOR = Executors.newCachedThreadPool() } </pre> <p></details></p> <h5 id="ダイアログを撮影する">ダイアログを撮影する</h5> <p>参考 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftakahirom%2Froborazzi%2Fpull%2F225" title="Add captureScreenRoboImage for capturing screen images, including dialogs by takahirom · Pull Request #225 · takahirom/roborazzi" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/takahirom/roborazzi/pull/225">github.com</a></cite></p> <p>Roborazzi 1.9.0でスクリーンに描画されたダイアログを撮影できる<code>captureScreenRoboImage()</code>が追加されました。Experimentalなのでダイアログの撮影にのみ用いています。</p> <p><details><summary>実装イメージ</summary></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synType">private</span> <span class="synType">fun</span> &lt;A : ComponentActivity&gt; AndroidComposeTestRule&lt;ActivityScenarioRule&lt;A&gt;, A&gt;.captureComponent( screenshotName: <span class="synType">String</span>, device: <span class="synType">Pair</span>&lt;<span class="synType">String</span>, <span class="synType">String</span>&gt;, body: <span class="synIdentifier">@Composable</span> () <span class="synType">-&gt;</span> <span class="synType">Unit</span>, roborazziOptions: RoborazziOptions = DefaultRoborazziOptions, ) { RuntimeEnvironment.setQualifiers(device.second) <span class="synStatement">this</span>.activity.setContent { CompositionLocalProvider( LocalInspectionMode provides <span class="synConstant">true</span>, ) { body() } } <span class="synType">val</span> filePath = <span class="synConstant">&quot;</span><span class="synIdentifier">$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH</span><span class="synConstant">/</span><span class="synIdentifier">$screenshotName</span><span class="synConstant">-</span><span class="synIdentifier">${</span>device.first<span class="synIdentifier">}</span><span class="synConstant">.png&quot;</span> <span class="synStatement">if</span> (screenshotName.contains(<span class="synConstant">&quot;Dialog&quot;</span>)) { <span class="synIdentifier">@OptIn</span>(ExperimentalRoborazziApi<span class="synStatement">::</span><span class="synType">class</span>) captureScreenRoboImage( filePath = filePath, roborazziOptions = roborazziOptions, ) } <span class="synStatement">else</span> { <span class="synStatement">this</span>.onRoot().captureRoboImage( filePath = filePath, roborazziOptions = roborazziOptions, ) } } </pre> <p></details></p> <h5 id="フォントスケールを変更する">フォントスケールを変更する</h5> <p>参考 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ftakahirom%2Froborazzi%2Fissues%2F127" title="A way to resize a font scale? · Issue #127 · takahirom/roborazzi" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/takahirom/roborazzi/issues/127">github.com</a></cite></p> <p><a href="https://github.com/robolectric/robolectric">Robolectric</a>では簡単にフォントスケールを変更できます。これによって、フォントサイズを大きくした際のアプリの描画崩れを確認し、改善箇所に優先度をつけることができます。</p> <p><figure class="figure-image figure-image-fotolife" title="スケールを2fに設定して撮影"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20240318/20240318202841.png" width="898" height="535" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>スケールを2fに設定して撮影</figcaption></figure></p> <p><details><summary>実装イメージ</summary></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synType">private</span> <span class="synType">fun</span> &lt;A : ComponentActivity&gt; AndroidComposeTestRule&lt;ActivityScenarioRule&lt;A&gt;, A&gt;.captureComponent( screenshotName: <span class="synType">String</span>, device: <span class="synType">Pair</span>&lt;<span class="synType">String</span>, <span class="synType">String</span>&gt;, body: <span class="synIdentifier">@Composable</span> () <span class="synType">-&gt;</span> <span class="synType">Unit</span>, roborazziOptions: RoborazziOptions = DefaultRoborazziOptions, ) { RuntimeEnvironment.setFontScale(<span class="synConstant">2.0f</span>) RuntimeEnvironment.setQualifiers(device.second) <span class="synStatement">this</span>.activity.setContent { CompositionLocalProvider( LocalInspectionMode provides <span class="synConstant">true</span>, ) { body() } } <span class="synType">val</span> filePath = <span class="synConstant">&quot;</span><span class="synIdentifier">$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH</span><span class="synConstant">/</span><span class="synIdentifier">$screenshotName</span><span class="synConstant">-</span><span class="synIdentifier">${</span>device.first<span class="synIdentifier">}</span><span class="synConstant">.png&quot;</span> <span class="synStatement">this</span>.onRoot().captureRoboImage( filePath = filePath, roborazziOptions = roborazziOptions, ) } </pre> <p></details></p> <h3 id="さいごに">さいごに</h3> <p>Roborazziを採用したことで、以前より短いテスト時間で意図しないUIの差分を検知し、端末のサイズやフォントスケールによる描画崩れも確認することができています。最近だとfeatureモジュールの細分化や、Compose 1.6.0対応 (includeFontPaddingが無効になる <a href="#f-b4e7e7d3" id="fn-b4e7e7d3" name="fn-b4e7e7d3" title="https://android-developers.googleblog.com/2024/01/whats-new-in-jetpack-compose-january-24-release.html">*2</a> )に、VRTが大いに役立ちました。この記事がVRT導入や知見共有のきっかけとなれば嬉しいです😊</p> <div class="footnote"> <p class="footnote"><a href="#fn-0e7f3b77" id="f-0e7f3b77" name="f-0e7f3b77" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://blog.studysapuri.jp/entry/2021-08-23/android-vrt-tips-1">https://blog.studysapuri.jp/entry/2021-08-23/android-vrt-tips-1</a></span></p> <p class="footnote"><a href="#fn-b4e7e7d3" id="f-b4e7e7d3" name="f-b4e7e7d3" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://android-developers.googleblog.com/2024/01/whats-new-in-jetpack-compose-january-24-release.html">https://android-developers.googleblog.com/2024/01/whats-new-in-jetpack-compose-january-24-release.html</a></span></p> </div> morux2 技術戦略策定のための Fact 収集術 hatenablog://entry/6801883189084418130 2024-03-19T08:00:00+09:00 2024-03-19T10:36:58+09:00 こんにちは。@chaspy です。プロダクト開発部の技術戦略グループのマネージャをしています。 技術戦略グループでは、日頃開発する上での課題の投げ込みや議論、解決するための計画をボトムアップで行っています。技術戦略グループの活動については過去のアウトプットもご覧ください。 blog.studysapuri.jp また、本稿のテーマである、組織やシステムの状況を把握するための Fact 収集については技術戦略 DevOps WG が担当しています。以前発表した資料もご覧ください。 このように、技術戦略グループではエンジニア1人1人が課題だと思うことを表明、宣言し、その課題をトリアージすること、お… <p>こんにちは。<a href="https://github.com/chaspy">@chaspy</a> です。プロダクト開発部の技術戦略グループのマネージャをしています。</p> <p>技術戦略グループでは、日頃開発する上での課題の投げ込みや議論、解決するための計画を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%E0%A5%A2%A5%C3%A5%D7">ボトムアップ</a>で行っています。技術戦略グループの活動については過去のアウトプットもご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.studysapuri.jp%2Fentry%2F2023%2F12%2F22%2F080000" title="スタディサプリ小中高の技術戦略について - スタディサプリ Product Team Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.studysapuri.jp/entry/2023/12/22/080000">blog.studysapuri.jp</a></cite></p> <p>また、本稿のテーマである、組織やシステムの状況を把握するための Fact 収集については技術戦略 DevOps WG が担当しています。以前発表した資料もご覧ください。</p> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/cbede6619ca44c818f9e58ff1656d918" title="自己診断能力の獲得を目指して / Toward the acquisition of self-diagnostic skills" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <p>このように、技術戦略グループではエンジニア1人1人が課題だと思うことを表明、宣言し、その課題を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%EA%A5%A2%A1%BC%A5%B8">トリアージ</a>すること、および課題を評価するための Fact の発見・提供を行う仕組みが組織として<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%E0%A5%A2%A5%C3%A5%D7">ボトムアップ</a>で行える状態になっています。一方、開発部長として、事業戦略と結びつける形で技術戦略を策定する際には、現場のエンジニアが直面している課題ベースではなく、俯瞰的に状況を判断できるメトリクスが必要です。</p> <p>この記事では、開発組織の状況をなるべく Fact に基づいて判断するための切り口とその方法を紹介します。開発組織のマネジメントに携わる EM, VPoE, CTO のような方々の参考になれば幸いです。</p> <h1 id="収集する-Fact">収集する Fact</h1> <p>以下を収集し、評価を行いました。</p> <ul> <li>システムの構成要素について <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>比率</li> <li>マイクロサービスごとの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>と LOC</li> <li>EOL を迎えたソフトウェア数</li> </ul> </li> <li>システムのパフォーマンスについて <ul> <li>リソース効率性</li> </ul> </li> <li>開発組織について <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>・領域別技術習熟度</li> <li>マイクロサービス別開発活発度</li> </ul> </li> </ul> <h2 id="システムの構成要素について">システムの構成要素について</h2> <h3 id="プログラミング言語比率"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>比率</h3> <p>我々が提供するプロダクトがどの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>で構成されているのか、その比率を把握しました。私たちは monorepo を採用しているため、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> で簡単に把握できます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240316/20240316113443.png" width="415" height="450" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この結果から言えることは以下です</p> <ul> <li>Backend は <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a> が圧倒的に比率が高い</li> <li>次点として Go</li> <li>SRE が管理している system-components は Go が支配的</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/CoffeeScript">CoffeeScript</a> が一定割合存在している</li> </ul> <p>この結果を受けて、以下のアクションを取りました。</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/CoffeeScript">CoffeeScript</a> については今後書かない方針の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>策定<a href="#f-265718fa" id="fn-265718fa" name="fn-265718fa" title="現在フロントエンドは React / TypeScript で大部分が書かれているため">*1</a><a href="#f-c6892139" id="fn-c6892139" name="fn-c6892139" title="ガイドラインの策定についてはこちらの記事も参照ください">*2</a></li> </ul> <h3 id="マイクロサービスごとのプログラミング言語と-LOC">マイクロサービスごとの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>と LOC</h3> <p>前項では monorepo に含まれるコードベース全体の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>比率を出しました。別の角度で分析するため、マイクロサービスの単位でも同様の分析を行いました。</p> <p>取得方法は簡単なツールを書きました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fchaspy%2Fgh-monorepo-stats" title="GitHub - chaspy/gh-monorepo-stats: gh extension to output language-specific statistics for services in monorepo" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/chaspy/gh-monorepo-stats">github.com</a></cite></p> <p>gh extension としてインストール、実行できます。monorepo 上での実行を前提としており、各<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リごとに言語をパッケージマネージャベースで判定し、LOC を出力しています。<a href="#f-a48c0bf4" id="fn-a48c0bf4" name="fn-a48c0bf4" title="この特性上、マイクロサービスごとに言語は1つであるという前提があります。例えばフロントエンドとバックエンドが同じディレクトリ上にある場合は正しい計測結果になりません">*3</a></p> <p>結果は以下の通りです。<a href="#f-ac388034" id="fn-ac388034" name="fn-ac388034" title="前述している system-components は対象外です">*4</a></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240316/20240316114013.png" width="1200" height="386" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>継続的に取得する上で、Spreadsheet だと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リが増えた場合面倒になったので、Datadog に送ることにしました。元々 GHA から Datadog に送る @int128 が作成した <a href="https://github.com/int128/send-datadog-action">send-datadog-action</a> を活用していました。今回のようにサービス単位で一気に送りたいので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/csv">csv</a> をインプットにしてもらうよう要望したら<a href="https://github.com/int128/send-datadog-action/pull/247">すぐに対応してもらえました。</a>最高です。</p> <p>こんな感じで送っています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> metrics / send-loc-to-datadog <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">schedule</span><span class="synSpecial">:</span> <span class="synComment"> # 3:00 UTC (= 12:00 JST) Everyday</span> <span class="synStatement">- </span><span class="synIdentifier">cron</span><span class="synSpecial">:</span> <span class="synConstant">0</span> <span class="synConstant">3</span> * * * <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">send-loc-to-datadog</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">timeout-minutes</span><span class="synSpecial">:</span> <span class="synConstant">10</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> gh extension install chaspy/gh-monorepo-stats <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">GH_TOKEN</span><span class="synSpecial">:</span> ${{ github.token }} <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> | echo <span class="synConstant">&quot;REPOSITORY_NAME=${GITHUB_REPOSITORY#&quot;</span>$GITHUB_REPOSITORY_OWNER&quot;/}&quot; &gt;&gt; <span class="synConstant">&quot;${GITHUB_ENV}&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> | gh monorepo-stats &gt; result while IFS=, read -r service_name _ lang loc; do service_name=$(echo <span class="synConstant">&quot;${service_name}&quot;</span> | xargs) lang=$(echo <span class="synConstant">&quot;${lang}&quot;</span> | xargs) loc=$(echo <span class="synConstant">&quot;${loc}&quot;</span> | xargs) if <span class="synSpecial">[</span> -n <span class="synConstant">&quot;$loc&quot;</span> <span class="synSpecial">]</span>; then echo <span class="synConstant">&quot;custom.monorepo.loc,GAUGE,$loc,repository:${REPOSITORY_NAME}, language:$lang, service:$service_name&quot;</span> &gt;&gt; metrics.csv fi done &lt; result <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">IGNORE_DIRS</span><span class="synSpecial">:</span> <span class="synConstant">&quot;(省略)&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> int128/send-datadog-action@v0.20.0 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">datadog-api-key</span><span class="synSpecial">:</span> ${{ secrets.DATADOG_API_KEY }} <span class="synIdentifier">metrics-csv-path</span><span class="synSpecial">:</span> | metrics.csv </pre> <p>Datadog だとこんな感じです。いい感じですね。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240316/20240316120607.png" width="1200" height="959" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ここでも <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a> が半数であることがわかります。また、このように LOC を出してみることで、感覚で語られている「あのサービスは大きい」「肥大化し続けている」という意見に対して、客観的に判断することができます。</p> <p>この結果からの直接的なアクションは現状ありませんが、4半期ごとに取得することで、変化を見ていこうと考えています。</p> <h3 id="EOL-を迎えたソフトウェア数">EOL を迎えたソフトウェア数</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリは多数の <a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a> / ライブラリに支えられています。そしてライブラリのアップデートは定期的に行わないと、将来の改修コストが高くなってしまうリスクがあります。また、セキュリティアップデートに追随するためにも重要です。</p> <p>これを直接的に取得するのは難しいので、先行指標として「放置されている Dependabot / Renovate PR 数」としました。</p> <p>取得方法として、簡単なツールを書きました。一言でいうと monorepo を前提とした gh コマンドのラッパーです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fchaspy%2Fgh-monorepo-pr-count" title="GitHub - chaspy/gh-monorepo-pr-count: gh extension to count the number of PRs with the same label as the directory name" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/chaspy/gh-monorepo-pr-count">github.com</a></cite></p> <p>わかりづらい重要な前提があるのですが、弊社の monorepo では PR が作成された際に、変更があった<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ(サービス)を検知し、同名の Label を Danger で付与する仕組みがあります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240317/20240317142313.png" width="1200" height="553" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この仕組みを利用して、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> 検索で Label で絞り込むことで PR をサービスごとに数えることができます。このツールでは <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> の検索オプションも指定できるため、author:dependabot と絞り込むことで Dependabot による PR 数を数えることができます。</p> <p>そしてこれも前述したものと同じ仕組みで Datadog に送っています。こんな感じです。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> metrics / send-dependencies-pr-count-to-datadog <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">schedule</span><span class="synSpecial">:</span> <span class="synComment"> # 3:00 UTC (= 12:00 JST) Everyday</span> <span class="synStatement">- </span><span class="synIdentifier">cron</span><span class="synSpecial">:</span> <span class="synConstant">0</span> <span class="synConstant">3</span> * * * <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">send-dependencies-pr-count-to-datadog</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">timeout-minutes</span><span class="synSpecial">:</span> <span class="synConstant">10</span> <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">GH_TOKEN</span><span class="synSpecial">:</span> ${{ github.token }} <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> gh extension install chaspy/gh-monorepo-pr-count <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> | echo <span class="synConstant">&quot;REPOSITORY_NAME=${GITHUB_REPOSITORY#&quot;</span>$GITHUB_REPOSITORY_OWNER&quot;/}&quot; &gt;&gt; <span class="synConstant">&quot;${GITHUB_ENV}&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Get dependabot PRs <span class="synIdentifier">run</span><span class="synSpecial">:</span> | bash metrics/generate_dependencies_pr_count_csv.sh dependabot &gt;&gt; metrics.csv <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">SEARCH_QUERY</span><span class="synSpecial">:</span> <span class="synConstant">&quot;author:app/dependabot&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Get renovate PRs <span class="synIdentifier">run</span><span class="synSpecial">:</span> | bash metrics/generate_dependencies_pr_count_csv.sh renovate &gt;&gt; metrics.csv <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">SEARCH_QUERY</span><span class="synSpecial">:</span> <span class="synConstant">&quot;author:app/renovate&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> int128/send-datadog-action@v0.20.0 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">datadog-api-key</span><span class="synSpecial">:</span> ${{ secrets.DATADOG_API_KEY }} <span class="synIdentifier">metrics-csv-path</span><span class="synSpecial">:</span> | metrics.csv </pre> <ul> <li>metrics/generate_dependencies_pr_count_<a class="keyword" href="https://d.hatena.ne.jp/keyword/csv">csv</a>.sh</li> </ul> <pre class="code bash" data-lang="bash" data-unlink>#!/bin/bash AUTHOR=$1 gh monorepo-pr-count --state open --since 2017-09-21 &gt; &#34;${AUTHOR}_result&#34; while IFS=, read -r service_name pr_count; do service_name=$(echo &#34;${service_name}&#34; | xargs) pr_count=$(echo &#34;${pr_count}&#34; | xargs) if [ -n &#34;$pr_count&#34; ]; then echo &#34;custom.monorepo-pr-count,GAUGE,$pr_count,repository:${REPOSITORY_NAME},service:$service_name,author:app/${AUTHOR}&#34; fi done &lt; ${AUTHOR}_result</pre> <p>Datadog 上ではこんな感じで見れます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240316/20240316120045.png" width="1200" height="818" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <a href="#f-35e2097e" id="fn-35e2097e" name="fn-35e2097e" title=".github 以下は GitHub Actions の更新である。これは Workflow ごとに CODEOWNER を付与することで気づいてもらえるようにしている。">*5</a></p> <p>普段から定期的にライブラリアップデートをしているおかげか、大量に溜まってないことがわかります。</p> <p>また、更新がされないのではなく、メンテナンスが止まっていることもリスクとなるため、<a href="https://github.com/kyoshidajp/dep-doctor">dep-doctor</a> を利用し、発見したら通知する仕組みを構築予定です。</p> <h2 id="システムのパフォーマンスについて">システムのパフォーマンスについて</h2> <h3 id="リソース効率性">リソース効率性</h3> <p>リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トしている Compute Resource に対して、どれぐらいパフォーマンスを発揮しているかを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%CE%CC">定量</a>的・継続的に把握したいと考えました。これは Datadog で出しています。</p> <p>http request hits / CPU or Memory usage で計算しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240316/20240316121527.png" width="1200" height="746" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>大切なことは Disclaimer に書いてあります。大事。</p> <p>左側のグラフは対数グラフになっています。上に行けば行くほど大量のリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを捌いており、右に行けば行くほど大量のリソースを利用しているということになります。繰り返しますが左上のサービスがえらいというわけではなく、各サービス単位での推移を見ていくことが重要だと考えています。</p> <h2 id="開発組織について">開発組織について</h2> <h3 id="プログラミング言語別技術習熟度"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>別技術習熟度</h3> <p>先ほど利用しているシステムの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>の割合を出しました。利用されるコードベースが多い<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>に対して、習熟度が一定レベル以上の人が多く存在することが期待されます。逆に、いくらコードベースの割合が少なくても、採用言語を使える技術者が1人もいなくなってしまうと事業継続上のリスクになってしまうでしょう。</p> <p>簡易的に凡例を作成し、Engineering Manager に入力してもらいました。</p> <ul> <li>3 <ul> <li>社内の他の人への教育ができる</li> <li>詳細な仕様を理解しており、未知のリスクに対処できる</li> <li>最新機能をキャッチアップし、それを自組織に活用を推進できる</li> </ul> </li> <li>2 1 と 3 の間</li> <li>1 調べながら PR を出すことができる</li> </ul> <p>結果や考察については割愛しますが、各<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>別のエンジニアの状況分析に役立ちました。</p> <h3 id="マイクロサービスごとの開発活発度">マイクロサービスごとの開発活発度</h3> <p>システムを分析する上で、どのマイクロサービスが活発に変更が行われているのか、あるいはどれぐらいの人数が関わっているのかを知ることも組織の状況を示すメトリクスになります。</p> <p>これも Renovate / Dependabot の PR 数を計測するツール<a href="#f-34969d77" id="fn-34969d77" name="fn-34969d77" title="前述したchaspy/monorepo-pr-count">*6</a>で計測しました。<a href="#f-c0bdabc3" id="fn-c0bdabc3" name="fn-c0bdabc3" title="月ごとに特定の Label がついた PR を計測するしています。例えば gh monorepo-pr-count --since 2023-11-01 --until 2023-11-30 というコマンドを実行すれば各サービスごとの2023年11月の PR 数が得られます。">*7</a></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240317/20240317141126.png" width="1200" height="410" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Conditional Format を使ってヒートマップとしました。どのサービスが恒常的に多いのか、あるいは時期的に多いのかを見ることができます。これも四半期ごとに定期的に取得しようと考えています。また、マージされた PR 数は Four Keys のうちの1つのデプロイ頻度や変更のリードタイムの先行指標になる Metrics だと考えます。この観点でも推移を追うと発見があるかもしれませんね。</p> <p>また、1つのサービスを多くの人が触っている場合、コミュニケーションのオーバーヘッドが多くかかっている可能性があります。Unique Author も出せるようにしています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240317/20240317141433.png" width="1200" height="461" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>月に20人が触っているサービスは多いと言える気もしますし、内部で Module がちゃんと分かれており、それごとにオーナーシップが決まっており、かつ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>になっているのであれば問題ないかもしれません。これも絶対的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%EF%E7%C3%CD">閾値</a>を見るというより、推移を見ていき、異常の予兆に気づけるようにしたいと思います。</p> <h1 id="おわりに">おわりに</h1> <p>様々な角度からシステムや開発組織の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%CE%CC">定量</a>化を試みました。これにより、感覚的な判断から客観的な判断に変えることができ、また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%CE%CC">定量</a>化することで変化を見ることができるようになりました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%E0%A5%A2%A5%C3%A5%D7">ボトムアップ</a>での改善活動に加えて、俯瞰的に状況を把握できることで早期に問題の種に気づけるようにしたり、中長期の技術戦略を考えるためのインプットを得られました。</p> <p>技術戦略について興味がある方は<a href="https://twitter.com/chaspy_">@chaspy_</a>までご連絡ください。話しましょう!</p> <div class="footnote"> <p class="footnote"><a href="#fn-265718fa" id="f-265718fa" name="f-265718fa" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">現在フロントエンドは React / TypeScript で大部分が書かれているため</span></p> <p class="footnote"><a href="#fn-c6892139" id="f-c6892139" name="f-c6892139" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>の策定については<a href="https://blog.studysapuri.jp/entry/2023/12/22/080000#%E3%82%AC%E3%82%A4%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3%E3%81%AE%E7%AD%96%E5%AE%9A">こちら</a>の記事も参照ください</span></p> <p class="footnote"><a href="#fn-a48c0bf4" id="f-a48c0bf4" name="f-a48c0bf4" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">この特性上、マイクロサービスごとに言語は1つであるという前提があります。例えばフロントエンドとバックエンドが同じ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ上にある場合は正しい計測結果になりません</span></p> <p class="footnote"><a href="#fn-ac388034" id="f-ac388034" name="f-ac388034" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">前述している system-components は対象外です</span></p> <p class="footnote"><a href="#fn-35e2097e" id="f-35e2097e" name="f-35e2097e" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">.<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a> 以下は <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の更新である。これは Workflow ごとに CODEOWNER を付与することで気づいてもらえるようにしている。</span></p> <p class="footnote"><a href="#fn-34969d77" id="f-34969d77" name="f-34969d77" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">前述した<a href="https://github.com/chaspy/gh-monorepo-pr-count">chaspy/monorepo-pr-count</a></span></p> <p class="footnote"><a href="#fn-c0bdabc3" id="f-c0bdabc3" name="f-c0bdabc3" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text">月ごとに特定の Label がついた PR を計測するしています。例えば gh monorepo-pr-count --since 2023-11-01 --until 2023-11-30 というコマンドを実行すれば各サービスごとの2023年11月の PR 数が得られます。</span></p> </div> quipper-ja A/B テストによるプロダクトエンハンスを支援する PLG(Product Led Growth) Team のご紹介 hatenablog://entry/6801883189068333464 2024-02-19T10:00:00+09:00 2024-02-19T10:16:49+09:00 こんにちは。@chaspy です。本記事で紹介するスタディサプリ中学講座の PLG(Product Led Growth) Team の Engineering Manager をしています。 本記事では、2023年2月に結成したこのチームの活動について紹介します。 スタディサプリ中学講座について PLG Team 発足の理由 なぜ A/B テストをやるのか A/B テストを行う方法 指標設計 事例紹介 学んだこと 指標設計の難しさ 必要なサンプル数を集める難しさ 意思決定を早くするためのモニタリングの重要さ おわりに スタディサプリ中学講座について スタディサプリ中学講座は、中学生向けのオン… <p>こんにちは。<a href="https://github.com/chaspy">@chaspy</a> です。本記事で紹介する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座の PLG(Product Led <a class="keyword" href="https://d.hatena.ne.jp/keyword/Growth">Growth</a>) Team の Engineering Manager をしています。</p> <p>本記事では、2023年2月に結成したこのチームの活動について紹介します。</p> <hr /> <ul> <li><a href="#%E3%82%B9%E3%82%BF%E3%83%87%E3%82%A3%E3%82%B5%E3%83%97%E3%83%AA%E4%B8%AD%E5%AD%A6%E8%AC%9B%E5%BA%A7%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">スタディサプリ中学講座について</a></li> <li><a href="#PLG-Team-%E7%99%BA%E8%B6%B3%E3%81%AE%E7%90%86%E7%94%B1">PLG Team 発足の理由</a></li> <li><a href="#%E3%81%AA%E3%81%9C-A/B-%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E3%82%84%E3%82%8B%E3%81%AE%E3%81%8B">なぜ A/B テストをやるのか</a></li> <li><a href="#A/B-%E3%83%86%E3%82%B9%E3%83%88%E3%82%92%E8%A1%8C%E3%81%86%E6%96%B9%E6%B3%95">A/B テストを行う方法</a></li> <li><a href="#%E6%8C%87%E6%A8%99%E8%A8%AD%E8%A8%88">指標設計</a></li> <li><a href="#%E4%BA%8B%E4%BE%8B%E7%B4%B9%E4%BB%8B">事例紹介</a></li> <li><a href="#%E5%AD%A6%E3%82%93%E3%81%A0%E3%81%93%E3%81%A8">学んだこと</a> <ul> <li><a href="#%E6%8C%87%E6%A8%99%E8%A8%AD%E8%A8%88%E3%81%AE%E9%9B%A3%E3%81%97%E3%81%95">指標設計の難しさ</a></li> <li><a href="#%E5%BF%85%E8%A6%81%E3%81%AA%E3%82%B5%E3%83%B3%E3%83%97%E3%83%AB%E6%95%B0%E3%82%92%E9%9B%86%E3%82%81%E3%82%8B%E9%9B%A3%E3%81%97%E3%81%95">必要なサンプル数を集める難しさ</a></li> <li><a href="#%E6%84%8F%E6%80%9D%E6%B1%BA%E5%AE%9A%E3%82%92%E6%97%A9%E3%81%8F%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%A2%E3%83%8B%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%AE%E9%87%8D%E8%A6%81%E3%81%95">意思決定を早くするためのモニタリングの重要さ</a></li> </ul> </li> <li><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></li> </ul> <h2 id="スタディサプリ中学講座について"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座について</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座は、中学生向けのオンライン学習サービスで、2022年2月にフルリニューアルされました。<a href="#f-7bb91110" id="fn-7bb91110" name="fn-7bb91110" title=" 最近では小学校1年生向けの講座もリニューアルしました https://studysapuri.jp/course/elementary/sho1/v1/">*1</a></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbrand.studysapuri.jp%2Fcareer%2Finterview%2Farticle%2FSaori_Suzuki%2F" title="鈴木 沙織 | スタッフインタビュー | スタディサプリ BRAND SITE" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://brand.studysapuri.jp/career/interview/article/Saori_Suzuki/">brand.studysapuri.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fstudysapuri.jp%2Fcourse%2Fjunior%2Fchu1%2F" title="中1の個別指導・映像授業|スタディサプリ中学講座" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://studysapuri.jp/course/junior/chu1/">studysapuri.jp</a></cite></p> <p>初期リリース後から現在に至るまでエンハンスを続け、プロダクトをブラッシュアップしています。</p> <h2 id="PLG-Team-発足の理由">PLG Team 発足の理由</h2> <p>PLG チームができる前、2022年2月時点でのチーム体制は以下の通りでした。</p> <ul> <li>Learning Core(学習コア): 講義を見て、問題を解く、という学習体験<a href="#f-e879d990" id="fn-e879d990" name="fn-e879d990" title="WebView で開発されている">*2</a></li> <li>Learning Encourage(学習促進): ログイン後から学習開始までの体験<a href="#f-b1de357e" id="fn-b1de357e" name="fn-b1de357e" title="おすすめレッスンを示すミッション機能、定期テスト日程登録機能、過去の学習統計を提供する”まなレポ&quot;機能、レッスン検索機能など">*3</a><a href="#f-d6c29938" id="fn-d6c29938" name="fn-d6c29938" title="Web(Frontend/Backend), Android, iOS の職能横断チーム">*4</a></li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Growth">Growth</a>: アプリを起動してログインするまで<a href="#f-9e76d26a" id="fn-9e76d26a" name="fn-9e76d26a" title="登録動線、オンボーディング、休眠ユーザへの通知等への利用喚起">*5</a><a href="#f-716acb8e" id="fn-716acb8e" name="fn-716acb8e" title="Web(Frontend/Backend), Android, iOS の職能横断チーム">*6</a></li> </ul> <p>いわゆる <a class="keyword" href="https://d.hatena.ne.jp/keyword/Growth">Growth</a> Hack と呼ばれる、獲得やチャーンレートと言った数値を見ながらプロダクトを改善していくということを <a class="keyword" href="https://d.hatena.ne.jp/keyword/Growth">Growth</a> チームでやりたかったのですが、実態は learning-core, learning-encourage の外側の「その他全て」を担うチームとして機能していました。また、小学1年生リニューアル対応も <a class="keyword" href="https://d.hatena.ne.jp/keyword/Growth">Growth</a> 開発チームが担ってくれていました。</p> <p>このような状況において、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Growth">Growth</a> 施策に集中して行うチームが必要ではないか、という経緯で PLG チームが結成されました。</p> <p>メンバーは以下各1名ずつで構成されています。</p> <ul> <li>Product Manager</li> <li>Engineering Manager: 私です</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> Engineer<a href="#f-af991f7e" id="fn-af991f7e" name="fn-af991f7e" title="なぜかiOSもちょっと書ける">*7</a></li> <li>Web Engineer</li> <li>Data Scientist<a href="#f-87c2e252" id="fn-87c2e252" name="fn-87c2e252" title="結成後2ヶ月ぐらいで加入してもらった">*8</a></li> </ul> <h2 id="なぜ-AB-テストをやるのか">なぜ A/B テストをやるのか</h2> <p>A/B テストとは、AとB、2つのパターンのどちらの方がクリック率やコンバージョン率などのKPIに効果があるかを検証する手法です。A/Bテストでは、ユーザーをランダムにAとBのグループに振り分けて、同時にテストを実施することが多いです。なぜ同時にテストをするのかというと、施策の効果以外の条件を2つのグループでそろえることで、施策の効果を正確に計測するためです。施策リリースの前後で数値を比較したとしても、他の施策との影響や季節・時期性の要因を排除できません。</p> <h2 id="AB-テストを行う方法">A/B テストを行う方法</h2> <p>私たちの組織では Feature Toggle 基盤<a href="#f-f4583c0e" id="fn-f4583c0e" name="fn-f4583c0e" title="最初は LaunchDarkly を利用していました。現在は内製の Darklaunch v2 を利用しています。参考: https://blog.studysapuri.jp/entry/2022/12/19/darklaunch-ujihisa">*9</a>を用いて実施しています。User ID を Key とし、A群とB群に 50% ずつ割り振るようにしています。</p> <p>多くの施策がフロントエンドで flag を取得し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の出し分けを行います。<a href="#f-db69ecc8" id="fn-db69ecc8" name="fn-db69ecc8" title="バックエンドで行った事例もあります">*10</a></p> <p>分析を行うために、判定結果をログとしてデータベースに送る必要があります。フロントエンドからは <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%20Analytics">Google Analytics</a> for Firebase にログを送り、分析時は BigQuery 経由でデータを取得しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240214/20240214171359.png" width="973" height="476" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>チーム結成直後に A/B テスト基盤の設計と実装を行い、早速最初の施策に取り組むことになりました。</p> <h2 id="指標設計">指標設計</h2> <p>何はともあれ、成否判定をする指標を決める必要があります。</p> <p>当時すでにプロダクトマネージャとデータチームによって KPI ツリーは策定されていました。ツリーの中では以下のような指標がありました。</p> <ul> <li>初回課金率: 無料期間後に課金に繋がる比率</li> <li>継続課金率: 現在の有料会員が翌月も課金継続する比率</li> </ul> <p>また、これらに相関がある学習指標として「L10」(エルテン)と呼ぶ指標があります。Lはレッスンを指し、過去30日間に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリで10レッスン完了すれば達成と判定される指標です。A/Bテストでは、ユーザーがA/Bテストに参加してから30日以内に10レッスンを完了した時点でL10達成として、A/BグループそれぞれのユーザのうちL10を達成したユニークユーザー率をL10率と定義しました。</p> <p>そして、我々は2つの継続課金率と L10率を指標としてモニタリングすることにしました。L10率ではA/B間で差が出ないこともあり、その場合は施策特有の別の前指標を用いることにしました。</p> <h2 id="事例紹介">事例紹介</h2> <p>記事公開日の 2024/02/15 現在まで、12個の施策を実施しました。そのうち、10件が採択、全てのユーザに展開され、2件が棄却されました。</p> <p>事例は以下のテンプレートで記載します。</p> <ul> <li>実施期間: yyyy-mm-dd ~ yyyy-mm-dd</li> <li>背景と仮説</li> <li>補足の画像</li> <li>指標</li> <li>結果</li> </ul> <p>事例は長い!学んだことが読みたいぞという方は<a href="#%E5%AD%A6%E3%82%93%E3%81%A0%E3%81%93%E3%81%A8">学んだこと</a>にジャンプしてください。</p> <h3 id="学習ガイドへの導線設置">学習ガイドへの導線設置</h3> <h4 id="実施期間">実施期間</h4> <p>2023-03-08 ~ 2023-04-30</p> <h4 id="背景と仮説">背景と仮説</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ登録直後のユーザに対して、私たちは利用方法を伝えきれていないのではという仮説がありました。そこで既存の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの活用方法を説明した「学習ガイド」への<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%B0%C0%FE">動線</a>を設置することで課金率・学習指標が向上するかどうかを検証する A/B テストを実施しました。</p> <p>グローバルナビゲーションに設置するほか、登録してすぐの体験期間中にユーザに対してはトップの目立つ場所に学習ガイドの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%B0%C0%FE">動線</a>を設置しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126142456.png" width="951" height="557" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標">指標</h4> <ul> <li>新規登録ユーザの初回課金率および L10率</li> </ul> <h4 id="結果">結果</h4> <ul> <li>初回課金率に有意差はなし</li> <li>バナーのクリック率は高かった</li> <li>レッスン数はバナーを表示した群の方が多かったが、有意差はなし</li> </ul> <p>結果、該当期間では有意差を検出できなかったが、ポジティブな影響があったこと、ネガティブな影響が見られなかったことから全展開としました🎉</p> <h3 id="ホーム画面で定期テストの導線を目立たせる施策">ホーム画面で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>の導線を目立たせる施策</h3> <h4 id="実施期間-1">実施期間</h4> <p>2023-03-28 ~ 2023-05-25</p> <h4 id="背景と仮説-1">背景と仮説</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>は中学生ユーザにとって非常に重要なイベントであり、ここで点数を取ることをプロダクトとしても重要視しています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座では<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>の期間を入力し、その期間に応じて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>向けの特別なコンテンツをミッションで訴求する機能が存在します。しかし、Web のユーザは Native に対して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>の利用率が低いという分析結果がありました。その理由として、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>への導線はグローバルナビゲーションにしか存在しないため、認知されていないのでは、という仮説を検証するために、ホーム画面上部に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>への<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%B0%C0%FE">動線</a>を追加する A/B テストを実施しました。</p> <h5 id="Before-Native">Before (Native)</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/quipper-ja/20240213233102" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240213/20240213233102.png" width="675" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></a></span></p> <h5 id="Before-Web">Before (Web)</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126152757.png" width="1200" height="643" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h5 id="After-Web">After (Web)</h5> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126152910.png" width="906" height="414" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-1">指標</h4> <ul> <li>L10率</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>日程入力率</li> </ul> <h4 id="結果-1">結果</h4> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>の日程入力率に有意差あり</li> </ul> <p>よって全ユーザに展開しました🎉</p> <h3 id="ミッション教科選択のデフォルト教科変更">ミッション教科選択のデフォルト教科変更</h3> <h4 id="実施期間-2">実施期間</h4> <p>2023-04-10 ~ 2023-05-01</p> <h4 id="背景と仮説-2">背景と仮説</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座では「ミッション」という、ユーザ一人一人の学習状況に合わせてレッスンをリコメンドする機能があります。ユーザ登録直後のオンボーディングで、ミッションで学習するコンテンツを選んでもらうことになっています。しかし、デフォルトは5教科が選択されており、多数のユーザがそのまま進むことで、ミッションの数自体が多くなってしまい、離脱してしまうユーザがいるのではないか、という仮説がありました。そこで、ミッションのデフォルト教科選択を [A]:デフォルト選択なし [B] 英数の2教科 [C] 従来通り5教科選択のA/B/C でテストすることにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126163721.png" width="1157" height="503" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-2">指標</h4> <ul> <li>ミッション完了率</li> <li>L10率</li> </ul> <h4 id="結果-2">結果</h4> <ul> <li>ミッション完了率は A のデフォルト選択なしが優位に高い結果となった <ul> <li>ミッション消化率、レッスン完了数は優位差なし</li> </ul> </li> </ul> <p>結果、デフォルト選択なしを全展開することにしました🎉</p> <h3 id="webのみ無料期間中オンボーディング用のタスクリストを表示する">(webのみ)無料期間中、オンボーディング用のタスクリストを表示する</h3> <h4 id="実施期間-3">実施期間</h4> <p>2023-06-29 ~ 2023-08-29</p> <h4 id="背景と仮説-3">背景と仮説</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座には純粋にレッスンを解く機能の他に「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>日程入力」「まなレポ」「サプモン」というユーザにとって有益な機能があります。これらの機能についてもユーザに認知してもらった方がよりプロダクトの利用率が上がるという仮説がありました。そのためオンボーディング期間後にこれらの機能を使ったかどうかを確認するタスクリストを表示する A/B テストを実施しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126163921.png" width="893" height="594" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-3">指標</h4> <p>タスクリストから誘導した各機能への UU</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>日程登録</li> <li>まなレポ閲覧</li> <li>サプモン</li> </ul> <h4 id="結果-3">結果</h4> <p>まなレポ閲覧率が優位差があったため、タスクリストを全展開することにしました🎉</p> <h3 id="web-ホーム画面定期テストパネルUI-の位置変更">(web) ホーム画面<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>パネルUI の位置変更</h3> <h4 id="実施期間-4">実施期間</h4> <p>2023-09-22 ~ 2023-10-16</p> <h4 id="背景と仮説-4">背景と仮説</h4> <p>"ホーム画面で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>の導線を目立たせる施策" で行った<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%B0%C0%FE">動線</a>追加施策ですが、右側よりも左側の方がより目立つのではないかという仮説を検証しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126164256.png" width="1010" height="345" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-4">指標</h4> <ul> <li>課金継続率</li> <li>L10率</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>日程入力率</li> </ul> <h4 id="結果-4">結果</h4> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>日程入力率に有意差があったため、全展開することにしました🎉</li> </ul> <h3 id="レッスン名から該当のレッスンに遷移できるようにさせたい">レッスン名から該当のレッスンに遷移できるようにさせたい</h3> <h4 id="実施期間-5">実施期間</h4> <p>2023-09-19 ~ 2023-11-07</p> <h4 id="背景と仮説-5">背景と仮説</h4> <p>過去の学習記録が閲覧できる「まなレポ」機能があります。このまなレポは過去に解いた問題の正答率が表示されます。その機能に対し、正答率の低いレッスンを解き直せるように<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%B0%C0%FE">動線</a>を表示してほしいというユーザ要望がありました。この仮説を検証するため、A/B テストを実施しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126165249.png" width="680" height="742" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-5">指標</h4> <ul> <li>課金継続率</li> <li>L10率</li> </ul> <h4 id="結果-5">結果</h4> <p>課金継続率、L10率 ともに有意差は見られませんでした。しかし、特定ユーザの UX を改善できている・また、ネガティブな影響はなかったことから全展開することにしました。</p> <h3 id="webまなレポ導線改善">(web)まなレポ導線改善</h3> <h4 id="実施期間-6">実施期間</h4> <p>2023-11-08 ~ 2023-11-16</p> <h4 id="背景と仮説-6">背景と仮説</h4> <p>過去の学習記録が閲覧できる「まなレポ」機能があります。ユーザは過去の学習記録を振り返って、モチベーションに繋げてもらったり、保護者と振り返るのに使ってもらっている機能です。このまなレポへの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%B0%C0%FE">動線</a>がグローバルナビゲーションにしかないため、閲覧率を高めるためにトップからの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%B0%C0%FE">動線</a>を追加する A/B テストを実施しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126165508.png" width="751" height="211" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-6">指標</h4> <ul> <li>課金継続率</li> <li>L10率</li> <li>まなレポへの遷移率・利用率</li> </ul> <h4 id="結果-6">結果</h4> <p>課金継続率、L10率 ともに有意差は見られませんでした。しかし、まなレポへの遷移率・利用率に有意差が見られたため、全ユーザに展開しました🎉</p> <p>また、この施策は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%F3">インターン</a>生の大石さん @umaidashi が開発してくれました。この場を借りてお礼申し上げます。ありがとうございました!</p> <h3 id="定期テスト対策講座TOPICの見せ方改善"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>対策講座、TOPICの見せ方改善</h3> <h4 id="実施期間-7">実施期間</h4> <p>2023-10-06 ~ 2023-11-09</p> <h4 id="背景と仮説-7">背景と仮説</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>対策講座は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリを利用するユーザにとって、効率的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>対策ができる有益なコンテンツです。「まずはこれだけ!厳選予想問題」「さらに応用!厳選予想問題(トッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%EC%A5%D9">プレベ</a>ル)」「つまづいたら 復習動画」という3つのトピックで構成されています。これらの講座は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリとしてユーザに訴求したい特別なコンテンツであり、それが UI 上訴求できておらず、ユーザから見つけてもらえていないのではないかという仮説がありました。そこで、ラベルを用いて講座を目立たせることによって、これらのコンテンツの消化率が向上し、結果学習結果につながるのではないか、という仮説を検証することにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/quipper-ja/20240126165725" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126165725.png" width="498" height="650" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></a></span></p> <h4 id="指標-7">指標</h4> <p>期間中1度でも「厳選予想」「応用問題」ラベルを見た日から計測した L10率</p> <h4 id="結果-7">結果</h4> <p>学習指標である L10率 については A/B で差分は生じませんでした。しかし、プラスもマイナスの影響もないこと、今後異なる目的のラベルでの実装を行う際に活用できることから、採用することになりました。</p> <h3 id="ノートに解く問題ラベルをレッスンにつけたい">「ノートに解く問題」ラベルをレッスンにつけたい</h3> <h4 id="実施期間-8">実施期間</h4> <p>2023-12-04 ~ 2023-12-25</p> <h4 id="背景と仮説-8">背景と仮説</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座では、オンラインのみで回答できる問題ではなく、実際にノートに書き取るタイプの問題も存在します。しかし、その問題タイプであることが実際に問題に取り組むまでわからないため、ユーザがノートを用意していない場合、問題に取り組むことができない課題がありました。そこで、ノートに解く問題であることを事前にユーザに伝えるために、レッスンに「ノートに解く問題」がわかるラベルをつけることにしました。</p> <p>選択肢として、(A)現状のまま、(B)「ノートに解く問題」という文字ラベル、(C)アイコンを表示のA/B/C テストを実施しました。</p> <ul> <li>(B)「ノートに解く問題」という文字ラベル</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126170156.png" width="704" height="408" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>(C)アイコンを表示</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126170159.png" width="863" height="412" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-8">指標</h4> <ul> <li>L10率</li> <li>ノートに解く問題の完了率</li> </ul> <h4 id="結果-8">結果</h4> <p>B「ノートに解く問題という文字ラベル」がノートに解く問題の完了率に有意差があったため、全展開することにしました🎉</p> <p>また、この施策も<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%F3">インターン</a>生の大石さん @umaidashi が開発してくれました!</p> <h3 id="ホーム画面に今月のレッスン数を表示する">ホーム画面に「今月のレッスン数」を表示する</h3> <h4 id="実施期間-9">実施期間</h4> <p>2023-12-18 ~ 2024-01-25</p> <h4 id="背景と仮説-9">背景と仮説</h4> <p>ユーザにとって今月どれぐらいレッスンを解いたかは「まなレポ」というページに遷移しないとわからない状況でした。これをユーザにもより簡単に認知してもらい、目標を持ってもらうために、今月のレッスン数をトップページに表示することにしました。これは内部で利用している L10 率という指標であり、これが改善するのではないか、という仮説がありました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126170744.png" width="1200" height="447" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>アニメーションでお花が咲きます。とっても可愛いです。</p> <h4 id="指標-9">指標</h4> <ul> <li>L10率</li> </ul> <h4 id="結果-9">結果</h4> <p>残念ながら有意差は見られませんでした。既存 UI の方が情報量が多いため、この案を棄却することにしました。しかし、有意差が見られないことがわかったことは私たちにとって大きな価値です。この結果を分析し、今後より良い施策を考えていければと思います。</p> <p>この案件も Backend 部分は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%F3">インターン</a>生の @umaidashi が開発してくれてました。</p> <h3 id="スタディサプリ活用動画の視聴をタスクリストに追加したい"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ活用動画の視聴をタスクリストに追加したい</h3> <h4 id="実施期間-10">実施期間</h4> <p>2023-12-25 ~ 2024-02-01</p> <h4 id="背景と仮説-10">背景と仮説</h4> <p>登録直後、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの機能を理解しているユーザは多くないため、活用方法を学んでもらうためにオンラインで受講できる「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ100%活用説明会」というコンテンツがあります。これを「無料期間中、オンボーディング用のタスクリストを表示する」という施策で作ったタスクリストにタスクとして追加し、L10率と、タスクリストから誘導した機能の利用率を検証しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240214/20240214144459.png" width="754" height="407" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-10">指標</h4> <ul> <li>L10率</li> <li>LP閲覧率(タスクリスト以外の導線も含む)</li> <li>サプモン率(タスクリスト経由のみ)</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>日程入力率(タスクリスト以外の導線も含む)</li> <li>まなレポ閲覧率(タスクリスト以外の導線も含む)</li> <li>タスクリスト全完了率</li> </ul> <h4 id="結果-10">結果</h4> <p>残念ながら LP への閲覧率に有意差がなく、かつタスクの全完了率とサプモン率にはネガティブに有意差があったため、棄却することにしました。</p> <h3 id="受験対策画面の作成ホームに導線設置">受験対策画面の作成&ホームに導線設置</h3> <h4 id="実施期間-11">実施期間</h4> <p>2024-01-18 ~ 2024-02-08</p> <h4 id="背景と仮説-11">背景と仮説</h4> <p>「受験対策講座」という、受験対策向けの専用コンテンツをリリースしています。これは受験生には非常に有益なコンテンツです。しかし、利用 UU が 10% 前後であり、認知されていないのでは、という仮説がありました。この仮説を検証するために、受験対策コンテンツ用のページを作成し、グローバルナビゲーションからのリンクを追加する A/B テストを実施しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240214/20240214145115.png" width="1200" height="552" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="指標-11">指標</h4> <ul> <li>L10率(中3のみ)</li> <li>受験対策講座のレッスン開始率(中3のみ)</li> <li>受験対策講座の平均レッスン完了数(中3のみ)</li> </ul> <h4 id="結果-11">結果</h4> <p>以下の理由により全展開されました 🎉</p> <ul> <li>L10UU、受験対策開始率について有意差はなかったが、Bのほうが勝っている</li> <li>平均レッスン完了数については1.6倍の差がついている</li> <li>17%のユーザーがクリックしており、使われている機能だといえる</li> </ul> <h2 id="学んだこと">学んだこと</h2> <p>チーム立ち上げから1年間、継続基盤実装から実際の施策展開をいくつもしてきました。その中で学んだことをまとめます。</p> <h3 id="指標設計の難しさ">指標設計の難しさ</h3> <p>最初は「初回課金率」というビジネス指標を目標にしていました。しかしこの指標は1つのエンハンスで差が出るものではありません。さらに L10率 という学習指標でさえ同様です。ほとんどの施策で L10率 以外の施策特有の指標を考える必要がありました。そしてその指標を計測可能にするための条件を考えたり、ログの送信条件を考えたり、実装する難しさなどさまざまな困難がありました。これはやってみないと分からないことでした。</p> <h3 id="必要なサンプル数を集める難しさ">必要なサンプル数を集める難しさ</h3> <p>一般的に統計的仮説検定で有意差を示すためには一定のサンプル数が必要です。施策によっては一部のユーザしかアクセスしない画面だったり、登録直後のユーザを登録するなど必要なサンプル数が集まりづらいケースもありました。その場合は意思決定が長引いてしまいますし、その間同一画面では A/B テストを実施することができません。このようなケースでは、より有意差が出やすい先行指標を検討することで意思決定をできるだけ早く行いました。</p> <h3 id="意思決定を早くするためのモニタリングの重要さ">意思決定を早くするためのモニタリングの重要さ</h3> <p>A/B テストの結果は BigQuery に保存されているため、Query を叩けばその結果を見ることができます。しかし、より手軽に見れるように <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Spreadsheet から Connected Sheet で BigQuery の結果を自動的に取り出し、可視化する仕組みを Data Scientist の @shoko-ota が作ってくれました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20240126/20240126172651.png" width="1099" height="636" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これは 「ノートに解く問題」ラベルをレッスンにつけたい」という施策の結果です。ノートに解く問題の完了率が明らかに差があることが一目でわかります。これを Daily standup でみんなで眺めることで、早期に意思決定することを助けてくれました。</p> <h2 id="おわりに">おわりに</h2> <p>本記事では<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座において A/B テストを実施するに至った背景と、結成から1年間で行った施策の紹介を行いました。</p> <p>最後に、チームメンバーの皆さん( @myamamic, @miyamizu, @masahiro-iwata, @shoko-ota, @umaidashi ) に心から感謝します。私は Engineering Manager としてチーム全体の Reviewer として携わり、本記事を執筆しましたが、実際の企画・設計・実装・分析はメンバーの皆さんがやってくれています。また、コードレビューで支援してくれた @indigolain, @YutaUra にも感謝します。この場を借りてお礼申し上げます。</p> <p>A/B テストの実施に興味がある方はぜひ <a href="https://twitter.com/chaspy_">@chaspy</a> に連絡ください。お話ししましょう!</p> <div class="footnote"> <p class="footnote"><a href="#fn-7bb91110" id="f-7bb91110" name="f-7bb91110" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">最近では小学校1年生向けの講座もリニューアルしました <a href="https://studysapuri.jp/course/elementary/sho1/v1/">https://studysapuri.jp/course/elementary/sho1/v1/</a></span></p> <p class="footnote"><a href="#fn-e879d990" id="f-e879d990" name="f-e879d990" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">WebView で開発されている</span></p> <p class="footnote"><a href="#fn-b1de357e" id="f-b1de357e" name="f-b1de357e" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">おすすめレッスンを示すミッション機能、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%B4%FC%A5%C6%A5%B9%A5%C8">定期テスト</a>日程登録機能、過去の学習統計を提供する”まなレポ"機能、レッスン検索機能など</span></p> <p class="footnote"><a href="#fn-d6c29938" id="f-d6c29938" name="f-d6c29938" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">Web(Frontend/Backend), <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>, <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a> の職能横断チーム</span></p> <p class="footnote"><a href="#fn-9e76d26a" id="f-9e76d26a" name="f-9e76d26a" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">登録<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C6%B0%C0%FE">動線</a>、オンボーディング、休眠ユーザへの通知等への利用喚起</span></p> <p class="footnote"><a href="#fn-716acb8e" id="f-716acb8e" name="f-716acb8e" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">Web(Frontend/Backend), <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>, <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a> の職能横断チーム</span></p> <p class="footnote"><a href="#fn-af991f7e" id="f-af991f7e" name="f-af991f7e" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text">なぜか<a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>もちょっと書ける</span></p> <p class="footnote"><a href="#fn-87c2e252" id="f-87c2e252" name="f-87c2e252" class="footnote-number">*8</a><span class="footnote-delimiter">:</span><span class="footnote-text">結成後2ヶ月ぐらいで加入してもらった</span></p> <p class="footnote"><a href="#fn-f4583c0e" id="f-f4583c0e" name="f-f4583c0e" class="footnote-number">*9</a><span class="footnote-delimiter">:</span><span class="footnote-text">最初は <a href="https://launchdarkly.com/">LaunchDarkly</a> を利用していました。現在は内製の Darklaunch v2 を利用しています。参考: <a href="https://blog.studysapuri.jp/entry/2022/12/19/darklaunch-ujihisa">https://blog.studysapuri.jp/entry/2022/12/19/darklaunch-ujihisa</a></span></p> <p class="footnote"><a href="#fn-db69ecc8" id="f-db69ecc8" name="f-db69ecc8" class="footnote-number">*10</a><span class="footnote-delimiter">:</span><span class="footnote-text">バックエンドで行った事例もあります</span></p> </div> quipper-ja detekt × Dangerで、プルリクコメントにルール名を表示する hatenablog://entry/6801883189078909987 2024-02-19T08:00:00+09:00 2024-02-19T08:00:03+09:00 こんにちは。スタディサプリ Androidエンジニアの@morayl です。 本記事では、Kotlinの静的解析ツールであるdetektの解析結果をDangerでプルリクにコメントする際に、ルール名も一緒にコメントするためにしたことを紹介します。 Dangerの基礎言語であるRubyは初心者なので、有識者から学びながらトライしました。 背景と結果 私が所属するチームでは最近、detektを導入し、Dangerを使ってプルリク上にコメントが出るようにしました。 この状態では、detektの指摘コメントだけが出ています。 一見問題無さそうですが、detektの指摘はルールで管理されているため、ルー… <p>こんにちは。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>エンジニアの<a href="https://github.com/morayl">@morayl</a> です。</p> <p>本記事では、Kotlinの静的解析ツールである<a href="https://github.com/detekt/detekt">detekt</a>の解析結果を<a href="https://github.com/danger/danger">Danger</a>でプルリクにコメントする際に、ルール名も一緒にコメントするためにしたことを紹介します。<br/> Dangerの基礎言語である<a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a>は初心者なので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CD%AD%BC%B1%BC%D4">有識者</a>から学びながらトライしました。</p> <h1 id="背景と結果">背景と結果</h1> <p>私が所属するチームでは最近、detektを導入し、Dangerを使ってプルリク上にコメントが出るようにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morayl/20240129/20240129130544.png" width="899" height="193" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この状態では、detektの指摘コメントだけが出ています。</p> <p>一見問題無さそうですが、detektの指摘はルールで管理されているため、ルール名があったほうが便利です。</p> <p>ルール名が分かることは、下記の点で重要です。</p> <ul> <li>修正する時に困る <ul> <li>指摘の詳細やどのように直したほうが良いかなどは、<a href="https://detekt.dev/docs/rules/comments">detektのルールガイド</a>に載っていますが、メッセージから何のルールに引っかかっているのかは調べづらい</li> </ul> </li> <li>設定値(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A4%B7%A4%AD%A4%A4%C3%CD">しきい値</a>など)を調整したり無効にしたりする場合、設定ファイルはルール名で管理されているので、探す必要がある</li> <li>理由があって指摘を個別に抑制したい場合にルール名が必要になる。(Kotlinでは、メソッドや変数に<code>@Suppress("ルール名")</code>と書くことで抑制できる。)</li> </ul> <p>メッセージからルールを推察することも出来ますが、detektをある程度経験しなければ難しく、よく使う人でもルール名がちゃんと分かるかというと難しいです。</p> <p>そこで、ルールも一緒にコメントされるようにしました。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morayl/20240129/20240129174050.png" width="884" height="214" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このコメントの場合、「MagicNumberというルールの指摘」ということがすぐに分かります。</p> <h1 id="前提条件">前提条件</h1> <p>detektの結果をDangerでコメントする際には、<a href="https://github.com/noboru-i/danger-checkstyle_format">danger-checkstyle_format</a> を使っています。</p> <p>確認したライブラリのバージョンは下記です。</p> <ul> <li>detekt:1.23.4</li> <li>Danger:9.4.2</li> <li>danger-<a class="keyword" href="https://d.hatena.ne.jp/keyword/checkstyle">checkstyle</a>_format:0.1.1</li> </ul> <h1 id="結論">結論</h1> <p>DangerFileのdetektの処理の前に、下記を加えるだけです。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># 加筆</span> <span class="synPreProc">class</span> ::<span class="synType">CheckstyleError</span> <span class="synPreProc">alias_method</span> <span class="synConstant">:original_message</span>, <span class="synConstant">:message</span> <span class="synPreProc">def</span> <span class="synIdentifier">message</span> <span class="synSpecial">&quot;</span><span class="synConstant">`</span><span class="synSpecial">#{</span>source<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synSpecial">\n\n#{</span>original_message<span class="synSpecial">}&quot;</span> <span class="synPreProc">end</span> <span class="synPreProc">end</span> <span class="synComment"># 元々あるdetektの処理</span> checkstyle_format.base_path = <span class="synType">Dir</span>.pwd <span class="synType">Dir</span>.glob(<span class="synSpecial">&quot;</span><span class="synConstant">check/reports/detekt/**/*.xml</span><span class="synSpecial">&quot;</span>).each <span class="synStatement">do</span> |file| checkstyle_format.report file <span class="synStatement">end</span> </pre> <h1 id="道筋と解説">道筋と解説</h1> <h2 id="ルールはどこにあるのか">ルールはどこにあるのか</h2> <p>まず、そもそもルールの情報があるのかを調べます。<br/> <a class="keyword" href="https://d.hatena.ne.jp/keyword/checkstyle">checkstyle</a>_format.reportに設定するファイルは<a class="keyword" href="https://d.hatena.ne.jp/keyword/xml">xml</a>です。ローカルでも出力できるので、detektをローカルで走らせます。<br/> すると、出力された<a class="keyword" href="https://d.hatena.ne.jp/keyword/xml">xml</a>の中には、<code>&lt;error</code>の中の<code>source</code>にあることが分かりました。</p> <pre class="code lang-xml" data-lang="xml" data-unlink><span class="synComment">&lt;?</span><span class="synType">xml version</span>=<span class="synConstant">&quot;1.0&quot;</span><span class="synType"> encoding</span>=<span class="synConstant">&quot;UTF-8&quot;</span><span class="synComment">?&gt;</span> <span class="synIdentifier">&lt;checkstyle </span><span class="synType">version</span>=<span class="synConstant">&quot;4.3&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;file </span><span class="synType">name</span>=<span class="synConstant">&quot;...HogeUtils.kt&quot;</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;error </span><span class="synType">line</span>=<span class="synConstant">&quot;7&quot;</span><span class="synIdentifier"> </span><span class="synType">column</span>=<span class="synConstant">&quot;17&quot;</span><span class="synIdentifier"> </span><span class="synType">severity</span>=<span class="synConstant">&quot;warning&quot;</span><span class="synIdentifier"> </span><span class="synType">message</span>=<span class="synConstant">&quot;This expression contains a magic number. Consider defining it to a well named constant.&quot;</span><span class="synIdentifier"> </span><span class="synType">source</span>=<span class="synConstant">&quot;detekt.MagicNumber&quot;</span><span class="synIdentifier"> /&gt;</span> <span class="synIdentifier">&lt;/file&gt;</span> <span class="synIdentifier">&lt;/checkstyle&gt;</span> </pre> <p>また、プルリクに出ているメッセージは<code>message</code>に記載されているものだということも分かります。</p> <h2 id="コメントはどのように作られるか">コメントはどのように作られるか</h2> <p>次に、コメント生成部分を探します。<br/> 使っている<code>danger-checkstyle_format</code>ライブラリを確認します。<br/> その生成部分にsourceを入れることが出来れば、目的が達成できるからです。<br/> <code>CheckstyleError</code>という型には、 <code>source</code>が定義してあります。(<a href="https://github.com/noboru-i/danger-checkstyle_format/blob/3b8656c834d7ff9522f5d912fbe223b008914d64/lib/checkstyle_format/checkstyle_error.rb#L9">参考</a>)</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synType">CheckstyleError</span>.new( parent_node[<span class="synConstant">:name</span>].sub(<span class="synSpecial">/^#{</span>base_path<span class="synSpecial">}/</span>, <span class="synSpecial">&quot;&quot;</span>), node[<span class="synConstant">:line</span>].to_i, node[<span class="synConstant">:column</span>].nil? ? <span class="synConstant">nil</span> : node[<span class="synConstant">:column</span>].to_i, node[<span class="synConstant">:severity</span>], node[<span class="synConstant">:message</span>], node[<span class="synConstant">:source</span>] ) </pre> <p>次に、Dangerでコメントする部分を確認すると、ここでは<code>source</code>は使われていません。(<a href="https://github.com/noboru-i/danger-checkstyle_format/blob/3b8656c834d7ff9522f5d912fbe223b008914d64/lib/checkstyle_format/plugin.rb#L76">参考</a>)</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink>warn(error.message, <span class="synConstant">file</span>: error.file_name, <span class="synConstant">line</span>: error.line) </pre> <h2 id="コメントをどうカスタマイズするか">コメントをどうカスタマイズするか</h2> <p>やりたいことは「warnの第一引数のerror.messageに、sourceも含めて表示したい」です。<br/> やり方はいくつかありますが、今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a>のモンキーパッチというものを使いました。</p> <p>これはすでに定義されたクラスの動きを変えるもので、無闇に使うべきものではありません。<br/> 正攻法としては、「ライブラリのPRを出す」「forkして使う」「ライブラリを使わず自前で書く」などがあります。<br/> 今回は、プロダクトに影響がない部分であること・修正範囲が小さくサクッと試したかったことから、モンキーパッチを試してみることにしました。</p> <p>Dangerfileが実行されるときには、すでにライブラリが読み込まれ、<code>danger-checkstyle_format</code>が使えるようになっています。<br/> そこで、下記Dangerのメソッド実行前に、<code>CheckstyleError</code>の<code>message</code>を書き換えて、<code>source</code>も表示することにしました。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink>warn(error.message, <span class="synConstant">file</span>: error.file_name, <span class="synConstant">line</span>: error.line) </pre> <p>そして、これがモンキーパッチです。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">class</span> ::<span class="synType">CheckstyleError</span> <span class="synPreProc">alias_method</span> <span class="synConstant">:original_message</span>, <span class="synConstant">:message</span> <span class="synPreProc">def</span> <span class="synIdentifier">message</span> <span class="synSpecial">&quot;</span><span class="synConstant">`</span><span class="synSpecial">#{</span>source<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synSpecial">\n\n#{</span>original_message<span class="synSpecial">}&quot;</span> <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">class</span> ::<span class="synType">CheckstyleError</span> </pre> <p>すでにある同じ名前のクラスの定義を書くことで、動きを上書きすることが出来ます。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink> <span class="synPreProc">alias_method</span> <span class="synConstant">:original_message</span>, <span class="synConstant">:message</span> </pre> <p><code>alias_method</code>を使うと、下記の形で元あるメソッドを新しい呼び名で複製することが出来ます。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">alias_method</span> 新しいメソッド名, すでにあるメソッド名 </pre> <p>ここでは、<code>original_message</code>をというメソッドを作成し、<code>message</code>と同じ動きをするようにしています。</p> <p>そして最後に、元ある<code>message</code>を上書きしています。</p> <pre class="code lang-ruby" data-lang="ruby" data-unlink> <span class="synPreProc">def</span> <span class="synIdentifier">message</span> <span class="synSpecial">&quot;</span><span class="synConstant">`</span><span class="synSpecial">#{</span>source<span class="synSpecial">}</span><span class="synConstant">`</span><span class="synSpecial">\n\n#{</span>original_message<span class="synSpecial">}&quot;</span> <span class="synPreProc">end</span> </pre> <p>alias_methodで作られた<code>original_message</code>は、その時点での<code>message</code>の動きをするため、上書きされたmessageが使われて循環参照になることはありません。 これで、以降で<code>message</code>が使われた場合は、<code>source</code>も含めた文字列を表示するように出来ました。</p> <h1 id="最後に">最後に</h1> <p>今回は、Dangerでコメントするdetektの指摘内容にルール名を含めることに方法について紹介しました。 今まで<a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a>を使う機会はほとんどありませんでしたが、今回のことを通じて<a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a>と少しだけ仲良くなれた気がします。<br/> 今回はモンキーパッチを使いましたが、コメントする機能はDangerにあり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/checkstyle">checkstyle</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/xml">xml</a>が解析できれば、ライブラリを使わず出力内容を自分で自由に決めることも出来ます。<br/> 更にやりたいこと出てきたら、今度は自前での実装に挑戦してみようと思いました。</p> morayl GitHub Copilot for Business 使っています hatenablog://entry/6801883189068842623 2024-01-22T10:00:00+09:00 2024-01-24T16:25:23+09:00 スタディサプリでエンジニアリングマネージャー等をしている @pankona です。 スタディサプリ (小中高、English) では GitHub Copilot for Business を使っています。本稿では、GitHub Copilot for Business を導入した背景と、導入後の活用方法について紹介します。 GitHub Copilot for Business とは GitHub Copilot 公式サイト: https://github.com/features/copilot もはや説明不要かもしれませんが、GitHub Copilot はいわゆる AI プログラミング… <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでエンジニア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%F3%A5%B0%A5%DE">リングマ</a>ネージャー等をしている @pankona です。<br/> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ (小中高、English) では <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot for Business を使っています。本稿では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot for Business を導入した背景と、導入後の活用方法について紹介します。</p> <h2 id="GitHub-Copilot-for-Business-とは"><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot for Business とは</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot 公式サイト: <a href="https://github.com/features/copilot">https://github.com/features/copilot</a></p> <p>もはや説明不要かもしれませんが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot はいわゆる AI プログラミングアシスタントです。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>やドキュメントを書いているときに、前後の文脈を考慮してコードを補完してくれる機能です。</p> <p>個人向けのプランと企業向けのプランが存在し、企業向けのプランは <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot for Business と呼ばれています。個人向けプランと Business プランでは機能的にはほとんど差がありませんが、Business プランでは以下のような配慮がなされています。</p> <ul> <li>自分が書いているコードやコードベースが <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot の学習データに含まれないようにすることができる (コードの流出防止のため)</li> <li>公開されている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のコードと一致するコードをサジェスションとして出すかどうかを設定する (ライセンス違反防止のため)</li> </ul> <p>我々のコードが知らぬ間に学習に使われて他の誰かのコードになっちゃう可能性があるんじゃないの?という懸念が当初はありましたが、Business プランを使っている限りにおいてはどうやらそういう心配なさようです。安心ですね。</p> <h2 id="弊社での導入状況">弊社での導入状況</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ (小中高、English) のソフトウェアエンジニア、QAエンジニア、SRE (パートナー含む) であれば、希望をしてもらえれば誰でも導入できる状態になっています。現在すでに 100 人以上が利用しており、普段コードを書く方には概ね行き渡っているような状況であろうと思います。</p> <h2 id="GitHub-Copilot-for-Business-を導入してみての感想"><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot for Business を導入してみての感想</h2> <p>弊社 Slack <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EF%A1%BC%A5%AF%A5%B9%A5%DA%A1%BC%A5%B9">ワークスペース</a>には、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot の知見を集める #copilot-ja というチャンネルがあります。Copilot 導入のヘルプや、知見の共有に役立てています。さて、このチャンネルに寄せられたよろこびの声をいくつかピックアップしつつ、私のコメントを添えてみます。</p> <h3 id="頻出するコードを書くのが楽になった">頻出するコードを書くのが楽になった</h3> <p>Go を書くときには <code>if err != nil</code> というお決まりのエラーハンドリングが頻発しますが、このような頻出コードは補完の精度が高く、Copilot の導入によってコーディングが楽になったという意見がありました。また <code>return fmt.Errorf</code> に続く内容 (どういうエラーであるかを表す文言) も文脈に沿ったものが提案される場合が多く、その点でも体験が向上したという意見が聞こえてきました。ボイラープレート的なコードが多めの言語とは、特に相性が良好であるという一面がありそうです。</p> <h3 id="GitHub-Actions-の-yaml-を書く作業が楽になった"><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の <a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> を書く作業が楽になった</h3> <p>複数のファイルに似たような修正を書く必要があるときに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot は強力なサポートをしてくれるようです。定形で退屈な作業をちょっと楽にしてくれるのはありがたいですね!</p> <h3 id="あまり馴染みのないコードを書くときに有用なサンプルを示してくれる">あまり馴染みのないコードを書くときに有用なサンプルを示してくれる</h3> <p>普段書かない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>を書く必要が出てきたときに、Copilot Chat に「このライブラリを使って○○したいときにどうすればいい?」と聞くと、そのライブラリを使ったコードをサジェストしてくれることがあります。提案されたコードはそのままで動かないときもあるので、最終的には自分でチェックする必要はあります。とはいえ大いに参考になりますよね。これも便利ですね!</p> <h3 id="テストコードを生成してくれる">テストコードを生成してくれる</h3> <p>特定の関数やメソッドに対して「これのテストコードを生成しておくれ」と Copilot Chat に聞くと、テストコードを生成してくれることがあります。こちらは普通のコードを書くときに比べて、そのままで動く (あるいは多少の手直しで済む) ものが提案される確率が高いような気がします。テストコード書くの億劫だなーと思うこともしばしばなので、これも大変便利ですね!</p> <h3 id="コミットメッセージを生成してくれる">コミットメッセージを生成してくれる</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a> の拡張や <a class="keyword" href="https://d.hatena.ne.jp/keyword/Vim">Vim</a> でコミットメッセージを書く場合には、コミットメッセージも Copilot が生成してくれます。こちらも多少手直しするだけで意味のあるコミットメッセージになるような気がします。これも便利ですね!</p> <h2 id="留意すべき点">留意すべき点</h2> <p>逆に気をつけなければならない点も色々分かってきました。嘆きの声もピックアップしつつ、私のコメントを添えてみます。</p> <h3 id="Copilot-が微妙な-typo-を織り交ぜてくる">Copilot が微妙な <a class="keyword" href="https://d.hatena.ne.jp/keyword/typo">typo</a> を織り交ぜてくる</h3> <p>"kobetsu" と書いてほしいところを "boketsu" と提案されてしまって気づかなかった、という報告がありました。たいへん面白い <a class="keyword" href="https://d.hatena.ne.jp/keyword/typo">typo</a> ですが、こういった提案がされてしまう可能性には気をつけないといけませんね。</p> <h3 id="Copilot-Chat-が正しくないことを教えてくる">Copilot Chat が正しくないことを教えてくる</h3> <p>ChatGPT などを利用されたことのある方はご存知のことかと思いますが、あたかも正しい情報であるかのように誤情報が提案されることがあります。これは Copilot Chat でも同様で、提案されたコードが正しいかどうかは自分で確認する必要があります。嘘を嘘であると見抜くレビューの力が問われますね。</p> <h2 id="総評-付き合い方が分かってくればとても便利">総評: 付き合い方が分かってくればとても便利</h2> <p>現状の Copilot の力は、あまり過度に期待しすぎると (なんでも空気を読んで生成してもらえると思っていると) 肩透かしを食うかもしれません。とはいえ、まず Copilot で下書きを作るイメージで大まかな部分を生成した上で、生成物を確認、適当な形に修正していくというプロセスを踏めばかなり有意義に使えるツールであると感じます。その精度や特性を理解して上手に付き合うことができれば、現状でも十分便利に使えます。今後も Copilot の精度向上や機能の追加が進んでいくと思いますので、期待していきたいですね。Let's enjoy coding!</p> <h2 id="おわりに">おわりに</h2> <p>本稿では、弊社の <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Copilot for Business 導入状況と、導入してみての感想を紹介しました。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでの開発に興味がある方は、<a href="https://twitter.com/pankona">@pankona</a> までお気軽にご連絡ください。カジュアルにお話できます。</p> gohan_pan_gohan スタディサプリ小中高プロダクト開発部2023年の登壇資料紹介 hatenablog://entry/6801883189075702545 2024-01-19T11:17:58+09:00 2024-01-19T11:17:58+09:00 こんにちは。技術広報チーム*1の @chaspy です。本記事では2023年に発表されたスタディサプリ小中高の登壇資料を紹介します。 Summary 2023年の登壇は合計19件ありました。 技術領域別の内訳は以下です。どの領域も満遍なく登壇がありました。 技術領域 登壇数 SRE 6 Platform 3 QA 3 iOS 3 Android 2 Web 2 登壇資料の紹介 それでは技術領域別に紹介していきます。 SRE インシデントにどう対応してきたか?各社のポストモーテムから学ぶ Lunch LT 登壇日: 2023/02/09 登壇者: @chaspy 想定読者: ポストモーテムの運用… <p>こんにちは。技術広報チーム<a href="#f-f3aa52bd" id="fn-f3aa52bd" name="fn-f3aa52bd" title="実は1年ほど前からブランディングチームを発足し、blog の執筆促進や社外イベントへの露出検討などをしています。対外的には&quot;技術広報”が一般的なのでそう呼んでみます">*1</a>の <a href="https://github.com/chaspy">@chaspy</a> です。本記事では2023年に発表された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小中高の登壇資料を紹介します。</p> <h2 id="Summary">Summary</h2> <p>2023年の登壇は合計19件ありました。</p> <p>技術領域別の内訳は以下です。どの領域も満遍なく登壇がありました。</p> <table> <thead> <tr> <th> 技術領域 </th> <th> 登壇数 </th> </tr> </thead> <tbody> <tr> <td> SRE </td> <td> 6 </td> </tr> <tr> <td> Platform </td> <td> 3 </td> </tr> <tr> <td> QA </td> <td> 3 </td> </tr> <tr> <td> <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a> </td> <td> 3 </td> </tr> <tr> <td> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> </td> <td> 2 </td> </tr> <tr> <td> Web </td> <td> 2 </td> </tr> </tbody> </table> <h2 id="登壇資料の紹介">登壇資料の紹介</h2> <p>それでは技術領域別に紹介していきます。</p> <hr /> <h3 id="SRE">SRE</h3> <h4 id="インシデントにどう対応してきたか各社のポストモーテムから学ぶ-Lunch-LT"><a href="https://findy.connpass.com/event/273197">インシデントにどう対応してきたか?各社のポストモーテムから学ぶ Lunch LT</a></h4> <ul> <li>登壇日: 2023/02/09</li> <li>登壇者: <a href="https://github.com/chaspy">@chaspy</a></li> <li>想定読者: ポストモーテムの運用に課題感を持つ SRE / Product Engineer</li> <li>内容サマリ: ポストモーテムを支える文化と技術について</li> </ul> <p><iframe id="talk_frame_987981" class="speakerdeck-iframe" src="//speakerdeck.com/player/c67f3b379e3d4443850c857c05cab6e9" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/chaspy/culture-and-technology-supporting-postmortem-operations">speakerdeck.com</a></cite></p> <h4 id="SLOconf-Tokyo-2023"><a href="https://connpass.com/event/282120/">SLOconf Tokyo 2023</a></h4> <ul> <li>登壇日: 2023/05/16</li> <li>登壇者: <a href="https://github.com/chaspy">@chaspy</a></li> <li>想定読者: SLI/SLO の運用をしている Product Engineer / それを支援する SRE</li> <li>内容サマリ: 一度決めた SLI/SLO を見直す実例</li> </ul> <p><iframe id="talk_frame_1026730" class="speakerdeck-iframe" src="//speakerdeck.com/player/d6c685a580d44398928e47774b4a0569" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/chaspy/slo-at-studysapuri">speakerdeck.com</a></cite></p> <h4 id="AWS-オンライン-セミナー-夏の-Amazon-EC2-祭り-2023-最新インスタンス活用編"><a href="https://aws.amazon.com/jp/blogs/news/event-report-wwso-compute-ec2-20230720/">AWS オンライン セミナー 夏の Amazon EC2 祭り 2023 最新インスタンス活用編</a></h4> <ul> <li>登壇日: 2023/07/20</li> <li>登壇者: <a href="https://github.com/kyontan">@kyontan</a></li> <li>想定読者: <a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon%20EC2">Amazon EC2</a> の最近のトレンドや活用方法に興味があるエンジニアなど</li> <li>内容サマリ: 大規模な MongoDB のコスト最適化のため Graviton <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>ストアを徹底的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a>した内容の共有</li> <li>登壇資料 <ul> <li>資料: <a href="https://pages.awscloud.com/rs/112-TZM-766/images/8-RECRUIT.pdf">スタディサプリ/Quipperを支えるデータベースをGravitonへ移行せよ!</a></li> <li>録画: <a href="https://www.youtube.com/watch?v=I2r3mTIxcRA">https://www.youtube.com/watch?v=I2r3mTIxcRA</a></li> </ul> </li> </ul> <h4 id="CloudNative-Days-Fukuoka"><a href="https://event.cloudnativedays.jp/cndf2023">CloudNative Days Fukuoka</a></h4> <ul> <li>登壇日: 2023/08/03</li> <li>登壇者: <a href="https://github.com/chaspy">@chaspy</a></li> <li>想定読者: 開発組織の能力を計測したい Engineering Manager, Engineer</li> <li>内容サマリ: Cloud Native なプロダクト開発組織における自己診断能力発掘の事例</li> </ul> <p><iframe id="talk_frame_1059503" class="speakerdeck-iframe" src="//speakerdeck.com/player/cbede6619ca44c818f9e58ff1656d918" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/chaspy/toward-the-acquisition-of-self-diagnostic-skills">speakerdeck.com</a></cite></p> <h4 id="SRE-NEXT-2023"><a href="https://sre-next.dev/2023/">SRE NEXT 2023</a></h4> <ul> <li>登壇日: 2023/09/29</li> <li>登壇者: <a href="https://github.com/chaspy">@chaspy</a></li> <li>想定読者: SRE, Product Engineer, Engineering Manager</li> <li>内容サマリ: Site Reliability Engineering を開発者とともに実現する事例</li> </ul> <p><iframe id="talk_frame_1084344" class="speakerdeck-iframe" src="//speakerdeck.com/player/6972fc8ee73f43648f5b36e67374aa2b" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/chaspy/sreing-with-developers">speakerdeck.com</a></cite></p> <h4 id="ゆるSRE勉強会-2"><a href="https://yuru-sre.connpass.com/event/293783/">ゆるSRE勉強会 #2</a></h4> <ul> <li>登壇日: 2023/10/20</li> <li>登壇者: <a href="https://github.com/chaspy">@chaspy</a></li> <li>想定読者: SLI/SLO を組織に実装したい SRE, Product Engineer</li> <li>内容サマリ: SLI/SLO を組織に実装するための観点</li> </ul> <p><iframe id="talk_frame_1094231" class="speakerdeck-iframe" src="//speakerdeck.com/player/3df8f292553744029dcb2c985ffca905" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/chaspy/if-i-had-to-do-the-slo-review-again">speakerdeck.com</a></cite></p> <h3 id="Platform">Platform</h3> <h4 id="SmartHRカケハシリクルート複雑化する開発体制におけるエンジニアの社内巻き込み術-プロダクト成長をリードするエンジニアたちの試行錯誤"><a href="https://techplay.jp/event/920430">【SmartHR/カケハシ/リクルート】複雑化する開発体制におけるエンジニアの社内巻き込み術 ‐プロダクト成長をリードするエンジニアたちの試行錯誤‐</a></h4> <ul> <li>登壇日: 2023/10/24</li> <li>登壇者: <a href="https://github.com/ojiry">ojiry</a></li> <li>想定読者: 複数のプロダクトから利用される基盤サービスの開発体制に興味のあるEngineer</li> <li>内容サマリ: 現場で実際に起きた問題とそれ対してどう対処したかの事例を紹介しています。詳細は<a href="https://logmi.jp/tech/articles/329888">ログミーTech</a>に詳しくまとめられています。</li> </ul> <p><iframe id="talk_frame_1134805" class="speakerdeck-iframe" src="//speakerdeck.com/player/ea692374e7c542a9b7b961c56f689800" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/recruitengineers/techplay_yoshioka">speakerdeck.com</a></cite></p> <h4 id="AWS-Dev-Day-2023-Tokyo"><a href="https://github.com/aws-events/aws-dev-day-tokyo-2023-cfp">AWS Dev Day 2023 Tokyo</a></h4> <ul> <li>登壇日: 2023/6/23</li> <li>登壇者: <a href="https://github.com/tooooooooomy">@tooooooooomy</a>, <a href="https://github.com/ujihisa">@ujihisa</a></li> <li>想定読者: マイクロサービスを考える規模のサービスを開発・運用している、またはFeatureTogglesに興味のあるEngineer</li> <li>内容サマリ: <a class="keyword" href="https://d.hatena.ne.jp/keyword/Rails">Rails</a>アプリケーションのモジュールとして存在していたDarklaunch(FeatureToggles)をGoアプリケーションとして<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EB%A5%B9%A5%AF%A5%E9%A5%C3%A5%C1">フルスクラッチ</a>でマイクロサービス化した話</li> </ul> <p><iframe id="talk_frame_1041956" class="speakerdeck-iframe" src="//speakerdeck.com/player/35d8508de44644c6b428248b4e8c2ef9" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/kazu9su/sutadeisapuri-railsapurikesiyonnomoziyurutositecun-zai-siteita-darklaunch-feature-toggles-wo-goapurikesiyontositehurusukuratutidemaikurosabisuhua-sitahua">speakerdeck.com</a></cite></p> <h4 id="西日暮里rb"><a href="https://nishinipporirb.doorkeeper.jp/events/149198">西日暮里.rb</a></h4> <ul> <li>登壇日: 2023/2/27</li> <li>登壇者: <a href="https://github.com/tooooooooomy">@tooooooooomy</a></li> <li>想定読者: ビジネスから一歩下がったレイヤーのシステムを開発したいと思っているEngineer</li> <li>内容サマリ: 小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループでやっていることの紹介</li> </ul> <p><iframe id="talk_frame_1061431" class="speakerdeck-iframe" src="//speakerdeck.com/player/78e33d8d21e04cd0af49b2b0742ae35d" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/kazu9su/sutadeisapurinointernal-platformkai-fa">speakerdeck.com</a></cite></p> <h3 id="QA">QA</h3> <h4 id="有料プランユーザー様限定オフラインMagicPodユーザーLT会"><a href="https://trident-qa.connpass.com/event/283709/">【有料プランユーザー様限定×オフライン】MagicPodユーザーLT会</a></h4> <ul> <li>登壇日: 2023/7/14</li> <li>登壇者: <a href="https://github.com/chaspy">chaspy</a></li> <li>想定読者: MagicPod の実行時間や成功率を計測したい利用者</li> <li>内容サマリ: MagicPod の実行時間と成功率を計測する事例</li> </ul> <p><iframe id="talk_frame_1050937" class="speakerdeck-iframe" src="//speakerdeck.com/player/20882c5382ce4f29a38a504dbae61677" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/chaspy/improved-e2e-testing-through-measurement">speakerdeck.com</a></cite></p> <h4 id="有料プランユーザー様限定オフラインMagicPodユーザーLT会-1"><a href="https://trident-qa.connpass.com/event/283709/">【有料プランユーザー様限定×オフライン】MagicPodユーザーLT会</a></h4> <ul> <li>登壇日: 2023/7/14</li> <li>登壇者: <a href="https://github.com/testtatto">testtatto</a></li> <li>想定読者: MagicPodを導入してみて、活用事例を参考にしたい方</li> <li>内容サマリ: 高速リリースを行うプロダクトチームにおけるMagicPodの運用方法</li> </ul> <p><iframe id="talk_frame_1053501" class="speakerdeck-iframe" src="//speakerdeck.com/player/a59e9b5f93614b9e8790cba143bb8bdb" width="710" height="491" style="aspect-ratio:710/491; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/testtatto/sutadeisapuri-zhong-xue-jiang-zuo-niokeru-e2e-test-noyun-yong-toji-ce-niyorugai-shan">speakerdeck.com</a></cite></p> <h4 id="チームで高速リリースを叶えるテスト戦略と自動化の実践"><a href="https://autify.com/ja/webinars/test-strategy?utm_source=other-campaigns&amp;utm_medium=compass&amp;utm_campaign=Recruit_Seminar_2023">チームで高速リリースを叶えるテスト戦略と自動化の実践</a></h4> <ul> <li>登壇日: 2023/6/14</li> <li>登壇者: <a href="https://github.com/testtatto">testtatto</a></li> <li>想定読者: E2Eテスト自動化ツールの導入を考えている方</li> <li>内容サマリ: <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリにおけるツール選定とAutify導入までの流れ</li> </ul> <p><iframe id="talk_frame_1061312" class="speakerdeck-iframe" src="//speakerdeck.com/player/87d9553bc69c4849964b1b3ecb42749a" width="710" height="491" style="aspect-ratio:710/491; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/testtatto/sutadeisapuri-timuniokerue2ezi-dong-tesutodao-ru-nojin-mefang">speakerdeck.com</a></cite></p> <h3 id="iOS"><a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a></h3> <h4 id="集まれSwift好きSwift愛好会-vol77--DeNA"><a href="https://love-swift.connpass.com/event/294968/">集まれSwift好き!Swift愛好会 vol.77 @ DeNA</a></h4> <ul> <li>登壇日: 2023/9/26</li> <li>登壇者: <a href="https://github.com/k-kohey">k-kohey</a></li> <li>想定読者: Swiftを利用している開発者</li> <li>内容サマリ: Swift Package Managerのバグを直した話</li> </ul> <p><iframe id="talk_frame_1082734" class="speakerdeck-iframe" src="//speakerdeck.com/player/3509eaeb1c324b92a035a9a0bd9d5097" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/k_koheyi/swift-package-mangernobaguwozhi-sitahua">speakerdeck.com</a></cite></p> <h4 id="iOSDC-2023"><a href="https://iosdc.jp/2023/">iOSDC 2023</a></h4> <ul> <li>登壇日: 2023/9/1</li> <li>登壇者: <a href="https://github.com/motoshima1150">motoshima1150</a></li> <li>想定読者: SwiftUI を利用している開発者</li> <li>内容サマリ: SwiftUI のバージョン移行方法とその運用の紹介</li> </ul> <p><iframe id="talk_frame_1070354" class="speakerdeck-iframe" src="//speakerdeck.com/player/3566c9a8af7447d88697c4e5ec55bac7" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/recruitengineers/iosdc-japan-2023">speakerdeck.com</a></cite></p> <h4 id="モバイルアプリの技術的負債-みんなで学ぶ-Lunch-LT"><a href="https://findy.connpass.com/event/276159/">モバイルアプリの技術的負債 みんなで学ぶ Lunch LT</a></h4> <ul> <li>登壇日: 2023/4/11</li> <li>登壇者: <a href="https://github.com/manicmaniac">manicmaniac</a></li> <li>想定読者: <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D7%A5%EA%B3%AB%C8%AF">アプリ開発</a>者</li> <li>内容サマリ: React Native 卒業後の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a> アプリの技術選定方針の紹介</li> </ul> <p><iframe id="talk_frame_1015267" class="speakerdeck-iframe" src="//speakerdeck.com/player/b5ecad4321f24527b194d924ca51b167" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/manicmaniac/react-native-zu-ye-hou-no-sutadeisapuri-nojin-lu">speakerdeck.com</a></cite></p> <h3 id="Android"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a></h3> <h4 id="Shibuyaapk-45"><a href="https://shibuya-apk.connpass.com/event/299317/">Shibuya.apk #45</a></h4> <ul> <li>登壇日: 2023/11/10</li> <li>登壇者: <a href="https://github.com/morux2">morux2</a></li> <li>想定読者: <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> エンジニア</li> <li>内容サマリ: <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小学・中学講座の Visual Regression Test の構成と、実装の工夫を紹介しています。サンプルコードもあります。</li> </ul> <p><iframe id="talk_frame_1104724" class="speakerdeck-iframe" src="//speakerdeck.com/player/e97989b13017485aac18110681662f91" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/morux2/fu-shu-duan-mo-de-visual-regression-test-wo-shi-xing-surushang-denogong-fu-toke-ti">speakerdeck.com</a></cite></p> <h4 id="YUMEMIgrow-Mobile"><a href="https://yumemi.connpass.com/event/283788">YUMEMI.grow Mobile</a></h4> <ul> <li>登壇日: 2023/6/21</li> <li>登壇者: <a href="https://github.com/maxfie1d">maxfie1d</a></li> <li>想定読者: <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> エンジニア</li> <li>内容サマリ: <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> 14 でさらに進化した <a href="https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture">Predictive Back</a> についての解説です。サンプルコードもあります。</li> </ul> <p><iframe id="talk_frame_1044017" class="speakerdeck-iframe" src="//speakerdeck.com/player/76c7ec1127ef40a18c100cecb3027020" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/recruitengineers/ishida">speakerdeck.com</a></cite></p> <h3 id="Web">Web</h3> <h4 id="Developers-Summit-2023-Summer"><a href="https://event.shoeisha.jp/devsumi/20230727">Developers Summit 2023 Summer</a></h4> <ul> <li>登壇日: 2023/7/27</li> <li>登壇者: <a href="https://github.com/highwide">highwide</a></li> <li>想定読者: 特に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B8%A5%E3%A5%A4%A5%EB">アジャイル</a>な開発現場に身を置くソフトウェア開発者</li> <li>内容サマリ: 意思決定にhookして記録する「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>デシジョンレコード」(<a class="keyword" href="https://d.hatena.ne.jp/keyword/ADR">ADR</a>)は、多くの開発現場にフィットするドキュメント手法です。</li> </ul> <p><iframe id="talk_frame_1056622" class="speakerdeck-iframe" src="//speakerdeck.com/player/1f6b4e70ecd3496f86329772e30e4c56" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/highwide/architectural-decision-records">speakerdeck.com</a></cite></p> <p>こちらの登壇は <a class="keyword" href="https://d.hatena.ne.jp/keyword/Developers%20Summit">Developers Summit</a> 2023 Summer でベストスピーカー賞に選ばれました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.studysapuri.jp%2Fentry%2F2023%2F09%2F15%2Fdevsumi-2023-summer" title="Developers Summit 2023 SummerでADRについて発表しました &amp; ベストスピーカー賞を受賞しました🎉 - スタディサプリ Product Team Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.studysapuri.jp/entry/2023/09/15/devsumi-2023-summer">blog.studysapuri.jp</a></cite></p> <p>セッションレポートもご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcodezine.jp%2Farticle%2Fdetail%2F18736" title="アジャイルな開発をより円滑に! 日々の意思決定を残す「アーキテクチャ・デシジョン・レコード」とは?" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://codezine.jp/article/detail/18736">codezine.jp</a></cite></p> <h4 id="リクルート--BASE--バイセル-第1回フロントエンド勉強会React--GraphQL"><a href="https://buysell-technologies.connpass.com/event/276135/">リクルート × BASE × バイセル 【第1回フロントエンド勉強会】React &amp; GraphQL</a></h4> <ul> <li>登壇日: 2023/3/15</li> <li>登壇者: <a href="https://github.com/ywada526">ywada526</a></li> <li>想定読者: マイクロサービスの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を検討している Engineer</li> <li>内容サマリ: マイクロサービスにおける frontend - backend 間の通信パターンの整理と GraphQL <a class="keyword" href="https://d.hatena.ne.jp/keyword/Gateway">Gateway</a> パターンの紹介</li> </ul> <p><iframe id="talk_frame_1005998" class="speakerdeck-iframe" src="//speakerdeck.com/player/fdcf62f8d6de402cb49636dd2dc69d92" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/ywada526/low-cost-graphql-gateway">speakerdeck.com</a></cite></p> <h2 id="おわりに">おわりに</h2> <p>昨年は多様な領域から登壇がありました。今後も機会があり、本人の意思があれば登壇を支援していきたいと思います。過去のブログ記事を参考に、登壇依頼等あればお気軽に <a href="https://twitter.com/chaspy_">@chaspy</a> までご連絡ください。</p> <div class="footnote"> <p class="footnote"><a href="#fn-f3aa52bd" id="f-f3aa52bd" name="f-f3aa52bd" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">実は1年ほど前から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%E9%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">ブランディング</a>チームを発足し、blog の執筆促進や社外イベントへの露出検討などをしています。対外的には"技術広報”が一般的なのでそう呼んでみます</span></p> </div> quipper-ja スタディサプリ小中高の技術戦略について hatenablog://entry/6801883189067889175 2023-12-22T08:00:00+09:00 2023-12-22T10:58:25+09:00 この記事は Enginnering Manager Advent Calendar その2の1日目の記事です。(大遅刻しました) こんにちは。@chaspy です。10月からスタディサプリ小中高*1プロダクト開発部の部長をしています。 本記事では、我々の組織で取り組んでいる技術戦略の現状と今後についてお伝えします。 技術戦略とは何か スタディサプリ小中高の技術戦略 開発比率適正化 課題発見と改善サイクルの確立 直近の取り組み ガイドラインの策定 マイクロサービスの命名 今後追加が予定されているもの monolith の方針検討 共有データベースに対する Model 層の管理方針 api end… <p>この記事は <a href="https://qiita.com/advent-calendar/2023/em">Enginnering Manager Advent Calendar その2</a>の1日目の記事です。(大遅刻しました)</p> <p>こんにちは。<a href="https://github.com/chaspy">@chaspy</a> です。10月から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小中高<a href="#f-78453057" id="fn-78453057" name="fn-78453057" title="スタディサプリ小学・中学・高校・大学受験講座のうち、一般の家庭に提供している領域を BtoC 領域と呼んでいます。https://studysapuri.jp/ ">*1</a>プロダクト開発部の部長をしています。</p> <p>本記事では、我々の組織で取り組んでいる技術戦略の現状と今後についてお伝えします。</p> <ul class="table-of-contents"> <li><a href="#技術戦略とは何か">技術戦略とは何か</a></li> <li><a href="#スタディサプリ小中高の技術戦略">スタディサプリ小中高の技術戦略</a><ul> <li><a href="#開発比率適正化">開発比率適正化</a></li> <li><a href="#課題発見と改善サイクルの確立">課題発見と改善サイクルの確立</a></li> </ul> </li> <li><a href="#直近の取り組み">直近の取り組み</a><ul> <li><a href="#ガイドラインの策定">ガイドラインの策定</a><ul> <li><a href="#マイクロサービスの命名">マイクロサービスの命名</a></li> <li><a href="#今後追加が予定されているもの">今後追加が予定されているもの</a></li> </ul> </li> <li><a href="#monolith-の方針検討">monolith の方針検討</a><ul> <li><a href="#共有データベースに対する-Model-層の管理方針">共有データベースに対する Model 層の管理方針</a></li> <li><a href="#api-endpoint-ごとのオーナーシップ策定">api endpoint ごとのオーナーシップ策定</a></li> </ul> </li> </ul> </li> <li><a href="#技術戦略グループとして実現したいこと">技術戦略グループとして実現したいこと</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="技術戦略とは何か">技術戦略とは何か</h2> <p>ざっくりいうと、事業計画に対して、技術投資をどこにするのか、しないか、です。"技術"投資と言っても範囲は広く、開発部の正社員/パートナーの人件費をどう配分するのか、増やすのか減らすのか、から、開発案件として何を取り組むのか(技術的負債解消を含む)、また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>/<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>のコスト方針までさまざまです。</p> <p>事業計画は半年に1度策定され<a href="#f-a2a49a5d" id="fn-a2a49a5d" name="fn-a2a49a5d" title="厳密には年に1度、決算のサイクルで行われ、半年で見直しがされる">*2</a>、プロダクトマネージャ、開発、コンテンツディレクター、マーケター、事業企画、FP&amp;A<a href="#f-31329151" id="fn-31329151" name="fn-31329151" title="Financial Planning &amp;amp; Analysis">*3</a> 他いろんな職種の方と協力して計画を策定します。その中で、開発投資額の計画も求められますし、直近1年について具体的にどういう案件を行うのかを計画します。</p> <h2 id="スタディサプリ小中高の技術戦略"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小中高の技術戦略</h2> <p>現在実行している技術戦略、およびその体制について説明します。</p> <h3 id="開発比率適正化">開発比率適正化</h3> <p>2年ほど前に、開発比率適正化として、開発内容を新規・エンハンス・負債解消と分類した上で、この比率を 1:1:1 にすることを事業開発を行うプロダクトマネージャと合意しました。これは事業を伸ばしていく上でどうしても新規偏重になってしまい、負債解消に手が回らない状況があったためです。</p> <p>現状、新規とエンハンスの割合はプロダクトマネージャに一任していますが、負債解消については毎年一定の予算を確保し、開発部として必要な案件を実施しています。</p> <h3 id="課題発見と改善サイクルの確立">課題発見と改善サイクルの確立</h3> <p>技術的負債を解消するためには、我々がシステムを自己診断できる能力と、診断した上で負債を評価し、来期計画に繋げる仕組みが必要です。</p> <p>我々は2021年、技術戦略を完全な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%C3%A5%D7%A5%C0%A5%A6%A5%F3">トップダウン</a>で行うのではなく、自己診断能力と課題発見・管理を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%E0%A5%A2%A5%C3%A5%D7">ボトムアップ</a>ベースで行うことに決めました。全員兼務の技術戦略グループが存在し、横断 / フロントエンド / DevOps という3つのワーキンググループでの活動を続けています。</p> <p>以下がそれぞれのワーキンググループとその周辺の役割を示した図です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20231220/20231220121753.png" width="922" height="718" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>それぞれのワーキンググループの役割を説明します。</p> <ul> <li>横断: <ul> <li>Issue Management &amp; Planning: 投げ込まれた技術的負債解消案件のハンドリングと来期計画への反映<a href="#f-45b6e734" id="fn-45b6e734" name="fn-45b6e734" title="バックエンド領域や横断課題を取り扱う">*4</a></li> <li>Research &amp; Development: 新規技術の検証と評価<a href="#f-4066266f" id="fn-4066266f" name="fn-4066266f" title="社内では Tech Darwin というプロジェクト名でやっている。命名は当時のメンバーの投票で決まった。">*5</a></li> <li>Technology Direction: 複数選択肢がある際にどの技術を利用し、どの技術をやめるのかの方針をファシリテートする</li> </ul> </li> <li>フロントエンド: Web フロントエンド領域の技術課題の管理と解決</li> <li>DevOps: 自己診断能力の獲得</li> </ul> <p>なお, Native(<a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>/<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>) についての技術戦略・技術課題管理は各グループ独立でやっています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> schema など横断的に関わる時にスポットで参加してもらっています。SRE も同様です。</p> <p>普段は下段やや左の"開発チーム"が Stream Aligned Team<a href="#f-2d800416" id="fn-2d800416" name="fn-2d800416" title="Team Topologies 用語">*6</a> として開発ロードマップの達成を目指します。その上で、開発チームで解決できない課題については技術戦略グループでハンドリングします。ハンドリングした結果、組織体制の変更が必要なものはマネジメントチームで、各チームの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンで行えるものは各チームで対応し、必要に応じて来期計画に反映します。</p> <p>開発部長としては事業戦略に対して開発部としての技術戦略策定の責任を負っており、事業戦略のインプットや全体を把握して方針をレビューする役割を担っています。</p> <p>過去のアウトプットもご覧ください。</p> <ul> <li><a href="https://blog.studysapuri.jp/entry/2022/11/10/090000">技術戦略横断ワーキンググループの活動報告 - スタディサプリ Product Team Blog</a></li> <li><a href="https://speakerdeck.com/chaspy/toward-the-acquisition-of-self-diagnostic-skills">自己診断能力の獲得を目指して / Toward the acquisition of self-diagnostic skills - Speaker Deck</a></li> <li><a href="https://blog.studysapuri.jp/entry/2020/08/17/dx-criteria-system">自分たちのシステムを診断するために DX Criteria"システム"テーマを実施しました - スタディサプリ Product Team Blog</a></li> </ul> <h2 id="直近の取り組み">直近の取り組み</h2> <h3 id="ガイドラインの策定"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>の策定</h3> <p>組織もシステムも徐々に大きくなり、新しいサービスの技術を選定する上で、何を選べばいいか迷うシーンも増えてきました。もちろん技術選定は状況・環境・サービスの求める特性によって異なるものですが、組織において十分使われているものがあれば、リスクも減らせますし、知見の共有も可能になります。</p> <p>そこで、組織全体で使う技術の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>を置く<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を作成しました。<a href="#f-81cd49ba" id="fn-81cd49ba" name="fn-81cd49ba" title="余談ですが、quipper/handbooks というドキュメント用のmonorepo が存在し、誰でも簡単にドキュメント作成できる便利な仕組みがあります。ツールとしては docsify が多く使われています。">*7</a></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>のドキュメントのトップには、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>」として、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>とは何かが記載されています。一部引用します。</p> <pre class="code" data-lang="" data-unlink>最初に: ガイドラインとは何か / 何ではないのか 「ガイドライン」の意味や使い方 わかりやすく解説 Weblio辞書 を抜粋すると、 &gt;どのように行動したらいいかまとめたものを指す英語表現である &gt;「guideline」の意味である「指針」とは、物事を進めるべき方針や頼りになるもの、もしくは手引きのことを示している。あくまで方向性を示しているものなので、守らなくても何らかのペナルティが発生することはない。 ガイドラインは &#34;良い意思決定を助けるため&#34; に存在します。いわゆる &#34;ルール&#34; とは異なり、強い強制力を持つものでも、守らないとペナルティが発生するものではありません。守ることで利益が見込まれる (あるいは逸脱すると不利益がある) 事柄について言語化したものと捉えるとよさそうです。</pre> <pre class="code" data-lang="" data-unlink>ガイドラインが役に立つと期待される例 意思決定において迷いが発生しがちなもの対し、よくある選択肢のメリデメと、デフォルトの選択肢を提供する プログラミング言語、フレームワーク、ライブラリ、など 放っておくと不利益が生じる選択肢を選びがちなものについて、どういう罠があるかを先んじて情報提供しつつ、オススメの考え方を提供する サービス名称の決め方、マイクロサービスの分割の仕方、モノリスの扱いなど</pre> <p>このように、ルールではなく、推奨事項が書かれることが期待されます。書く人にとっても読む人にとってもメリットがもたらされることが期待されており、逆に言えばそうではない議論が分かれるものは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>としては不適切であると言えます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>に書かれていたからと言って、サービス固有の特性に従い推奨外のものを選択することももちろん可能ですし、そうすべきです。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>はそのような選択をするための観点を提供する役割もあります。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>の例としては、以下のようなものがあります。</p> <h4 id="マイクロサービスの命名">マイクロサービスの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE">命名</a></h4> <p>以下、実際の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>本文です。</p> <pre class="code" data-lang="" data-unlink>## ガイドライン 1. サービス名は可能な限り実態を表し、将来新しく加入したメンバーにとって無理なく認知できるものを推奨します 2. サービス名は可能な限り時間の経過によって意味が変わりにくいものを推奨します 3. サービス名の長さは26文字以内としてください ## 理由 1. 無理なく認知できない場合、名前と実態の変換表という新たなドメイン知識が必要になります - この変換表をメンテナンスし続けないといけませんし、新しく入った人に周知し続けないといけません。忘れてしまった人にもしつこく言い続けなければなりません - これは、業務の遂行を不必要に複雑化させます。 2. 時間の経過によって意味が変わってしまう場合、1 のように無理なく認知できることが難しくなることが予想されます 3. 26文字を超える場合、quipper/terraform リポジトリの label 上限に引っかかるためです - また、あまりサービス名が長いと書きづらく、発音しにくいため、略称を使われるなど、1 のように無理なく認知されることが難しくなります。また略称を使われることで Slack や GitHub 等でのググラビリティの低下も懸念されます - 略称は避けるべきですが、やむを得ずどうしても略称が必要ならば、公式に略称を提供して、その使用を徹底してください。</pre> <h4 id="今後追加が予定されているもの">今後追加が予定されているもの</h4> <p>数年前に策定された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>で、ドキュメントに残っているものを改めて Pull Request を用いて現状に適しているかどうかレビューする予定です。</p> <ul> <li>サービス間通信の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a> over http)</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a> の静的型付け(sorbet)</li> <li>S3 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>の権限管理(IAM を利用する)</li> <li>Redis を利用するサービス(Redis Cloud or Elasticache)</li> </ul> <h3 id="monolith-の方針検討">monolith の方針検討</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小中高では一部のシステムは共有データベースを利用しています。複数の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Rails">Rails</a> Application から利用されているため、データベースのアクセスは共有ライブラリを利用しています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Rails">Rails</a> の Model 層にあたるものです。また、この複数の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Rails">Rails</a> Application のうち、生徒向けサービスのバックエンド <a class="keyword" href="https://d.hatena.ne.jp/keyword/api">api</a> はいろんな用途で利用される巨大なアプリケーションとなっており、このデータベース、およびアプリケーションのことを monolith と呼んでいます。</p> <p>サービス構成の詳細は以下の記事も参照ください。<a href="https://blog.studysapuri.jp/entry/2023/03/17/studysapuri-development">スタディサプリのWebアプリケーションはこうやって開発されている - スタディサプリ Product Team Blog</a></p> <p>以降具体を説明します。これらはいずれも議論に決着がついた場合、前述した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>としてドキュメント化する予定です。</p> <h4 id="共有データベースに対する-Model-層の管理方針">共有データベースに対する Model 層の管理方針</h4> <p>現在、共有データベースへのアクセスのためのライブラリは2種類あります。1つは以前から存在する、内部で"schema"と呼ばれるもの。もう1つは monolith からの段階的なマイクロサービス移行を支援するために作成された"qmonolith"と呼ばれるものです。新規に共有データベース向けの処理を書く場合や、マイクロサービス化のために monolith から移行する場合は qmonolith を利用することが推奨されています。読み書きには qmonolith を利用する <a class="keyword" href="https://d.hatena.ne.jp/keyword/Rails">Rails</a> Application である qmonolith-service を利用します。</p> <p>当時の方針から年月が経過し、現状どうすべきか方向性が人によって異なる状況になってきました。そこで今回改めてこの monolith に対する実装方針を議論中です。</p> <h4 id="api-endpoint-ごとのオーナーシップ策定"><a class="keyword" href="https://d.hatena.ne.jp/keyword/api">api</a> endpoint ごとのオーナーシップ策定</h4> <p>前述した生徒向けサービスのバックエンド <a class="keyword" href="https://d.hatena.ne.jp/keyword/api">api</a> は非常に大きく、複数のチームが触ることで、サービスのコストパフォーマンスや、変更時の影響が読めないなど開発生産性への影響が懸念されています。また、このサービス個別の SLO は定められているものの、実際にパフォーマンスの問題が現れた時にどのチームに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンすればいいかがわかりづらい問題も起こっています。</p> <p>そこで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/api">api</a> endpoint ごとにオーナーシップを策定し、オーナーシップなし(全員で共通で使っているもの)の割合が一定割合以下になるような方針を検討しました。また、それを endpoint の増減に関わらず維持できるように、設定自動化も検討中です。<a href="#f-aa0f9171" id="fn-aa0f9171" name="fn-aa0f9171" title="Datadog API Catalog が便利そうである">*8</a></p> <h2 id="技術戦略グループとして実現したいこと">技術戦略グループとして実現したいこと</h2> <p>10年先もユーザへの価値と事業利益を作り続ける組織とプロダクトを作ることを目標としています。</p> <p>そのために、現場メンバーが誰でも課題を表明できること、その課題を適切に評価できることが重要で、その取り組み自体がグループ体制により<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%B9%A5%C6%A5%CA%A5%D6%A5%EB">サステナブル</a>にできることが重要だと考えています。</p> <p>そしてそれを支えるのが、Ownership を持って(垣根をこえて自分ごととして)システムに向き合うこと、率直だが思いやりのあるコミュニケーション、事実を元にした意思決定をする Fact-Based な文化です。</p> <p>今の組織ではこの文化を大切にしながら、技術戦略の活動を通じてユーザへの価値を提供し続けていきたいと考えています。</p> <h2 id="おわりに">おわりに</h2> <p>本稿では<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小中高における技術戦略の体制および直近の取り組みを紹介しました。</p> <p>技術戦略に興味がある方はぜひ <a href="https://twitter.com/chaspy_">@chaspy</a> まで連絡ください。カジュアルに話しましょう!</p> <div class="footnote"> <p class="footnote"><a href="#fn-78453057" id="f-78453057" name="f-78453057" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小学・中学・高校・大学受験講座のうち、一般の家庭に提供している領域を BtoC 領域と呼んでいます。<a href="https://studysapuri.jp/">https://studysapuri.jp/</a> </span></p> <p class="footnote"><a href="#fn-a2a49a5d" id="f-a2a49a5d" name="f-a2a49a5d" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">厳密には年に1度、決算のサイクルで行われ、半年で見直しがされる</span></p> <p class="footnote"><a href="#fn-31329151" id="f-31329151" name="f-31329151" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">Financial Planning &amp; Analysis</span></p> <p class="footnote"><a href="#fn-45b6e734" id="f-45b6e734" name="f-45b6e734" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">バックエンド領域や横断課題を取り扱う</span></p> <p class="footnote"><a href="#fn-4066266f" id="f-4066266f" name="f-4066266f" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">社内では Tech <a class="keyword" href="https://d.hatena.ne.jp/keyword/Darwin">Darwin</a> というプロジェクト名でやっている。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE">命名</a>は当時のメンバーの投票で決まった。</span></p> <p class="footnote"><a href="#fn-2d800416" id="f-2d800416" name="f-2d800416" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://teamtopologies.com/">Team Topologies</a> 用語</span></p> <p class="footnote"><a href="#fn-81cd49ba" id="f-81cd49ba" name="f-81cd49ba" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text">余談ですが、quipper/handbooks というドキュメント用のmonorepo が存在し、誰でも簡単にドキュメント作成できる便利な仕組みがあります。ツールとしては <a href="https://docsify.js.org/#/">docsify</a> が多く使われています。</span></p> <p class="footnote"><a href="#fn-aa0f9171" id="f-aa0f9171" name="f-aa0f9171" class="footnote-number">*8</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://docs.datadoghq.com/tracing/api_catalog/">Datadog API Catalog</a> が便利そうである</span></p> </div> quipper-ja 社内技術ドキュメンテーションを科学する hatenablog://entry/6801883189066682113 2023-12-15T08:00:00+09:00 2024-02-26T22:01:03+09:00 部署内の技術トーク会にて、理想的なドキュメンテーションにおいて回避不可能なトレードオフと、それを踏まえた実践的な手順を発表しました。 その際のワークショップで得た他エンジニアが持つ Pains や知見、そして彼らからの反響を共有致します。 <p>最終更新日: 2024年02月27日(月)</p> <ul class="table-of-contents"> <li><a href="#1-ご挨拶">1. ご挨拶</a></li> <li><a href="#2-本記事執筆のモチベーション">2. 本記事執筆のモチベーション</a></li> <li><a href="#3-ワークショップを通じて得たフィードバック">3. ワークショップを通じて得たフィードバック</a><ul> <li><a href="#3-1-Pains--過去抱えた現在進行形で抱えている辛み-">3-1. Pains -過去抱えた/現在進行形で抱えている辛み-</a></li> <li><a href="#3-2-ApproachesSolutions--Pains-を解消するために取った方策や導き出した解決策-">3-2. Approaches/Solutions -Pains を解消するために取った方策や導き出した解決策-</a><ul> <li><a href="#3-2-1-えいやで場所を決め打ちしてしまうeg-GitHub-Wiki--Google-docs-しか使わない">3-2-1. えいやで場所を決め打ちしてしまう(e.g., GitHub Wiki + Google docs しか使わない)</a></li> <li><a href="#3-2-2-個人的に20231205時点でみたいな書き方を心がけている">3-2-2. 個人的に、2023/12/05時点で〜みたいな書き方を心がけている</a></li> </ul> </li> <li><a href="#3-3-Tips--効果的な手法-">3-3. Tips -効果的な手法-</a></li> </ul> </li> <li><a href="#4-オーディエンスからの反響">4. オーディエンスからの反響</a><ul> <li><a href="#4-1-気づきや学びNEXT-ACTIONS">4-1. 気づきや学び・NEXT ACTIONS</a></li> <li><a href="#4-2-プレゼンターhayat01sh1daへのフィードバック">4-2. プレゼンター(@hayat01sh1da)へのフィードバック</a></li> <li><a href="#4-3-Slack-での反応">4-3. Slack での反応</a></li> </ul> </li> <li><a href="#5-おわりに">5. おわりに</a></li> </ul> <h2 id="1-ご挨拶">1. ご挨拶</h2> <p>初投稿となります。<br/> <a class="keyword" href="https://d.hatena.ne.jp/keyword/ToB">ToB</a> 高校向け<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発エンジニアをしている <a href="https://github.com/hayat01sh1da">@hayat01sh1da</a> です。<br/> 私のチームでは先生方が生徒さんに向けて配信するアンケートや志望校調査など、進路選択という学事に関わる機能の開発を行なっています。</p> <p>当社は部活動が盛んで、私自身は卓球部・カラオケ部・山岳部(非正規部員)に所属しています。<br/> 趣味はカラオケ・旅行・サウナ・お酒です。<br/> 今年は長めの冬季休暇を頂き、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%CB%A5%E5%A1%BC%A5%B8%A1%BC%A5%E9%A5%F3%A5%C9">ニュージーランド</a>(Queenstown → Mount Cook → Lake Tekapo → Christchurch) を旅行して来ます。<br/> 大学 4 年次を休学してオーストラリアで 1 年間ワーキングホリデーをしていた時に Queenstown を旅行した以来なので、とても楽しみです!</p> <p>宝物は実家にいる御年 21 歳の愛猫です(写真は 2014 年当時)。</p> <p><img src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/53f1e0a2-d0e3-4e31-bc46-510cbb973d63" width="700" /></p> <h2 id="2-本記事執筆のモチベーション">2. 本記事執筆のモチベーション</h2> <p>私が所属する部署ではテーマフリーの LT 会や技術<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>会の定期的な開催があり、そこで時々発表をしています。<br/> これまでは大学時代の専門であった<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B3%D8">言語学</a>(英語・日本語)を主なテーマとしていましたが、今回は社内<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>という実用的なテーマを取り上げました。</p> <p>昨年、とある年次メンテナンスプロジェクトのメイン作業者に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンされた際、文書が散在しており情報の参照や知見の獲得にとても骨を折ったことが原体験です。<br/> 次の年の担当者には同じ思いはして欲しくないと思い、苦労の末必要な情報を集約・体系化して決定版となる Handbook を作成しました。<br/> その経験で得た知見を共有しようと考えましたが、私個人の経験論だけで物事を語るのは良くないと考え、以下のステップを踏むことにしました。</p> <ol> <li>「ソフトウェアエンジニアにとっての<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>とは?」の理論をインプットする。</li> <li>理論を実践するにおいて遭遇する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%EC%A1%BC%A5%C9%A5%AA%A5%D5">トレードオフ</a>を踏まえ、実用的な再現可能な手順を洗い出す。</li> <li>「一般論編」「実践編」の 2 パートに分け、前者は非エンジニアも対象の LT 会で、後者はエンジニアだけが参加する技術<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>会で発表・議論する。</li> </ol> <p>その過程で作成したのが以下のスライドです。<br/> ※ <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>・<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>でご覧の方は初期表示が正しくされないことがありますが、スライド下部の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%CF%A5%A4%A5%D1%A1%BC%A5%EA%A5%F3%A5%AF">ハイパーリンク</a>を押下して別タブを開き、閉じて本記事に戻ってくると正しく表示されるようになります。</p> <p><details><summary>英語版はこちら</summary></p> <p> <script defer class="speakerdeck-embed" data-id="27f7cf3d963b4a11bc7c5b4b8062ce33" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script><div class="speakerdeck-link"><a href="https://speakerdeck.com/hayat01sh1da/science-of-corporate-technical-documentation-for-software-engineers-theories" target="_blank">Science of Technical Documentation for Software Engineers -Theories- by @hayat01sh1da</a></div></p> <p> <script defer class="speakerdeck-embed" data-id="ba0e682efcc34c9d8d24a503c2ccd395" data-ratio="1.7777777777777777" src="//speakerdeck.com/assets/embed.js"></script><div class="speakerdeck-link"><a href="https://speakerdeck.com/hayat01sh1da/science-of-corporate-technical-documentation-for-software-engineers-practices" target="_blank">Science of Technical Documentation for Software Engineers -Practices- by @hayat01sh1da</a></div></p> <p></details></p> <p><details><summary>日本語はこちら</summary></p> <p> <script async class="docswell-embed" src="https://www.docswell.com/assets/libs/docswell-embed/docswell-embed.min.js" data-src="https://www.docswell.com/slide/5M12VG/embed" data-aspect="0.5625"></script><div class="docswell-link"><a href="https://www.docswell.com/s/hayat01sh1da/5M12VG-science-of-corporate-technical-documentation-for-software-engineers-theories" target="_blank">ソフトウェアエンジニアの社内技術ドキュメンテーションへの向き合い方 -一般論編- by @hayat01sh1da</a></div></p> <p> <script async class="docswell-embed" src="https://www.docswell.com/assets/libs/docswell-embed/docswell-embed.min.js" data-src="https://www.docswell.com/slide/KRXJ9J/embed" data-aspect="0.5625"></script><div class="docswell-link"><a href="https://www.docswell.com/s/hayat01sh1da/KRXJ9J-science-of-corporate-technical-documentation-for-software-engineers-practices" target="_blank">ソフトウェアエンジニアの社内技術ドキュメンテーションへの向き合い方 -実践編- by @hayat01sh1da</a></div></p> <p></details></p> <p>この記事では、特に「実践編」の発表を通じて私が学んだことを共有したいと思います。</p> <h2 id="3-ワークショップを通じて得たフィードバック">3. ワークショップを通じて得たフィードバック</h2> <p>技術<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>会では「実践編」の発表を行うだけでなく、そこでエンジニアに簡単なワークショップを行ってもらいました。</p> <p><img width="700" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/8a3e8791-db28-4748-b2b2-776fbe52ea7a" /></p> <ol> <li>個人またはチームが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>において抱えている問題を 'Pains' に列挙する。</li> <li>1 を解決するための取り込みや効果的な解決策があれば 'Approaches/Solutions' に列挙する。</li> <li>その他、参照性・変更容易性・効率を担保するためなどの雑多な秘訣があれば 'Tips' に列挙する。</li> </ol> <h3 id="3-1-Pains--過去抱えた現在進行形で抱えている辛み-">3-1. Pains -過去抱えた/現在進行形で抱えている辛み-</h3> <p>エンジニアが抱えている Pains を要約すると、以下の 3 つに集約出来ます。</p> <ul> <li>集約・体系化されていないことに起因する情報検索コストの上昇</li> <li>執筆作業コストや管理・編集コストに起因する文書の陳腐化</li> <li>口頭や Slack で雑に伝えてしまっているが、幸か不幸かそれで完結出来るので<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>の必要性を感じないという現実</li> </ul> <p>エンジニアにブレストしてもらった以下の内容は、きっとどの開発組織も共通して抱えているものではないでしょうか。<br/> 私自身ヘビーな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>を行った中で感じたことはきっと共感されないと思っていましたが、意外にも同じ Pains を抱える仲間が多くいて安心しました(←するな)。</p> <p><img width="700" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/fb54dd21-a69b-4132-84c2-551646e5d789"></p> <table> <thead> <tr> <th>Category</th> <th>Pains</th> <th>コメント</th> </tr> </thead> <tbody> <tr> <td rowspan="4">情報検索</td> <td>ドキュメントが散逸していて探せない、集約された各場所がない</td> <td rowspan="2">集約・体系化を見越さず「とりあえず」書いたドキュメントが散在すると陥る状況ですね。</td> </tr> <tr> <td>ドキュメントが散在しており探すのが難しい</td> </tr> <tr> <td>情報の検索が職人技</td> <td>ドキュメントの管理を適切に行わないと欲しい情報がスムーズに手に入らないことが往々にしてあります。</td> </tr> <tr> <td>書いても存在を思い出せない</td> <td>集約・体系的でない断片的なドキュメントは時間の経過とともに(物理・記憶ともに)埋もれていきますので、必要なものはブックマークしておくのが良いですね。ブックマークも多くなると散らかってくるので、定期的に整理・断捨離しましょう。</td> </tr> <tr> <td rowspan="5">文書の陳腐化</td> <td>更新すべきドキュメントはあるが、どれを更新すべきか把握しきれない</td> <td>集約されていない断片的な複数のドキュメントが存在するので、どれをアップデートすべきか分からないということですかね。</td> </tr> <tr> <td>どれが最新のドキュメントかが分かりづらい</td> <td>集約されていない断片的な複数のドキュメントが存在する場合に発生するので、ある時点で決定版を作成するのが良いですね。</td> </tr> <tr> <td>ストック情報なのかフロー情報なのかの区別が曖昧</td> <td>ストック情報とは後から何度も見返すことが多い情報のことで、フロー情報とはメールやチャットでやりとりされる一時的な情報のことです。フロー情報の中の重要な情報をストック情報化されていないと起こり得る現象ですので、こまめに清書する習慣が大切ですね。</td> </tr> <tr> <td>ドキュメントが古い</td> <td>Ownership が不在もしくは不明確で、誰もドキュメントの品質保持責任を持っていないと陥る現象ですね。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>に取り組む最初期にきちんと握り合っておくことが大切ですね。</td> </tr> <tr> <td>ドキュメントが更新されない(ドキュメントがあることを知らない)</td> <td>Ownership が不在もしくは不明確、もしくは適切に引き継がれていないことが原因かと思われるので、成果物は然るべき<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%AF%A5%DB%A5%EB%A5%C0%A1%BC">ステークホルダー</a>に惜しみなく広報すると良いでしょう。</td> </tr> <tr> <td rowspan="6">作業コスト</td> <td>ドキュメントを書くのが退屈</td> <td>得手・不得手や向き・不向きがありますが、作業が長期化すると退屈度が増すので、短期決戦で済むような分業体制やワークフロー設計などの工夫をすると良いでしょう。</td> </tr> <tr> <td>プロダクトが絡む仕様などは devs だけでなく <a class="keyword" href="https://d.hatena.ne.jp/keyword/TPM">TPM</a> とも連携しなくてはならず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>コストが一気に跳ね上がる</td> <td>これは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>に限らず技術者だけで完結する業務は多くはないので、非技術者にも理解出来る<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>能力は必須スキルですね。</td> </tr> <tr> <td>ドキュメント化されていない知識がある</td> <td>口頭伝承で完結するのは短期的には楽ですが、中長期的には知見が形に残らないのでこまめに明文化すべきですね。一気に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>をすると作業コストがとても高くなります。</td> </tr> <tr> <td>仕様が複雑過ぎてドキュメントに起こすための調査コストが高すぎると感じる</td> <td>未知の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知見を明文化する場合に発生するケースですね。作業コストが高くつくのは確かに辛みです。一方でその中で知見を獲得し、明文化することで知識に血が通う嬉しみもあるので悪いことばかりではないと思います。</td> </tr> <tr> <td>どこにどんな性質(動的・静的)の情報があるか把握するのに慣れるまで時間がかかる</td> <td>理論(基本的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知見)をインプットとした上で、それだけ実践(実装・テスト)の経験を積んで理解を深められるかによると思います。ある意味でこれは時間が解決するところでしょう。</td> </tr> <tr> <td>ドキュメント業もコードと同じでスキルがいる</td> <td>確かに一定の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>スキルは必要で一朝一夕には身に付かないものです。しかし、特殊能力を要求される訳ではないので、普段の明文化習慣の積み重ねで「当たり前」を鍛えるしかないと思います。</td> </tr> <tr> <td rowspan="2">管理・編集コスト</td> <td>二重管理を避けるために英語で書いても読んでもらえないがち</td> <td>当部署 SRE チームは国内と海外のインフラを管理しているという特殊な背景に起因する事情です。日本語と英語それぞれで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>をすると、両方を同期的に最新化しなければならず管理コストが嵩んでしまいます。それを避けるため英語文書に一本化しているのですが、英語が得意ではないエンジニアには敬遠されてしまうという辛みを抱えています。この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%EC%A1%BC%A5%C9%A5%AA%A5%D5">トレードオフ</a>はどのように解消すべきなのか、現時点で私にも解決策が思い浮かんでいません。</td> </tr> <tr> <td>その内容の記述に至った背景 (合意や議論など) が不明で、変えていいのか分からないことがある</td> <td>古の議論や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>を紐解く考古学をしなければ対象の情報を更新して良いかの判断が付かないことがあります。当事者が既に退職済みである場合はもはや打ち手はなくなるので、少なくとも私たちは後学のためにきちんと記録や知見を明文化して残しておく責任を果たしていきましょう。</td> </tr> <tr> <td rowspan="5">その他</td> <td>「いつもの」みたいな情報(PWなど)って関係者は結局全員把握しているので、ドキュメントに書いてしまってはだめか、または秘匿情報をまとめるドキュメントがあるべきとか?</td> <td>※ 誤解を避けるため念の為補足しますが、ここでの PW は開発ユーザのパスワード等を指しており、本番環境については適切に管理を行なっています。</td> </tr> <tr> <td>読者への告知</td> <td>書いた文書を通して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%AF%A5%DB%A5%EB%A5%C0%A1%BC">ステークホルダー</a>に知見をインプットしてもらうのが目的で、読んでもらわないことには何も始まりません。出来上がった成果物は惜しみなくその存在をアピールすると良いですね。</td> </tr> <tr> <td>雑に Slack のリンクを貼って終わらせがち(反省はしている)</td> <td>共有される側は辛いです。最新の情報と背景をシンプルに知りたいのに、それに至るまでの侃侃諤諤とした議論はノイズとなりがちなので、フロー情報の中の重要な情報は適切にストック情報化しておくと良いですね。</td> </tr> <tr> <td>口伝で伝わっている物事が結構ある</td> <td>コストを短期的な視点ではなく中長期的な視点で捉えると行動が変わってくると思います。</td> </tr> <tr> <td>snippets-ja (<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%CB%A5%DA%A5%C3%A5%C8">スニペット</a>ではない)</td> <td>重要な情報が snippets-ja という雑情報置き場に書かれていることがあります。重要な情報は然るべきタイミングで集約・体系化して決定版ドキュメントを起こすと良いですね。</td> </tr> <tbody> </table> <h3 id="3-2-ApproachesSolutions--Pains-を解消するために取った方策や導き出した解決策-">3-2. Approaches/Solutions -Pains を解消するために取った方策や導き出した解決策-</h3> <p>エンジニアが抱えている Approaches/Solutions は残念ながら文書の管理に関する 2 つしか挙がりませんでしたが、とても大事な意見なので深掘りしたいと思います。</p> <p><img width="700" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/f7e37a7e-3a38-4ff6-acf3-91786ce8af6f"></p> <h4 id="3-2-1-えいやで場所を決め打ちしてしまうeg-GitHub-Wiki--Google-docs-しか使わない">3-2-1. えいやで場所を決め打ちしてしまう(e.g., <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Wiki">Wiki</a> + <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%20docs">Google docs</a> しか使わない)</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>においてまず一番最初に直面することが多いのは「どこに書こう?」という問題です。<br/> 参照/編集のしやすさやリッチな描画などを求めて公開場所を延々と探求すると、いつまで経っても知見を明文化する時がやって来ません。<br/> まずはどこかに知見をアウトプットして具象化(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%AB%A4%A8%A4%EB%B2%BD">見える化</a>)することが大切で、そのためには公開場所を決め打ちしてしまうのが良いです。<br/> その公開場所では都合が悪くなったら、その時により適当な公開場所を検討すれば良いのです。</p> <p><strong>まずは文書化して、集約・体系化したい情報を明文化する</strong>ことが大切です。</p> <h4 id="3-2-2-個人的に20231205時点でみたいな書き方を心がけている">3-2-2. 個人的に、2023/12/05時点で〜みたいな書き方を心がけている</h4> <p>文書化された情報はある時点のスナップショットに過ぎず、放置するとあっという間に情報は古くなって陳腐化してしまいます。<br/> 更新日時が明記されていないと、その情報が最新なのか古いのかの見分けが第<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BB%B0%BC%D4">三者</a>には付きません。<br/> 古い情報を鵜呑みにしてしまうと、正確な情報と乖離が生じて害になりかねません。<br/> 一方、第<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BB%B0%BC%D4">三者</a>が参照することを前提に「2023/12/05時点」というタイムスタンプを明記しておくと、第<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BB%B0%BC%D4">三者</a>は最新情報との差分がある可能性を意識しながら読むことが出来ます。<br/> また、タイムスタンプがあまりに古いものは情報探索対象から外せるので、ノイズを最小限に抑えることが出来ます。</p> <p><strong>どの時点のスナップショットなのかを明記することは読み手に対する思いやりである</strong>、と言えます。</p> <h3 id="3-3-Tips--効果的な手法-">3-3. Tips -効果的な手法-</h3> <p><img width="700" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/200a33a1-99c8-49e3-8007-e8ebc0069dae"></p> <table> <thead> <tr> <th>Category</th> <th>Tips</th> <th>コメント</th> </tr> </thead> <tbody> <tr> <td rowspan="3">ツールの活用</td> <td>ChatGPT に聞く</td> <td>文明の利器の活用は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>においても必須スキルかも知れないですね。</td> </tr> <tr> <td>使用するツール制限するが無理に一つに定めない</td> <td>選択肢がありすぎて逆に選べない状態を回避するため縛りは設けますが、それはあくまで手段であり目的ではないので柔軟に対応するということですね。</td> </tr> <tr> <td>ブラウザの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%A1%BA%F7%A5%A8%A5%F3%A5%B8%A5%F3">検索エンジン</a>に <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> を追加する</td> <td>情報検索対象に <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> も含めることで最新の仕様や要件を色濃く反映したコードから情報を読み取るという意図ですかね。</td> </tr> <tr> <td rowspan="3">文書の管理</td> <td>コードと同じ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理する。</td> <td>同一の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>であれば、コードと文書の更新はワンセットとなるので陳腐化を防ぐのに有効そうですね。</td> </tr> <tr> <td>最終更新日を必ず付ける</td> <td> 「 <a href="#3-2-2-個人的に20231205時点でみたいな書き方を心がけている"> 3-2-2. 個人的に20231205時点でみたいな書き方を心がけている </a> 」と同一の内容ですね。 </td> </tr> <tr> <td><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> で管理する。Pull Request 便利。</td> <td>差分が分かりますし、suggestion 機能はレビュー時にとても便利ですね。</td> </tr> <tr> <td rowspan="2">文書化の準備</td> <td><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>を読む</td> <td>最新の仕様や要件を色濃く反映したコードから情報を読み取るという意図ですかね。</td> </tr> <tr> <td>英語を勉強する</td> <td>英語で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>をすることが要求される場合は必須スキルですね。</td> </tr> <tr> <td rowspan="3">執筆方法</td> <td>真面目に文章で書かない、箇条書きを使う</td> <td>文章にすると文と文の論理展開を気にしなければなりませんが、箇条書きにすることでそれを気にせず書けるので知見のアウトプットが容易になりますね。</td> </tr> <tr> <td>一つの <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%20Docs">Google Docs</a> に書き連ねていく</td> <td>これは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%EC%A1%BC%A5%C9%A5%AA%A5%D5">トレードオフ</a>が存在していて、情報が集約化されるメリットがある一方で、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BD%C5%B8%FC%C4%B9%C2%E7">重厚長大</a>になると検索性が悪くなるデメリットもありますね。</td> </tr> <tr> <td>長大なドキュメントは <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%20Docs">Google Docs</a> でレビューを受けてから <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Issue に転記する(<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Issue だとレビューしづらい)</td> <td>どのツールを使うかはケースバイケースですが、情報の正確性を担保するにはレビューしやすい環境が必要であるということですね。</td> </tr> </tbody> </table> <h2 id="4-オーディエンスからの反響">4. オーディエンスからの反響</h2> <p>「<a href="#3-%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%83%E3%83%97%E3%82%92%E9%80%9A%E3%81%98%E3%81%A6%E5%BE%97%E3%81%9F%E3%83%95%E3%82%A3%E3%83%BC%E3%83%89%E3%83%90%E3%83%83%E3%82%AF">ワークショップ</a>」を行った日とは別日に行いましたが、その日は参加者が少なかったため少数のフィードバックしかもらうことが出来ませんでした。<br/> しかし、その少ないフィードバックはどれも重要な意見だったので漏れなく共有させて頂きます。</p> <h3 id="4-1-気づきや学びNEXT-ACTIONS">4-1. 気づきや学び・NEXT ACTIONS</h3> <p><img width="700" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/f01825e2-a2a9-496c-bbe9-d504bf242bec"></p> <p>私自身の NEXT ACTIONS は以下の 2 つです。</p> <ul> <li>aya-issue の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Wiki">Wiki</a> ページを年明けのリハビリ期間に全部整理する <ul> <li>aya-issues という社内 <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Wiki">Wiki</a> に様々な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知見に関するページが乱立していて検索性が非常に悪いので、年明け仕事に身体を慣らすために全て Handbook として体系化しようと企んでいます</li> </ul> </li> <li>StudySapuri Product Team Blog に今回の発表内容を記事としてエントリーしよう <ul> <li>この記事のことです</li> </ul> </li> </ul> <p>また、別のエンジニアは以下の気づきと学びを書き出してくれました。</p> <ul> <li>「読み手に知見をインプットしてもらうことが前提なので、書いたらちゃんと広報する」という点について、個人的にもっとできる部分があるなと感じました <ul> <li>書いた文書を通して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%AF%A5%DB%A5%EB%A5%C0%A1%BC">ステークホルダー</a>に知見をインプットしてもらうのが目的であり、読んでもらわないことには始まりませんので、出来上がった成果物は惜しみなくその存在をアピールすべきだと考えます</li> </ul> </li> <li>入社したころは未来の自分の記憶を信用しまくっており忘れたりしていたので、「信用しない」という共通点にはとても共感しました <ul> <li>人間は忘れる生き物なので、それを見越して情報を整理して残しておく必要があります。</li> </ul> </li> </ul> <h3 id="4-2-プレゼンターhayat01sh1daへのフィードバック">4-2. プレゼンター(@hayat01sh1da)へのフィードバック</h3> <p>スライドの構想〜執筆まで 2 ヶ月ほど費やして「社内<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>とは?」のテーマに本気で向き合った甲斐がありました。<br/> とても嬉しいコメントです。<br/> 弊部署の各チームの文化として根付くような活動に繋げられると組織全体でより良くなると思いますが、まずは私の所属するチームにしっかりと根付かせて成果を出す足元を固めるという草の根活動から着実に進めて行きたいと思います。</p> <p><img width="700" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/d0aa612e-5b9a-4a67-b343-831aa6e9595c"></p> <blockquote><p>いい話だった。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>のわかり手として、横断的に展開していって欲しいと思った</p></blockquote> <h3 id="4-3-Slack-での反応">4-3. Slack での反応</h3> <p>Slack 上の実況中継では以下のような感想が流れていたので、キャプチャでご紹介します。</p> <p><img width="503" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/a5180715-81fb-4d2d-bf12-f2700b949a02"></p> <p><img width="151" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/78295ede-1552-48d3-8289-aa85dede4b94"></p> <p><img width="424" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/c387108b-23f1-4496-a1f9-e0d6f787496d"></p> <p><img width="194" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/ff16ea6b-9ca8-4ad9-9b29-bce155cd9817"></p> <p><img width="646" src="https://github.com/quipper/monorepo-deploy-actions/assets/37478830/10e3daa4-9bbe-4ca4-8886-cc6a9d3913ef"></p> <h2 id="5-おわりに">5. おわりに</h2> <p>構想〜執筆と、学生時代〜社会人までの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>スキルの磨き込みや経験の積み重ねに相当の時間を費やし、万全の準備をして発表に臨みました。<br/> ただ、その分<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BD%C5%B8%FC%C4%B9%C2%E7">重厚長大</a>な内容になってしまい、ご清聴頂いたエンジニアには負担をかけて申し訳ない気持ちになりました。<br/> この記事を読んで下さっている読者の方も同じ感覚をお持ちになるかも知れません。</p> <p>ですが、それだけの思いがあるのだと受け取って頂けると幸いです。<br/> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>はその難易度と重要度の割に過小評価されている業務であり、そのスキルも単体では高く買われることは多くありません。<br/> しかし、サービスを作り提供する仕事に就いている以上、業務が技術者だけで完結することは少なく、技術者・非技術者の双方が理解出来る<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>能力はソフトウェアエンジニアにとってとても重要なスキルです。</p> <p>この記事を通して、読者の皆さんが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>について考えるきっかけやヒント、Tips を得て下さればとても嬉しく思います。</p> hayat01sh1da 入稿devsチームで、校閲PDF出力機能のリニューアルをおこなった話【2/2 振り返り編】 hatenablog://entry/6801883189059313422 2023-11-20T09:00:00+09:00 2023-11-20T09:00:02+09:00 こんにちは、スタディサプリで開発をしている @motorollerscalatron です。 前編では、「入稿」「校閲」ドメインの発足からサブチームへの切り出しと、校閲出力サービスのリニューアルのエピソードを技術的観点を交えながら共有させていただきました。この後編では、プロジェクトが完成しきった時点での自身の省みを中心に共有させていただきます。 今回の案件で、問題解決に対して大事だと感じたこと 知識をもっている人をうまく引き込むためには、質問は端折らない ペアプロと壁打ち 少しでも難しく見えてきたら issue を分割・追加/1回で完璧なものを作ろうとしない まとめ 今回の案件で、問題解決に対… <p>こんにちは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリで開発をしている @motorollerscalatron です。</p> <p>前編では、「入稿」「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>」<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>の発足からサブチームへの切り出しと、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力サービスのリニューアルのエピソードを技術的観点を交えながら共有させていただきました。この後編では、プロジェクトが完成しきった時点での自身の省みを中心に共有させていただきます。</p> <ul class="table-of-contents"> <li><a href="#今回の案件で問題解決に対して大事だと感じたこと">今回の案件で、問題解決に対して大事だと感じたこと</a><ul> <li><a href="#知識をもっている人をうまく引き込むためには質問は端折らない">知識をもっている人をうまく引き込むためには、質問は端折らない</a></li> <li><a href="#ペアプロと壁打ち">ペアプロと壁打ち</a></li> <li><a href="#少しでも難しく見えてきたら-issue-を分割追加1回で完璧なものを作ろうとしない">少しでも難しく見えてきたら issue を分割・追加/1回で完璧なものを作ろうとしない</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="今回の案件で問題解決に対して大事だと感じたこと">今回の案件で、問題解決に対して大事だと感じたこと</h2> <h3 id="知識をもっている人をうまく引き込むためには質問は端折らない">知識をもっている人をうまく引き込むためには、質問は端折らない</h3> <p>今回の作業では、結果、私自身はメインで開発をしつつも、フロントエンド部分であれば @indigolain さんとは定期的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DA%A5%A2%A5%D7%A5%ED">ペアプロ</a>を組んでもらうようにしました。 @indigolain さんはチームとしても近く、今回ある程度リソースを割いてもらうことも合意していたので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DA%A5%A2%A5%D7%A5%ED">ペアプロ</a>のペースはスケジュール感を持ちつつも、ある程度質問の切り口をフランクな形で伝えることができました。</p> <p>一方、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>実装後の実際のマイクロサービス化については、複数メンバーのいる SRE へのグループとしてのメンションから始めて、どういったテンプレートが参考になるか、をきくところから始めていきました。開発部内の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>は、基本的に開発メンバーであれば他の PJ のものも見られるので、新しいサービスを作るときには、特に、社内に似た事例を持っているかをコード検索で調べていくことができます。一方で、実際のやりたいことに対する正解、というのは必ずしも過去例が既にあるとも限りません。</p> <p>確かに、新規にマイクロサービスを立てるときに共通で必要になるようなテンプレート的な設定があるので、ある程度のアウトラインは見えていたとも言えます。そうやってチェックリスト化できていたものについては指示をうけつつファイルを揃えていくことができたものの、前編で述べたような新しい設計にしたことで、自分でも気がつかないうちに、他の既存サービスにはないようなイレギュラーな面も出てきていたことが、SRE とのコミュニケーションの中でわかってきました。</p> <p>この時、(自分の近くにいるメンバーでなく)もう一つ外側にいるメンバーとでは、少し伝え方のニュアンスに注意が必要だということを悟りました。というのも、私は自分の想定の部分で、 <em>ここは既存の設定の焼き直しでできる、と思い込んでいた</em> のですが、SRE には自分が新しくやろうとしていること正確に伝えられていない/確認ができていない状態で、仮説でできると思った延長で先に「こういう設定をしてほしい」というコミュニケーションを始めてしまっていたのでした。</p> <p>一般的に、自分の一つ外側の集団に属する人に質問をする、というのはきく側・きかれる側、双方に内輪でのコミュニケーションより大きいコストが見込まれると考える傾向があると思います。開発の後半での特にサービス間通信を含んだような疎通確認や、新規で書いたインフラ部分の設定の作業では、コミュニケーションの最初に先にやりたいことをクリアに伝えていられるとよかったんだな、と感じました。</p> <p><figure class="figure-image figure-image-fotolife" title="質問をなるべく小さくしようとして、若干端折ってしまいがち"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116190640.png" width="1200" height="943" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>質問をなるべく小さくしようとして、若干端折ってしまいがち</figcaption></figure></p> <p>私は、自分で調べていくと(中途半端な好奇心が先走ったり、それぞれ別の担当業務を持っているであろう開発グループの集団心理を変に推測してしまったり、で)、自分で前もって調べている量が多ければ多いほど、相談相手にとってコストが少なるかのように思い込んでしまいます。今回の開発の後半フェーズでは、PR ごとに push 後の CI を通してしか確認ができないものがでてきたり、中には SRE 側との密な連携をしながら先に進めていく必要のある作業が出てきていました。この時、質問時に伝えているもともとやりたかったこと、がわかりやすい形で残っているかどうかで、結果、継続的なコミュニケーションの質に大きく差が出てきました。</p> <p>上の slack の例は、いつもお世話になっている SRE <a href="https://github.com/kyontan">@kyontan</a> さんに、ついつい先行して伝えた情報の鮮度などを垣間見ずに書いてしまったと思っているもので、この後 PR は少しずつ完成して進めていけたのですが、エラーの内容自体がうまく理解できてないのに、それを要約して伝えようとしてしまったのですが、結果、そこで質問に質問を被せる形のコミュニケーションを生み出してしまうこともありました。</p> <p><figure class="figure-image figure-image-fotolife" title="伝わりにくかった部分を端的に切り直して質問を書き直してみた例"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116191111.png" width="1200" height="563" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>伝わりにくかった部分を端的に切り直して質問を書き直してみた例</figcaption></figure></p> <p>これは、スレッドのメッセージ数が3桁ぐらいになったところで、最初に言おうとしていたことを、改めて別のフォーマットで同じ質問をした例です。 ご存じ <a href="https://github.com/chaspy">@chaspy</a> さんの誘導もあって、私も改めて最初の前提として伝えたかったのかが、この3つであることに気がついたのでした。文章の長さは同じくらいですが、質問を受け取る側として、どのあたりに回答を期待しているのかの感じ方には、大きく差が出るはずです。 (本当のところは、同じスレがずいぶん受け答えで追いづらくなっていたので、当時 @kyontan さんの ボスであった @chaspy さんがいいタイミングでヒントをくれた感じです)</p> <p>結局、このやりとりの中で、 <code>tara-content-data</code> (前編の説明で出てきていた、「学習画面問題コンテンツ」の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>名) へのアクセスには、このサービス専用の <a href="https://docs.github.com/en/apps/creating-github-apps/about-creating-github-apps/about-creating-github-apps">GitHub Apps</a> を用意するのが最適だ、という結論にたどりついたのでした。私自身は、今回のサービスは既存で同じアクセスのパターン(と思い込んでいて)既存の <a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a> token の利用を前提として実装をしたままだったのですが、 その妥当性をSRE に確認するのを遅延していたのを、すっかり忘れていたのでした。</p> <h3 id="ペアプロと壁打ち"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DA%A5%A2%A5%D7%A5%ED">ペアプロ</a>と壁打ち</h3> <p>Next.js の <a class="keyword" href="https://d.hatena.ne.jp/keyword/SSR">SSR</a> や、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Apps 等を利用する部分などを含めて、今回の開発は、「自分は使ったことがないのだが、識者の云うことによれば、これでできるはずだ!」を1つ1つ証明しながら進んでいくような、不確定要素が大きい開発でした。 識者との<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DA%A5%A2%A5%D7%A5%ED">ペアプロ</a>は今回も何回もお願いをしていた中、(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DA%A5%A2%A5%D7%A5%ED">ペアプロ</a>、モブプロ自体については、私は以前に<a href="https://blog.studysapuri.jp/entry/2020/08/03/mob-programming-and-onboarding-in-coaching-team">記事化</a> させてもらっているので、気になる方は、そちらを是非参照下さい。)、開発メンバーは、皆、並行して業務を持っているのので、全く理想通りに時間を合わせられるとも限りませんでした。そんな時は、担当者でないようなメンバーとの壁打ちも有効に活用しました。</p> <p>私は、思い込みが強くて、ふと時間がたつと仮定を残したまま調査に踏み切っていることを忘れて、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> 検索で文字列的なマッチ度の高いものを見ては、「世の中に、いかにも今体験しているのと近い現象があるじゃないか」→「これが怪しい!」と短絡的に因果関係を推測したものを、そのまま共有して騒いでしまいがちだという自覚があります。そして、この共有した瞬間は、覚えていた仮説と検証済み部分の境界を、時間がたつとともに、いとも簡単に忘れてしまいます。</p> <p>日々の業務では、どうしても時間的な制約ときりのいい報告の単位がある以上、不正確な部分を全て排除するまで報告を遅延する、というのも現実的ではないでしょう。ある程度のわだ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A4%AB%A4%DE%A4%EA">かまり</a>は含んだまま、それは「ある程度わかりやすい話の流れにした状態で、まわりの人に打ち明けたほうが話の入り口としては入りやすいのでは?」と思う自分も中にいて、細かいニュアンスを伝えるのはいったん人を引き込んでしまわないと、先に進めない。そんな時、「どこまでが試した組み合わせか?」「影響する因子は洗い出せているか」など見直すには、ほどよく自分に近いチームメンバーとの壁打ちを利用すると良い、ということを、より実感しました。</p> <p>これは壁打ちの相手は、必ずしも前提などを知っているかどうかは、あまり気にしません。それよりかは、自分の考えの抜け穴などを、側で聞いてもらって、指摘してもらったり(時には自分で話している段階でおかしいと気づくことすらあります)することを狙っているので、時には自分と思考回路のパターンを辿らないよう、あえてその案件をあまり一緒に見ていない人がよかったりします。後半、Next.js による実装部分が完成したあとは、 @indigolain さんは本来の所属プロジェクトのほうに専念することになっていたので、私は今までのような壁打ちの相手を失っていました。そんな時、チームの相方の <a href="https://github.com/teppei-kitagawa">@teppei-kitagawa</a> さんに壁打ちをよく依頼しました。teppei さんには、私が今回この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力のリニューアルをしている間、入稿チームの他の色々な開発案件を担当してもらっていて、普段の daily では進捗を伝えたりしていたものの、コードの詳細などを一緒に途中でみてもらうのは、PR レビュー以外のタイミングではあまり行なっていませんでした。それでも、毎回壁打ちをするたびに、自分の見落としていた内容が何かしら明らかになる体験をしました。</p> <p>壁打ちをする目的は、気づきだと思っています。「これのここを質問したい」と自分が思っている内容自体に対しての直接的な回答ではない部分に収穫があると考えれば、必ずしも今課題と認識している問題範囲でのプロである必要はないと思います。ですので、「ここを知っている ◯◯ さんの手があくまで待つ・・」というよりは、きてくれる人が見つかったら、その人とのセッションを優先しました。スケジュールも必ずしも先に入れるとも限らず、カレンダーの空きを見つけて 直近の日時で予定を送ることもあれば、朝のチームの daily ミーティングで話して、画面共有をそのまま使って延長でコードや <a class="keyword" href="https://d.hatena.ne.jp/keyword/VSCode">VSCode</a> を開く、という導入の仕方で行うこともありました。</p> <h3 id="少しでも難しく見えてきたら-issue-を分割追加1回で完璧なものを作ろうとしない">少しでも難しく見えてきたら issue を分割・追加/1回で完璧なものを作ろうとしない</h3> <p>今回紹介したリニューアルの開発業務は、入稿 devs というチームの開発タスクの1つとして位置づけた以上、issue 化はもちろん行っていました。 入稿 devs チームでは、他の開発チームと同じように、2週間単位のスプリント開発は踏襲しており、その流れで、issue にはストーリーポイントをふるのは、本件でも変わりありませんでした。しかし、このポイントの見積もりの目的は、規模感の相対的な見積もりであり、途中、チームのベロシティの値としての見え方で時折不安になるものの、実際値が(見積もりから)外れてしまうことには、それほど神経質にはならないようにしました。それであっても、他の案件に比べると、かなり「ここが最大の山場だろう」という報告を何度か繰り返しては、すぐ次に別の問題が構えていることも多かったです。そうした時は、 issue を分割することにしました。</p> <p>当初、PBI issue の1つとして、この案件を定義したとき、分割 issue は以下の3つにとどまっていました。</p> <ul> <li>PDF 出力の設計</li> <li>(実装)<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の作成</li> <li>(実装)刷新版エンドポイントが見にいく Next.js server を動かす</li> </ul> <p>マイクロサービスが独立して立つことは視野に入れつつも、この段階ではこの3つの分け方と、相対的なストーリーポイントの割り振りにとどまりました。</p> <p>一方、以下は issue クローズ時に結果として出たものです。</p> <p><figure class="figure-image figure-image-fotolife" title="プロジェクト終了時点で、結びついた 6 つの issue。3 つ目の puppeteer アップデートなどは、まさか入ると思わず"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116191622.png" width="1200" height="765" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>プロジェクト終了時点で、結びついた 6 つの issue。3 つ目の puppeteer アップデートなどは、まさか入ると思わず</figcaption></figure></p> <p>もともとあった issue とは別に、「puppeteer アップデート(3 つ目)」「選択肢シャッフルが ON かどうか(5 つ目、比較的コードベースの近い改修を、ついでに対応したもの)」などが加えられています。 また、最初の issue 起票で「Next.js server を動かす」としていた issue も、後で2つの issue「Next.js server を動かすための tara アプリケーションの開発(2 つめ)」「Next.js server を動かすためのサービス立ち上げ(1 つめ)」 に分割しました。これらの分割は、想定より大きく掛かってしまっているという心理から、最初は自身ではなかなか気が進まなかったものです。しかしながら、時間の経過とともに、issue のスコープが知らぬうちに膨れていたりするのを見直す上でも重要でした。( Estimate に表示されているポイントは、後から実績値として入れ直したものなので、最初とは規模が変わったりもしました。)</p> <p><figure class="figure-image figure-image-fotolife" title="issue分割"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116191454.png" width="1200" height="488" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>issue分割</figcaption></figure></p> <p>また、試行錯誤の多かった Next.js <a class="keyword" href="https://d.hatena.ne.jp/keyword/SSR">SSR</a> 部分の PR の作成では、ア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>を全て試していいものを採用したかったので、</p> <ul> <li>1つの実験パターンは1つの PR で実装(コミットで詰んで全部表現、とはしなかった)</li> <li>寿命がある程度長くなったら、部分的なマージができない限りは、いったん捨てる</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/RFC">RFC</a>, PoC など位置付けは明確にしておき、スコープがずれないようにする</li> </ul> <p>などの心にとどめながら、少し回り道でも、後から決断の正当性を客観的に保証できるようにしていました。 お陰でなのか、普通、こういった機能のリリースは直後に予期せぬ不具合が出たりしがちで、内心ひやひやしていたのですが、想定した動作レベルで問い合わせもほとんどない状態で、使ってもらっています。</p> <h2 id="まとめ">まとめ</h2> <p>以上、今回は前編・後編と通じて、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%EA%A5%D7%A5%ED">プリプロ</a>ダクト開発の中でも、ちょっと毛色の変わった<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>の紹介と、その開発の事例について語らせていただきました。また、この記事では、新しいサブチーム「nyuko devs」の紹介を若干兼ねたつもりでもあります。 最後までお読みいただき、ありがとうございました。</p> motorollerscalatron_manabi 入稿devsチームで、校閲PDF出力機能のリニューアルをおこなった話【1/2 入稿ドメインとは?〜開発事例編】 hatenablog://entry/6801883189059302640 2023-11-20T09:00:00+09:00 2023-11-20T09:00:02+09:00 こんにちは、スタディサプリで開発をしている @motorollerscalatron です。 私は、5 年前に web エンジニアとしてスタディサプリに join していますが、今年に入ってから、今までの社内(中学講座の開発プロジェクト(通称 tara、最近は 小学講座も加わっています) の web アプリケーション開発に比べて、少し細分化されたドメイン領域である「入稿」のコード開発と運用をおこなっています。今回は、その中でNext.jsを使った校閲出力に特化したマイクロサービスを1つ立てることになったプロジェクトが最近完成したので、そのお話をさせてください。 「入稿」というドメイン領域の切り… <p>こんにちは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリで開発をしている @motorollerscalatron です。</p> <p>私は、5 年前に web エンジニアとして<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに join していますが、今年に入ってから、今までの社内<a href="https://studysapuri.jp/course/junior/">(中学講座</a>の開発プロジェクト(通称 tara、最近は <a href="https://studysapuri.jp/course/elementary/">小学講座</a>も加わっています) の web アプリケーション開発に比べて、少し細分化された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>領域である「入稿」のコード開発と運用をおこなっています。今回は、その中で<a href="https://nextjs.org/">Next.js</a>を使った<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力に特化したマイクロサービスを1つ立てることになったプロジェクトが最近完成したので、そのお話をさせてください。</p> <ul class="table-of-contents"> <li><a href="#入稿というドメイン領域の切り出し">「入稿」というドメイン領域の切り出し</a></li> <li><a href="#なぜ新規校閲出力システムを作ることになったか">なぜ新規校閲出力システムを作ることになったか</a><ul> <li><a href="#理由その1既存校閲出力機能を仕上げたとき理想的な形ではなかったこと">理由その1:既存校閲出力機能を仕上げたとき、理想的な形ではなかったこと</a></li> <li><a href="#理由その2タイミング">理由その2:タイミング</a></li> </ul> </li> <li><a href="#実際の開発エピソード">実際の開発エピソード</a><ul> <li><a href="#各コンポーネントの設計として視野に入れたこと">各コンポーネントの設計として視野に入れたこと</a><ul> <li><a href="#Ver1">Ver1</a></li> <li><a href="#しかしながら">しかしながら、、、</a></li> <li><a href="#Ver2">Ver2</a></li> <li><a href="#思わぬ落とし穴">思わぬ落とし穴</a></li> </ul> </li> <li><a href="#SSR-のために独立したマイクロサービスを立ち上げる">SSR のために独立したマイクロサービスを立ち上げる</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="入稿というドメイン領域の切り出し">「入稿」という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>領域の切り出し</h2> <p>サービスの話をする前に、今回の話の中で扱う「入稿」、そして「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>」という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>用語を最初に軽く説明します。</p> <p>下図に示した通り、コンテンツはさまざまなフォーマットで制作され、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリのシステムで扱うことができる専用の形式で <a class="keyword" href="https://d.hatena.ne.jp/keyword/CMS">CMS</a> にアップロード(入稿)されています。 このコンテンツ制作の過程の中で、学習コンテンツとしての内容の正しさを担保するため、社外の専門家にコンテンツの内容をチェックしていただくフェーズがあり、それを「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>」と言います。</p> <p><figure class="figure-image figure-image-fotolife" title="スタディサプリのコンテンツ入稿システムと、各ステップで使われているツール群"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116170648.png" width="1200" height="670" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリのコンテンツ入稿システムと、各ステップで使われているツール群</figcaption></figure></p> <p>これらコンテンツ制作に関わる、<a class="keyword" href="https://d.hatena.ne.jp/keyword/CMS">CMS</a> や <a class="keyword" href="https://d.hatena.ne.jp/keyword/CMS">CMS</a> に付随するツールの開発・運用・保守を担当しているのが、私が所属する<code>tara-contents-nyuko-devs</code>(以下 nyuko devs)です。 コンテンツとシステムの繋がりに関すること全般を、社内では「入稿」と呼ぶことがあり、そこから名付けられました。</p> <p>「入稿」という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>を、tara 開発部内で詳細<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>の一つとして切り出し、nyuko devs チームが発足されたのは、わりと最近のことでした。このチームは入稿業務に関する各種 <a class="keyword" href="https://d.hatena.ne.jp/keyword/CMS">CMS</a> の技術的な側面からの開発・運用・保守を行うことになっています。2022 年の tara のローンチのタイミングから、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>の必要性とシステム的なソリューションは、当然視野に入っていました。</p> <p>長年の既存システムでは、入稿のツールは社内で公開される管理画面の形で提供されていました。管理画面型で運用するうちに発覚していた運用と拡張性の問題に対する反省から、 後発である tara プロジェクトでは、コンテンツ管理の目的でこのような管理画面自体を開発する代わりに <a class="keyword" href="https://d.hatena.ne.jp/keyword/VScode">VScode</a> の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>として提供されるプレビュー画面を用いたり、学習画面に出される問題コンテンツそのものを構造も含めて <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> 管理にすることで、開発・運用コストを抑えるようにしました(<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> 画面が提供する各種のバージョン管理インタフェースもコンテンツ運用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>とマッチしていました)。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>の特性上、チームの管轄となるサービスなどのコードベースの境界線も独特となっています。主要サービスの開発チームでは、サービスごとにきっちりチームの担当が分かれやすい傾向がありますが、nyuko devs チームでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%EC%A5%D3%A5%E5%A1%BC%B5%A1%C7%BD">プレビュー機能</a>のような独立したサービス以下であることもあれば、学習画面フロントエンドの開発チームとオーバーラップする部分があったり、という面白みもあります。</p> <p>なお、一般的に「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>」というと、紙で印刷するものに対する修正指摘のような形でその中にフィードバックを含めていく編集作業部分も含めて考えられるかと思いますが、今回システム的に話すのは、この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>の出発点となる学習画面出力をキャプチャして PDF 出力を行うところ、に焦点をおくこととします。</p> <h2 id="なぜ新規校閲出力システムを作ることになったか">なぜ新規<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力システムを作ることになったか</h2> <p><figure class="figure-image figure-image-fotolife" title="既存校閲出力システム"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116173646.png" width="1200" height="528" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>既存<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力システム</figcaption></figure></p> <p>上の図は、既存の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力システムの概略フローです。</p> <ol> <li>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>を行いたいユーザーが <a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a> (Jenkins) job を実行。</li> <li>tara-content が Puppeteer を動かして tara.yml 情報を <a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a>-view に渡す</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a>-view が受け取った情報を元に描画される (Puppeteer は描画待ち)</li> <li>描画されたのを確認した後 Puppeteer が Page.PDF() を使って画面の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を撮る</li> <li>生成した PDF 群を zip 圧縮し、 S3 へアップロード</li> <li>実行結果は Jenkins ログとして出力</li> </ol> <p>tara-content は入稿<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>に属する別の既存サービスです。ここでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> を input すると、それを描画に必要な HTML に変換してくれているコードを持っている状態でした。</p> <p>学習画面の問題コンテンツの <a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> ファイル (以下、 <code>tara.yml</code> と表記 ※) は、前項の図で示した通り、学習問題コンテンツの <a class="keyword" href="https://d.hatena.ne.jp/keyword/CMS">CMS</a> としての <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>上のデータとして登録されており、プログラム側で利用する際には、 <a href="https://github.com/octokit/rest.js"><code>@octkit/rest</code></a> のライブラリを使って取得を行うようなコードになっています。</p> <p>※ 学習問題コンテンツは厳密には <a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> ファイルは構造上の一部で、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>には講座・講義などの階層を表現するために別のファイル構造も含んだ形で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に登録されています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> のインタフェースは、もともと差分確認やコード検索性には優れていて、当然バージョン管理もできるので、こういった管理には便利である一方、ある種の情報はなかなか検出しづらかったり、CI との相性が良くない差分、というのも運用の中で、発覚してきました。そちらはまた別の機会にお話させていただこうと思います。</p> <p>新規の tara <a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力システム改善というテーマは、前半期から構想があったものの、今期リソースをあてることには理由がありました。</p> <h3 id="理由その1既存校閲出力機能を仕上げたとき理想的な形ではなかったこと">理由その1:既存<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力機能を仕上げたとき、理想的な形ではなかったこと</h3> <p>ひとつは、既存の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力システムがいくつか構造上の問題を抱えていたことです。Puppeteer の専用メソッド( [Puppeteer <a href="https://pptr.dev/api/puppeteer.page.pdf">Page.PDF()</a>] ) こそ既に使っていたものの、既存学習画面表示<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を再利用した最低限の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>でエンハンスしてプレビューのエンドポイントとして振る舞うように実装していたため、特に縦書き(国語)のコンテンツを扱う際に、本来画面では横スクロールになって出るものに問題がありました。</p> <p><figure class="figure-image figure-image-fotolife" title="PDF 化時のページの切り出しイメージ。縦書き(右)の場合は、コンテンツの続いていく方向とページの出力していく方向が異なるため、PDF に収めるための加工を行なっていた"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116175158.png" width="1200" height="642" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>PDF 化時のページの切り出しイメージ。縦書き(右)の場合は、コンテンツの続いていく方向とページの出力していく方向が異なるため、PDF に収めるための加工を行なっていた</figcaption></figure></p> <p>当時は、残り時間リソース的な問題から、この横に長い出力をページ横幅に収まる形で切り分けて縦に並び替えなおしたものを PDF 化することで解決していました。 この時、通常の横書きのテキストでのコンテンツでは、ちょうどブラウザで見た時そのままの感じで、横幅が固定された状態で、中身が増えるごとに縦に伸びていく形で自然に PDF としてキャプチャするので、動作に問題はありませんでした。その一方で、縦方向に切れ目の存在する PDF 形式にしたとき,ページの境目に文字が来てしまうことがありました。これらの一時退避策としては、軽量なエンハンスとして「画像出力と PDF 出力を選択可能にする」「出力の倍率指定」などを入れましたが、根本的な解決は難しく、また、小手先で修正を入れることで別の不具合要因を生むこともありました。</p> <p><figure class="figure-image figure-image-fotolife" title="既存の校閲 PDF 出力では、縦書きの出力では内容次第でページ切れ目にコンテンツが来てしまい、校閲に支障があった。"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116174916.png" width="1103" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>既存の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a> PDF 出力では、縦書きの出力では内容次第でページ切れ目にコンテンツが来てしまい、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>に支障があった。</figcaption></figure></p> <h3 id="理由その2タイミング">理由その2:タイミング</h3> <p>もうひとつは、需要的なタイミングとこの課題の解決のためのリソース確保のタイミングでした。入稿チームが発足したのは今年の初めだったのですが、発足した当時は年度末と重なり、コンテンツ制作の繁忙期にもあたっていたため、この手の開発系かつ大きめの案件より、運用ベースであがってきているシステム的な問題を解決するほうに注力していました。4 月を超えると、そういった課題は落ち着く一方で、最近リリースのあった小学コンテンツのリソースに向けて大量に入稿を行うことが予測されました。そういった中、何回か保留課題としていたこの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力システムの改善を行うタイミングは今が効率が良い、ということになり、チームのカンバン上の PBI issue の1つとして、扱うことになりました。 この時、既に問題の複雑度が他の関連開発案件により垣間見えていました。「入稿<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>の中でも、特に既存学習画面<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>思想や構造の理解が必要である」「Next.js に関する知見・試行錯誤が必要になる」といった観点で、厳密に nyuko devs チーム所属ではないのですが、フロントエンド開発で活躍している <a href="https://github.com/indigolain">@indigolain</a> さんの力を大きく借りることになりました。</p> <h1 id="実際の開発エピソード">実際の開発エピソード</h1> <h2 id="各コンポーネントの設計として視野に入れたこと">各<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の設計として視野に入れたこと</h2> <h3 id="Ver1">Ver1</h3> <p><figure class="figure-image figure-image-fotolife" title="校閲出力システムのサービスコンポーネント図(関連する部分をある程度抽象化して抜粋)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116181340.png" width="1200" height="751" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力システムのサー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D3%A5%B9%A5%B3">ビスコ</a>ンポーネント図(関連する部分をある程度抽象化して抜粋)</figcaption></figure></p> <p>上記は Ver1 のシステム図となります。(※ <code>yaki-tarako</code> というのは今回<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a> PDF 出力を行うサービスについて開発部内でつけた通称のようなものです )図にもありますが、アクションの順番は</p> <p>[トリガー] (<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>を使いたいユーザーが yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a> (Jenkins) job を実行。</p> <ol> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a> Jenkins job) content_code と対象 branch 名をリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トパラメータに乗せる</li> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a>-view-endpoint) content_code と対象 branch(*1) を元に <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> から <a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> を取得</li> <li>(tara-content)(※2) tara.yml を元にコンテンツ描画に必要なデータを取得してくる</li> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a>-view-endpoint)描画に必要なデータを component に渡す</li> <li>(React) ReactDOM の <a href="https://react.dev/reference/react-dom/server/renderToString"><code>renderToString</code></a> で HTML String へ変換</li> <li>変換した HTML string を画面に描画</li> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a> Jenkins job) Puppeteer ライブラリの PDF メソッドで画面 PDF として出力したものを job 内で保存する</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>を使いたいユーザーは job が完了したら保存された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a> PDF を取得する</li> </ol> <p>としていました。</p> <p>(※1) 先で述べていたように、 コンテンツの管理自体を <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> で行っている関係で、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力を出すときは master のものではなく、それぞれ作業者が作っているブランチの中で登録している <a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>対象とする必要があります。</p> <p>幸い、初期開発時に既に <code>renderToString</code> を使ってテスト実装をしたコードが残っていました。新しい実装によって、既存システムで存在していた諸問題とどの程度干渉するのか?どの程度同時に解決するか?を見てみたいのもあって、まず、この延長で実装を行って、どう動くのかを試していきました。</p> <h3 id="しかしながら">しかしながら、、、</h3> <p>この形で実際に実装していくと、いくつかの問題がありました。</p> <ol> <li>content_code はキー情報であって、実体でない:<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>へ問題データをとりにいく時に、content_code を渡すことで問題データを fetch しにいく時に find, fetch という2段階になり冗長になる</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/css">css</a> 外部ファイルの参照: <code>renderToString</code> を使っていることで、数式の表示であったり、装飾などに使う一部の外部参照の <a class="keyword" href="https://d.hatena.ne.jp/keyword/CSS">CSS</a> に問題があった</li> </ol> <p>1 つめは、当初 URL パラメータになることを前提として考慮して、それなら URL が簡潔な長さ・表記になるといいだろう、という思想から設計したものでした。ですが、 <a href="https://github.com/octokit/rest.js"><code>@octkit/rest</code></a> が提供しているメソッドを組み合わせて特定コンテンツを取得する部分については id をキーにした設計としてしまうと、ほしいのは1つのデータなのにまずレポジトリの全体のデータを取得しなければならず、さらに実際のデータの中身は条件で見つけた <a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> ファイルの中身を取得するためにもう1度 fetch を行う、という形になって、あまり効率がよくないことがわかりました。</p> <p>2 つめは、これは実装をしてみないと分からなかった部分になるのですが、一部の問題を表現するような <a class="keyword" href="https://d.hatena.ne.jp/keyword/CSS">CSS</a> に関し、外部ベンダーが <a class="keyword" href="https://d.hatena.ne.jp/keyword/CDN">CDN</a> 経由で <a class="keyword" href="https://d.hatena.ne.jp/keyword/CSS">CSS</a> を提供し(たとえば、数式表示に使う <a href="https://github.com/KaTeX/KaTeX">KaTeX</a> のようなもの)、さらにその <a class="keyword" href="https://d.hatena.ne.jp/keyword/CSS">CSS</a> の中から画像が参照される、といった組み合わせがあったときに、 <code>renderToString</code> が思ったようにパスを解決をしてくれずに、思った通りに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>されない(あるいは、問題に対応する形でファイルや実装を加えていくうちに、思っていたフォルダ設計より複雑化していく)、といった問題が発生しました。</p> <h3 id="Ver2">Ver2</h3> <p>次のバージョンでは、 Puppeteer が見にいくエンドポイントで Next.js の <a href="https://nextjs.org/docs/pages/building-your-application/rendering/server-side-rendering">Server-side Rendering (SSR)</a> を使うようにしました。</p> <p>Ver1 でわかったパラメーターの修正も含め、Ver2 は以下のようになりました。</p> <p><figure class="figure-image figure-image-fotolife" title="校閲出力システムのサービスコンポーネント図(Ver2)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116181509.png" width="1200" height="754" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力システムのサー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D3%A5%B9%A5%B3">ビスコ</a>ンポーネント図(Ver2)</figcaption></figure></p> <p>[トリガー] (<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>を使いたいユーザーが yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a> (Jenkins) job を実行。</p> <ol> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a> Jenkins job) 問題ファイルのフルパス と対象 branch 名をリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トパラメータに乗せる</li> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a>-view-endpoint) content_code と対象 branch を元に <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> から <a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> を取得</li> <li>(tara-content) tara.yml を元にコンテンツ描画に必要なデータを取得してくる</li> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a>-view-endpoint)描画に必要なデータを component に渡す</li> <li>(Next.js) pages で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a></li> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a> Jenkins job) 直接書かれた DOM をもとに描画されが画面を、Puppeteer ライブラリの PDF メソッドで画面 PDF として出力したものを job 内で保存する</li> <li>(yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a> Jenkins job) 生成した PDF 群を zip 圧縮し、 S3 へアップロード</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>を使いたいユーザーは job が完了したら保存された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a> PDF を取得する</li> </ol> <p>この場合、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トパラメータは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内でのフォルダ階層も含めた上でファイル名を渡す形となりました。若干長くはなってしまったのですが、Next.js の <a href="https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts">pages</a>との仕組みと相性は良いように感じました。 (例えば、 <code>/[branch]/[filepath].tsx</code> のように明示的なパラメタ指定を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>のファイル構造としても残すことができること、などです。) さらには、これにより、開発途中の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>、動作確認もブラウザで直接 URL を叩くような形でアクセスができることもメリットでした(エンドポイントのパス設計は、外部公開されるものであると、直接見せていいような値なのかなどの別の観点を考慮に入れている必要がありそうですが、今回のものは内部限定と考えて良い)。</p> <p>設計として責務がクリアになったのも収穫でした。以前は job と <a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a>-view がお互いの情報を知っているような状態でコードとして少しわかりづらかったという反省がありました(例:既存の view のコードに job 側が Puppeteer で探そうとする 描画 wait のための<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BB%A5%EC%A5%AF%A5%BF">セレクタ</a>を入れていて、job はその<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BB%A5%EC%A5%AF%A5%BF">セレクタ</a>文字列が存在していることを期待していた点、等)。新規の yaki-<a class="keyword" href="https://d.hatena.ne.jp/keyword/tarako">tarako</a>-view は自身に渡されたパラメータで <a class="keyword" href="https://d.hatena.ne.jp/keyword/SSR">SSR</a> するという構造にしたことにより、Puppeteer はページのコンテンツを知らなくても、コンテンツに飛んでいけば良い、という形になりました。</p> <h3 id="思わぬ落とし穴">思わぬ落とし穴</h3> <p>この状態で、style の問題は解消、ブラウザでエンドポイントを見にいってみても、出力は予想した通りとなるようになりました。 ところが、Puppeteer の仕組みを使って PDF 出力をすると、なぜか縦書きコンテンツが複数ページ分が小さく1ページ内におさまって、出力される形になりました。 PDF の形になった状態できれいに出すことが本題なので、これではまだ未完成です。</p> <p><figure class="figure-image figure-image-fotolife" title="原稿の内容が長いのであれば、当然 PDF でもその分ページ数を使ってほしいのだが・・"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116182217.png" width="1200" height="605" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>原稿の内容が長いのであれば、当然 PDF でもその分ページ数を使ってほしいのだが・・</figcaption></figure></p> <p>まずは、この問題に近い現象を、色々既存のバグ起票や Stack Overflow などから探り当てようとしました。挙動の切り分けをする上で(Puppeteer が使うヘッドレスブラウザは <a class="keyword" href="https://d.hatena.ne.jp/keyword/Chromium">Chromium</a> のはずで、<code>PDF()</code> メソッドが出すものも基本的には普段私たちが使っている <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%20Chrome">Google Chrome</a> ブラウザと同じになるだろうと、)<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%20Chrome">Google Chrome</a> で見たエンドポイントの内容を 「印刷」「PDF 出力」として比較してみたり、もしてみましたが、なかなか手がかりが見つからず。</p> <p>そんな中、私の調査記録の中で観点が全く抜けていたものを、 indigolain さんが見つけてくれました。</p> <p>「Puppeteer のバージョン最新でしたっけ」</p> <p>確認してみると、Puppeteer のバージョンが現行最新からだいぶ下のメジャーバージョンであることがわかりました。そしてさらに <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>アップデート系の管理 issue を検索すると、起票はされていたものの、複合的な問題から様子見を行なったまま pending 状態になっていた未解決のままだったことがわかりました。早速対応、このタイミングでバージョンを上げるのは、既存の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>出力機能の保証も行う必要がある関係で、思った以上に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>を吸い取られてしまいました・・。が、結局、丁寧にバージョンを上げていくと、あるバージョン以降で、Puppeteer が期待通りの挙動をすることがわかり、ついに、縦書きもきれいに複数ページに分けて表示するような理想の形に達することができました。</p> <p><figure class="figure-image figure-image-fotolife" title="改修後、横書きと同じように、縦書きのコンテンツでもページ内に内容をいい感じに配置できるようになりました"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/motorollerscalatron_manabi/20231116/20231116182422.png" width="1009" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>改修後、横書きと同じように、縦書きのコンテンツでもページ内に内容をいい感じに配置できるようになりました</figcaption></figure></p> <h2 id="SSR-のために独立したマイクロサービスを立ち上げる"><a class="keyword" href="https://d.hatena.ne.jp/keyword/SSR">SSR</a> のために独立したマイクロサービスを立ち上げる</h2> <p>なんとか、ここまで設計通りに実装ができました。</p> <p>最後は Next.js の <a class="keyword" href="https://d.hatena.ne.jp/keyword/SSR">SSR</a> で独立したエンドポイントにするために、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%BB%B1%DC">校閲</a>用出力の view エンドポイント部分をマイクロサービス化することになります。</p> <p>私たちの tara 開発チームでは、他にも小さな目的に応じたマイクロサービス立ち上げがあり、直近では小学講座リリースの目玉となった手書き認識機能も、直近で登録されたマイクロサービスとして monorepo となった<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の中で、参考用に確認できる状態になっていました。</p> <p>私は、このサービス立ち上げフェーズをほとんど不確定要素のない定型作業のように見立てていました。ある程度新規にマイクロサービスを立てるときに共通で必要になるような設定、</p> <ul> <li>新規サービス対応分の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> の rollout, service 設定追加</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/ingress">ingress</a> リソースの追加</li> <li>reverse-proxy に関する設定</li> </ul> <p>などは、もともと汎用的にチェックリスト化できていたものについては指示を受けつつ、ファイルを揃えていくことができたものの、先に述べた図のような新しい設計にしたことで変化していたものとして、もう 1 つ、見落としていたものがありました。・・それは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>から学習コンテンツの <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を参照するときの認証のための設定です。従来版では、学習コンテンツの <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>からの実取得まわりを担う tara-content の中からの呼び出しだった処理が、 新サービスのコードそのものからの呼び出しになっていたので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>同士のたどるルートが変わっていたのでした。</p> <p>弊社の SRE チームは、開発チームが自立して自チームのオーナーシップの持つサービスの IaC 部分も含めて拡張を任せてくれる一方で、相談にも気軽に乗ってくれます。SRE とのコミュニケーションを重ねていった結果、今回の認証部分には <a href="https://docs.github.com/en/apps">GitHub Apps</a> として新しい登録を行うことにしました。権限上、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Apps の登録と ID の発行は SRE に依頼して行い、私は発行された ID 情報を使って コンテンツの fetch 処理部分を書き直しました。</p> <p>以下、アプリ側の簡略化したコードを示します。</p> <p><a href="https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation#using-the-octokitjs-sdk-to-authenticate-as-an-app-installation">Octokit.js 経由での GitHub Apps 認証をする</a>場合、</p> <pre class="code" data-lang="" data-unlink>import { Octokit } from &#39;@octokit/rest&#39; const { GITHUB_ACCESS_TOKEN } = process.env const octokit = new Octokit({ auth: GITHUB_ACCESS_TOKEN, }) const result = await octokit.repos.getContent({ owner: REPOSITORY_OWNER, repo: REPOSITORY_NAME, path: filePath, )}</pre> <p><sub>BEFORE: <a href="https://octokit.github.io/rest.js/v20">@octokit/rest での直接の呼び出し</a></sub></p> <p>に比べて、</p> <pre class="code" data-lang="" data-unlink>import { Octokit } from &#39;@octokit/rest&#39; import { createAppAuth } from &#39;@octokit/auth-app&#39; const { APP_PRIVATE_KEY, APP_ID, APP_INSTALLATION_ID } = process.env const octokit = new Octokit({ authStrategy: createAppAuth, auth: { appId: APP_ID, privateKey: APP_PRIVATE_KEY, installationId: APP_INSTALLATION_ID, }, }) await octokit.rest.apps.getAuthenticated() const result = await octokit.rest.repos.getContent({ owner: REPOSITORY_OWNER, repo: REPOSITORY_NAME, path: filePath, )}</pre> <p><sub>AFTER: <a href="https://github.com/octokit/octokit.js#authentication">Apps としての認証</a> を挟む</sub></p> <p>のような形になりました。また、先頭行の <code>process.env</code> から分割代入している内容も増えています。インフラ側でも、発行してもらった Apps 用の ID は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>経由でとってくるのですが、これも実行環境ごとにとれるようにする必要があるので、新サービスの <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> Kustomization 設定に、 新しい configmap を設定することになりました。(※)</p> <p>結果、アプリ側のコード部分には、前のコードとそれほど差分ない形でこのアプリに必要な権限を明確にコード上に表現することができるようになりました。</p> <p>一度疎通確認ができた後は、サービス自体は想定通りに動くことが確認できました・・!</p> <p>※ ここではさらっと書いてしまっていますが、実際は、色々試行錯誤していました。私自身が <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Apps という概念自体にあまり馴染みがなかったのも一因かもしれません。SRE 側からは、既に使用しているサンプルコードなども提供してもらいました。そして、1つ1つのオペレーションがうまくいっているかをログに出し、時には<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DA%A5%A2%A5%D7%A5%ED">ペアプロ</a>で「開発コードのブランチをきちんと指しているか」など指差し確認しながら、「ここまでは正しく通っている」をたどって、最後まで通った・・という感じです。</p> <h1 id="まとめ">まとめ</h1> <p>以上、本稿では、新チーム発足の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>的な文脈と、その中で担当した開発のエピソードを共有させていただきました。 後半では、この開発プロジェクトの振り返りについて、もう少し私自身の内省的な観点から、書き記していこうと思います。 最後までお読みいただき、ありがとうございました。</p> motorollerscalatron_manabi スタディサプリにおけるKarpenterの導入トラブル振り返り hatenablog://entry/6801883189059464508 2023-11-20T08:00:00+09:00 2023-11-20T10:12:04+09:00 スタディサプリにおけるKarpenterの導入トラブル振り返り こんにちは。スタディサプリ小中高SREの@aoi1です。 スタディサプリでは、Kubernetesを利用しているのですが、Nodeの運用自動化のために2023年3月から本番環境を含む全環境でKarpenterを導入しています。 Karpenterのおかげで開発者体験を向上させることができたり、コスト削減を行うことができました。便利で良いことが沢山ある一方、本番環境で問題が発生するなどいくつかハマったこともありました。 本ブログでは私たちがハマったポイントを通じて、Karpenterの導入を検討している方、あるいは既に本番環境でKa… <h1 id="スタディサプリにおけるKarpenterの導入トラブル振り返り"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリにおけるKarpenterの導入トラブル振り返り</h1> <p>こんにちは。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小中高SREの@aoi1です。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>を利用しているのですが、Nodeの運用自動化のために2023年3月から本番環境を含む全環境で<a href="https://karpenter.sh/">Karpenter</a>を導入しています。</p> <p><a href="https://karpenter.sh/">Karpenter</a>のおかげで開発者体験を向上させることができたり、コスト削減を行うことができました。便利で良いことが沢山ある一方、本番環境で問題が発生するなどいくつかハマったこともありました。</p> <p>本ブログでは私たちがハマったポイントを通じて、<a href="https://karpenter.sh/">Karpenter</a>の導入を検討している方、あるいは既に本番環境で<a href="https://karpenter.sh/">Karpenter</a>を運用している方にとって参考になればと思います。</p> <h2 id="Karpenterとは"><a href="https://karpenter.sh/">Karpenter</a>とは</h2> <p><a href="https://karpenter.sh/">Karpenter</a>は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> Web Sevice(<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>)が開発している<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>で、「Karpenter simplifies <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> infrastructure with the right nodes at the right time.」と公式ホームページに記載されています。</p> <p>つまり<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>のNodeを適切なタイミングで適切なサイズにスケールすることができるツールです。</p> <p><a href="https://karpenter.sh/karpenter-overview.png" class="http-image"><img src="https://karpenter.sh/karpenter-overview.png" class="http-image" alt="https://karpenter.sh/karpenter-overview.png"></a></p> <blockquote><p>Karpenter公式ホームページに描かれている How It Works</p></blockquote> <p><a href="https://karpenter.sh/">Karpenter</a>を利用するためにはNodePoolというリソースを用意し、NodePoolの設定に応じてNodeの選定が行われます。 各PodはそれぞれNodePoolと紐づいており、Podのリソース要求×NodePoolの設定によって最終的に利用するNodeが決定されます。</p> <p>NodePoolは複数用意することができ、それぞれのNodePoolにはそれぞれのスペックを指定することができます。</p> <p>例えば<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリではJob専用のNodePool, <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions self-hosted runner専用のNodePoolを用意しています。JobのPodが起動する際にはJob用NodePoolに書かれたNodeのスペック内から最適なNodeが選定される、という仕組みになっています。</p> <p>そしてなんと先日<a href="https://aws.amazon.com/jp/blogs/containers/karpenter-graduates-to-beta/">betaに昇格</a>しましたね!おめでとうございます!</p> <p>実はこのブログを書いている今、<a href="https://karpenter.sh/">Karpenter</a>のバージョンアップを計画している最中です。 用語などはbeta版にあわせていますが、起きたトラブルは全てalpha版で発生したものです。</p> <h2 id="スタディサプリでKarpenterを導入して得た効果"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでKarpenterを導入して得た効果</h2> <p>ハマりどころを説明する前に、まず<a href="https://karpenter.sh/">Karpenter</a>で得られた効果を説明します。</p> <h3 id="効果1-Nodeの起動が早い">効果1: Nodeの起動が早い</h3> <p>私たちの環境では<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsのself-hosted runnerを利用しています。ジョブの増減がかなり激しいため、Node数をあらかじめ用意するということも難しく、適宜Node数も増減させる必要があります。</p> <p><a href="https://karpenter.sh/">Karpenter</a>はCluster AutoscalerよりもNodeの起動が早く、私たちのself-hosted runnerの起動時間を大幅に短縮することができました。 <a href="https://karpenter.sh/">Karpenter</a>の導入により、CIに関わる開発体験の向上にもつながりました。</p> <p>また、本番環境でも極端に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>が増えた際にNodeの供給がおいつかない、ということもなく今日まで運用できています。</p> <h3 id="効果2-コスト削減ができる">効果2: コスト削減ができる</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>では<a href="https://aws.amazon.com/jp/ec2/spot/">スポットインスタンス</a>を活用することで大幅にコスト削減を行うことができます。</p> <p>スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>とは最大90%割引されるなど非常に安い代わりに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の価格変動などによって<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が突然終了することが頻繁に発生します。 頻繁に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が終了してしまうので、ステートフルなアプリケーションには使えないのはもちろんのこと、ステートレスなアプリケーションでも安全にシャットダウンできるように実装できている必要があります(実はこのスポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の利用でハマったことがありますが、後ほど説明します)。</p> <p>スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>自体は<a href="https://karpenter.sh/">Karpenter</a>とは関係なく利用可能なものですが、<a href="https://karpenter.sh/">Karpenter</a>を利用することでスポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の不便な点を補うことができます。</p> <p>頻繁に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が終了してしまう、ということはNodeの起動が頻繁に行われる、つまりNodeの起動が早い<a href="https://karpenter.sh/">Karpenter</a>と相性が良いということです。 また、スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>には在庫がなくなるということがありますが、このときオンデマンドな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>に切り替えるときの速さも<a href="https://karpenter.sh/">Karpenter</a>を利用していれば数秒で完了します。</p> <h2 id="Karpenter導入後のトラブル振り返り">Karpenter導入後のトラブル振り返り</h2> <p>沢山良い効果がある一方、導入後に一定の苦労はしました。ここでは私たちがハマったポイントを紹介します。</p> <h3 id="ハマりポイント1-スポットインスタンスの利用">ハマりポイント1: スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の利用</h3> <p><a href="https://karpenter.sh/">Karpenter</a>を本番に導入する前に数ヶ月間ステージング環境で運用を試していました。ステージング環境では問題なくても本番環境で問題が発生する...あるあるですね。 <a href="https://karpenter.sh/">Karpenter</a>ももれなく本番環境でいくつか問題が発生し、ハマりました。特にスポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>に関連した問題が多かったです。</p> <p>いくつかトラブル事例を紹介する前に、スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>についてもう少し詳しく説明します。</p> <p>スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>は安い代わりに空きキャパシティがなくなると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が終了してしまう、というのは前述した通りですが、これをSpot Instance Terminationと呼びます。 このSpot Instance Terminationが引き起こしたいくつかのトラブルについてみていきましょう。</p> <h4 id="アプリケーションが504を頻発する">アプリケーションが504を頻発する</h4> <p>ある日を境に、アプリケーションの多くが504を頻発するようになってしまった、という報告がありました。これが最も大きいトラブルだったといえるでしょう。</p> <p>原因は、スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の中断通知を受け取ってから<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が終了するまでにアプリケーションがシャットダウンできなかったことでした。</p> <p>スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の価格変動や<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>の事情によって頻繁に入れ替わるのですが、入れ替わる前に通知を受け取ることができます。 スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の中断通知は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon%20EC2">Amazon EC2</a>がスポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を停止または終了する2分前に発行される警告です(ref. <a href="https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html">https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/spot-instance-termination-notices.html</a> )。</p> <p>2分もあれば十分なのではないかと思われたのですが、Pod Disruption Budget(<a class="keyword" href="https://d.hatena.ne.jp/keyword/PDB">PDB</a>)の値が2分でアプリケーションが安全にシャットダウンしきることを妨げていました。</p> <p>ではなぜ<a class="keyword" href="https://d.hatena.ne.jp/keyword/PDB">PDB</a>の値が適切に設定できていなかったのでしょうか?</p> <p>新規マイクロサービスを作成する際の開発フローでテンプレートから<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>を作成できるようになっているのですが、 <a class="keyword" href="https://d.hatena.ne.jp/keyword/PDB">PDB</a>の値が標準で<code>maxUnavailable: 1</code>となっていたため、Pod数が多いアプリケーションにおいてはこの値が適切ではありませんでした。 <a href="https://karpenter.sh/">Karpenter</a>によって1Nodeあたりの集約効率が向上した結果、1Nodeがシャットダウンするときに大量のPodが影響を受けるようになり、<code>maxUnavailable: 1</code>を遵守していたのでは2分には到底間に合わないということが起こっていたのです。</p> <p>また、<a href="https://karpenter.sh/">Karpenter</a>では<a href="https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/rebalance-recommendations.html">EC2 instance rebalance recommendation</a>をサポートしていません(ref. <a href="https://github.com/aws/karpenter/issues/2813">https://github.com/aws/karpenter/issues/2813</a> )。</p> <p>これは何かというと、スポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の中断通知よりも早く代替Nodeを用意するというものです。 Managed Node Groupでスポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を利用している環境ではこの再調整によって2分よりももっと長い時間リバランスにかけることができます<a href="#f-49e400f4" id="fn-49e400f4" name="fn-49e400f4" title="Karpenterのリバランスに関しては株式会社MIXI みてね事業部の技術ブログが参考になります。Karpenterを導入した話">*1</a>。</p> <p>しかし、Karpenterではサポートされていないため、なんとしても2分を守る必要があります。</p> <p>KarpenterにEC2<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の再調整に関する推奨事項をサポートして欲しいと思いつつ、私たちは<a class="keyword" href="https://d.hatena.ne.jp/keyword/PDB">PDB</a>の値を調整することでこの問題を回避することができました。</p> <h4 id="アプリケーションが度々接続できなくなる">アプリケーションが度々接続できなくなる</h4> <p>最初に開発者の方から「アプリケーションが度々接続できなくなる」という報告がありました。みなさんはここから原因を推測できるでしょうか?</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>という分散システムを利用していると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D6%A5%EB%A5%B7%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0">トラブルシューティング</a>が難しいというのは読んでいるみなさんは身に染みているところかもしれませんが、 Karpenterを導入したことでNode自体が常に流動するようになり、これにより<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D6%A5%EB%A5%B7%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0">トラブルシューティング</a>が難しくなりました。</p> <p>本件の原因は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> SQS キューの名前を変更したことにより、IAM Roleが適切に割り当たっていなかったことでした。 これだけ聞くと、本ブログの趣旨であるKarpenterと何の関係があるんだ!?と思われるかもしれませんが、一つずつ解説して行きます。</p> <p>Karpenterは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> EventBridge→<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> SQS→KarpenterでSpot Instance Terminationが通知される仕組みになっています(ref. <a href="https://karpenter.sh/v0.32/concepts/disruption/#interruption">https://karpenter.sh/v0.32/concepts/disruption/#interruption</a> )。</p> <p>ここで利用している<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> SQSの権限が足りず、Karpenterがキューを読み取ることができていませんでした。</p> <p>これにより、Spot Instance Terminationが発生してもKarpenterには伝わらず、安全にNodeをdrainできないまま強制的にNodeが失われる状態になっていました。</p> <p>安全にNodeがシャットダウンできないので、アプリケーションももちろん安全にシャットダウンできません。 Nodeが突然失われる状況が頻発し、アプリケーションが接続できなくなっていました。</p> <p>権限が足りていなかったことが原因だったため権限追加で問題を解決することができましたが、表題の問い合わせから問題解決までがなかなか難しかった一件でもあります。</p> <h4 id="Jobに関連するPodが消えてしまう">Jobに関連するPodが消えてしまう</h4> <p>これはトラブルというよりは困りごとに近いのですが、私たちはJob用にNodePoolを用意している関係上Jobが完了時にNodeごと消えるということがよくあります。</p> <p>Jobの実行頻度は低いため、Jobを利用するタイミングでNodeが起動し、Jobの完了とともにNodeがシャットダウンします。 こうなると、Jobが失敗した時にPodを確認したくてもPodが消えている(failedJobsHistoryLimitの値が意味をなさない)という状況が頻発します。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> CloudWatchでログを収集しているため、ログが参照できなくなるということはありませんが、kubectl logsで簡単にログを参照しづらくなっていました。</p> <p>現状これに対する対応策はありませんが、<a href="https://argoproj.github.io/argo-workflows/">Argo Workflows</a>を利用することでこの問題を解決できるかもしれないと考えています(絶賛導入検証中!)。</p> <h3 id="ハマりポイント2-デカすぎるNode">ハマりポイント2: デカすぎるNode</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>では公式に<a href="https://kubernetes.io/docs/setup/best-practices/cluster-large/">1Nodeあたり110Podまでを推奨値</a>としています。</p> <p>しかし私たちのステージング環境ではコストメリットを重視し、1Nodeあたり詰め込めるだけPodを詰め込んでいます。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>公式推奨値から外れているため、<a href="https://karpenter.sh/">Karpenter</a>のハマりどころというよりは「推奨値を外れるとどうなるのか」という話になりますが、 <a href="https://karpenter.sh/">Karpenter</a>を利用していると簡単にNodeにPodをつめこめてしまうため、参考までにどうなるか書いておきます。</p> <h4 id="Datadog-Agentがメモリ不足になる">Datadog Agentがメモリ不足になる</h4> <p>Datadog Agentはさまざまなメトリクスを取得するためのPodで、1Nodeにつき1つ動かします。</p> <p>Datadog Agentは自身がスケジュールされているNodeのPod数に応じて使用するメモリ量が変わるため、 大量のPodが動いているNodeにDatadog Agentを動かすとメモリ不足になります。 Nodeのサイズの違いが大きくDatadog Agentのメモリ使用量も一律に設定することはできません。</p> <p>今の所大きな問題にはなっていないため、私たちはDatadog AgentのOOMは許容することにしています。</p> <h4 id="1Nodeを再起動したときの影響が大きい">1Nodeを再起動したときの影響が大きい</h4> <p>1Nodeに大量のPodが動いていると、1Nodeを再起動したときに影響が大きくなります。</p> <p>大量のPodがNode間の移動を行うため、安全にシャットダウンする設計がうまく効かず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%A4%A5%E0%A5%A2%A5%A6%A5%C8">タイムアウト</a>など発生してしまう可能性があります。 ステージング環境はある程度落ちても問題ない、としてもどれくらい大きいNodeを使用するかは検討した方が良いでしょう。</p> <h3 id="ハマりポイント3-preemptionの発生">ハマりポイント3: preemptionの発生</h3> <p>preemptionが何かを知らない方もいらっしゃるのではないでしょうか。私も<a href="https://karpenter.sh/">Karpenter</a>が導入されるまでは知りませんでした。</p> <p>preemptionとは日本語で「強制排除」という意味で、より優先度の高いPodをスケジュールするために、優先度の低いPodが強制的に終了されることを指します。 <a href="https://karpenter.sh/">Karpenter</a>では使用予定ギリギリのスペックのNodeを選定するため、Podが想定より少し多くリソースを使用するなどでpreemptionが発生しやすくなります。</p> <p>特に私たちが使用しているJob用のNodePoolでは、普段Nodeがそもそも存在しないため、Jobが実行されるたびにNodeが起動します。</p> <p>前述した通り、ギリギリのスペックでNodeを起動するため、preemptionが発生し、JobのPodがNodeから追い出されてしまうことが度々発生するようになりました。</p> <p>preemptionの発生源となっている理由がdatadog-agentのPodがスケジュールできていないことによるものであり、Node内のメトリクスをできるだけ取得するためにはdatadog-agentを最優先でスケジュールする必要があります。 そのためPodの優先度は変えれない一方、JobのPodがスケジュールできないとなるとJobがエラーとなってしまいます。</p> <p>現状この問題に対してできる手立てとしてはJobのリトライ回数を増やすことや、Jobのリソースをチューニングし、よりキャパシティのあるNodeが選定されやすくすることです。 しかしDatadog Agentのリソースが想定外に増えることを防げないことや、たまたま同時に別のJobが起動してしまうなどpreemptionの発生を完全に防げるものではありません。</p> <p>Job用のNodePoolの利用をやめるという方法もありますが、今の所Jobのリトライ回数を増やすことで対応しています。</p> <h2 id="学びと実践へのヒント">学びと実践へのヒント</h2> <p>かなりハマりにハマった<a href="https://karpenter.sh/">Karpenter</a>ですが、現在はかなり安定して運用できています。 ハマりポイントから、今後<a href="https://karpenter.sh/">Karpenter</a>の導入を検討している方に向けてこういうことに注意をすれば良いのではと思ったことをまとめます。</p> <h3 id="アプリケーションが安全にシャットダウンできるように実装することまたマニフェストの設定としても安全なシャットダウンが実施できるようになっていること">アプリケーションが安全にシャットダウンできるように実装すること。また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>の設定としても安全なシャットダウンが実施できるようになっていること</h3> <p>私たちの環境ではステートレスなアプリケーションがメインで、かつ実装としては安全にシャットダウンできるようになっていたためこれだけのトラブルで済んだのかなと思います。</p> <p>本番導入前に万全を期したいのであれば、一定期間ステージング環境でも本番同様に監視をすることでアプリケーション接続断に気づけたかもしれません。また、私たちは利用しませんでしたが、<a href="https://docs.aws.amazon.com/ja_jp/fis/latest/userguide/fis-tutorial-spot-interruptions.html">AWS FIS を使用してスポットインスタンスの中断をテストする</a>こともできるようです。</p> <p>特にスポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を利用するとかなりの頻度でNodeが増減することになるため、重要な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>はスポット<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を使わないという選択肢もありだと思います。</p> <h3 id="Nodeに関するメトリクスやEventPodのログなどを収集できているようにしておくこと">Nodeに関するメトリクスやEvent、Podのログなどを収集できているようにしておくこと</h3> <p>これまで半固定だったNodeという要素が流動的になったことで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D6%A5%EB%A5%B7%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0">トラブルシューティング</a>がかなり難しくなったと感じました。</p> <p>「アプリケーションが接続できなくなった」という抽象的な問い合わせからSQSの権限が原因だと特定できたのは、ひとえにさまざまなメトリクスを収集できていたからだと思います。</p> <h3 id="使用するNodeは環境にあわせて選定すること">使用するNodeは環境にあわせて選定すること</h3> <p><a href="https://karpenter.sh/">Karpenter</a>ではこまかく使用するNodeのスペックを指定することができます。</p> <p>例えば私たちはデカすぎるNodeを使わないよう本番環境の標準設定ではNodeのメモリ上限を72GiBに指定するなど、さまざまな指定をしています。</p> <p>皆様も導入の際には環境に合わせて細かくスペックを指定することをおすすめします。</p> <h2 id="おわりに">おわりに</h2> <p><a href="https://karpenter.sh/">Karpenter</a>の導入の参考になったでしょうか。以前はバージョンをあげると壊れてしまうようなことがありましたが、先日betaに昇格したこともあり今ではかなり安定しています。 それでは良い<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>ライフを!</p> <div class="footnote"> <p class="footnote"><a href="#fn-49e400f4" id="f-49e400f4" name="f-49e400f4" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">Karpenterのリバランスに関しては株式会社<a class="keyword" href="https://d.hatena.ne.jp/keyword/MIXI">MIXI</a> みてね事業部の技術ブログが参考になります。<a href="https://team-blog.mitene.us/karpenter-b48ca7cdc22a">Karpenterを導入した話</a></span></p> </div> blux Android チームが使っている GitHub Actions のユニークな自動化レシピ集🍞👨‍🍳 hatenablog://entry/6801883189052727659 2023-11-13T09:00:00+09:00 2023-11-13T09:00:01+09:00 スタサプ小中高を開発している Android エンジニアの@maxfie1d、@morayl とスタサプ ENGLISHを開発している Android エンジニアの田村です。 GitHub Actions(以下 GHA) はアプリをビルドしたりストアに配信したりすることに使えるのはもちろん、もっともっと色々なタスクを自動化することができます。本記事では Androidチームによる GHA を使った自動化レシピをご紹介します。 まずはスタサプ小中 Android版での取り組みを紹介します。 自動でラベルを付与する 2023年9月に リニューアルをしたスタディサプリ 小学講座をリリースしました。ア… <p>スタサプ小中高を開発している <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> エンジニアの<a href="https://github.com/maxfie1d">@maxfie1d</a>、<a href="https://github.com/morayl">@morayl</a> とスタサプ ENGLISHを開発している <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> エンジニアの田村です。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions(以下 GHA) はアプリをビルドしたりストアに配信したりすることに使えるのはもちろん、もっともっと色々なタスクを自動化することができます。本記事では <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>チームによる GHA を使った自動化レシピをご紹介します。</p> <hr /> <p>まずはスタサプ小中 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>版での取り組みを紹介します。</p> <h2 id="自動でラベルを付与する">自動でラベルを付与する</h2> <p>2023年9月に <a href="https://studysapuri.jp/course/elementary/">リニューアルをしたスタディサプリ 小学講座</a>をリリースしました。アプリとしては<a href="https://studysapuri.jp/course/junior/">スタディサプリ 中学講座</a> と同じで 1アプリ内に中学生向けの機能と小学生向けの機能があります。</p> <p>コードは中学生向けの機能と小学生向けの機能で大きく <code>original/</code> <code>elementary/</code> という2つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リに分かれています。PRが中学のものか小学のものかが分かるようにこれまで手動で <code>中学</code> <code>小学</code> というラベルを付与していましたが <a href="https://github.com/actions/labeler">actions/labler</a> を使用して自動化することができました。</p> <p><figure class="figure-image figure-image-fotolife" title="これまで手動でつけていた中学・小学ラベルの付与を自動化"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231023/20231023100128.png" width="1116" height="450" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>これまで手動でつけていた中学・小学ラベルの付与を自動化</figcaption></figure></p> <p>以下のように labler を設定することで <code>elementary/</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ内に差分があるPRに自動で <code>小学</code> ラベルが付与されるようになります。中学も同様に設定しています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># labeler.yml</span> <span class="synIdentifier">小学</span><span class="synSpecial">:</span> <span class="synStatement">- </span>elementary/** <span class="synIdentifier">中学</span><span class="synSpecial">:</span> <span class="synStatement">- </span>original/** </pre> <p>自動ラベル付与のルールとして他に <code>.github</code> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ内の差分には <code>CI</code> ラベル、拡張子が <code>.graphql</code> のファイルの差分には <code>GraphQL operations changed</code> ラベルを設定しています。</p> <p>ちなみに開発メンバーも中学と小学で分かれています。そのためレビュアーの設定もこれまで手動で行っていたのですが、 <a href="https://docs.github.com/ja/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners">コードオーナー</a> を設定することによりレビュアーの設定も自動化することができました。手順について詳しくはドキュメントをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fja%2Frepositories%2Fmanaging-your-repositorys-settings-and-features%2Fcustomizing-your-repository%2Fabout-code-owners" title="コードオーナーについて - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/ja/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners">docs.github.com</a></cite></p> <h2 id="自動で自分自身をPull-Requestのアサインに設定する">自動で自分自身をPull Requestの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンに設定する</h2> <p>PR作成時にPRを作成した人を自動的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンする方法です。<code>gh</code> コマンド(<a href="https://docs.github.com/ja/github-cli/github-cli/about-github-cli">GitHub CLI</a>)を使用しています。Depandabot のように <a class="keyword" href="https://d.hatena.ne.jp/keyword/bot">bot</a> が作成するPRではスキップするのがポイントです。 自分自身を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンするというちょっとした手間も一度自動化しておくと結構楽です。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Auto self assign <span class="synComment"># ... 略</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">self-assign</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> Self assign <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synComment"> # Ignore dependabot</span> <span class="synIdentifier">if</span><span class="synSpecial">:</span> github.actor <span class="synType">!=</span> <span class="synConstant">'dependabot[bot]'</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Self assign <span class="synComment"> # Document https://cli.github.com/manual/gh_pr_edit</span> <span class="synIdentifier">run</span><span class="synSpecial">:</span> gh pr edit ${{ github.event.number }} --add-assignee ${{ github.actor }} --repo ${{ github.repository }} <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">GH_TOKEN</span><span class="synSpecial">:</span> ${{ github.token }} </pre> <hr /> <p>ここからはスタサプENGLISH <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>版の取り組みをいくつか紹介します。</p> <h2 id="ビルド時間のベンチマークを計測するマン">ビルド時間の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D9%A5%F3%A5%C1%A5%DE%A1%BC%A5%AF">ベンチマーク</a>を計測するマン</h2> <p>弊チームではビルド時間を定期的にCI上でビルド時間の計測を行っています。 計測を行ったら特定のIssueに追記して履歴を追えるようにしています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231023/20231023100732.png" width="1200" height="794" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>CIを組むポイントとしてはキャッシュによる差分ビルドが走らないようにしながら、依存関係のDLの時間は省くことです。 ネットワークのスピードに左右されないようビルド時間を計測することが大事です。 また、ビルド時間の計測には <a href="https://github.com/gradle/gradle-profiler">Gradle Profiler</a> を用います。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Prefetch Gradle Dependencies <span class="synIdentifier">run</span><span class="synSpecial">:</span> ./gradlew --no-daemon :app:assembleDebug <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> | curl -s <span class="synConstant">&quot;https://get.sdkman.io&quot;</span> | bash source <span class="synConstant">&quot;$HOME/.sdkman/bin/sdkman-init.sh&quot;</span> sdk install gradleprofiler 0.18.0 gradle-profiler --benchmark build --scenario-file ./gradle/build.benchmark.scenarios --gradle-user-home $HOME/.gradle </pre> <p>ここまでで計測はできているので、結果を<a class="keyword" href="https://d.hatena.ne.jp/keyword/Github">Github</a> Issueへ記載してみましょう。 若干煩雑な処理になるので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>-scriptを使って <a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> で記述します。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/github-script@v6 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">script</span><span class="synSpecial">:</span> | const script = require('./.github/workflows/scripts/sync_build_benchmark.js'); await script({ github, context, glob }); </pre> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> fs = require(<span class="synConstant">'fs'</span>).promises; module.exports = async (<span class="synIdentifier">{</span> github, context, glob <span class="synIdentifier">}</span>) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> today = <span class="synStatement">new</span> <span class="synType">Date</span>().toLocaleDateString(); <span class="synStatement">const</span> marker = <span class="synConstant">'--DB--'</span>; <span class="synStatement">const</span> issue_number = <span class="synIdentifier">[</span>記載したいGithub Issue番号<span class="synIdentifier">]</span>; <span class="synStatement">const</span> issue = await github.rest.issues.get(<span class="synIdentifier">{</span> owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number, <span class="synIdentifier">}</span>); <span class="synStatement">const</span> records = JSON.parse(issue.data.body.split(marker)<span class="synIdentifier">[</span>1<span class="synIdentifier">]</span>); <span class="synStatement">const</span> benchmarkResult = await fs.readFile(<span class="synConstant">'./profile-out/benchmark.csv'</span>, <span class="synConstant">'utf8'</span>) <span class="synStatement">const</span> mesuredBuildTimes = benchmarkResult.split(<span class="synSpecial">'\n'</span>).map(line =&gt; <span class="synIdentifier">{</span> <span class="synStatement">if</span> (line.split(<span class="synConstant">','</span>)<span class="synIdentifier">[</span>0<span class="synIdentifier">]</span>.includes(<span class="synConstant">'measured build'</span>)) <span class="synIdentifier">{</span> <span class="synStatement">return</span> parseInt(line.split(<span class="synConstant">','</span>)<span class="synIdentifier">[</span>1<span class="synIdentifier">]</span>) <span class="synIdentifier">}</span> <span class="synStatement">else</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synStatement">undefined</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span>).filter(<span class="synType">Boolean</span>) <span class="synStatement">const</span> min = Math.min(...mesuredBuildTimes) <span class="synStatement">const</span> max = Math.max(...mesuredBuildTimes) <span class="synStatement">const</span> average = Math.round(mesuredBuildTimes.reduce((a, b) =&gt; a + b) / mesuredBuildTimes.length); records.push(<span class="synIdentifier">{</span> date: today, min: min, max: max, average: average, times: <span class="synIdentifier">[</span>...mesuredBuildTimes<span class="synIdentifier">]</span> <span class="synIdentifier">}</span>); <span class="synStatement">const</span> tableSection = <span class="synConstant">`</span> <span class="synConstant">| date | min | max | average |</span> <span class="synConstant">| --- | --- | --- | --- |</span> <span class="synSpecial">${records.map(({ date, min, max, average }</span><span class="synConstant">) =&gt; `</span>| <span class="synSpecial">${date}</span> | <span class="synSpecial">${min}</span> | <span class="synSpecial">${max}</span> | <span class="synSpecial">${average}</span> |<span class="synConstant">`).join('</span><span class="synSpecial">\n</span><span class="synConstant">')}</span> <span class="synConstant"> `</span> <span class="synStatement">const</span> hiddenDbSection = <span class="synConstant">`</span> <span class="synConstant">&lt;!--</span> <span class="synSpecial">${marker}</span> <span class="synSpecial">${JSON.stringify(records)}</span> <span class="synSpecial">${marker}</span> <span class="synConstant">--&gt;</span> <span class="synConstant"> `</span> await github.rest.issues.update(<span class="synIdentifier">{</span> owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number, body: <span class="synConstant">`</span> <span class="synConstant"># :app:assembleToeicDevDebug</span> <span class="synConstant"> </span> <span class="synSpecial">${tableSection}</span> <span class="synSpecial">${hiddenDbSection}</span> <span class="synConstant"> `</span>, <span class="synIdentifier">}</span>); <span class="synIdentifier">}</span>; </pre> <p>ここまで記述することで自動的にIssueにビルド時間が書き込まれて、客観的にビルド時間の増減を図ることができます。 もちろんCIマシンのスペックやCPU状況にも左右されるので、必ずしも毎回同一条件とは限りませんがある程度の参考にはなるかと思います。</p> <h2 id="Proguard-に差分があるかをチェックするマン">Proguard に差分があるかをチェックするマン</h2> <p>Proguard(R8)周りの不具合や記述ミスはみなさんも一度は経験があるかと思います。 最近はライブラリ側でルールが組み込まれていることも多く、その場合基本的には意識する必要はないですが ライブラリアップデートによるルール変更があるということ自体に気づきにくくなっています。</p> <p>自動ライブラリアップデート機構である<a href="https://github.com/renovatebot/renovate">Renovate</a>と組み合わせて、注意すべきライブラリアップデートか否かの判断材料の一つとして、弊チームではProguardルールの差分をPRに表示しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231023/20231023101008.png" width="1200" height="685" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>まず、アプリモジュールのproguard-rules.proに下記を追記すると指定した場所にルールが統合されたproguard-rulesが吐き出されるようになります。</p> <pre class="code" data-lang="" data-unlink>-printconfiguration build/proguard-rules-full.pro</pre> <p>これをもとに差分を検出していきます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> 現在のBranchのProguardを取得するためにAssemble <span class="synIdentifier">run</span><span class="synSpecial">:</span> | ./gradlew app:assembleToeicProdRelease <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/upload-artifact@v3 <span class="synIdentifier">name</span><span class="synSpecial">:</span> 現在のBranchのProguardを一旦ArtifactsへUpload <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> proguard-rules-full.pro <span class="synIdentifier">path</span><span class="synSpecial">:</span> ./app/build/proguard-rules-full.pro <span class="synIdentifier">retention-days</span><span class="synSpecial">:</span> <span class="synConstant">1</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">ref</span><span class="synSpecial">:</span> develop <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> develop branchのProguardを取得するためにAssemble <span class="synIdentifier">run</span><span class="synSpecial">:</span> | ./gradlew app:assembleToeicProdRelease <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/download-artifact@v3 <span class="synIdentifier">name</span><span class="synSpecial">:</span> 現在のBranchのProguardをArtifactsからDownload <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> proguard-rules-full.pro </pre> <p>Renovateで生成されたPRに対して、上記のようなステップを組むとライブラリアップデート前後のproguard-rules-full.proが揃います。 これを元にPRコメントとして差分を表示してみます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> で記載していきます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/github-script@v6 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">script</span><span class="synSpecial">:</span> | const script = require('./.github/workflows/scripts/run_renovate_post_r8_difference.js'); await script({ github, context }); </pre> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synStatement">const</span> <span class="synIdentifier">{</span> execSync <span class="synIdentifier">}</span> = require(<span class="synConstant">'child_process'</span>); <span class="synStatement">const</span> fs = require(<span class="synConstant">'fs'</span>).promises; module.exports = async (<span class="synIdentifier">{</span> github, context <span class="synIdentifier">}</span>) =&gt; <span class="synIdentifier">{</span> <span class="synComment">// ./proguard-rules-full.pro は現在のBranchのProguard</span> <span class="synComment">// ./app/build/proguard-rules-full.pro はdevelop branchのProguard</span> <span class="synStatement">const</span> diff = execSync(<span class="synConstant">&quot;diff ./app/build/proguard-rules-full.pro ./proguard-rules-full.pro -U 0 -a -B -w -I '#.*' || true&quot;</span>).toString().trim(); <span class="synStatement">const</span> body = <span class="synConstant">`</span> <span class="synConstant">## Proguardの差分だよ</span> <span class="synSpecial">\`\`\`</span><span class="synConstant">diff</span> <span class="synSpecial">${diff}</span> <span class="synSpecial">\`\`\`</span> <span class="synConstant"> `</span>; await github.rest.issues.createComment(<span class="synIdentifier">{</span> issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: body <span class="synIdentifier">}</span>); <span class="synIdentifier">}</span>; </pre> <p>仮にProguardルールに起因する問題が発生した場合も、差分を見ることでライブラリ側の問題にも気づきやすくなります。</p> <h2 id="残りPresenter数の可視化マン">残りPresenter数の可視化マン</h2> <p>弊プロダクトではPresenterからViewModelへの移行を進めています。 移行モチベーションを上げるにあたり、残タスクの可視化のために毎週自動計測を行っています。</p> <table> <thead> <tr> <th> Presenterの一覧 </th> <th> Presenter数の推移 </th> </tr> </thead> <tbody> <tr> <td> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231023/20231023101231.png" width="1200" height="1106" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> </td> <td> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231023/20231023101239.png" width="962" height="864" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> </td> </tr> </tbody> </table> <p>特定の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Github">Github</a> Issueに追記する形で履歴を残しています。 こちらも <a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a> で記載していきます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/github-script@v6 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">script</span><span class="synSpecial">:</span> | const script = require('./.github/workflows/scripts/sync_issue_presenter.js'); await script({ github, context, glob }); </pre> <pre class="code lang-javascript" data-lang="javascript" data-unlink>module.exports = async (<span class="synIdentifier">{</span> github, context, glob <span class="synIdentifier">}</span>) =&gt; <span class="synIdentifier">{</span> <span class="synStatement">const</span> today = <span class="synStatement">new</span> <span class="synType">Date</span>().toLocaleDateString(); <span class="synStatement">const</span> marker = <span class="synConstant">'--DB--'</span>; <span class="synStatement">const</span> issue_number = <span class="synIdentifier">[</span>記載したいGithub Issue番号<span class="synIdentifier">]</span>; <span class="synStatement">const</span> issue = await github.rest.issues.get(<span class="synIdentifier">{</span> owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number, <span class="synIdentifier">}</span>); <span class="synStatement">const</span> targetFilePaths = await (await glob.create(<span class="synConstant">'./**/src/**/*Presenter.kt'</span>)).glob(); <span class="synStatement">const</span> checkedPresenters = <span class="synIdentifier">[</span>...issue.data.body.matchAll(/- <span class="synIdentifier">\[</span>x<span class="synIdentifier">\]</span> <span class="synIdentifier">\</span>d+ <span class="synIdentifier">\[</span>(.*).kt<span class="synIdentifier">]</span>/g)<span class="synIdentifier">]</span>.map(x =&gt; x<span class="synIdentifier">[</span>1<span class="synIdentifier">]</span>) <span class="synStatement">const</span> sortedPresenterInfos = targetFilePaths .map(path =&gt; <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> path: path, lineCount: lineCount(path), checked: checkedPresenters.some(presenter =&gt; path.includes(presenter)) <span class="synIdentifier">}</span> <span class="synIdentifier">}</span>) .sort((a, b) =&gt; a.lineCount - b.lineCount); <span class="synStatement">const</span> records = JSON.parse(issue.data.body.split(marker)<span class="synIdentifier">[</span>1<span class="synIdentifier">]</span>); records.push(<span class="synIdentifier">{</span> date: today, count: targetFilePaths.length <span class="synIdentifier">}</span>); <span class="synStatement">const</span> filesSection = <span class="synConstant">`</span> <span class="synConstant">### Presenter files &lt;/summary&gt;</span> <span class="synSpecial">${sortedPresenterInfos.map(x =&gt; toMarkdownLink(x.path, x.lineCount, x.checked)).join(</span><span class="synConstant">'</span><span class="synSpecial">\n</span><span class="synConstant">'</span><span class="synSpecial">)}</span> <span class="synConstant">&lt;/details&gt;</span> <span class="synConstant"> `</span> <span class="synStatement">const</span> tableSection = <span class="synConstant">`</span> <span class="synConstant">| date | count |</span> <span class="synConstant">| --- | --- |</span> <span class="synSpecial">${records.map(({ date, count }</span><span class="synConstant">) =&gt; `</span>| <span class="synSpecial">${date}</span> | <span class="synSpecial">${count}</span> |<span class="synConstant">`).join('</span><span class="synSpecial">\n</span><span class="synConstant">')}</span> <span class="synConstant"> `</span> <span class="synComment">// 前回までの履歴を引き継ぎたいので, issue上の文字列を雑にDBとして使っている</span> <span class="synStatement">const</span> hiddenDbSection = <span class="synConstant">`</span> <span class="synConstant">&lt;!--</span> <span class="synSpecial">${marker}</span> <span class="synSpecial">${JSON.stringify(records)}</span> <span class="synSpecial">${marker}</span> <span class="synConstant">--&gt;</span> <span class="synConstant"> `</span> await github.rest.issues.update(<span class="synIdentifier">{</span> owner: context.repo.owner, repo: context.repo.repo, issue_number: issue_number, body: <span class="synConstant">`</span> <span class="synSpecial">${filesSection}</span><span class="synConstant"> </span> <span class="synSpecial">${tableSection}</span> <span class="synSpecial">${hiddenDbSection}</span> <span class="synConstant"> `</span>, <span class="synIdentifier">}</span>); <span class="synIdentifier">}</span>; <span class="synIdentifier">function</span> toMarkdownLink(path, lineCount, checked) <span class="synIdentifier">{</span> <span class="synStatement">const</span> displayName = path.split(<span class="synConstant">'/'</span>).slice(-1)<span class="synIdentifier">[</span>0<span class="synIdentifier">]</span>; <span class="synStatement">const</span> link = path.replace(/.*<span class="synIdentifier">\</span>/<span class="synIdentifier">[</span>REPOSITORY<span class="synIdentifier">]\</span><span class="synComment">//, 'https://github.com/[ORGANIZATION]/[REPOSITORY]/blob/develop/');</span> <span class="synStatement">return</span> <span class="synConstant">`- [</span><span class="synSpecial">${checked ? </span><span class="synConstant">&quot;x&quot;</span><span class="synSpecial"> : </span><span class="synConstant">&quot; &quot;</span><span class="synSpecial">}</span><span class="synConstant">] </span><span class="synSpecial">${lineCount}</span><span class="synConstant"> [</span><span class="synSpecial">${displayName}</span><span class="synConstant">](</span><span class="synSpecial">${link}</span><span class="synConstant">)`</span>; <span class="synIdentifier">}</span> <span class="synStatement">const</span> fs = require(<span class="synConstant">'fs'</span>); <span class="synIdentifier">function</span> lineCount(path) <span class="synIdentifier">{</span> <span class="synStatement">const</span> fileBuffer = fs.readFileSync(path); <span class="synStatement">const</span> str = fileBuffer.toString(); <span class="synStatement">const</span> splitLines = str.split(<span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>); <span class="synStatement">return</span> splitLines.length; <span class="synIdentifier">}</span> </pre> <p>仕組み的にはファイル名を軸に数を数えているだけなので、状況によっては正確では無いかもしれませんがこの場ではある程度の目安がわかることが大事と考えています。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>移行のような中長期的な取り組みに対しては、このような仕組みを先んじて組んでおくことで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%CA%C4%BD%B4%C9%CD%FD">進捗管理</a>もしやすくなり目標設定や振り返りにも役立つのでおすすめです。</p> <hr /> <p>ここからはスタサプ高校講座チームの取り組みをいくつか紹介します。</p> <h2 id="GooglePlayConsoleにaabをアップロードしてリリースissueタグを作成する">GooglePlayConsoleにaabをアップロードして、リリース・issue・タグを作成する</h2> <p>リリースする際に行うことになっている、「リリースの作成」「リリースissueの作成」「タグを打つ」作業を、aabアップロード後に自動的に行うようにしました。</p> <p>まずは全体像から。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">build-upload</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> build-upload <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Prepare environment <span class="synIdentifier">run</span><span class="synSpecial">:</span> | echo <span class="synConstant">&quot;VERSION_NAME=$(echo ${{ github.ref_name }} | sed 's/release\///')&quot;</span> &gt;&gt; $GITHUB_ENV echo ${{ env.VERSION_NAME }} <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Build and upload <span class="synIdentifier">run</span><span class="synSpecial">:</span> | echo <span class="synConstant">&quot;${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_BASE64 }}&quot;</span> | base64 --decode &gt; app/api-project.json ./gradlew publishReleaseBundle <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Retrieve short SHA <span class="synIdentifier">run</span><span class="synSpecial">:</span> echo <span class="synConstant">&quot;COMMIT_SHA=$(git rev-parse --short ${{ github.sha }})&quot;</span> &gt;&gt; $GITHUB_ENV <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Prepare release note <span class="synIdentifier">run</span><span class="synSpecial">:</span> | sed -i -e <span class="synConstant">&quot;s/{date}/$(date '+%Y\/%m\/%d')/g&quot;</span> .github/release_issue_template.md sed -i -z <span class="synConstant">'s/\n/@@/g'</span> app/src/main/play/release-notes/ja-JP/default.txt sed -i -e <span class="synConstant">&quot;s/{release_note}/$(cat app/src/main/play/release-notes/ja-JP/default.txt)/g&quot;</span> .github/release_issue_template.md sed -i -z <span class="synConstant">'s/@@/\n/g'</span> .github/release_issue_template.md <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create release note <span class="synIdentifier">run</span><span class="synSpecial">:</span> gh issue create -t <span class="synConstant">&quot;[WIP][Android] StudySapuri リリースノート ver ${{ env.VERSION_NAME }}&quot;</span> -b <span class="synConstant">&quot;$(cat .github/release_issue_template.md)&quot;</span> -R quipper/aya-issues &gt; release_issue_output.txt <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create release and tag <span class="synIdentifier">run</span><span class="synSpecial">:</span> gh release create ${{ env.VERSION_NAME }} -t <span class="synConstant">&quot;Release ${{ env.VERSION_NAME }}&quot;</span> --target ${{ github.sha }} -n <span class="synConstant">&quot;$(cat release_issue_output.txt)&quot;</span> </pre> <p>細かく見ていきます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Prepare environment <span class="synIdentifier">run</span><span class="synSpecial">:</span> | echo <span class="synConstant">&quot;VERSION_NAME=$(echo ${{ github.ref_name }} | sed 's/release\///')&quot;</span> &gt;&gt; $GITHUB_ENV echo ${{ env.VERSION_NAME }} </pre> <p>私のチームでは、リリースブランチからリリースを行っていて、ブランチ名が <code>release/バージョン名</code> となっています。 ワークフローをそのブランチから実行することになっているので、ブランチ名からバージョン名を抜き出すようにしています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Prepare release note <span class="synIdentifier">run</span><span class="synSpecial">:</span> | sed -i -e <span class="synConstant">&quot;s/{date}/$(date '+%Y\/%m\/%d')/g&quot;</span> .github/release_issue_template.md sed -i -z <span class="synConstant">'s/\n/@@/g'</span> app/src/main/play/release-notes/ja-JP/default.txt sed -i -e <span class="synConstant">&quot;s/{release_note}/$(cat app/src/main/play/release-notes/ja-JP/default.txt)/g&quot;</span> .github/release_issue_template.md sed -i -z <span class="synConstant">'s/@@/\n/g'</span> .github/release_issue_template.md </pre> <p>リリースissueの元となるテンプレートファイル(release_issue_template.md)をバージョン管理していて、それの文字列を置換して完成させています。 アプリのバージョンアップ文言もバージョン管理されていて、それをテンプレートファイルに入れ込むようにしています。 改行を含んだ文字列をcatして<a class="keyword" href="https://d.hatena.ne.jp/keyword/sed">sed</a>で置換することが難しかったため、関係ない文字(@@)に一度変換してテンプレートに流してから、最後に元に戻しています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create release note <span class="synIdentifier">run</span><span class="synSpecial">:</span> gh issue create -t <span class="synConstant">&quot;[WIP][Android] StudySapuri リリースノート ver ${{ env.VERSION_NAME }}&quot;</span> -b <span class="synConstant">&quot;$(cat .github/release_issue_template.md)&quot;</span> -R quipper/aya-issues &gt; release_issue_output.txt </pre> <p><code>gh issue create</code> コマンドを利用して、リリースissueを作成します。</p> <p>先程作成したテンプレートを-b(body)に指定します。 また、チームではコードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>とissueの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>が分かれているので、-Rを使って別の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にissueを作成しています。 また、作ったissueのURLが出力されるので、txtとして保存して次のstepで利用します。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create release and tag <span class="synIdentifier">run</span><span class="synSpecial">:</span> gh release create ${{ env.VERSION_NAME }} -t <span class="synConstant">&quot;Release ${{ env.VERSION_NAME }}&quot;</span> --target ${{ github.sha }} -n <span class="synConstant">&quot;$(cat release_issue_output.txt)&quot;</span> </pre> <p>最後にリリースとタグの作成です。<code>gh release create</code> を用います。-nでリリースノートとしてissueのURLを記載します。</p> <h2 id="FirebaseAppDistributionにアップロードしてそのリリースのURLをプルリクに通知する">FirebaseAppDistributionにアップロードして、そのリリースのURLをプルリクに通知する</h2> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># ...省略</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Execute Gradle assemble <span class="synIdentifier">run</span><span class="synSpecial">:</span> ./gradlew bundleRc <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Upload aab to Firebase App Distribution <span class="synIdentifier">id</span><span class="synSpecial">:</span> firebase_outputs <span class="synIdentifier">uses</span><span class="synSpecial">:</span> wzieba/Firebase-Distribution-Github-Action@1.7.0 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">appId</span><span class="synSpecial">:</span> ${{ secrets.FIREBASE_DEV_APP_ID }} <span class="synIdentifier">serviceCredentialsFileContent</span><span class="synSpecial">:</span> ${{ secrets.FIREBASE_DISTRIBUTION_CREDENTIAL_FILE_CONTENT }} <span class="synIdentifier">groups</span><span class="synSpecial">:</span> group-hoge, group-fuga <span class="synIdentifier">file</span><span class="synSpecial">:</span> app/build/outputs/bundle/rc/app-rc.aab <span class="synIdentifier">releaseNotes</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Release note sample.&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Set firebase url to env <span class="synIdentifier">run</span><span class="synSpecial">:</span> echo <span class="synConstant">&quot;FIREBASE_URL=${{ steps.firebase_outputs.outputs.TESTING_URI }}&quot;</span> &gt;&gt; $GITHUB_ENV <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Notify with Danger <span class="synIdentifier">run</span><span class="synSpecial">:</span> bundle exec --gemfile=Gemfile danger --dangerfile=&quot;script/ci/danger/notify_upload.Dangerfile&quot; --danger_id='upload Rc aab' --remove-previous-comments </pre> <p>ビルドした後、<a href="https://github.com/wzieba/Firebase-Distribution-Github-Action">Firebase App Distribution にアップロードするライブラリ</a>を使ってアップロードしています。</p> <p>このライブラリでは最近アウトプットを受け取ることができるようになりました。 アップロードが完了したとき、Dangerでプルリクにコメントするため、TESTING_<a class="keyword" href="https://d.hatena.ne.jp/keyword/URI">URI</a>(テスターが当該リリースを開くことが出来るURL)を<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>に入れ、Dangerファイルから参照できるようにしています。 Dangerでは、渡されたURLとともにメッセージを送ります。</p> <h2 id="特定のブランチにpushされた場合に2つのバリアントのaabをアップロードする動的にmatrixを生成する">特定のブランチにpushされた場合に、2つのバリアントのaabをアップロードする(動的にmatrixを生成する)</h2> <p>チームのプロダクトには、ビルドバリアントとしてdebug, rc, releaseなどがあります。 master, develop, releaseブランチにpushがあったときにaabをアップロードするルールになっているのですが、</p> <ul> <li>releaseブランチのときは、rcとrelease両方をアップロードしたい</li> <li>それ以外の場合は、rcだけアップロードしたい</li> </ul> <p>という要望がありました。 GitHubActionsで複数のバリアントを実行するにはmatrixが利用できますが、ブランチ名によってmatrixを動的に変更するジョブを作成したので紹介です。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Auto upload aab <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">push</span><span class="synSpecial">:</span> <span class="synIdentifier">branches</span><span class="synSpecial">:</span> <span class="synStatement">- </span>master <span class="synStatement">- </span>develop <span class="synStatement">- </span><span class="synConstant">'release/**'</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">prepare-matrix-values</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">outputs</span><span class="synSpecial">:</span> <span class="synIdentifier">matrix_values</span><span class="synSpecial">:</span> ${{ steps.set-matrix.outputs.value }} <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> make Release matrix <span class="synIdentifier">id</span><span class="synSpecial">:</span> set-matrix <span class="synIdentifier">run</span><span class="synSpecial">:</span> | if <span class="synSpecial">[[</span> $<span class="synSpecial">{{</span> github.ref_name <span class="synSpecial">}}</span> == release/* <span class="synSpecial">]]</span>; then MATRIX=[\&quot;rc\&quot;,\&quot;release\&quot;] else MATRIX=[\&quot;rc\&quot;] fi echo <span class="synConstant">&quot;value=$MATRIX&quot;</span> &gt;&gt; $GITHUB_OUTPUT <span class="synIdentifier">aab-upload-deploygate</span><span class="synSpecial">:</span> <span class="synIdentifier">needs</span><span class="synSpecial">:</span> prepare-matrix-values <span class="synIdentifier">name</span><span class="synSpecial">:</span> aab-upload-deploygate <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">strategy</span><span class="synSpecial">:</span> <span class="synIdentifier">matrix</span><span class="synSpecial">:</span> <span class="synIdentifier">variant</span><span class="synSpecial">:</span> ${{ fromJson(needs.prepare-matrix-values.outputs.matrix_values) }} <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> echo for confirming <span class="synIdentifier">run</span><span class="synSpecial">:</span> echo ${{ matrix.variant }} <span class="synComment"># ...matrixを使ってビルドやアップロードを行う</span> </pre> <p>matrixは動的に指定できるようになっていて、配列を<a class="keyword" href="https://d.hatena.ne.jp/keyword/json">json</a>で受け渡しすることで実現することが出来ます。 最初のjob <code>prepare-matrix-values</code> でoutputに<a class="keyword" href="https://d.hatena.ne.jp/keyword/json">json</a>を入れています。 パターンが限られるため直書きで配列を生成していますが、shellの配列を使ったり、文字列をjqでフォーマットして生成することも出来ます。</p> <h2 id="まとめ">まとめ</h2> <p>いかがでしたか。けっこうなボリュームになりましたが、実際にはこの記事で紹介したものよりももっと多くのタスクが <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions で自動化されています。 タスクの大小に関わらず自動化をすることでエンジニアが日々のルーチンの作業から解放され、結果的に生産性を向上させることができると思っています。 この記事がヒントになれば幸いです。</p> maxfieldwalker SLI の計測のために Envoy や service mesh を選択しなかった理由 hatenablog://entry/6801883189057662450 2023-11-13T08:00:00+09:00 2023-11-14T09:56:40+09:00 こんにちは。スタディサプリの小中高プロダクト開発部で主にコミュニケーション機能の開発をしている @snowfield702 です。 今回はスタディサプリで SLI の計測に Envoy と service mesh を使うのをやめて Datadog APM を利用することにしたという話をしたいと思います。 また、スタディサプリ小中高プロダクト開発部では、スタディサプリ ブランドのうちの複数のプロダクトを開発しております。 この記事では「スタディサプリ 小学/中学/高校/大学受験講座」を対象に話をしていきたいと思います。 Envoy を導入した経緯 スタディサプリでは主にマイクロサービスの SL… <p>こんにちは。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト開発部で主にコミュニケーション機能の開発をしている @snowfield702 です。 今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリで SLI の計測に Envoy と service mesh を使うのをやめて Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> を利用することにしたという話をしたいと思います。</p> <p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト開発部では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ ブランドのうちの複数のプロダクトを開発しております。 この記事では「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ 小学/中学/高校/大学受験講座」を対象に話をしていきたいと思います。</p> <h2 id="Envoy-を導入した経緯">Envoy を導入した経緯</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは主にマイクロサービスの SLI を計測する目的で Envoy が導入されていました。 この時期は service mesh の概念が出始めたばかりということもあり、いきなり service mesh を導入をせずに自分たちで Envoy container を sidecar に定義するという方法を取りました。</p> <p><a href="https://blog.studysapuri.jp/entry/2020/01/30/slo-review#%E7%9B%B4%E6%8E%A5%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E9%96%93%E9%80%9A%E4%BF%A1%E3%82%92%E3%81%99%E3%82%8B%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE-SLI-%E3%81%A8%E3%81%AA%E3%82%8B-Metrics-%E3%81%8C%E5%AD%98%E5%9C%A8%E3%81%97%E3%81%AA%E3%81%84">参考:SRE NEXT 2020 で「SLO Review」というタイトルで登壇しました #srenext</a></p> <p>マイクロサービスが利用可能であるかのSLIを計測する場合、そのマイクロサービス自身が計測した metrics を使うことはできません。 何故なら、そのマイクロサービス自身が死んでしまうと metrics を計測することができなくなってしまうからです。 そのため、SLIを取りたいマイクロサービスに対してリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを投げている別のマイクロサービスが metrics の計測を行うようにしております。</p> <p>リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを投げているマイクロサービスは複数あり、それぞれが言語、利用ライブラリ、実装、運用が違っております。 そのため、そのままでは全サービスで同じように SLI の計測を行えるようにすることができません。</p> <p>そこで、全てのサービスで Envoy を経由してリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを投げるようにし、Envoy で計測できる metrics を SLI に利用することにしました。 Envoy の導入はサービスの言語、利用ライブラリ、実装、運用によらず同じように行うことができます。</p> <h2 id="Envoy-を運用していくなかで出た課題">Envoy を運用していくなかで出た課題</h2> <h3 id="メンテナンスできなくなってしまった">メンテナンスできなくなってしまった</h3> <p>Envoy の導入までは良かったのですが、多くのサービスでアップグレードが行われない状況になってしまいました。 これには<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発体制が大きく関わってきます。</p> <p>私たちは現時点で70サービスを運用する大きな組織になっていますが、それに対してSREチームはたった8人しかいません。 そのため、各サービスの <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> manifest と Envoy の運用はサービスを運用しているアプリケーションエンジニアのチームが責任を負い、SREチームはそのフォローをするという体制になっております。</p> <p><a href="https://blog.studysapuri.jp/entry/future-with-kubernetes#SRE%E6%A5%AD%E5%8B%99%E3%82%92%E9%83%A8%E5%88%86%E7%9A%84%E3%81%ABWeb%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%B8">参考:Kubernetes導入で実現したい世界とその先にあるMicroservices</a></p> <p>しかし、全てのチームに <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> と Envoy に強い人がいるというわけではなく熟練度はチーム毎にばらつきがあります。 また、開発チームが増えていく中で、SREチームが手厚くフォローを継続することも現実的ではなくなってきていました。</p> <p>2020年頃 Envoy 1.14.0 のリリースに伴って Envoy の設定ファイルに deprecated の変更が入ったこともあり、各開発チームが Envoy のアップグレードをできていないという状況になってしまいました。</p> <p>また、通信するマイクロサービスを増やす際にはアプリケーションエンジニアが Envoy の設定ファイルに記述を追加する必要がありますが、これがエンジニアの認知負荷になり生産性を下げることになってしまっていました。</p> <h3 id="コンテナの起動順の制御にややこしさがある">コンテナの起動順の制御にややこしさがある</h3> <p>ワーカーやジョブなど外部からのリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを受けずに自動で処理が始まる Pod の場合、Envoy コンテナが起動する前にアプリケーションコンテナの処理が始まると外部通信ができずにエラーになります。 また、同様の理由で Pod が終了する際には Envoy コンテナより先にアプリケーションコンテナを終了させる必要があります。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/kubernetes">kubernetes</a> ではコンテナの起動順を制御できる設定値がないため、以下の方法を使ってコンテナの起動順を制御しています。 1. コンテナを定義する順番は Envoy を先にし、アプリケーションコンテナを後にする 2. Envoy コンテナに postStart を定義して、Envoy コンテナが起動するまでアプリケーションコンテナの起動を待たせる。 3. Envoy コンテナに preStop を定義して、アプリケーションコンテナが終了してから Envoy コンテナが終了するようにする。</p> <p>このような複雑な設定を入れていることが、アプリケーションエンジニアが <a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> manifet を運用することの負担になってしまっていました。</p> <h3 id="Kustomize-で-Rollout-の設定を上書きする際にコンテナが複数あることで複雑になってしまう">Kustomize で Rollout の設定を上書きする際に、コンテナが複数あることで複雑になってしまう</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは <a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> manifest を以下のように運用しております。</p> <ul> <li>canary release するために <a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> Rollout を使って Pod をデプロイする</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> Rollout で設定する Memory や CPU を kustomize で環境毎に変更する</li> </ul> <p>Kustomize の patchesStrategicMerge で <a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> Rollout に定義されている Memory と CPU を上書きする際に、spec.template.spec.containers 配下の設定が全て上書きされてしまうという問題があり、patchesStrategicMerge ではなく patchesJson6902 を使っていました。</p> <p>patchesJson6902 で設定を上書きする場合、上書き対象の設定値のパスを指定する必要があります。 spec.template.spec.containers の中は配列になっているため、以下のようにパスに配列番号を指定することになります。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># 0番目のコンテナの resources を上書きするという内容</span> <span class="synStatement">- </span><span class="synIdentifier">op</span><span class="synSpecial">:</span> replace <span class="synIdentifier">path</span><span class="synSpecial">:</span> /spec/template/spec/containers/0/resources <span class="synIdentifier">value</span><span class="synSpecial">:</span> <span class="synIdentifier">limits</span><span class="synSpecial">:</span> <span class="synIdentifier">memory</span><span class="synSpecial">:</span> 825Mi <span class="synIdentifier">requests</span><span class="synSpecial">:</span> <span class="synIdentifier">cpu</span><span class="synSpecial">:</span> 100m <span class="synIdentifier">memory</span><span class="synSpecial">:</span> 825Mi </pre> <p>このように patchesJson6902 を使う場合は配列番号が出てくるため、Envoy コンテナとアプリケーションコンテナの2つがある場合はその順序を意識する必要があります。 しかし、上記の<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a>だけではどちらのコンテナを上書きする設定なのかが読み取れず、エンジニアが混乱するということが起きていました。</p> <p>なお、patchesStrategicMerge で Rollout をうまく上書きできないという問題は Kustomize v4.5.5 で解消しております。 当時 Envoy を廃止する方向性に決めた要因の1つだったのですが、現在この問題は起きなくなっております。</p> <p>参考:<a href="https://argo-rollouts.readthedocs.io/en/latest/features/kustomize/">https://argo-rollouts.readthedocs.io/en/latest/features/kustomize/</a></p> <h2 id="service-mesh-の導入検討">service mesh の導入検討</h2> <p>service mesh はサービス間通信を管理するためのインフラスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>チャ層の実装で、サービス間通信における信頼性と可観測性の観点でメリットがあります。 SLI を計測するのに必要な機能 (ここでは Envoy) を全てのマイクロサービスに対して一括で挿入することも可能な技術です。</p> <p>service mesh を導入し Envoy の運用をSREチームでまとめて行えるようにすることで課題を解決しようとしました。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> EKS を基盤にしているため、service mesh には <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> App Mesh の利用を前提として検討しました。 しかし、検証を進めていく中でいくつかの課題があることに気がつきました。</p> <h3 id="kubernetes-manifest-の運用"><a class="keyword" href="https://d.hatena.ne.jp/keyword/kubernetes">kubernetes</a> manifest の運用</h3> <p>App Mesh を導入するとアプリケーションエンジニアが Envoy を運用する必要がなくなるため、認知負荷や運用コストが減ることを期待していました。 しかし、App Mesh を使うには代わりに VirtualNode, VirtualRouter, VirtualService などの <a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> リソース をアプリケーションエンジニアが運用する必要があるということが分かりました。</p> <p>バージョンアップはインフラスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>チャ層で行われるため、アプリケーションエンジニアが意識することは減ります。 しかし、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト先サービスとの繋ぎこみなどアプリケーションエンジニアがやることは残ってしまい、期待していたよりも認知負荷や運用コストが減らないということが分かりました。</p> <h3 id="AWS-リソースを作れなかった際にアプリケーションエンジニアが気付けない"><a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> リソースを作れなかった際にアプリケーションエンジニアが気付けない</h3> <p>App Mesh では VirtualNode, VirtualRouter, VirtualService などの <a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> リソース をデプロイすることで、それらに対応した <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> App Mesh のリソースが作られるようになっております。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> リソースと <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> App Mesh リソースのそれぞれでバリデーションがあるのですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> App Mesh のバリデーションの方がより細かく設定されています。 そのため、<a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> リソースのデプロイは成功したにも関わらず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> App Mesh のリソースが作られないということが起きてしまいます。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> リソースの内容を<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> App Mesh に反映する仕組みはインフラスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>チャ層にあるため、そこでエラーが出ていてもアプリケーションエンジニアが気づくのは難しいです。 そのため、アプリケーションエンジニアからするとデプロイは成功したのに設定が反映されないということが起きてしまいます。</p> <h3 id="App-Mesh-のリソース作成上限">App Mesh のリソース作成上限</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは <a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a> で pull request を作ると、自動的に検証用の環境 (<a class="keyword" href="https://d.hatena.ne.jp/keyword/k8s">k8s</a> Namespace) が作られるようになっています。 そのため、App Mesh を導入した場合 pull request の数だけ <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> App Mesh のリソースが作られることになってしまいます。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> のリソース作成数には上限が決められており、上限に引っかかってしまった場合に pull request で環境を作れなくなってしまうという課題がありました。</p> <h3 id="App-Mesh-で取得できる-metrics-が不便である">App Mesh で取得できる metrics が不便である</h3> <p>App Mesh を導入する主目的は Datadog で SLI を計測できるようにすることですが、そのためには各HTTPリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トの情報をリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト先サービス毎に分けて計測できる必要があります。 そのため、HTTPリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト情報の metrics にはリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト先サービス名のタグがついている必要があります。</p> <p>App Mesh で取得できる metrics にはリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト先サービス名だけのタグはなく、1つのタグにさまざまな情報が含まれてしまいます。 また、datadog でタグの絞り込みに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EF%A5%A4%A5%EB%A5%C9%A5%AB%A1%BC%A5%C9">ワイルドカード</a>を使う場合、前方一致か後方一致のどちらかでしか絞り込みが行えません。 そのため、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト先サービス名の前後両方に他の情報がついてしまっているタグでは上手く絞り込みができません。</p> <p>この問題は App Mesh で自動挿入される Envoy の設定ファイルを変更することで、Datadog に送信する metrics のタグを分解するという方法で解決することができます。 しかし、Envoy の設定ファイルだけを差し替えるということはできず、差し替える場合は独自の Envoy image を作る必要があります。 自前で App Mesh で使われる Envoy image を作成し保守するとなると SRE チームの負荷になってしまうという問題が残ってしまいました。</p> <h3 id="これらを踏まえた判断">これらを踏まえた判断</h3> <p>このまま導入をしてもうまく運用していくことは難しく、Envoy を運用保守していた時より改善はされないと判断をして App Mesh の導入を見送りました。</p> <h2 id="Datadog-APM-の登場">Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> の登場</h2> <p>今回の件とは別のところで、アプリケーションの監視や調査に Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> を利用したいという話が持ち上がり、ほとんどのマイクロサービスに Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> が導入されることになりました。 そこで、SLI に使う metrics は Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> で計測できるものを使うようにすることで Envoy の利用を廃止できるのではと考えました。</p> <p>Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> は Envoy と違って各アプリケーションにライブラリを導入する必要があるため、Envoy のように全てのチームで導入方法や運用を一般化することはできません。 しかし、ほぼ全てのチームで Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> を利用するモチベーションが高く、一般化しなくても自然と導入が進み、問題なく運用されるようになりました。</p> <p>また、Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> のライブラリで計測できる metrisc は私たちが利用している全ての言語で同じなため、SLI の計算方法は一般化することができます。 そのため、全てのチームで同じように SLI の運用を行うことができています。 こうして各開発チームを Envoy の運用から解放し、それでいて SLI の計測を行えるという世界を実現することができました。</p> <h2 id="Datadog-APM-の課題">Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> の課題</h2> <p>Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> はアプリケーション毎にライブラリを導入して利用する必要があるため、公式のライブラリが未対応の新しい言語などで利用することは難しいです。 Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> は多くの言語に対応しておりますが、今後の言語選定に制約をかけることになってしまいました。</p> <p><a href="https://docs.datadoghq.com/ja/tracing/trace_collection/dd_libraries/">参考:Add the Datadog Tracing Library</a></p> <p>より技術が発展し私達にとって使い勝手の良い service mesh が登場したら、改めて service mesh の導入を検討していけたらと考えています。</p> <h2 id="得られた学び">得られた学び</h2> <p>service mesh の思想はとても良いものですが、技術的な成熟度という観点で私たちの開発体制には合っていないと考え、今回は採用には至りませんでした。 一方、Datadog <a class="keyword" href="https://d.hatena.ne.jp/keyword/APM">APM</a> を使う方法は言語や環境に依存するにも関わらず、私たちの開発体制にマッチし問題を解決してくれるという結果になりました。 技術選定をする際には単に技術が良いものであるかどうかだけではなく、導入する組織やチームの事情を理解した上でマッチしているかどうか考えることが大切です。</p> <p>また、プロダクトを開発している @snowfield702 と SRE の @int128, @44smkn が一緒にプロジェクトを進めることで、プロダクト開発とSREそれぞれの面で感じている課題をお互いに共有しながらうまく解決に持っていくことができました。 課題解決には多方面からの視点が必要になってくるため、チーム横断でプロジェクトを進めることができるというのはとても重要になります。</p> snowfield702 単一アプリでユーザに応じた機能切り替えを実現するために hatenablog://entry/6801883189055444705 2023-11-08T09:00:00+09:00 2023-11-08T09:00:20+09:00 はじめに iOSエンジニアのkomajiです。2023年9月に「スタディサプリ 小学講座」をリニューアルし、既存の「スタディサプリ 中学講座」アプリに組み込む形でリリースしました。 この小学講座は既存の中学講座とは機能が全く異なっていますが、単一アプリで両方の機能を提供しています。これらはユーザの学年に応じて切り替わるようになっているのですが、本記事では、単一アプリでこの機能切り替えをどのようにして実現してきたのかについて紹介します。 小学講座について 小学講座では、子どもが自ら学び、その子に一番合った最高の学習体験が提供されることを目指しています*1。これを実現する仕掛けを組み込むために、小… <h2 id="はじめに">はじめに</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>エンジニアのkomajiです。2023年9月に「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ 小学講座」をリニューアルし、既存の「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ 中学講座」アプリに組み込む形でリリースしました。</p> <p>この小学講座は既存の中学講座とは機能が全く異なっていますが、単一アプリで両方の機能を提供しています。これらはユーザの学年に応じて切り替わるようになっているのですが、本記事では、単一アプリでこの機能切り替えをどのようにして実現してきたのかについて紹介します。</p> <h2 id="小学講座について">小学講座について</h2> <p>小学講座では、子どもが自ら学び、その子に一番合った最高の学習体験が提供されることを目指しています<a href="#f-455f82bc" name="fn-455f82bc" title="https://www.recruit.co.jp/newsroom/pressrelease/2023/0919_12620.html">*1</a>。これを実現する仕掛けを組み込むために、小学講座では以下の画像のように既存の中学講座とは全く異なる機能を提供しています<a href="#f-5dd8b570" name="fn-5dd8b570" title="2023/11/08時点では小学生は1年生のみが利用できますが、他学年も順次サポートしていく予定です。">*2</a>。</p> <table> <thead> <tr> <th>小学講座 </th> <th> 中学講座</th> </tr> </thead> <tbody> <tr> <td><figure class="figure-image figure-image-fotolife" title="小学講座"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/komaji504/20231102/20231102135235.jpg" alt="&#x5C0F;&#x5B66;&#x8B1B;&#x5EA7;" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure> </td> <td> <figure class="figure-image figure-image-fotolife" title="中学講座"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/komaji504/20231102/20231102135805.jpg" alt="&#x4E2D;&#x5B66;&#x8B1B;&#x5EA7;" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure></td> </tr> </tbody> </table> <p>機能が全く異なるため、別アプリとしてリリースすることも方法の一つですが、学習体験や運用・実装コストなどを総合的に判断し、同一のアプリとしてリリースしています。</p> <h2 id="単一アプリでの機能切り替えの実現">単一アプリでの機能切り替えの実現</h2> <h3 id="マルチモジュール">マルチモジュール</h3> <p>単純に2種類の機能を提供するとなるとコード量が2倍になります。小学講座は中学講座に比べてシンプルな機能構成となっていたり、会員登録やログインといった共通部分があったりするため2倍ではないですが、それ相応のコード量となります。</p> <p>両方の機能を単一のモジュールで管理すると、それぞれ独立すべき実装の境界が曖昧になり<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C1%C2%B7%EB%B9%E7">疎結合</a>を維持しづらくなってしまい、保守性が低下してしまいます。そのため、それぞれが干渉せず独立して開発できるように、小学講座・中学講座をそれぞれのモジュールで管理することにしました。いわゆるマルチモジュール化です。これを小学講座の開発前の段階で事前に整備しました。</p> <p>モジュールはSwift Package Mangerで管理しており、Packageを複数作り、その中に機能ごとのTargetを作ることでモジュール化を実現しています。現在の主なモジュールの構成は以下のようになっています。</p> <p><figure class="figure-image figure-image-fotolife" title="主なモジュールの構成"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/komaji504/20231107/20231107111938.png" alt="&#x4E3B;&#x306A;&#x30E2;&#x30B8;&#x30E5;&#x30FC;&#x30EB;&#x306E;&#x69CB;&#x6210;" width="897" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>主なモジュールの構成</figcaption></figure></p> <p>モジュール化の大まかな方針は以下のとおりです。</p> <ul> <li>モジュール内で完結するように実装する <ul> <li>同様の実装を複数箇所で利用する場合のみ共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>モジュール(CoreやUIComponent)にて共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>する</li> </ul> </li> <li>画像などのリソースも各モジュール内で保持する</li> <li>JuniorHighSchool/LowerElementarySchool Package内のTargetにはPackageを示すPrefixを付与する <ul> <li>同じ機能のTargetがそれぞれのPackageに存在するので認知負荷の観点から付与する</li> </ul> </li> </ul> <p>それぞれ独立したモジュールで管理することで、小学講座の機能を開発する際にはLowerElementarySchool Packageを触るだけでよく、中学講座への影響がほとんどない状態で開発できるようになりました。加えて、モジュール単位でのビルドが可能となることで、ビルド時間が削減されたり<a class="keyword" href="https://d.hatena.ne.jp/keyword/Xcode">Xcode</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Preview">Preview</a>でUIを確認しながらの実装が可能になったりと、開発効率・開発体験が大きく向上したと感じています。</p> <h3 id="学年に応じた機能の切り替え">学年に応じた機能の切り替え</h3> <p>小学・中学講座の機能を学年に応じて切り替える必要がありますが、この切り替えの実装をどのように実現しているかを紹介します。</p> <p>アプリ起動から対象講座のホーム画面が表示されるまでのフローは大まかに以下のようになっています。</p> <p><figure class="figure-image figure-image-fotolife" title="学年に応じた機能表示フロー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/komaji504/20231106/20231106152613.png" alt="&#x5B66;&#x5E74;&#x306B;&#x5FDC;&#x3058;&#x305F;&#x6A5F;&#x80FD;&#x8868;&#x793A;&#x30D5;&#x30ED;&#x30FC;" width="1200" height="562" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>学年に応じた機能表示フロー</figcaption></figure></p> <p>まずアプリを起動するとログイン画面が表示されます。そこからログインまたは会員登録してログイン状態になると、ユーザの学年に応じて小学講座または中学講座が表示されます。これらの切り替えはRootScreenというビューで管理しています。</p> <p>学年に応じた機能の選択ロジックはバックエンドで管理しているため、RootScreenはユーザがログインした際にどちらの機能を表示すべきかを<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>から取得し、それに応じて対応する機能のビューを子ビューとして表示しています。具体的な実装は以下のようになっています(一部わかりやすくするために省略・変更しています)。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">enum</span> <span class="synIdentifier">StartupScreen</span> { <span class="synStatement">case</span> loading <span class="synComment">// ローディング画面の表示</span> <span class="synStatement">case</span> login <span class="synComment">// ログイン画面の表示</span> <span class="synStatement">case</span> lowerElementrySchoolHome <span class="synComment">// 小学講座のルートビューの表示</span> <span class="synStatement">case</span> juniorHighSchoolHome <span class="synComment">// 中学講座のルートビューの表示</span> } <span class="synPreProc">struct</span> <span class="synIdentifier">RootScreen</span> { <span class="synType">@StateObject</span> <span class="synType">var</span> viewModel<span class="synSpecial">:</span> <span class="synType">RootScreenViewModel</span> <span class="synPreProc">var</span> <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synType">some</span> View { contents .onAppear { viewModel.fetch() } } <span class="synType">@ViewBuilder</span> <span class="synType">var</span> contents<span class="synSpecial">:</span> <span class="synType">some</span> View { <span class="synStatement">switch</span> viewModel.screen { <span class="synStatement">case</span> .loading<span class="synSpecial">:</span> <span class="synType">LoadingView</span>() <span class="synStatement">case</span> .login<span class="synSpecial">:</span> <span class="synType">NavigationView</span> { LoginScreen() } <span class="synStatement">case</span> .lowerElementrySchoolHome<span class="synSpecial">:</span> <span class="synType">NavigationView</span> { LowerElementrySchool.HomeScreen() } <span class="synStatement">case</span> .juniorHighSchoolHome<span class="synSpecial">:</span> <span class="synType">NavigationView</span> { JuniorHighSchool.HomeScreen() } } } } <span class="synStatement">final</span> <span class="synPreProc">class</span> <span class="synIdentifier">RootScreenViewModel</span><span class="synSpecial">:</span> <span class="synType">ObservableObject</span> { <span class="synType">@Published</span> <span class="synType">private</span>(<span class="synStatement">set</span>) <span class="synPreProc">var</span> <span class="synIdentifier">screen</span><span class="synSpecial">:</span> <span class="synType">StartupScreen</span> <span class="synIdentifier">=</span> .loading <span class="synPreProc">func</span> <span class="synIdentifier">fetch</span>() { <span class="synComment">// ログイン状態やバックエンドで選択された機能に基づいてscreenを更新する</span> } </pre> <p>このように機能の切り替えをRootScreenに集約したり、前述したマルチモジュール化によって機能をモジュールとして切り出したりすることで、それぞれの機能の実装をそれぞれ閉じた状態で行えるなり、保守性の低下を防いでいます。</p> <h3 id="機能ごとの画面回転制御">機能ごとの画面回転制御</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/iPad">iPad</a>で利用した場合、中学講座では画面回転を許容していますが小学講座は横方向のみをサポートしています(小学講座は<a class="keyword" href="https://d.hatena.ne.jp/keyword/iPad">iPad</a>でのみの提供)。そのため、許容する画面方向を動的に(コードで)変更する必要があります。そこで、以下による画面方向制御を組み合わせて実施した場合にどのように機能するのかを検証しました。</p> <ul> <li>Info.plistによる設定 <ul> <li><a href="https://developer.apple.com/documentation/bundleresources/information_property_list/uisupportedinterfaceorientations">UISupportedInterfaceOrientations</a></li> </ul> </li> <li>コードによる設定 <ul> <li><a href="https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623107-application"><code>UIApplicationDelegate.application(_:supportedInterfaceOrientationsFor:)</code></a></li> </ul> </li> </ul> <p>検証結果は以下のようになりました。</p> <ul> <li>Info.plist で有効化→コードで無効化 <ul> <li>❌ できない</li> </ul> </li> <li>Info.plist で無効化→コードで有効化 <ul> <li>✅ できる</li> </ul> </li> </ul> <p>この結果から以下の対応を実施し、中学講座では両方向の許可・小学講座では横方向のみの許可を実現できました。</p> <ul> <li>Info.plistでは横方向のみ許可する</li> <li>ログイン・会員登録を終えて中学講座へ遷移した場合にコードで縦方向を許可する</li> </ul> <p>なお、ログイン・会員登録画面においては、もともと許容されていた縦方向に回転ができなくなるという既存動作との差分が生じてしまいましたが、今回は許容しています。</p> <h2 id="おわりに">おわりに</h2> <p>本記事では、全く機能の異なる小学講座と中学講座を単一のアプリとして提供する上で、機能切り替えをどのようにして実現してきたのかについて紹介しました。単一アプリとすることでシンプルには対応できない点もありましたが、保守性を考慮しながら適切に実装できたと感じています。</p> <p>小学講座のリリースを無事終えたのでひと段落ではありますが、デザインシステムに基づいた<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>の実装やモジュール化の展開など課題はまだまだ残っているため、より良い学習体験を提供できるように引き続き改善していきたいです。</p> <div class="footnote"> <p class="footnote"><a href="#fn-455f82bc" name="f-455f82bc" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://www.recruit.co.jp/newsroom/pressrelease/2023/0919_12620.html">https://www.recruit.co.jp/newsroom/pressrelease/2023/0919_12620.html</a></span></p> <p class="footnote"><a href="#fn-5dd8b570" name="f-5dd8b570" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">2023/11/08時点では小学生は1年生のみが利用できますが、他学年も順次サポートしていく予定です。</span></p> </div> komaji504 SwiftUI: Explicit Identity の正しい使い方と落とし穴 hatenablog://entry/6801883189053777323 2023-10-31T08:00:00+09:00 2023-10-31T08:00:02+09:00 こんにちは。iOS エンジニアの @_nkmrh です。 SwiftUI が発表されてからはや4年が経ち、昨今のプロダクションコードでも多く活用されているのではないでしょうか。 そこで本稿では SwiftUI を活用する上で欠かすことのできない SwiftUI.View の Explicit Identity についておさらいしていきたいと思います。 Explicit Identity とは まずはじめに、SwiftUI の Explicit Identity とはなんなのかを説明しておきたいと思います。 Explicit Identity は SwiftUI が個々のView を識別するため… <p>こんにちは。<a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a> エンジニアの <a href="https://twitter.com/_nkmrh">@_nkmrh</a> です。</p> <p>SwiftUI が発表されてからはや4年が経ち、昨今のプロダクションコードでも多く活用されているのではないでしょうか。</p> <p>そこで本稿では SwiftUI を活用する上で欠かすことのできない SwiftUI.View の Explicit Identity についておさらいしていきたいと思います。</p> <h2 id="Explicit-Identity-とは">Explicit Identity とは</h2> <p>まずはじめに、SwiftUI の Explicit Identity とはなんなのかを説明しておきたいと思います。</p> <p>Explicit Identity は SwiftUI が個々のView を識別するための識別子です。UIView は class オブジェクトでしたので個々の UIView を識別するためにはオブジェクトのポインタを使っていましたが、SwiftUI の View は struct ですのでポインタはありません。そこで id を付与し個々の View を識別します。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DE">プログラマ</a>が <code>.id()</code> モディファイアを付与しなくても SwiftUI は自動的に body の中で宣言されている個々の View に id(implict identity) を付与し識別しています。</p> <p>これらの id は SwiftUI が View のライフサイクルを管理するために利用されます。</p> <h2 id="良い-id-の条件">良い id の条件</h2> <ul> <li>id はユニークでなければいけない(複数の View が1つのidを共有することはできない)</li> <li>id は時間と共に変化してはいけない(コンピューテッドプロパティなど都度計算されるものは用いない)</li> </ul> <h2 id="悪い-id-指定の例">悪い id 指定の例</h2> <p>ここからは悪い id 指定の例をご紹介します。</p> <p>下記の swift コードは説明のための簡略化したコードです。 Lesson 構造体からなる lessons 配列があり、ForEach を使って title の値を Text で表示させています。 Text の表示の他に、index の値によって追加する View があるとします。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">struct</span> <span class="synIdentifier">Lesson</span> { <span class="synPreProc">var</span> <span class="synIdentifier">title</span><span class="synSpecial">:</span> <span class="synType">String</span> } <span class="synComment">// ...省略</span> <span class="synType">@State</span> <span class="synType">var</span> lessons<span class="synSpecial">:</span> <span class="synSpecial">[</span><span class="synType">Lesson</span><span class="synSpecial">]</span> <span class="synIdentifier">=</span> [ Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;a&quot;</span>), Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;b&quot;</span>), Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;c&quot;</span>), Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;d&quot;</span>), ] ForEach(lessons.indices, id<span class="synSpecial">:</span> \.<span class="synIdentifier">self</span>) { index <span class="synStatement">in</span> Text(lessons[index].title)) <span class="synComment">// index を利用した処理</span> } </pre> <p>このコードは ForEach メソッドの id 引数に lessons 配列のインデックス値を指定しています。 このように書くと Text に id モディファイアでインデックス値を指定しているのと同じような意味として SwiftUI に伝えていることになります。</p> <pre class="code lang-swift" data-lang="swift" data-unlink> Text(lessons[index].title) .id(index) </pre> <p>実際、<a href="https://developer.apple.com/documentation/swiftui/foreach/init(_:id:content:)-82hm4">公式ドキュメント</a>には、</p> <blockquote><p>「If the id of a data element changes, then the content view generated from that data element will lose any current state and animations.」</p></blockquote> <p>と明記されています。これは、データ要素の id が変わると、そのデータ要素から生成されたコンテンツビューは現在の状態やアニメーションを失うことを意味しています。</p> <p>この書き方が良くない理由として、lessons 配列の先頭に新たな値(<code>Lesson(Lessons(title: "z")</code>) が挿入された場合、配列の値は</p> <pre class="code lang-swift" data-lang="swift" data-unlink> [ Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;z&quot;</span>), Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;a&quot;</span>), Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;b&quot;</span>), Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;c&quot;</span>), Lesson(title<span class="synSpecial">:</span> <span class="synConstant">&quot;d&quot;</span>), ] </pre> <p>となりますが、この時 ForEach が生成するそれぞれの View は <code>Text("z").id(0)</code> <code>Text("a").id(1)</code> のようにインデックス値が1つずれるため、SwiftUI の立場から見ると View の id が変化したため、これまでの <code>Text("a").id(0)</code> を破棄して新しい <code>Text("a").id(1)</code> を保持し描画することになります。</p> <p>これは先ほどの、「id は時間と共に変化してはいけない」ということや、「id は View のライフサイクルに利用される」ということを示しています。 本来であれば同じ <code>Text("a")</code> を表示するための View なので View を破棄し再生成/再描画する必要は無いのですが、このように id が更新されることにより、ForEach で作成される全ての View を破棄し再生成/再描画することになります。これでは SwiftUI の差分<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>によるパフォーマンスの恩恵を受けられなくなってしまいます。</p> <p>また、それだけではなくアニメーションを指定していた場合、適切なアニメーション効果が View に適用されないバグとなって現れてしまいます。</p> <h2 id="良い-id-指定の例">良い id 指定の例</h2> <p>良い id 指定の例を見てみましょう。</p> <p>Identifiable に準拠させ、前述した良い id の条件を満たす値を id プロパティから返すようにします。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">struct</span> <span class="synIdentifier">Lesson</span><span class="synSpecial">:</span> <span class="synType">Identifiable</span> { <span class="synPreProc">var</span> <span class="synIdentifier">databaseId</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synPreProc">var</span> <span class="synIdentifier">title</span><span class="synSpecial">:</span> <span class="synType">String</span> <span class="synPreProc">var</span> <span class="synIdentifier">id</span><span class="synSpecial">:</span> <span class="synType">String</span> { databaseId } } <span class="synComment">// ...省略</span> ForEach(lessons) { lesson <span class="synStatement">in</span> Text(lesson.title) <span class="synStatement">if</span> <span class="synPreProc">let</span> <span class="synIdentifier">index</span> <span class="synIdentifier">=</span> lessons.firstIndex { <span class="synIdentifier">$0</span>.id <span class="synIdentifier">==</span> lesson.id } { <span class="synComment">// index を利用した処理</span> } } </pre> <p>このように良い id を指定することで、SwiftUI の意図した設計通りの動作をすることができます。</p> <h2 id="まとめ">まとめ</h2> <p>このように Explicit Identity は SwiftUI を使う上で基本的で重要な概念ですが、普段開発をしているとこのような概念をうっかり忘れてしまったり、悪い id の例のように画面を実装しても、要件を満たすことが可能なケースもあるかもしれません。</p> <p>しかし、SwiftUI は id の取り扱いに特に依存しており、その仕組みの中で動作しています。適切な id を提供することは、ユーザーにとって質の高いアプリ体験を実現するために不可欠です。</p> <p>今回は以上となります。 それでは良い Swift ライフを!</p> <h2 id="採用情報">採用情報</h2> <p>SwiftUI や <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>開発に興味があり、新しい技術や知識を追求したい方は、以下のリンクから採用情報をご確認ください。</p> <p><a href="https://brand.studysapuri.jp/career/category/engineer/#openPositions">https://brand.studysapuri.jp/career/category/engineer/#openPositions</a></p> nkmrh デザインをそのままの形でユーザーにお届けするためのデザインQA hatenablog://entry/6801883189049767140 2023-10-30T09:00:00+09:00 2023-10-30T09:00:01+09:00 スタサプ小中で Android エンジニアをしている石田とデザイナーの竹本です。 2023年9月に リニューアルをしたスタディサプリ 小学講座をリリースしました。小学開発ではエンジニアが実装したアプリの画面をデザイナーがレビューするプロセスを デザインQA という形で明文化したので本記事ではその紹介をしたいと思います。 デザインQA デザインQAの導入経緯 これまでデザイナーが作成したデザインをもとにエンジニアが実装した成果物の確認はお互いによしなにやるという暗黙的な運用になっていました。この運用ではメンバーの裁量に依る部分が大きくなってしまうため、小学開発を機にエンジニアが実装後に必ずデザイ… <p>スタサプ小中で <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> エンジニアをしている石田とデザイナーの竹本です。</p> <p>2023年9月に <a href="https://studysapuri.jp/course/elementary/">リニューアルをしたスタディサプリ 小学講座</a>をリリースしました。小学開発ではエンジニアが実装したアプリの画面をデザイナーがレビューするプロセスを <strong>デザインQA</strong> という形で明文化したので本記事ではその紹介をしたいと思います。</p> <p><figure class="figure-image figure-image-fotolife" title="デザインQA"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231012/20231012104510.png" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デザインQA</figcaption></figure></p> <h2 id="デザインQAの導入経緯">デザインQAの導入経緯</h2> <p>これまでデザイナーが作成したデザインをもとにエンジニアが実装した成果物の確認はお互いによしなにやるという暗黙的な運用になっていました。この運用ではメンバーの裁量に依る部分が大きくなってしまうため、小学開発を機にエンジニアが実装後に必ずデザイナーにチェックを受ける運用にしそのプロセスを <strong>デザインQA</strong> と呼ぶことにしました。</p> <p><figure class="figure-image figure-image-fotolife" title="デザインQAまでのステップ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231012/20231012104524.png" width="1200" height="661" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デザインQAまでのステップ</figcaption></figure></p> <p>ちなみにデザインQAのようなア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>は Issue を作ってメンバーと相談しながら検討します。</p> <p><figure class="figure-image figure-image-fotolife" title="デザインQA 導入の Issue"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231012/20231012104616.png" width="1200" height="511" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デザインQA 導入の Issue</figcaption></figure></p> <h2 id="実際の-デザインQA-でのやりとり">実際の デザインQA でのやりとり</h2> <p>デザインQA のイメージが分かるように先に実際のやりとりの様子をご紹介します。例としてトピック一覧、レッスン一覧と呼んでいる画面を取り上げます。</p> <table> <thead> <tr> <th> トピック一覧 </th> <th> レッスン一覧 </th> </tr> </thead> <tbody> <tr> <td> <figure class="figure-image figure-image-fotolife" title="トピック一覧"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231012/20231012104656.png" width="1024" height="768" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure> </td> <td> <figure class="figure-image figure-image-fotolife" title="レッスン一覧"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231012/20231012104711.png" width="1024" height="768" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure> </td> </tr> </tbody> </table> <p>こちらがデザインQAを行っている様子の全体像です。デザイナーは <a class="keyword" href="https://d.hatena.ne.jp/keyword/Figma">Figma</a> でデザインを作成しますが、デザインQA もまた <a class="keyword" href="https://d.hatena.ne.jp/keyword/Figma">Figma</a> 上で行っています。同一ツールを使う方が行き来がしやすいためです。デザイナーが何か問題や相談したい点が見つかったら <a class="keyword" href="https://d.hatena.ne.jp/keyword/Figma">Figma</a> のコメント機能でやりとりを行います。この例では「レビュー → 修正」のやり取りを3回繰り返しています。</p> <p><figure class="figure-image figure-image-fotolife" title="デザインQA 実施の様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231012/20231012105112.png" width="1200" height="448" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デザインQA 実施の様子</figcaption></figure></p> <p>具体的なやり取りを抜粋して紹介します。こちらはレッスンカードについてのやり取りで「カードの Corner <a class="keyword" href="https://d.hatena.ne.jp/keyword/Radius">Radius</a> が上部と下部で異なっている点」と「カードのドロップシャドウの下部が見切れている点」の2点が指摘されています。</p> <p>この指摘をもらってエンジニアの僕が正直に思ったのは「デザイナーの目ってすごい。。。」ということです。エンジニアの目ではなかなか気づくことができないこともあるのではないでしょうか。</p> <p><figure class="figure-image figure-image-fotolife" title="わずかな問題もデザイナーは見逃さない"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20231012/20231012105316.png" width="1200" height="674" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>わずかな問題もデザイナーは見逃さない</figcaption></figure></p> <h2 id="デザインQA-のガイドライン">デザインQA の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a></h2> <p>さて、デザインQAを実施するにあたり一定の約束が必要だろうということで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>を作成したのでその一部を紹介します。</p> <ol> <li><p>エンジニアは次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を用意する。</p> <ul> <li>[必ず] 画面が取りうる様々な状態の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a> (例: 空やエラー状態など)</li> <li>[必ず] アニメーションなど動きのあるものは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>ではなく動画もしくは画面共有をする</li> <li>[できれば] 等倍の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a> (クロップしたりスケールはしない)</li> <li>[できれば] 複数の端末サイズの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a> (例: <a class="keyword" href="https://d.hatena.ne.jp/keyword/iPad%20mini">iPad mini</a>/<a class="keyword" href="https://d.hatena.ne.jp/keyword/iPad">iPad</a> Pro)</li> </ul> </li> <li><p>デザイナーは気持ち <strong>ゆるめ</strong> にレビューをする</p> <ul> <li>これはエンジニアは可能な限り<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>パーフェクトを目指すが、デザイナーは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D4%A5%AF%A5%BB%A5%EB">ピクセル</a>パーフェクトを求めすぎないでということです。なぜなら <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>/<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> のプラットフォーム固有の問題や実装コストの観点で妥協が必要になる場合があるからです。その場合はエンジニアからデザイナーに事前に妥協点を共有します。</li> </ul> </li> <li>実装中にデザイン上の問題が見つかった場合はデザインQAを待たずに相談をする。</li> </ol> <h2 id="デザインQA-の利点と課題">デザインQA の利点と課題</h2> <p>以下にデザインQAを実際に運用して感じた利点をまとめます。</p> <ul> <li>クオリティが上がる...!デザイナーのお墨付きがもらえるので自信を持ってリリースできた。</li> <li>デザインQAで必ずエンジニアとデザイナーがコミュニケーションを取ることになるので、認識の違いや不明点について細かく会話をしながら相談をしやすくなった。いつどうやって実装をチェックしてもらうかで悩まなくなった。</li> <li>デザイナーによるチェックが入るので、エンジニアがコードレビュー時にデザイン通りに正しく実装されているかに注意を払う必要がなくなった。</li> </ul> <h2 id="デザインQA-の課題">デザインQA の課題</h2> <p>同様に課題をまとめます。</p> <ul> <li>AS-IS(実装) と TO-BE(デザイン) の違いを伝えるのに時間がかかることがあった。</li> <li>動きのある画面のチェックが難しい。画面共有では多少カクツキが発生することがあった。 <ul> <li>こちらについては画面をキャプチャしたものをSlackで共有することをいったん推奨しています。一方でデザイナーが実機で実際に動作させながら確認する手段もあると考えています。</li> </ul> </li> <li>デザインQAの実施コストが一定かかる <ul> <li>特にレビューと修正の往復が発生するため実作業の時間はそれほどかからなくても事実としてリードタイムは大きくなりがちです。しかしながら、エンジニアとデザイナーがしっかりとコミュニケーションを取る機会を作り、確実なものをリリースできるという利点が上回っていると考えています。</li> <li>また、そもそもの「デザイン→実装」の過程で差分が起きづらくなるような仕組みづくりもしていきたいと考えています。</li> </ul> </li> </ul> <h2 id="まとめ">まとめ</h2> <p>本記事ではリニュアールをした<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ 小学講座の開発で導入したデザインQA について紹介しました。デザイナーが作成したデザインはエンジニアがコードにしてはじめてユーザーの元に届きます。デザイナーとエンジニアの認識をデザインQAでしっかりと合わせることによって、「チームで考えたデザインをそのままの形でユーザーにお届けすること」が可能になったように感じています。</p> <p>今後はデザインQAの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AC%A5%A4%A5%C9%A5%E9%A5%A4%A5%F3">ガイドライン</a>のアップデートやより効率的な運用を検討していきたいと思っています。本記事がデザイナーとエンジニアのコミュニケーションの取り方の参考になれば嬉しいです。</p> maxfieldwalker Firebase Remote Config の変更内容を Slack に通知する hatenablog://entry/6801883189053112149 2023-10-26T10:53:03+09:00 2023-10-30T20:41:29+09:00 こんにちは、@manicmaniac です。スタディサプリ iOS アプリ開発チームのエンジニアリングマネージャーをしています。 小ネタみたいな話ではありますが、Firebase Remote Config の変更を Slack に通知するようにしたらちょっと便利だったので記事にしようと思いました。 Firebase Remote Config とは Firebase Remote Config が何か知らない方もいると思うので、公式サイトから引用すると、 Firebase Remote Config Firebase Remote Config は、ユーザーにアプリのアップデートをダウンロー… <p>こんにちは、<a href="https://github.com/manicmaniac">@manicmaniac</a> です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D7%A5%EA%B3%AB%C8%AF">アプリ開発</a>チームのエンジニア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%F3%A5%B0%A5%DE">リングマ</a>ネージャーをしています。</p> <p>小ネタみたいな話ではありますが、Firebase Remote Config の変更を Slack に通知するようにしたらちょっと便利だったので記事にしようと思いました。</p> <h2 id="Firebase-Remote-Config-とは">Firebase Remote Config とは</h2> <p>Firebase Remote Config が何か知らない方もいると思うので、公式サイトから引用すると、</p> <p><a href="https://firebase.google.com/docs/remote-config?hl=ja">Firebase Remote Config</a></p> <blockquote><p>Firebase Remote Config は、ユーザーにアプリのアップデートをダウンロードしてもらわなくても、アプリの動作や外観を変更できる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスです。Remote Config を使用して、アプリの動作や外観を制御するためのアプリ内デフォルト値を作成できます。</p></blockquote> <p>です。まったくその通りなのですが、使っていないとイメージがつきにくいですね。</p> <p>要するに Firebase <a class="keyword" href="https://d.hatena.ne.jp/keyword/SDK">SDK</a> を通じて、なんらかの設定値をいろんなアプリに共有する、読み取り専用の KVS のようなものだと思っておくとだいたい合っていそうです。</p> <p>これができると何が嬉しいのかというと、たとえば feature toggle のように特定の機能だけをリリースしたり、A/B テストのようにユーザーのセグメントごとに機能や UI を切り替えるということが柔軟にできます。</p> <h2 id="スタディサプリでの-Firebase-Remote-Config-の使い方"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでの Firebase Remote Config の使い方</h2> <p>実際に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは Firebase Remote Config を以下のような用途で使っています。</p> <ol> <li>機能を先に実装しておいて、期日になったらリリースするため</li> <li>外的要因によって変更される変数を差し込むため <ul> <li>例: アプリの最低サポートバージョンを差し込むことで、それより古いバージョンのユーザーにアップデート通知を出す</li> <li>例: アプリに表示されるバナー画像を入稿する</li> <li>例: アプリからウェブビューで参照している外部サービスの URL を設定する</li> </ul> </li> </ol> <p>かつては <em>特定のユーザー属性を持つユーザーにのみ機能を解放するため</em> に使っていたこともありましたが、現在はあまり行われていません。</p> <p>Firebase Remote Config が変更され、それがアプリに反映されるまでには若干のタイムラグがあるため、課金によって解放される機能など「条件を満たしたら即使えないと致命的になる機能」には使いにくいという点も注意が必要です。</p> <h2 id="変更内容を-Slack-に通知したいモチベーション">変更内容を Slack に通知したいモチベーション</h2> <p>これまで少なくとも5年以上にわたって Firebase Remote Config を運用してきたのですが、たまに以下のような運用上の課題が発生していました。</p> <ul> <li>開発環境の Firebase Remote Config の値を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>用途などで一時的に変更したことが共有されておらず、別チームの検証に支障が出る</li> <li>バナー画像を <a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a> フォーマットで入稿しているが、バリデーションがないため <a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a> の文法エラーでバナーが表示されなくなる</li> <li>本番環境の不具合調査などで、特定の Firebase Remote Config の値をいつ、どのように変更したか正確に知りたいことがある</li> </ul> <p>そこまで高い頻度で変更するものでもないので、これまではお互い声かけしながらなんとかやっていましたが、比較的安いコストで自動化が可能そうだったのでやってみることにしました。</p> <p><figure class="figure-image figure-image-fotolife" title="実際に不具合を起こしている様子"> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/manicmaniac/20231024/20231024191838.png" width="1064" height="492" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実際に不具合を起こしている様子</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="変更はやる前、やった後で関係者に通知する必要がありました"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/manicmaniac/20231024/20231024191938.png" width="1200" height="501" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>変更はやる前、やった後で関係者に通知する必要がありました</figcaption></figure></p> <h2 id="実際やってみた様子">実際やってみた様子</h2> <p>というわけで、実際にやってみた様子をお見せします。</p> <p><figure class="figure-image figure-image-fotolife" title="バナーを変更した通知が流れている様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/manicmaniac/20231025/20231025142427.png" width="1038" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>バナーを変更した通知が流れている様子</figcaption></figure></p> <p>単に変更された事実だけでなく、</p> <ul> <li>変更内容の diff を表示する</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a> を要求するフィールドは <a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a> schema を利用して検証し、エラーがあれば通知する</li> <li>画像 URL が入稿されたら画像も表示する</li> </ul> <p>などの機能を盛り込んでいます。</p> <h2 id="通知するための実装">通知するための実装</h2> <p>Cloud Functions for Firebase を使うと Firebase Remote Config が変更されたタイミングで関数を実行することができるので、実装にはこれを利用します。</p> <p><a href="https://firebase.google.com/docs/functions?hl=ja">公式サイト</a> の紹介を引用すると、Cloud Functions for Firebase とは以下のようなものです。</p> <blockquote><p>Cloud Functions for Firebase はサーバーレス <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%EC%A1%BC%A5%E0%A5%EF%A1%BC%A5%AF">フレームワーク</a>で、Firebase の機能と <a class="keyword" href="https://d.hatena.ne.jp/keyword/HTTPS">HTTPS</a> リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トによってトリガーされたイベントに応じて、バックエンド コードを自動的に実行できます。</p></blockquote> <p>Slack 通知の基本的な実装は <a href="https://github.com/firebase/functions-samples/tree/main/Node/remote-config-diff">remote-config-diff (Cloud Functions for Firebase の公式サンプル実装)</a> を参考にしています。</p> <p>紹介の順番が前後してしまいましたが、実は今回この <a class="keyword" href="https://d.hatena.ne.jp/keyword/bot">bot</a> を作成しようと思ったのは、このサンプルコードをたまたま見て意外と簡単にできそうだと思ったことがきっかけでした。</p> <p>実装にともなっていくつか機能を足したり <a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a> Diff の整形結果を調整したりしたため、元のコードより少し長くなりましたが、TypeScript のコードは全体で 230 行程度で済みました。</p> <p>実装のメイン部分は以下のようになっています。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span>WebClient<span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;@slack/web-api&quot;</span><span class="synStatement">;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span>onConfigUpdated<span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;firebase-functions/v2/remoteConfig&quot;</span><span class="synStatement">;</span> <span class="synStatement">import</span> <span class="synIdentifier">{</span>getRemoteConfig<span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;firebase-admin/remote-config&quot;</span><span class="synStatement">;</span> <span class="synStatement">import</span> * <span class="synStatement">as</span> jsonDiff <span class="synStatement">from</span> <span class="synConstant">&quot;json-diff&quot;</span><span class="synStatement">;</span> <span class="synComment">// 定数を環境変数や secret として定義しておきます</span> <span class="synType">const</span> slackChannel <span class="synStatement">=</span> defineString<span class="synStatement">(</span><span class="synConstant">&quot;NOTIFY_REMOTE_CONFIG_SLACK_CHANNEL&quot;</span><span class="synStatement">);</span> <span class="synType">const</span> slackApiToken <span class="synStatement">=</span> defineSecret<span class="synStatement">(</span><span class="synConstant">&quot;NOTIFY_REMOTE_CONFIG_SLACK_API_TOKEN&quot;</span><span class="synStatement">);</span> <span class="synStatement">export</span> <span class="synType">const</span> notifyRemoteConfigDiff <span class="synStatement">=</span> onConfigUpdated<span class="synStatement">(</span><span class="synIdentifier">{</span>secrets: <span class="synIdentifier">[</span>slackApiToken<span class="synIdentifier">]}</span><span class="synStatement">,</span> <span class="synStatement">async</span> <span class="synStatement">(</span>event<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synSpecial">try</span> <span class="synIdentifier">{</span> <span class="synComment">// 最新と一つ前のバージョン番号を取得します</span> <span class="synType">const</span> currentVersion <span class="synStatement">=</span> event.data.versionNumber<span class="synStatement">;</span> <span class="synType">const</span> previousVersion <span class="synStatement">=</span> currentVersion - <span class="synConstant">1</span><span class="synStatement">;</span> <span class="synComment">// バージョン番号から RemoteConfigTemplate オブジェクトを取り出します</span> <span class="synType">const</span> remoteConfig <span class="synStatement">=</span> getRemoteConfig<span class="synStatement">(</span>app<span class="synStatement">);</span> <span class="synType">const</span> <span class="synIdentifier">[</span>previousTemplate<span class="synStatement">,</span> currentTemplate<span class="synIdentifier">]</span> <span class="synStatement">=</span> <span class="synStatement">await</span> <span class="synSpecial">Promise</span>.<span class="synStatement">all(</span><span class="synIdentifier">[</span> remoteConfig.getTemplateAtVersion<span class="synStatement">(</span>previousVersion<span class="synStatement">),</span> remoteConfig.getTemplateAtVersion<span class="synStatement">(</span>currentVersion<span class="synStatement">),</span> <span class="synIdentifier">]</span><span class="synStatement">);</span> <span class="synComment">// 変更をした人のメールアドレスを取り出します</span> <span class="synType">const</span> author <span class="synStatement">=</span> currentTemplate.version?.updateUser?.email ?? <span class="synConstant">&quot;unknown user&quot;</span><span class="synStatement">;</span> <span class="synComment">// RemoteConfigTemplate を受け取って、特定のキーの値が書式通りになっているかを検証し、エラーメッセージを返します</span> <span class="synComment">// 実装は省略しますが、JSON 型のフィールドに対して別途定義した JSON schema と照らし合わせています</span> <span class="synType">const</span> errorMessages <span class="synStatement">=</span> validateTemplate<span class="synStatement">(</span>currentTemplate<span class="synStatement">);</span> <span class="synComment">// diff ヘッダを出力します</span> <span class="synType">const</span> header <span class="synStatement">=</span> <span class="synConstant">`--- version </span><span class="synSpecial">${</span>previousVersion<span class="synSpecial">}</span><span class="synConstant">\n+++ version </span><span class="synSpecial">${</span>currentVersion<span class="synSpecial">}</span><span class="synConstant">\n`</span><span class="synStatement">;</span> <span class="synComment">// JSON に変換して diff を取ります</span> <span class="synType">const</span> diff <span class="synStatement">=</span> header + jsonDiff.diffString<span class="synStatement">(</span> <span class="synComment">// RemoteConfigTemplate オブジェクトをプレーンな JSON 互換オブジェクトに変換します (実装は省略)</span> normalizeFields<span class="synStatement">(</span>previousTemplate<span class="synStatement">),</span> normalizeFields<span class="synStatement">(</span>currentTemplate<span class="synStatement">),</span> <span class="synStatement">);</span> <span class="synType">const</span> slack <span class="synStatement">=</span> <span class="synStatement">new</span> WebClient<span class="synStatement">(</span>slackApiToken.value<span class="synStatement">());</span> slack.chat.postMessage<span class="synStatement">(</span><span class="synIdentifier">{</span> channel: slackChannel.value<span class="synStatement">(),</span> blocks: <span class="synIdentifier">{</span>...<span class="synIdentifier">}</span> <span class="synComment">// メッセージを組み立てます</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synSpecial">catch</span> <span class="synStatement">(</span>error<span class="synStatement">)</span> <span class="synIdentifier">{</span> logger.error<span class="synStatement">(</span>error<span class="synStatement">);</span> <span class="synIdentifier">}</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> </pre> <p>Cloud Functions for Firebase が提供する基本的な機能を使った短いコードですが、意外と簡単に実装できることがわかるのではないでしょうか。</p> <h2 id="まとめ">まとめ</h2> <p>Firebase Remote Config の変更通知についてご紹介しました。</p> <p>当初はやや趣味的な改善で、あまり検討せずにエイヤッと導入してみましたが、意外と Firebase Remote Config の変更が他のチームからも頻度高くされていることがわかったり、エラーが起こっている原因を調べやすくなったり、わずかにチーム全体の <a class="keyword" href="https://d.hatena.ne.jp/keyword/QOL">QOL</a> が上がっているのを感じます。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発チームはこういった <strong>斧を研ぐ</strong> 時間を大事にする伝統があり、プロダクトに直接入らない部分についても各自が積極的に改善を進めています。</p> <p>製品そのものも、またそれを開発する過程自体も、こうやって少しずつ改善を重ねてより良いものを生み出していきたいと思っています。</p> <p>このような文化で開発したい方は、ぜひ<a href="https://brand.studysapuri.jp/career/category/engineer">採用ページ</a>もご覧ください。</p> manicmaniac WebアプリケーションにGoの並行処理アーキテクチャを導入してSLOを改善し、WebAPIを100倍速くした話 hatenablog://entry/820878482972920602 2023-10-10T09:00:00+09:00 2023-10-10T13:04:25+09:00 こんにちは。スタディサプリの小中高プロダクト基盤開発グループでProduct Platform Engineer兼テックリードをやっている@tooooooooomyです。 今回は、WebアプリケーションにGoの並行処理機構を導入してSLOを改善し、WebAPIを100倍速くした話をしたいと思います。 前提条件 システムを0から作らない場合、アーキテクチャの改善の際には前提条件が付きものです。そこでまずは今回のシステムの前提条件をお話します。 対象となるシステムと、アーキテクチャ 今回対象とするシステムは、ここでは security-tracker と呼び、Webアプリケーション本体はGoで書か… <p>こんにちは。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループで<a href="https://blog.studysapuri.jp/entry/introduce-product-platform-engineer-position">Product Platform Engineer</a>兼テッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A1%BC%A5%C9">クリード</a>をやっている<a href="https://twitter.com/tooooooooomy">@tooooooooomy</a>です。 今回は、WebアプリケーションにGoの並行処理機構を導入してSLOを改善し、WebAPIを100倍速くした話をしたいと思います。</p> <h2 id="前提条件">前提条件</h2> <p>システムを0から作らない場合、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の改善の際には前提条件が付きものです。そこでまずは今回のシステムの前提条件をお話します。</p> <h3 id="対象となるシステムとアーキテクチャ">対象となるシステムと、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a></h3> <p>今回対象とするシステムは、ここでは <code>security-tracker</code> と呼び、Webアプリケーション本体はGoで書かれています。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの各アプリケーションにおけるユーザーのログ<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>を、<a href="https://aws.amazon.com/jp/kinesis/data-firehose/">Amazon Kinesis Firehose</a>を通して、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>全体のセキュリティチームが管理するS3<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリから見て、外部システム)に送信するシステムです。 セキュリティチームでは、このログから不正利用の兆候を検知するという業務を行っています。</p> <p><figure class="figure-image figure-image-fotolife" title="全体のアーキテクチャ図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tooooooooomy/20231004/20231004113105.png" width="970" height="436" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>全体の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>図</figcaption></figure></p> <h3 id="SLOの運用開始">SLOの運用開始</h3> <p>security-trackerのサービス立ち上げ当初、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリではService Level Objective(SLO)をサービスのオーナーシップを持つチームが管理する取り組みがまだはじまっていませんでした。<br/> security-trackerがリリースされ、運用後しばらくして各アプリケーションのSLOが設定されていきました。security-tracker の可用性のSLOとしても、99.9% という目標が設定されました。</p> <h3 id="旧アーキテクチャの問題点">旧<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の問題点</h3> <p>SLOを導入したことで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の問題点が浮かび上がってきました。<a href="https://aws.amazon.com/jp/about-aws/whats-new/2019/01/amazon-kinesis-data-firehose-announces-99-9-service-level-agreement/">Amazon Kinesis Firehoseの公式で発表されている可用性のSLAは99.9%</a>です。<br/> security-trackerのSLOは、Webアプリケーション単体のSLOと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>である<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kinesis">Kinesis</a> Firehoseの<a class="keyword" href="https://d.hatena.ne.jp/keyword/SLA">SLA</a>の掛け算によって決まります。つまり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kinesis">Kinesis</a> Firehoseの<a class="keyword" href="https://d.hatena.ne.jp/keyword/SLA">SLA</a>が99.9%である以上、security-trackerのSLOとして99.9%を設定するのは、現実的に無理がありました。</p> <p>例) Webアプリケーション単体のSLOを99.9%とすると、security-trackerサービス全体のSLOは 99.9 * 99.9 = 99.8%となる<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup>。Webアプリケーションの可用性を100%と仮定すれば達成可能だが、それは不可能。</p> <p>しかし、security-trackerの使われ方を考えると、可用性のSLOを下げる(≒ ユーザー体験を損なう)ことはチームとして許容できず、対策を考えることとなりました。</p> <h3 id="security-trackerの外部要件">security-trackerの外部要件</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を考えるに当たって重要な、そもそものシステムの外部要件として、security-trackerが各アプリケーションから受け取ったログは、異常時には必ずしも100%保存できなくてもよい。というものがありました。<br/> これは、security-trackerが扱うログが、ユーザーの行動を記録することが目的ではなく、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%D4%C0%B5%A5%A2%A5%AF%A5%BB%A5%B9">不正アクセス</a>が行われていないかを検知することが目的のためです。<br/> 初期段階で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>に可用性がAurora RDSや<a class="keyword" href="https://d.hatena.ne.jp/keyword/Dynamo">Dynamo</a> DBより劣る<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kinesis">Kinesis</a> Firehoseが採用されたのも、この要件があったためでした。</p> <h2 id="どのようにアーキテクチャを改善したか">どのように<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を改善したか</h2> <p>さて、上記のような前提条件の中で問題解決のためにどんなアプローチを取ったかをご紹介します。<br/> システムとして目標とするSLOを達成する方法としては、目標とするSLOよりも高い可用性が保証される<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>を採用するのが一般的です。<br/> しかし今回のようにすでにシステムが稼働していて、さらに外部システムと連携していれば、新たな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>の導入は大きなコストとなります。<br/> そこで今回は外部要件の、</p> <blockquote><p>security-trackerが各アプリケーションから受け取ったログは、異常時には必ずしも100%保存できなくてもよい</p></blockquote> <p>という部分に着目し、アプリケーションレイヤー、より厳密にはGoのレイヤーで問題の解決を試みました。<br/> 具体的には</p> <ul> <li>httpリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを捌くゴルーチン(http context goroutine)</li> </ul> <p>のみを起動し、httpリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを受け、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>にリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トをした後クライアントにレスポンスを返すという同期的な処理から、</p> <ul> <li>httpリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを捌くゴルーチン(http context goroutine)</li> <li>channelを監視し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>にリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トをするゴルーチンを起動するゴルーチン(observer context goroutine)</li> </ul> <p>という2つの役割を持つゴルーチンを起動し、2つのcontext間でchannelを通してデータをやりとりするというGoによる一般的な並行処理を採用しました。<sup id="fnref:3"><a href="#fn:3" rel="footnote">3</a></sup><br/> http context goroutineは、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを受信したらログデータをchannelに渡し、即座にクライアントにレスポンスを返します。<br/> observer context goroutineは、常にchannelを監視し、ログデータを受信したら<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>にデータを送信する子ゴルーチン(worker context goroutine)を起動します。(実際の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>へのリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トはこのworker context goroutineで行われます)<br/> この変更によって、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>である<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kinesis">Kinesis</a> Firehoseの可用性とsecurity-trackerとしての可用性を切り分けて考えることが可能となり、security-trackerとしての可用性を99.9%以上に保つことが理論上可能になりました。<br/> そう、お気づきの方もいらっしゃるかもしれませんが、この設計パターンは<a href="https://www.oreilly.co.jp/books/9784873118468/">Go言語による並行処理</a>の「5.3 ハートビート」や「5.6 不健全なゴルーチンを渡す」で紹介されている並行処理パターンを応用したものです。</p> <p><figure class="figure-image figure-image-fotolife" title="Goの並行処理を実装した内部アーキテクチャ図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tooooooooomy/20231004/20231004113135.png" width="1200" height="554" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Goの並行処理を実装した内部<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>図</figcaption></figure></p> <p>一方、この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の問題点として、データの揮発性が挙げられます<sup id="fnref:4"><a href="#fn:4" rel="footnote">4</a></sup>が、外部要件としてそれが許容できることがわかっていたので、この問題点を許容するという意思決定をしています。</p> <h2 id="アーキテクチャ変更のステップ"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>変更のステップ</h2> <p>理論上問題ないであろうことが確認され、開発環境で動くことが確認されても、実際に本番環境で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を変更するのは勇気のいる作業です。<sup id="fnref:5"><a href="#fn:5" rel="footnote">5</a></sup><br/> この作業を行うのに、チームで開発・運用しているDarklaunch v2<sup id="fnref:6"><a href="#fn:6" rel="footnote">6</a></sup> が大活躍しました。<br/> Darklaunch v2について簡単に説明すると、機能のリリースを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>のデプロイと切り分けることができるWebAPIです。<br/> このDarklaunch v2を利用することで、今回の本番環境の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>変更に置いては、</p> <ol> <li>同期処理のみを稼働させる</li> <li>同期処理と並行処理を動かすことのできる最新のコードベースをデプロイする</li> <li>同期処理と並行処理を同時に動かす</li> <li>同期処理を止めて並行処理単体で動かす</li> <li>しばらく様子を見る</li> <li>最後に、問題がなければ同期処理のコードを削除し、並行処理のコードだけを残した最新のコードベースをデプロイする</li> </ol> <p>といった感じで、新しい<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>のリリースを1回だけのコードベースデプロイで「お試し」することができました。<br/> さらに、問題がないと判断できるまでは、いつでもコードの変更なしに元の同期処理に切り替える状態を維持しながら、監視を続けることができました。</p> <h2 id="アーキテクチャ変更後システムはどうなったか"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>変更後、システムはどうなったか</h2> <p>2023-09-28現在、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を同期処理からGoによる並行処理に切り替えてから2ヶ月が経過し、新しい<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>に問題ないことが確認できましたので、同期処理のコードは削除し、並行処理のみの新<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>でシステムは稼働しています。<br/> この間にも<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon">Amazon</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kinesis">Kinesis</a> Firehoseへのリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トは数回エラーとなりましたが、security-trackerのSLOはそのエラーに影響されず、切り替え以降100 %のSLOを保っています。<br/> さらにこの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>変更を行ったことで、嬉しい副作用がありました。security-trackerの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%A4%A5%C6%A5%F3%A5%B7%A1%BC">レイテンシー</a>に同期処理のときの100倍以上(約50ms -> 約0.5 ms)の向上が見られたのです。<br/> みなさんもご存知のように、Webアプリケーションにおける処理時間は、Webアプリケーション外との通信時間が大部分を占めています。並行処理によって非同期化したことにより改善することができたのですが、改めて通信時間の重さを実感する出来事となりました。<br/> このエントリのタイトルでは「WebAPIを100倍速くした」と書きましたが、内部処理そのものを高速にしたのではなく、外部システムへの通信という時間のかかる処理を非同期に逃したことで、WebAPIのレイテンシ <em>だけ見れば</em> 、同期処理の100倍以上の向上が見られた...というのが実際のところです。<sup id="fnref:7"><a href="#fn:7" rel="footnote">7</a></sup> ただし、このWebAPIのレイテンシの短縮は、ユーザーのログイン時間の短縮にダイレクトに効いてくるので、UX改善という意味でもとても有意義なものとなりました。<br/> 一方、並行処理を導入したことで、</p> <ul> <li>http contextだけでなく、すべてのcontextでのerror handling</li> <li>http contextだけでなく、すべてのcontextでのgraceful shutdown</li> </ul> <p>のようなことを考慮する必要性が生まれています。デメリット、とまでは言いませんがシステムの複雑度が増したことは確かでしょう。</p> <h2 id="終わりに">終わりに</h2> <p>いかがでしょうか。今回はGoの並行処理を利用した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>変更のお話をご紹介しました。<br/> 前提条件や制約次第ですが、一般的なWebアプリケーション開発においても、Go言語の並行処理の扱いやすさという優位性を示すことのできる事例だったのではないかと思います。<br/> 小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループではこのように、技術的なアプローチによってよりよいシステムを作っていく仲間を募集しています。<br/> もしそんな小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループで開発することにもしご興味がありましたら、是非<a href="https://brand.studysapuri.jp/career/position/product-platform-engineer/">こちらのリンク</a>をご活用いただければと思います。よろしくお願いします!</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> ユーザーの個人情報は含まない、不正検知のみに使用する行動ログです<a href="#fnref:1" rev="footnote">&#8617;</a></li> <li id="fn:2"> <code>0.999*0.999 = 0.998001</code><a href="#fnref:2" rev="footnote">&#8617;</a></li> <li id="fn:3"> アプリケーションレイヤーでの変更のほうが、新しい<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DF%A5%C9%A5%EB%A5%A6%A5%A7%A5%A2">ミドルウェア</a>の導入よりも低コストだと判断したということです<a href="#fnref:3" rev="footnote">&#8617;</a></li> <li id="fn:4"> channelを通したデータの受け渡しは、メモリ上で行われるので、処理途中でシステムがダウンした場合は、メモリ上のデータは失われます。これを本記事ではデータの揮発性と呼んでいます<a href="#fnref:4" rev="footnote">&#8617;</a></li> <li id="fn:5"> 当然ですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>変更時にシステムエラーを出してしまった場合、システムのSLOは下がります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>に時間がかかると、それだけでリスクです。<a href="#fnref:5" rev="footnote">&#8617;</a></li> <li id="fn:6"> <a href="https://blog.studysapuri.jp/entry/2023/07/05/090000">https://blog.studysapuri.jp/entry/2023/07/05/090000</a><a href="#fnref:6" rev="footnote">&#8617;</a></li> <li id="fn:7"> <a href="https://www.oreilly.co.jp/books/9784873118468/">Go言語による並行処理</a> 「4.11 キュー」でも言及があるように、処理全体の処理時間が短縮されるものではありません<a href="#fnref:7" rev="footnote">&#8617;</a></li> </ol> </div> tooooooooomy スタディサプリ小学・中学講座でRoborazziを導入しました hatenablog://entry/820878482970849288 2023-10-05T09:00:00+09:00 2023-10-05T09:00:04+09:00 こんにちは、Androidエンジニアの@morux2です。本記事ではスクリーンショットの撮影にRoborazziを導入した経緯をご紹介できればと思います。 はじめに きっかけ RoborazziとPaparazziの比較 書きやすさ 複数端末での撮影 スクロールした画面の撮影 Showkaseの流用 Roborazziの採用理由 現状の運用 さいごに はじめに スタディサプリ小学・中学講座では、UnitTestに加えてVisual Regression Test (以下 VRT)を行っています。VRTは画像比較によるUIの回帰テストです。変更前後のコードそれぞれに対する画面のスクリーンショット… <p>こんにちは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>エンジニアの@morux2です。本記事では<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>の撮影にRoborazziを導入した経緯をご紹介できればと思います。</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#きっかけ">きっかけ</a></li> <li><a href="#RoborazziとPaparazziの比較">RoborazziとPaparazziの比較</a><ul> <li><a href="#書きやすさ">書きやすさ</a></li> <li><a href="#複数端末での撮影">複数端末での撮影</a></li> <li><a href="#スクロールした画面の撮影">スクロールした画面の撮影</a></li> <li><a href="#Showkaseの流用">Showkaseの流用</a></li> </ul> </li> <li><a href="#Roborazziの採用理由">Roborazziの採用理由</a></li> <li><a href="#現状の運用">現状の運用</a></li> <li><a href="#さいごに">さいごに</a></li> </ul> <h3 id="はじめに">はじめに</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小学・中学講座では、UnitTestに加えてVisual Regression Test (以下 VRT)を行っています。VRTは画像比較によるUIの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%F3%B5%A2%A5%C6%A5%B9%A5%C8">回帰テスト</a>です。変更前後のコードそれぞれに対する画面の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を比較し、意図しない差分を検知することができます。<a href="#f-0e7f3b77" name="fn-0e7f3b77" title="https://blog.studysapuri.jp/entry/2021-08-23/android-vrt-tips-1">*1</a></p> <p>VRTは、画面の用意・撮影・比較の3ステップに分けることができます。これまでは以下の実装方法になっていました。<a href="#f-2848ba8b" name="fn-2848ba8b" title="https://speakerdeck.com/recruitengineers/abceed-tech-night?slide=52">*2</a><a href="#f-666d4f41" name="fn-666d4f41" title="https://cats-234205.web.app/2020/visual-regression-testing-with-android/">*3</a></p> <ol> <li>画面の用意 <ul> <li>@<a class="keyword" href="https://d.hatena.ne.jp/keyword/Preview">Preview</a>のついたComposable <ul> <li><a href="https://github.com/airbnb/Showkase">Showkase</a> + <a href="https://developer.android.com/jetpack/compose/testing?hl=ja">ComposeTestRule</a></li> </ul> </li> <li>スクロールが必要な画面 <ul> <li>ComposeTestRule(<a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/test/SemanticsNodeInteraction">performScrollToNode</a>)</li> </ul> </li> </ul> </li> <li>撮影 <ul> <li>ComposeTestRule(<a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/test/package-summary">captureToImage</a>) + <a href="https://firebase.google.com/docs/test-lab?hl=ja">Firebase Test Lab</a></li> </ul> </li> <li>比較 <ul> <li><a href="https://github.com/reg-viz/reg-suit">reg-suit</a></li> </ul> </li> </ol> <p>今回は3つのステップのうち、撮影の部分をRoborazziに移行しました。<a href="https://github.com/takahirom/roborazzi">Roborazzi</a>は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>端末を使わずに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JVM">JVM</a>上で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を撮影することができるライブラリです。<a href="https://github.com/robolectric/robolectric/releases/tag/robolectric-4.10">Robolectricのグラフィック機能</a>がベースとなっており、AndroidTestではなくUnitTestとして実行することができます。</p> <h3 id="きっかけ">きっかけ</h3> <p>2023年9月、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座に小学コンテンツが追加され、小学・中学講座に生まれ変わりました。<a href="#f-189512f7" name="fn-189512f7" title="https://studysapuri.jp/course/elementary/sho1/">*4</a> 小学講座は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>専用のUIになっているので、これまでの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A1%BC%A5%C8%A5%D5%A5%A9%A5%F3">スマートフォン</a>でのVRTに加えて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>での実行が必要不可欠になりました。</p> <ul> <li>小学講座 <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>専用UI (横向き固定)</li> </ul> </li> <li>中学講座 <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A1%BC%A5%C8%A5%D5%A5%A9%A5%F3">スマートフォン</a>・<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>両者で利用可能 <figure class="figure-image figure-image-fotolife" title="スタディサプリ小学・中学講座"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20230926/20230926195911.png" width="450" height="600" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小学・中学講座</figcaption></figure></li> </ul> </li> </ul> <p>そこで、近年主流となっている<a class="keyword" href="https://d.hatena.ne.jp/keyword/JVM">JVM</a>上で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を撮影できるライブラリの導入を検討しました。コストや実行時間の削減、複数端末での撮影を期待したためです。</p> <h3 id="RoborazziとPaparazziの比較">RoborazziとPaparazziの比較</h3> <p>今回<a class="keyword" href="https://d.hatena.ne.jp/keyword/JVM">JVM</a>上で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を撮影できるライブラリとして、<a href="https://github.com/cashapp/paparazzi">Paparazzi</a>とRoborazziを比較検討しました。観点は以下になります。</p> <ul> <li>書きやすさ</li> <li>複数端末での撮影</li> <li>スクロールした画面の撮影</li> <li>Showkaseの流用</li> </ul> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A1%BC%A5%C8%A5%D5%A5%A9%A5%F3">スマートフォン</a>・<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>両者での撮影はもちろん、複数の画面サイズでの品質担保も必要でした。特に小学講座は8インチ以上の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>を動作対象としているため、10~11インチの推奨サイズに加えて小さい端末での見た目も確認する必要があります。また、中学講座ではほとんどの画面で縦スクロールするため、スクロールした画面の撮影が必要でした。</p> <h5 id="書きやすさ">書きやすさ</h5> <p>Paparazzi, Roborazziともにメソッドを呼び出すだけで手軽に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>が撮影出来ます。どちらも現状はJUnit4で実行することになるので、JUnit5に移行をしている場合は<a href="https://mvnrepository.com/artifact/org.junit.vintage/junit-vintage-engine">Vintageライブラリ</a>を使って実行することになります。<a href="#f-ee9d4db7" name="fn-ee9d4db7" title="https://github.com/takahirom/roborazzi/issues/152">*5</a></p> <p><details><summary>PaparazziとRoborazziのサンプルコード</summary> <strong>Paparazzi</strong></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synPreProc">import</span> app.cash.paparazzi.DeviceConfig.Companion.PIXEL_5 <span class="synPreProc">import</span> app.cash.paparazzi.Paparazzi <span class="synPreProc">import</span> org.junit.Rule <span class="synPreProc">import</span> org.junit.Test <span class="synType">class</span> MySnapshotTest { <span class="synIdentifier">@get:Rule</span> <span class="synType">val</span> paparazzi = Paparazzi(deviceConfig = PIXEL_5) <span class="synIdentifier">@Test</span> <span class="synType">fun</span> captureMyComposableScreen() { paparazzi.snapshot { MyComposableScreen() } } } </pre> <p><strong>Roborazzi</strong></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synPreProc">import</span> androidx.compose.ui.test.junit4.createComposeRule <span class="synPreProc">import</span> androidx.compose.ui.test.onRoot <span class="synPreProc">import</span> androidx.test.ext.junit.runners.AndroidJUnit4 <span class="synPreProc">import</span> com.github.takahirom.roborazzi.RobolectricDeviceQualifiers <span class="synPreProc">import</span> com.github.takahirom.roborazzi.captureRoboImage <span class="synPreProc">import</span> org.junit.Rule <span class="synPreProc">import</span> org.junit.Test <span class="synPreProc">import</span> org.junit.runner.RunWith <span class="synPreProc">import</span> org.robolectric.<span class="synType">annotation</span>.Config <span class="synPreProc">import</span> org.robolectric.<span class="synType">annotation</span>.GraphicsMode <span class="synIdentifier">@RunWith</span>(AndroidJUnit4<span class="synStatement">::</span><span class="synType">class</span>) <span class="synIdentifier">@GraphicsMode</span>(GraphicsMode.Mode.NATIVE) <span class="synIdentifier">@Config</span>(qualifiers = RobolectricDeviceQualifiers.Pixel5) <span class="synType">class</span> MySnapshotTest { <span class="synIdentifier">@get:Rule</span> <span class="synType">val</span> composeTestRule = createComposeRule() <span class="synIdentifier">@Test</span> <span class="synType">fun</span> captureMyComposableScreen() { composeTestRule.setContent { MyComposableScreen() } composeTestRule .onRoot() .captureRoboImage() } } </pre> <p></details></p> <h5 id="複数端末での撮影">複数端末での撮影</h5> <p>両者ともに複数端末での撮影は可能です。</p> <p><details><summary>PaparazziとRoborazziのサンプルコード</summary> <strong>Paparazzi</strong></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synPreProc">import</span> app.cash.paparazzi.DeviceConfig <span class="synPreProc">import</span> app.cash.paparazzi.Paparazzi <span class="synPreProc">import</span> com.google.testing.junit.testparameterinjector.TestParameter <span class="synPreProc">import</span> com.google.testing.junit.testparameterinjector.TestParameterInjector <span class="synPreProc">import</span> org.junit.Rule <span class="synPreProc">import</span> org.junit.Test <span class="synPreProc">import</span> org.junit.runner.RunWith <span class="synIdentifier">@RunWith</span>(TestParameterInjector<span class="synStatement">::</span><span class="synType">class</span>) <span class="synType">class</span> MySnapshotTest { <span class="synIdentifier">@get:Rule</span> <span class="synType">val</span> paparazzi = Paparazzi() <span class="synType">enum</span> <span class="synType">class</span> Device(<span class="synType">public</span> <span class="synType">val</span> deviceConfig: DeviceConfig) { PIXEL_5(DeviceConfig.PIXEL_5), PIXEL_C(DeviceConfig.PIXEL_C) } <span class="synIdentifier">@Test</span> <span class="synType">fun</span> captureMyComposableScreen( <span class="synIdentifier">@TestParameter</span> device: Device, ) { paparazzi.unsafeUpdateConfig(deviceConfig = device.deviceConfig) paparazzi.snapshot(name = device.name) { MyComposableScreen() } } } </pre> <p><strong>Roborazzi</strong></p> <p>@Config<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>を用いる方法<a href="#f-d1f3a156" name="fn-d1f3a156" title="https://github.com/takahirom/roborazzi/issues/47#issuecomment-1519013180">*6</a></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synPreProc">import</span> androidx.compose.ui.test.junit4.ComposeContentTestRule <span class="synPreProc">import</span> androidx.compose.ui.test.junit4.createComposeRule <span class="synPreProc">import</span> androidx.compose.ui.test.onRoot <span class="synPreProc">import</span> androidx.test.ext.junit.runners.AndroidJUnit4 <span class="synPreProc">import</span> com.github.takahirom.roborazzi.captureRoboImage <span class="synPreProc">import</span> org.junit.Rule <span class="synPreProc">import</span> org.junit.Test <span class="synPreProc">import</span> org.junit.runner.RunWith <span class="synPreProc">import</span> org.robolectric.<span class="synType">annotation</span>.Config <span class="synPreProc">import</span> org.robolectric.<span class="synType">annotation</span>.GraphicsMode <span class="synIdentifier">@RunWith</span>(AndroidJUnit4<span class="synStatement">::</span><span class="synType">class</span>) <span class="synIdentifier">@GraphicsMode</span>(GraphicsMode.Mode.NATIVE) <span class="synType">class</span> MySnapshotTest { <span class="synIdentifier">@get:Rule</span> <span class="synType">val</span> composeTestRule = createComposeRule() <span class="synIdentifier">@Test</span> <span class="synIdentifier">@Config</span>(qualifiers = RobolectricDeviceQualifiers.Pixel5) <span class="synType">fun</span> captureMyComposableScreenPixel5()= captureMyComposableScreen() <span class="synIdentifier">@Test</span> <span class="synIdentifier">@Config</span>(qualifiers = RobolectricDeviceQualifiers.PixelC) <span class="synType">fun</span> captureMyComposableScreenPixelC()= captureMyComposableScreen() <span class="synType">private</span> <span class="synType">fun</span> captureMyComposableScreen() { composeTestRule.setContent { MyComposableScreen() } composeTestRule .onRoot() .captureRoboImage() } } </pre> <p>Parametarizedテストを用いる方法</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synPreProc">import</span> androidx.compose.ui.test.junit4.ComposeContentTestRule <span class="synPreProc">import</span> androidx.compose.ui.test.junit4.createComposeRule <span class="synPreProc">import</span> androidx.compose.ui.test.onRoot <span class="synPreProc">import</span> com.github.takahirom.roborazzi.RobolectricDeviceQualifiers <span class="synPreProc">import</span> com.github.takahirom.roborazzi.captureRoboImage <span class="synPreProc">import</span> org.junit.Rule <span class="synPreProc">import</span> org.junit.Test <span class="synPreProc">import</span> org.junit.runner.RunWith <span class="synPreProc">import</span> org.robolectric.ParameterizedRobolectricTestRunner <span class="synPreProc">import</span> org.robolectric.RuntimeEnvironment <span class="synPreProc">import</span> org.robolectric.<span class="synType">annotation</span>.GraphicsMode <span class="synIdentifier">@RunWith</span>(ParameterizedRobolectricTestRunner<span class="synStatement">::</span><span class="synType">class</span>) <span class="synIdentifier">@GraphicsMode</span>(GraphicsMode.Mode.NATIVE) <span class="synType">class</span> MySnapshotTest( <span class="synType">private</span> <span class="synType">val</span> device: Device, ) { <span class="synIdentifier">@get:Rule</span> <span class="synType">val</span> composeTestRule = createComposeRule() <span class="synType">enum</span> <span class="synType">class</span> Device(<span class="synType">public</span> <span class="synType">val</span> robolectricDeviceQualifiers: <span class="synType">String</span>) { PIXEL_5(RobolectricDeviceQualifiers.Pixel5), PIXEL_C(RobolectricDeviceQualifiers.PixelC) } <span class="synIdentifier">@Test</span> <span class="synType">fun</span> captureMyComposableScreen() { RuntimeEnvironment.setQualifiers(device.robolectricDeviceQualifiers) composeTestRule.setContent { MyComposableScreen() } composeTestRule .onRoot() .captureRoboImage() } <span class="synType">companion</span> <span class="synType">object</span> { <span class="synIdentifier">@ParameterizedRobolectricTestRunner.Parameters</span> <span class="synIdentifier">@JvmStatic</span> <span class="synType">fun</span> data(): <span class="synType">Array</span>&lt;Device&gt; { <span class="synStatement">return</span> Device.values() } } } </pre> <p></details></p> <h5 id="スクロールした画面の撮影">スクロールした画面の撮影</h5> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>で閲覧する中学講座アプリはほとんどの画面で縦スクロールするため、スクロール後UIの品質担保を重視しました。RoborazziはRobolectricベースなので、composeTestRuleをUnitTestで呼び出してスクロールを実行することが可能です。一方Paparazziではスクロールを実行できません。代わりに<a href="https://github.com/cashapp/paparazzi/pull/543">縦長画像を撮影できる機能</a>が検討されていますが、1年近く進展がありません。</p> <p><details><summary>Roborazziのサンプルコード</summary></p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synPreProc">import</span> androidx.compose.ui.test.hasText <span class="synPreProc">import</span> androidx.compose.ui.test.junit4.createComposeRule <span class="synPreProc">import</span> androidx.compose.ui.test.onNodeWithTag <span class="synPreProc">import</span> androidx.compose.ui.test.performScrollToNode <span class="synPreProc">import</span> androidx.test.ext.junit.runners.AndroidJUnit4 <span class="synPreProc">import</span> com.github.takahirom.roborazzi.RobolectricDeviceQualifiers <span class="synPreProc">import</span> com.github.takahirom.roborazzi.captureRoboImage <span class="synPreProc">import</span> org.junit.Rule <span class="synPreProc">import</span> org.junit.Test <span class="synPreProc">import</span> org.junit.runner.RunWith <span class="synPreProc">import</span> org.robolectric.<span class="synType">annotation</span>.Config <span class="synPreProc">import</span> org.robolectric.<span class="synType">annotation</span>.GraphicsMode <span class="synIdentifier">@RunWith</span>(AndroidJUnit4<span class="synStatement">::</span><span class="synType">class</span>) <span class="synIdentifier">@GraphicsMode</span>(GraphicsMode.Mode.NATIVE) <span class="synIdentifier">@Config</span>(qualifiers = RobolectricDeviceQualifiers.Pixel5) <span class="synType">public</span> <span class="synType">class</span> MySnapshotTest { <span class="synIdentifier">@get:Rule</span> <span class="synType">val</span> composeTestRule = createComposeRule() <span class="synIdentifier">@Test</span> <span class="synType">fun</span> captureMyComposableScreen() { composeTestRule.setContent { MyComposableScreen() } composeTestRule .onNodeWithTag(<span class="synConstant">&quot;lazyColumn&quot;</span>) .performScrollToNode(hasText(<span class="synConstant">&quot;Hello&quot;</span>)) .captureRoboImage() } } </pre> <p></details></p> <h5 id="Showkaseの流用">Showkaseの流用</h5> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Preview">Preview</a>流用によるテストコードの削減も欠かせませんでした。Paparazzi・RoborazziともにShowkaseを流用し、@<a class="keyword" href="https://d.hatena.ne.jp/keyword/Preview">Preview</a>のついたComposableのキャプチャを一括で撮影することが可能です。詳細な説明は割愛しますので、<a href="https://github.com/DroidKaigi/conference-app-2022/pull/97">DroidKaigi conference-app-2022</a>および <a href="https://github.com/DroidKaigi/conference-app-2023/pull/217">DroidKaigi conference-app-2023</a>をご確認ください。</p> <h3 id="Roborazziの採用理由">Roborazziの採用理由</h3> <p>書きやすさ・複数端末での撮影・Showkaseの流用は両者差がありませんでしたが、スクロールの実行はRoborazziのみ可能でした。Roborazziの<a href="https://github.com/takahirom/roborazzi#paparazzi-and-roborazzi-a-comparison">README</a>でも同様の言及があり、RobolectricベースであるためにHiltでFakeをInjectできること、UIに対してスクロールやタップアクションを実施できることが優位性として述べられています。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google%20I/O">Google I/O</a> 2023 では、公式からHost-side Screenshot Testingの発表もありましたが、AndroidStudioの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Preview">Preview</a>機能がベースとなっているため、こちらもスクロールの実行は厳しいのではないかと推測されています。<a href="#f-006b24ab" name="fn-006b24ab" title="https://qiita.com/takahirom/items/9960bafea96d8891fede">*7</a></p> <p>以上を踏まえて、我々はRoborazziの採用を決定しました。検討の段階で<a href="https://github.com/android/nowinandroid/pull/876">nowinandroidにRoborazziが導入</a>されたことも大きな決め手となりました。</p> <h3 id="現状の運用">現状の運用</h3> <p>中学講座は今まで通り実機で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>を撮影し、小学講座のみRoborazziを利用しています。今後は中学もRoborazziに移行し、小学については複数端末での実行を検討していきます。</p> <ol> <li>画面の用意 <ul> <li>@<a class="keyword" href="https://d.hatena.ne.jp/keyword/Preview">Preview</a>のついたComposable <ul> <li>Showkase + ComposeTestRule</li> </ul> </li> <li>スクロールが必要な画面 <ul> <li>ComposeTestRule(performScrollToNode)</li> </ul> </li> </ul> </li> <li>撮影 <ul> <li>小学講座 (<strong>new!</strong>) <ul> <li>Roborazzi</li> </ul> </li> <li>中学講座 <ul> <li>ComposeTestRule(captureToImage) + Firebase Test Lab</li> </ul> </li> </ul> </li> <li>比較 <ul> <li>reg-suit</li> </ul> </li> </ol> <p><figure class="figure-image figure-image-fotolife" title="VRTの結果画面"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20230928/20230928120832.gif" width="1200" height="652" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>VRTの結果画面</figcaption></figure></p> <h3 id="さいごに">さいごに</h3> <p>今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ小学・中学講座でRoborazziを導入した経緯を紹介しました。Roborazziの移行が進みましたら、Tips等も共有できればと思います。まだまだ試行錯誤の段階なので、常により良い方法やア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>を歓迎しています。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは、一緒に最高のプロダクトを作っていってくれる仲間を募集しています! 少しでもご興味がある方はこちらのページからご連絡ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbrand.studysapuri.jp%2Fcareer%2Fcategory%2Fengineer%2F%23openPositions" title="キャリア | スタディサプリ BRAND SITE" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://brand.studysapuri.jp/career/category/engineer/#openPositions">brand.studysapuri.jp</a></cite></p> <div class="footnote"> <p class="footnote"><a href="#fn-0e7f3b77" name="f-0e7f3b77" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://blog.studysapuri.jp/entry/2021-08-23/android-vrt-tips-1">https://blog.studysapuri.jp/entry/2021-08-23/android-vrt-tips-1</a></span></p> <p class="footnote"><a href="#fn-2848ba8b" name="f-2848ba8b" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://speakerdeck.com/recruitengineers/abceed-tech-night?slide=52">https://speakerdeck.com/recruitengineers/abceed-tech-night?slide=52</a></span></p> <p class="footnote"><a href="#fn-666d4f41" name="f-666d4f41" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://cats-234205.web.app/2020/visual-regression-testing-with-android/">https://cats-234205.web.app/2020/visual-regression-testing-with-android/</a></span></p> <p class="footnote"><a href="#fn-189512f7" name="f-189512f7" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://studysapuri.jp/course/elementary/sho1/">https://studysapuri.jp/course/elementary/sho1/</a></span></p> <p class="footnote"><a href="#fn-ee9d4db7" name="f-ee9d4db7" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://github.com/takahirom/roborazzi/issues/152">https://github.com/takahirom/roborazzi/issues/152</a></span></p> <p class="footnote"><a href="#fn-d1f3a156" name="f-d1f3a156" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://github.com/takahirom/roborazzi/issues/47#issuecomment-1519013180">https://github.com/takahirom/roborazzi/issues/47#issuecomment-1519013180</a></span></p> <p class="footnote"><a href="#fn-006b24ab" name="f-006b24ab" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://qiita.com/takahirom/items/9960bafea96d8891fede">https://qiita.com/takahirom/items/9960bafea96d8891fede</a></span></p> </div> morux2 GraphQL Tokyo Meetup #21 で会場スポンサーしました #GraphQLTokyo hatenablog://entry/820878482967166220 2023-09-19T08:00:00+09:00 2023-09-19T08:00:01+09:00 こんにちは。@chaspy です。 9/13 に開催された GraphQL Tokyo にて弊社九段下オフィスの会場を提供させていただきました。 www.meetup.com 九段下オフィスについて 九段下駅から徒歩3分でとっても便利な立地です。 goo.gl 以下の記事も合わせてご覧ください。 www.recruit.co.jp 当日は30名近くの方が参加されました。 発表について なんと今回はオープンソース開発者グループ The Guild のメンバーである Laurin Quast さんがドイツから東京に来てトークをしてくださりました。 彼のトークテーマでもある graphql-code… <p>こんにちは。<a href="https://github.com/chaspy">@chaspy</a> です。</p> <p>9/13 に開催された GraphQL Tokyo にて弊社九段下オフィスの会場を提供させていただきました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.meetup.com%2Fgraphql-tokyo%2Fevents%2F295606838%2F" title="Login to Meetup | Meetup" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.meetup.com/graphql-tokyo/events/295606838/">www.meetup.com</a></cite></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20230915/20230915132952.jpg" width="900" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="九段下オフィスについて">九段下オフィスについて</h1> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B6%E5%C3%CA%B2%BC%B1%D8">九段下駅</a>から徒歩3分でとっても便利な立地です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgoo.gl%2Fmaps%2FTRS3xtEFJBouzTbx8" title="九段坂上Ksビル · 〒102-0073 東京都千代田区九段北1丁目14−6" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://goo.gl/maps/TRS3xtEFJBouzTbx8">goo.gl</a></cite></p> <p>以下の記事も合わせてご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.recruit.co.jp%2Fblog%2Fculture%2F20230316_3883.html" title="Z世代の新入社員が取材!リクルート九段下オフィスの「働きやすさ」 | 株式会社リクルート" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.recruit.co.jp/blog/culture/20230316_3883.html">www.recruit.co.jp</a></cite></p> <p>当日は30名近くの方が参加されました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20230915/20230915133021.jpg" width="900" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20230915/20230915133038.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20230915/20230915133053.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/q/quipper-ja/20230915/20230915133110.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h1 id="発表について">発表について</h1> <p>なんと今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>開発者グループ <a href="https://the-guild.dev/">The Guild</a> のメンバーである <a href="https://github.com/n1ru4l">Laurin Quast</a> さんがドイツから東京に来て<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>をしてくださりました。</p> <p>彼の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>テーマでもある graphql-code-generator は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発でも利用しており、無くてはならない <a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a> の一つです。</p> <table> <thead> <tr> <th> 時刻 time </th> <th> 内容 contents </th> </tr> </thead> <tbody> <tr> <td> 18:30 - </td> <td> Door open 受付開始 </td> </tr> <tr> <td> 19:00 - </td> <td> Introduction </td> </tr> <tr> <td> 19:05 - </td> <td> Sponsor <a class="keyword" href="https://d.hatena.ne.jp/keyword/Talk">Talk</a> </td> </tr> <tr> <td> 19:10 - </td> <td> LT * 7 </td> </tr> <tr> <td> </td> <td> Hasuraを採用した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の紹介と所感(Introduction and Insights of the Hasura-based Architecture)(はやせ) </td> </tr> <tr> <td> </td> <td> Traversing the GraphQL AST and Calculating Query Costs (joe_re) </td> </tr> <tr> <td> </td> <td> Learning from performance improvements on GraphQL <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a> (mtsmfm) </td> </tr> <tr> <td> </td> <td> GraphQL Client on <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a> (qsona) </td> </tr> <tr> <td> </td> <td> How to test GraphQL <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> using Postman (Yoichi Kawasaki) </td> </tr> <tr> <td> </td> <td> Efficient Feature Implementation Using Type Merging (Motoya Kondo) </td> </tr> <tr> <td> </td> <td> Precondition with schema directives (Quramy) </td> </tr> <tr> <td> 20:30 - </td> <td> The Evolution of GraphQL Code Generation (Laurin) </td> </tr> </tbody> </table> <h1 id="発表資料">発表資料</h1> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/fba49448c2c2463c89c299918c688f66" title="Introduction and Insights of the Hasura-based Architecture" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/06e72a2c86b444698ce33b64d37545cb" title="Traversing the GraphQL AST and Calculating Query Costs" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/ae81dfc6817748aea5d927ec79649fdf" title="Learning from performance improvements on GraphQL Ruby" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/5731e21a04e84966a5d8ac9875cbf584" title="3 Practices about
Service-to-Service GraphQL Ruby Client" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/332dfa3e1a4d4d739dbde453ab6f992b" title="How to test GraphQL API using Postman" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/34033871facb4aa8b835cb402a6e8030" title="Efficient Feature Implementation Using Type Merging" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/dc4295b413cd42a5aa57f035d7b4ec6c" title="Precondition with schema directives" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/7080a071350b4d04858fb8f30c8fc76a" title="The Evolution of GraphQL Code Generation" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"></iframe> <h1 id="個人的な感想">個人的な感想</h1> <p>卒業された <a href="https://github.com/mtsmfm">@mtsmfm</a> や <a href="https://github.com/qsona">@qsona</a> と久しぶりに話せて嬉しかったです。</p> <h1 id="おわりに">おわりに</h1> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは GraphQL を使って schema-driven な開発をしたい仲間を募集しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbrand.studysapuri.jp%2Fcareer%2F" title="キャリア | スタディサプリ BRAND SITE" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://brand.studysapuri.jp/career/">brand.studysapuri.jp</a></cite></p> quipper-ja Developers Summit 2023 SummerでADRについて発表しました & ベストスピーカー賞を受賞しました🎉 hatenablog://entry/820878482965606145 2023-09-15T17:37:47+09:00 2023-09-15T17:37:47+09:00 こんにちは。スタディサプリでプロダクトプラットフォームの開発を行っている @highwide です。 少し前の話になってしまいますが、2023-07-27に行われた「Developers Summit 2023 Summer」(以下、「デブサミ」と書きます)にて「アーキテクチャデシジョンレコード」(ADR)についての発表をしましたので、その報告をさせていただきます。 「日々の意思決定の積み重ねを記録するアーキテクチャ・デシジョン・レコード」というタイトルで発表しました。 発表資料はこちらです。 また、デブサミのサイトでは、発表の当日の録画が見られるようです。 途中、自分の声に反応してしまったA… <p>こんにちは。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでプロダクトプラットフォームの開発を行っている <a href="https://twitter.com/highwide">@highwide</a> です。</p> <p>少し前の話になってしまいますが、2023-07-27に行われた「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Developers%20Summit">Developers Summit</a> 2023 Summer」(以下、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D6%A5%B5%A5%DF">デブサミ</a>」と書きます)にて「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>デシジョンレコード」(<a class="keyword" href="https://d.hatena.ne.jp/keyword/ADR">ADR</a>)についての発表をしましたので、その報告をさせていただきます。</p> <p>「日々の意思決定の積み重ねを記録する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>・デシジョン・レコード」というタイトルで発表しました。</p> <p>発表資料はこちらです。</p> <script defer class="speakerdeck-embed" data-id="1f6b4e70ecd3496f86329772e30e4c56" data-ratio="1.7772511848341233" src="//speakerdeck.com/assets/embed.js"></script> <p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D6%A5%B5%A5%DF">デブサミ</a>のサイトでは、発表の当日の録画が見られるようです。 途中、自分の声に反応してしまった<a class="keyword" href="https://d.hatena.ne.jp/keyword/Apple%20Watch">Apple Watch</a>に焦る様子なども見られるかと思います...(恥ずかしい...)</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcodezine.jp%2Fdevonline%2Farchive%2Fsession%2F149" title="【B-9】日々の意思決定の積み重ねを記録するアーキテクチャ・デシジョン・レコード" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://codezine.jp/devonline/archive/session/149">codezine.jp</a></cite></p> <h2 id="ベストスピーカー賞受賞-">ベストスピーカー賞受賞 🎉</h2> <p>また、この度、本カンファレンスにおけるベストスピーカー賞(1位)という賞を名誉あることにいただくことができました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcodezine.jp%2Farticle%2Fdetail%2F18298" title="デブサミ2023夏のベストスピーカーが発表、受賞セッションを含む一部セッションのアーカイブ動画を公開" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://codezine.jp/article/detail/18298">codezine.jp</a></cite></p> <p>イベント後のアンケートによると、「非常に満足」が47.8%、「満足」が40.3%と、多くの方に満足いただけたようです。</p> <ul> <li>避けて通れない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%AD%A5%E5%A5%E1%A5%F3%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">ドキュメンテーション</a>における、軽量ドキュメントという<a class="keyword" href="https://d.hatena.ne.jp/keyword/ADR">ADR</a>の絶妙な立ち位置</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> issueを使ったドキュメント管理という手法</li> <li>「どうしたか」は覚えていても「なぜそうしたか」という点は忘れがちという点への共感</li> </ul> <p>といった点への好評を特にフィードバックとしてたいだいています。</p> <h2 id="登壇の舞台裏">登壇の舞台裏</h2> <p>本発表は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D6%A5%B5%A5%DF">デブサミ</a>運営の方が私が数年前にこのブログに書いた以下のエントリを見て、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>」をテーマにした今回イベントでの発表を打診してくださったことがきっかけとなりました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.studysapuri.jp%2Fentry%2Farchitecture_decision_records" title="〜その意思決定を刻め〜「アーキテクチャ・デシジョン・レコード(ADR)」を利用した設計の記録 - スタディサプリ Product Team Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.studysapuri.jp/entry/architecture_decision_records">blog.studysapuri.jp</a></cite></p> <p>一方で、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>」をテーマにしたカンファレンスでドキュメント手法(名前に「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>」と付くはものの...)について語る若干のミスマッチさや、ブログで完結している<a class="keyword" href="https://d.hatena.ne.jp/keyword/ADR">ADR</a>の紹介を40分という比較的長い発表枠の中で話す...といった点をハードルに感じたのも事実です。とはいえ、最終的には「こんな機会をいただくこともなかないし、なんとかなるだろう」の精神で快諾しました。</p> <p>登壇が決まってから、ブログに書いたこと以上のネタ集めをするため、社内の「random-tech-<a class="keyword" href="https://d.hatena.ne.jp/keyword/talk">talk</a>」という毎週行っているイベントに話を持ち込みました。 random-tech-<a class="keyword" href="https://d.hatena.ne.jp/keyword/talk">talk</a>は、その名のとおり技術に関する雑談などを行う任意参加の社内イベントで、毎週1時間の枠がとられています。</p> <p><figure class="figure-image figure-image-fotolife" title="random-tech-talkが始まる際のアナウンス"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/highwide/20230908/20230908064803.png" width="1152" height="614" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>random-tech-<a class="keyword" href="https://d.hatena.ne.jp/keyword/talk">talk</a>が始まる際のアナウンス</figcaption></figure></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/highwide/20230908/20230908065013.png" width="1200" height="420" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <figure class="figure-image figure-image-fotolife" title="random-tech-talkにおいてオンラインで会話しつつログをSlackに書き連ねる雰囲気"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/highwide/20230908/20230908065010.png" width="1200" height="323" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>random-tech-<a class="keyword" href="https://d.hatena.ne.jp/keyword/talk">talk</a>においてオンラインで会話しつつログをSlackに書き連ねる雰囲気</figcaption></figure></p> <p>ここで、自分が所属したことのあるチーム以外での<a class="keyword" href="https://d.hatena.ne.jp/keyword/ADR">ADR</a>利用事例を聞いて、「軽量なフォーマットゆえの、運用に自由が効く懐の広さ」というものが見えてきたように思います。</p> <p>また、SREやエンジニア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%F3%A5%B0%A5%DE">リングマ</a>ネージャーといった、自分とは異なるロールの人からの<a class="keyword" href="https://d.hatena.ne.jp/keyword/ADR">ADR</a>に対する印象を聞いて、プロダクトマネージャーにも聞いてみようと思うなどの着想を得ました。</p> <p>自分の登壇準備は業務時間を充てさせてもらい、チームにおいても「今スプリントはhighwideは登壇準備をする」ということが織り込まれた状態でスプリントプランニングをしました。先日、同じチームのtooooooooomyとujihisaが<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> Dev Dayでの登壇を行っていますが「登壇は自社のプレゼンスを向上させる重要な業務」というマインドが特に現チームにはあるように感じます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.studysapuri.jp%2Fentry%2F2023%2F07%2F05%2F090000" title="AWS Dev Day 2023 Tokyo で スタディサプリのDarklaunch について発表してきました - スタディサプリ Product Team Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://blog.studysapuri.jp/entry/2023/07/05/090000">blog.studysapuri.jp</a></cite></p> <p>発表資料ができあがってからは、チームを問わず、多くの人からのフィードバックをもらって資料をブラッシュアップしました。ありがたいことです...。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/highwide/20230908/20230908070159.png" width="1100" height="472" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <figure class="figure-image figure-image-fotolife" title="Slackでアツい感想もらえるのありがたい"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/highwide/20230908/20230908070510.png" width="1082" height="708" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Slackでアツい感想もらえるのありがたい</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Google Slidesに直接コメントくれるのもありがたい"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/highwide/20230908/20230908070214.png" width="1200" height="664" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Slidesに直接コメントくれるのもありがたい</figcaption></figure></p> <p>発表はオンラインでの登壇でした。 プライベートな話になってしまいますが、発表時間がちょうど自分が子供を保育園に迎えにいく時間と重なってしまって、妻のお父さんやお母さんにも助けてもらうことになりました。いつもよりも早く子供を迎えにいったあと、子供を見てもらいつつ、発表を行う部屋に3歳児が突入してこないよう<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%EA%A5%B1%A1%BC%A5%C9">バリケード</a>を張って、発表に臨みました 😂</p> <p>発表が終わったあと、同じチームの同僚が"<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B4%A5%B5">エゴサ</a>"(あなたの"エゴ"ではないのに、ありがとう...)をしてくれていたのが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B4%A5%B5%A1%BC%A5%C1">エゴサーチ</a>で見えた実際の反応と同じくらいうれしかったです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/highwide/20230908/20230908065048.png" width="1200" height="191" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="最後に">最後に</h2> <p>社外発表の社内での捉えられ方や、オンラインでのフラットな社内コミュニケーションの雰囲気をこのエントリを通じて知っていただけたらうれしいです。</p> <p>特に、全体では100名を超えるようなそれなりの規模の開発組織においても、チームやロールを超えたフィードバックがもらいやすいのは本当にありがたいことだなぁと思っています。 というわけで、組織みんなでいただいたベストスピーカー賞...という「いい話」にさせてもらおうかなと思います!</p> <p>そんな組織で、採用をやっています。ご興味持っていただけた方は是非詳細を見てみてください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbrand.studysapuri.jp%2Fcareer%2F" title="キャリア | スタディサプリ BRAND SITE" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://brand.studysapuri.jp/career/">brand.studysapuri.jp</a></cite></p> highwide 会社ブログの更新を継続するための取り組み hatenablog://entry/820878482965385709 2023-09-08T10:00:00+09:00 2023-09-08T10:00:01+09:00 こんにちは。Webアプリケーションエンジニアの @ttokutake です。 スタディサプリでは定期的にプロダクトブログを更新するようにしています。 他社様と比較すると更新頻度は少ないかもしれませんが、少なくとも月に1回程度の更新をするように頑張っています。 今回は定期的にブログを更新するために取り組んでみていることを紹介しようと思います。 ブログ運用の目的 スタディサプリのプロダクトブログは中長期的な採用活動の活性化を目的として運用されています。 スタディサプリというサービスやスタディサプリの開発の現場についてご紹介することで、少しでもスタディサプリの開発の雰囲気を知っていただくことを狙って… <p>こんにちは。Webアプリケーションエンジニアの <a href="https://github.com/ttokutake">@ttokutake</a> です。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは定期的にプロダクトブログを更新するようにしています。 他社様と比較すると更新頻度は少ないかもしれませんが、少なくとも月に1回程度の更新をするように頑張っています。</p> <p>今回は定期的にブログを更新するために取り組んでみていることを紹介しようと思います。</p> <h2 id="ブログ運用の目的">ブログ運用の目的</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリのプロダクトブログは中長期的な採用活動の活性化を目的として運用されています。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリというサービスや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発の現場についてご紹介することで、少しでも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発の雰囲気を知っていただくことを狙っています。</p> <p>もちろん自分の成長のために自発的にどんどん書いてくれる方もいらっしゃいますが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリではそうでない人も多くいます。 会社としてブログを継続的に出すのはそれなり苦労しております。</p> <h2 id="過去の運用方法とその課題">過去の運用方法とその課題</h2> <p>以前はブログ番長と呼ばれる<a class="keyword" href="https://d.hatena.ne.jp/keyword/Bot">Bot</a>が個々のメンバーにブログの執筆を依頼し、毎週の更新を目指していました。 詳しくは <a href="https://blog.studysapuri.jp/entry/2021/02/01/080000">過去のブログ</a> をご参照いただけると幸いです。</p> <p>1年以上運用してみた結果、実際にブログを書いてもらえる確率は2割程度だということが判明しました。 つまりブログ番長によるブログの更新は、実質2ヶ月に1回以下の更新となっていました。</p> <p>更新が滞る理由としては以下のようなものがあったのではないかなと推測されます。</p> <ul> <li>当然ながら仕事が忙しい</li> <li>ブログを書きたくない</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Bot">Bot</a>の発言は無視しやすい🙈</li> </ul> <p>実際は個人で書いてくれている人もいたので、それなりのペースでブログは更新されていました。 しかし、たくさん書いてくれていた人が転職されたりもしてブログの更新は減少に向かっていく雰囲気が漂っておりました。 そのためブログ番長は2022年5月にいったん停止して、新たな運用方法を考えることにしました。</p> <h2 id="新しい運用方法とその成果">新しい運用方法とその成果</h2> <p>ブログの存続に危機感を覚えた <a href="https://github.com/pankona">@pankona</a> さんが「個人にお願いするよりも、チームにお願いするほうが "やらねば" みたいな気持ちになるんではないか。しかも複数人数だからブログのネタの相談ができたり<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>の調整もしやすいのでは?」という仮説を立て、2022年7月から実際に試してみることにしました。</p> <p>経緯の詳細は省きますが、現在では以下の要領でブログの執筆を依頼しています。</p> <ul> <li>依頼する担当: 技術<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%E9%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">ブランディング</a>チームと呼ばれるチーム <ul> <li>私も <a href="https://github.com/pankona">@pankona</a> さんもそのチームの一員です。</li> </ul> </li> <li>依頼先: <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリのいずれかの開発チーム <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの組織は、例えば「<a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>チーム」「SREチーム」「決済チーム」「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A1%BC%A5%C1%A5%F3">コーチン</a>グプロダクトチーム」のようにチームが分かれています。</li> </ul> </li> <li>依頼するペース: 月に1回</li> <li>依頼するタイミング: 執筆を担当してもらう月の1ヶ月前 <ul> <li>早めに依頼することでチームの開発スケジュールにブログの執筆を組み込んでもらう狙いがあります。</li> </ul> </li> <li>依頼方法: 技術<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%E9%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">ブランディング</a>チームのメンバーから温かみのある手作業による依頼 <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Bot">Bot</a>の発言は無視されやすいかもしれないので。</li> </ul> </li> </ul> <p><figure class="figure-image figure-image-fotolife" title="温かみのある執筆依頼の様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/ttokutake1/20230907/20230907095717.png" width="1200" height="198" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>温かみのある執筆依頼の様子</figcaption></figure></p> <p>各開発チームがどの月にブログ執筆を担当するかは2024年以降まですでに決まっています。 開発チームに依頼が回ってくるペースは1年に1回もないので、チームとしての負担もそこまで大きくないのではと思います。</p> <p>この "チームによるブログローテーション" の導入により、個人の負担を軽減すると同時にブログの定期的な更新を維持することができています。 もちろんチーム全員が忙しくて書けないということもありますが、ほぼ毎月1本はブログを更新するのを1年以上継続できています。</p> <p>参考までに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの月毎のブログ投稿数は以下のようになっています。 ブログローテーション以外で投稿しているブログも含まれていますのでご注意ください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/ttokutake1/20230907/20230907095648.png" width="600" height="371" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ちなみに「最低でも月に1回はブログ記事が出る」ことを目標としているだけなので、個人が自発的にブログを書くことは変わらず推奨しております。 今回のブログもローテーションによって担当しているわけではなく、個人的に書いています。</p> <h2 id="今後の取り組み">今後の取り組み</h2> <p>引き続き、ブログの運用については改善をしていく予定です。 例えば「もう少しブログの更新ペースを上げる」であったり、「ブログの質を上げる」ような取り組みをしていければと思っております。</p> <p>また、技術<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%E9%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">ブランディング</a>チームという名前がちらっと出てきましたが、その名の通りこのチームは技術<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%E9%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">ブランディング</a>のための活動をしております。 ブログ以外による<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D6%A5%E9%A5%F3%A5%C7%A5%A3%A5%F3%A5%B0">ブランディング</a>施策も現在検討中ですので、何か成果があればそれもまたご紹介できればと思います。</p> <h2 id="最後に">最後に</h2> <p>今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリのプロダクトブログの運用についての近況をご紹介しました。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでの取り組みがみなさまの参考になれば幸いです。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに興味が出てきた方は、ぜひカジュアル面談なんかを受けていただけると嬉しいです! 詳細は <a href="https://brand.studysapuri.jp/career/category/engineer#openPositions">こちら</a> をご確認ください!</p> ttokutake1 入社3ヶ月で取得した育休記録 hatenablog://entry/820878482960296602 2023-08-28T08:00:00+09:00 2023-08-28T08:00:03+09:00 iOSエンジニアのkomajiです。昨年の9月に入社しましたが、そのわずか3ヶ月後の12月末から育児休業・出産育児休暇(以下、育休)を合わせて3ヶ月間取得したので、記憶が色褪せないうちに記録として残しておきます。 育休まで 取得の相談 選考段階で、入社の3ヶ月後あたりから育休を取得したい旨を伝えていました。入社の3ヶ月後となると試用期間もまだ終わっていない時期なので、そもそも取得できるのか、取得できるとしても長期間取得するのは憚られるなという思いがあり、不安を感じていました。そんな中「入社直後でも取得できます。期間も遠慮せずにおっしゃってください。」との回答をいただきました。取得できる旨だけで… <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>エンジニアのkomajiです。昨年の9月に入社しましたが、そのわずか3ヶ月後の12月末から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%E9%BB%F9%B5%D9%B6%C8">育児休業</a>・出産育児休暇(以下、育休)を合わせて3ヶ月間取得したので、記憶が色褪せないうちに記録として残しておきます。</p> <h2 id="育休まで">育休まで</h2> <h3 id="取得の相談">取得の相談</h3> <p>選考段階で、入社の3ヶ月後あたりから育休を取得したい旨を伝えていました。入社の3ヶ月後となると試用期間もまだ終わっていない時期なので、そもそも取得できるのか、取得できるとしても長期間取得するのは憚られるなという思いがあり、不安を感じていました。そんな中「入社直後でも取得できます。期間も遠慮せずにおっしゃってください。」との回答をいただきました。取得できる旨だけでなく、不安を汲み取ってもらったかのように期間に関しても言及いただき、育休に対する組織の向き合い方が伺えてとても安心しました。</p> <h3 id="育児休業と出産育児休暇"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%E9%BB%F9%B5%D9%B6%C8">育児休業</a>と出産育児休暇</h3> <p>一般的に、育休というと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%E9%BB%F9%B5%D9%B6%C8">育児休業</a>を指すことが多いと思います。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%E9%BB%F9%B5%D9%B6%C8">育児休業</a>とは、法律に基づいて取得できることの休業のことです。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>には、これとは別で福利厚生として出産育児休暇があります。簡単に説明すると、妊娠〜育児期間に利用できる有給休暇で、最大40日付与されます。有給休暇ということを強調しておきたいです。まとめて取得しなければならないという制限もなく、こどもが体調を崩した時や、行事等のスポットで利用することも可能です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>はそもそも年間休日が145日と多く、休暇周りの制度の充実さは、育児していく中で改めてありがたいと感じています。休暇の詳細は<a href="https://www.recruit.co.jp/employment/mid-career/benefits/">福利厚生</a>をご覧ください。</p> <h3 id="育休の取り方を決める">育休の取り方を決める</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%E9%BB%F9%B5%D9%B6%C8">育児休業</a>と福利厚生の出産育児休暇は組み合わせて利用できるので、これらをどのようにして取得するかを決めます。</p> <p>何も考えなければ1年くらいこどもだけを見ている時間が欲しいと思っていましたが、特に入社直後ということもあり(入社前にも2ヶ月の休みがありました)、それだけ長い期間休むと仕事の感覚を取り戻すのが大変になりそうと考え、数ヶ月に留めることにしました。出産育児休暇が2ヶ月付与されたので、それをまるっと取得して終わりにしようかと思っていたのですが、周囲の先輩パパの方々に聞いて回ってみると、2ヶ月はちょうど慣れてくる頃でもう少し長く取っておけば良かったという声もあったので、出産育児休暇2ヶ月+<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%E9%BB%F9%B5%D9%B6%C8">育児休業</a>1ヶ月で合計3ヶ月としました。</p> <p>当時を振り返ると、たしかに3ヶ月目からは少しずつ自分たちの時間を過ごすことができるようになってきていたので、3ヶ月にしておいて正解だったなと思っています。</p> <h2 id="育休へ">育休へ</h2> <h3 id="突然の育休">突然の育休</h3> <p>初産ということもあり、予定日より遅くなることはあっても早まることは無いだろうと思って準備を進めていたのですが、その日は予定日よりも2週間早くやってきました。</p> <p>早朝の出産でした。産後は母子ともに1週間ほど入院するので父である私の出番はすぐにはなく、帰宅後は少し休んでから最終引き継ぎのために午後から出勤し、夕方には退勤して育休突入となりました。</p> <p><figure class="figure-image figure-image-fotolife" title="突然の育休宣言"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/komaji504/20230821/20230821123558.jpg" alt="&#x7A81;&#x7136;&#x306E;&#x80B2;&#x4F11;&#x5BA3;&#x8A00;" width="1200" height="236" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>突然の育休宣言</figcaption></figure></p> <p>引き継ぎとは言っても、当時所属していたチームでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a>エンジニアは私を含め2名でしたが、基本的に要件定義や設計は一緒に進めており、実装に関してもキリが良くなるように事前に調整していたので、引き継ぎらしい作業はほとんどありませんでした。そのため、チームメンバーから温かいお言葉をいただいて退勤となりました。</p> <h3 id="育児スケジュール">育児スケジュール</h3> <p>育児をする上で心の余裕を持つことを大事にしようと夫婦で話していました。余裕がないとどうしても態度に表れてしまいます。赤ちゃんは大人の生活リズムと全く異なるため、これに対処するために1日のスケジュールを立て、紙に書き出しました。このスケジュールを私と妻の二人チーム体制でこなすことが我々の育児となります。このスケジュールには、お互いの睡眠の時間、ミルクの時間、家事の時間と担当などを書いていました。大変だなと思うことがあれば都度更新します。</p> <p>スケジュール表はキッチンの壁に貼っていて、定期的にスケジュール表の前に立って相談会を実施していました。アナログで管理するのが結構良く、同期的に一つのものを共有しながら会話することで、チームワークがより強固なものとなっているように感じました。</p> <h3 id="想像を上回る大変さ">想像を上回る大変さ</h3> <p>新生児の睡眠は、昼夜を問わず3時間おきに寝て起きてを繰り返す多層性睡眠です。親もこのサイクルに合わせて睡眠をとることになるので、私たちも3時間寝て、起きてを繰り返すことになるかと思いきや、実際は3時間もまとまって寝られませんでした。ミルクをあげたり、おむつを替えたりする必要があります。ミルクをあげる際には、お湯を沸かす、哺乳瓶を消毒する、ミルクを冷ますなどの工程があり、ミルクを飲むのにも30分かかったりします。実際にはまとまって寝られるのは2時間程度でした。</p> <p>そのため、少しでも私たちが休める時間を確保できるように、さまざまな育児便利グッズを購入し、家事はロボット掃除機、食洗機、洗濯乾燥機などで可能な限り自動化し、食事は宅配弁当などを活用しました(思っていたより出費がかさみました….)。</p> <h2 id="職場復帰">職場復帰</h2> <p><figure class="figure-image figure-image-fotolife" title="職場復帰宣言"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/komaji504/20230821/20230821123605.jpg" alt="&#x8077;&#x5834;&#x5FA9;&#x5E30;&#x5BA3;&#x8A00;" width="1192" height="390" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>職場復帰宣言</figcaption></figure></p> <h3 id="最初の2週間の壁">最初の2週間の壁</h3> <p>育児と仕事でてんやわんやでした。圧倒的に時間が足りません。1日30時間は欲しいです。育児自体には慣れてきていた頃だったのですが、仕事に関しては転職後すぐに育休に入ったのでそもそも業務知識が乏しく、その中でさらにチーム体制もガラッと変わっていたため、何もかもが新しいことのように感じました。</p> <p>そして、コードが全然書けませんでした。育休中は一切コードを書いていなかったのですが、3ヶ月書かないだけでこれだけのブランクを感じるとは思っていなかったです。</p> <p>そのため、仕事に慣れるまではかなり大変で、加えて家族全員風邪をひいてしまったこともあり、最初の週は1週間が10日くらいあったかのような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C8%E8%CF%AB">疲労</a>感でした。それでも2週間ほど経つとだいぶ慣れることができました。</p> <h3 id="キャッチアップ">キャッチアップ</h3> <p>3ヶ月間とそう長くない期間であったとはいえ、その期間の情報量は多いので育休中に起こった全てを詳細に把握することは諦めていました。後から必要になった時に詳細を追うことができるように脳内インデックスを作る目的で全体感を把握し、その上で今実務に取り組む上で必要なことに注力しました。</p> <p>以下では、復帰直後のキャッチアップについて簡単にまとめてみました。</p> <ul> <li>All Hands Meetingの資料漁り <ul> <li>育休中の事業における変更点やそれに付随する組織的な変更点の全体像を把握する目的で All Hands Meeting資料を眺めました。より差分を把握しやすくするために、育休以前の資料も漁りました。</li> </ul> </li> <li>上司との復帰後面談 <ul> <li>組織体制の変更に伴い自身が所属するチームも変わっていたので、組織体制の変更点について説明してもらいました。組織体制変更の背景や目的を説明してもらうことで新たな業務に対する納得感を持つことができました。</li> </ul> </li> <li>同僚による育休前後の主な差分共有 <ul> <li>育休中に行われた<a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%D7%A5%EA%B3%AB%C8%AF">アプリ開発</a>における意思決定や変更点をissueにまとめて説明してくれました。ここで主な差分を把握することができたので、その後のキャッチアップコストが下がりました。</li> </ul> </li> </ul> <p><figure class="figure-image figure-image-fotolife" title="Welcome back issue"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/komaji504/20230821/20230821123609.jpg" alt="Welcome back issue" width="1200" height="415" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Welcome back issue</figcaption></figure></p> <ul> <li>コーディング <ul> <li>コードを書く感覚を取り戻したく、実装量が多いタスクに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンしてもらいました。初めのうちは思うように手が動かなかったので正直焦りもありましたが、2週間ほどである程度感覚を取り戻すことができて安心しました。コードを書く感覚を取り戻すことが精神衛生上大切と感じました。</li> </ul> </li> <li><a href="https://blog.studysapuri.jp/entry/2018/11/14/working-out-loud">Working Out Loud</a> <ul> <li>着手するタスクのissueを読む際に、Working Out Loudとして理解した内容や疑問点をチームメンバーから見えるようにSlackのスレッドにまとめていきました。目にしたメンバーが補足したりリアクションをくれたりして理解が捗りました。</li> </ul> </li> </ul> <p><figure class="figure-image figure-image-fotolife" title="Working Out Loud の様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/komaji504/20230821/20230821123602.jpg" alt="Working Out Loud &#x306E;&#x69D8;&#x5B50;" width="900" height="286" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Working Out Loud の様子</figcaption></figure></p> <p>この他にも、入社時に書いた作業ドキュメントが早速役に立ちました。Nヶ月後の自分は他人とよく言いますが、育休前に行っていた作業手順がすっかり頭から消え去っていてまさしく他人状態だったので、ドキュメント書いておいて良かったです。</p> <h3 id="生活リズム">生活リズム</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>では、リモートワーク・フ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EB%A5%D5%A5%EC">ルフレ</a>ックス制度が導入されています。休憩も自由に取れるため、私は以下のようなタイムスケジュールの日々を送っていることが多いです。</p> <p><strong>午前</strong></p> <ol> <li>起床</li> <li>保育園の準備</li> <li>こどもを保育園に送る</li> <li>犬の散歩</li> <li>勤務開始</li> </ol> <p><strong>午後</strong></p> <ol> <li>ランチ</li> <li>勤務再開</li> </ol> <p><strong>夕方</strong></p> <ol> <li>保育園お迎え</li> <li>夕飯</li> <li>勤務再開</li> <li>退勤</li> <li>お風呂入ったりなど</li> <li>就寝</li> </ol> <p>こどもが生まれる前はお昼前に勤務開始するほどの夜型でしたが、今では朝からバリバリ活動するようになり、自分でも驚いています。</p> <p>夜はこどもが泣かずにぐっすり眠ってくれるようになったので、私も十分に睡眠を取れるようになりました。とはいえ、毎日バタバタで(特に午前と夕方)最低限のことをこなすだけで精一杯です。ただ、リモートワーク・フ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EB%A5%D5%A5%EC">ルフレ</a>ックス制度のおかげで夕飯の後に勤務を再開できたり、夜ワンオペになる日には早めに退勤したりと柔軟に働くことができているため、なんとか心の余裕を保ちながら日々過ごせています。</p> <p>ちなみに、日々の一番の楽しみは保育園のお迎えで、お迎えに行った際に見せてくれる笑顔が最高です。</p> <h2 id="おわりに">おわりに</h2> <p>育休を取得して本当に良かったと感じています。入社直後の育休を応援してくださったチーム・組織には感謝しかないです。</p> <p>出産前は親になる感覚をあまり持てていなかったのですが、育休を通してこどもと全力で向き合うことで、親としての責任と喜びを感じることができました。そして、育児という一つの目的に対して、夫婦で同じ目線で同じ方向を向いて取り組めたことが一番良かったと感じています。これは、仕事でも大切にしていきたいです。</p> <p>以上、育休記録でした。育休を考えている方の参考になると嬉しいです。</p> komaji504 AWS Dev Day 2023 Tokyo で スタディサプリのDarklaunch について発表してきました hatenablog://entry/820878482947048510 2023-07-05T09:00:00+09:00 2023-07-05T09:00:01+09:00 こんにちは。スタディサプリの小中高プロダクト基盤開発グループでProduct Platform Engineer兼テックリードをやっている@tooooooooomyです。 今回は、先日開催された、AWS Dev Day 2023 Tokyo にて、チームメンバーで Senior Product Platform Engineerの@ujihisaとともに [スタディサプリ] Railsアプリケーションのモジュールとして存在していたDarklaunch(FeatureToggles)を Goアプリケーションとしてフルスクラッチでマイクロサービス化した話 という発表を行ってきたので、そのことについ… <p>こんにちは。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループで<a href="https://blog.studysapuri.jp/entry/introduce-product-platform-engineer-position">Product Platform Engineer</a>兼テッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A1%BC%A5%C9">クリード</a>をやっている<a href="https://twitter.com/tooooooooomy">@tooooooooomy</a>です。</p> <p>今回は、先日開催された、<a href="https://aws.amazon.com/jp/events/devday/japan/">AWS Dev Day 2023 Tokyo</a> にて、チームメンバーで Senior Product Platform Engineerの<a href="https://blog.studysapuri.jp/search?q=ujihisa">@ujihisa</a>とともに<br/> <a href="https://jpdevday.awsevents.com/public/session/view/56">[スタディサプリ]<br/> Railsアプリケーションのモジュールとして存在していたDarklaunch(FeatureToggles)を<br/> Goアプリケーションとしてフルスクラッチでマイクロサービス化した話</a><br/> という発表を行ってきたので、そのことについてお話できればと思います。</p> <h2 id="発表の簡単なまとめ">発表の簡単なまとめ</h2> <p>スライドは公開していますが、このブログで簡単なまとめを改めてご紹介できればと思います。</p> <script defer class="speakerdeck-embed" data-id="35d8508de44644c6b428248b4e8c2ef9" data-ratio="1.77725118483412" src="//speakerdeck.com/assets/embed.js"></script> <h3 id="アジェンダ"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B8%A5%A7%A5%F3%A5%C0">アジェンダ</a></h3> <p>発表の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B8%A5%A7%A5%F3%A5%C0">アジェンダ</a>は以下になります。</p> <ol> <li>Darklaunchと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの歴史</li> <li>WebAPIとして作り直した2つの理由</li> <li>Go言語でのアプリケーション開発</li> <li>6週間で初期<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を公開した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a></li> <li>アプリケーションの性能要件</li> </ol> <h3 id="1-Darklaunchとスタディサプリの歴史">1. Darklaunchと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの歴史</h3> <p><a href="https://blog.studysapuri.jp/search?q=ujihisa">@ujihisa</a>が前半パートを担当し、まず第1章で、Darklaunchと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの歴史についてご紹介しました。<br/> <a href="https://blog.studysapuri.jp/search?q=ujihisa">@ujihisa</a>はカナダ、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%F3%A5%AF%A1%BC%A5%D0%A1%BC">バンクーバー</a>からZoomを用いてのオンライン登壇でした<a href="#f-5d7cd57c" name="fn-5d7cd57c" title="AWS Dev Day運用事務局の皆様、ご協力ありがとうございました">*1</a>。 <a href="https://blog.studysapuri.jp/search?q=ujihisa">@ujihisa</a>は過去数年にわたりこの問題と向き合っており<a href="#f-63b52377" name="fn-63b52377" title="https://blog.studysapuri.jp/entry/2022/12/19/darklaunch-ujihisa">*2</a> 、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリという枠を超え、Feature Togglesという設計パターンそのもののについてのエキスパートでもあります。<br/> そんな<a href="https://blog.studysapuri.jp/search?q=ujihisa">@ujihisa</a>から、Darklaunchという仕組みが、プロダクト開発においてなぜ便利なのかを解説させていただきました。</p> <p><figure class="figure-image figure-image-fotolife" title="ujihisa発表の様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tooooooooomy/20230704/20230704125000.jpg" width="425" height="567" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ujihisa発表の様子</figcaption></figure></p> <h3 id="2-WebAPIとして作り直した2つの理由">2. WebAPIとして作り直した2つの理由</h3> <p>続いて第2章では、元々<a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a>モジュールとして存在していたDarklaunchを、WebAPIとして再設計した背景を紹介しました。<br/> ビジネスアプリケーションと同じく、社内向けのプロダクトにおいても、</p> <ul> <li>小さく初めて需要を確認する</li> <li>最初からやりすぎない</li> </ul> <p>という基本原則は同じです。<br/> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリというプロダクトの成長とともに、Darklaunchに対する新たな需要が生まれていきました。</p> <h3 id="3-Go言語でのアプリケーション開発">3. Go言語でのアプリケーション開発</h3> <p>第3章では、</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Rails">Rails</a>のアプリケーション開発をメインとしているチームが、なぜGo言語でのアプリケーション開発を選択したか</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Rails">Rails</a>を書き慣れている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DE">プログラマ</a>が、Go言語でのアプリケーション開発にスムーズに入るために知っておくと便利なテクニック</li> </ul> <p>などのお話をしました。<br/> 小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループのカルチャー、技術選択の際に大事にしている考え方などにも注目していただければと思います。</p> <h3 id="4-6週間で初期APIを公開した開発プロセス">4. 6週間で初期<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を公開した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a></h3> <p>第4章からは私<a href="https://twitter.com/tooooooooomy">@tooooooooomy</a>から、Darklaunch WebAPI(以下Darklaunch V2)の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>についてお話しさせていただきました。</p> <p>今振り返ると、6週間の中で何をしたかというよりも、6週間で開発〜デプロイをするために何をしたかという内容が中心で、タイトル詐欺だった気もしていますが気にせず行きます。<br/> 開発期間がどれほどのものであれ、開発計画を遅らせる一番の原因は「迷い」なのではないでしょうか。</p> <p>「なぜ」このプロダクトが必要なのか<br/> 「どうやって」実現させるのか</p> <p>迷いの原因となるものを最初に明らかにし、チームが合意することで、如何に迷わずに開発するか。またその手段としてのプロトタイプ開発についてご紹介しました。<br/> おまけとして、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは、チームの課題のみにフォーカスするための便利なインフラ基盤が整っているのでそのご紹介もさせていただきました。<a href="#f-7c0b9ad0" name="fn-7c0b9ad0" title="本当に便利です。最高の開発体験をお求めの方はぜひスタディサプリ開発チームへ!">*3</a></p> <p><figure class="figure-image figure-image-fotolife" title="スタサプのインフラ基盤、便利です"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/tooooooooomy/20230704/20230704125208.png" width="986" height="1134" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>スタサプのインフラ基盤、便利です<a href="#f-5dcac00d" name="fn-5dcac00d" title="https://twitter.com/chaspy_/status/1672143398105673729?s=20">*4</a></figcaption></figure></p> <h3 id="5-アプリケーションの性能要件">5. アプリケーションの性能要件</h3> <p>最後に、Darklaunch V2の性能要件をどのように定義し、検証していったかについてご紹介しました。</p> <ul> <li>Read Heavyなアプリケーションであれば必ずCache層は必要なのか?DBのみでは本当に負荷に耐えられないのか?</li> <li>性能要件はどうやって決めるのか?</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a>で想定負荷をどうやって表現するのか?</li> </ul> <p>などのお話が、特に興味を持っていただけたかなと思います。</p> <h2 id="小中高プロダクト基盤開発グループが目指すものについて">小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループが目指すものについて</h2> <p>以上、<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> Dev Dayで行ったDarklaunch V2の発表について簡単にご紹介させていただきました。<br/> <a href="https://blog.studysapuri.jp/search?q=ujihisa">@ujihisa</a>にとっては数年温めていたアイディアの集大成であり、また我々小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループとして正式にリリースした最初のプロダクトであったため、カンファレンスにお越しいただいた方々にお話を聞いてもらえて感無量でした。<br/> 小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループは、今回ご紹介したDarklaunch V2の様に、社内のユーザー(プロダクトチーム)の問題を解決し、事業開発を促進するプロダクトを今後とも作っていきたいと考えています。<br/> これから開発予定のプロダクトも、ビジネスから一歩下がった抽象的なレイヤーになることが多く、技術的にもチャレンジングなものになりえます。そしてそれを今回のように外部に向けて発表する機会も多くあります。<br/> もしそんな小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループで開発することにもしご興味がありましたら、是非<a href="https://brand.studysapuri.jp/career/position/product-platform-engineer/">こちらのリンク</a>をご活用いただければと思います。よろしくお願いします!</p> <h2 id="終わりに">終わりに</h2> <p>来る2023-07-27 に開催される<a href="https://event.shoeisha.jp/devsumi/20230727">Developers Summit 2023 Summer</a> にて、小中<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%E2%A5%D7%A5%ED">高プロ</a>ダクト基盤開発グループの<a href="https://twitter.com/highwide">@highwide</a>が<a href="https://event.shoeisha.jp/devsumi/20230727/session/4470/">日々の意思決定の積み重ねを記録するアーキテクチャ・デシジョン・レコード</a>というタイトルで 17:50 ~ 18:30の枠で発表予定です。<br/> こちらもご興味ありましたら是非観に行ってみてください。<br/> それでは!</p> <div class="footnote"> <p class="footnote"><a href="#fn-5d7cd57c" name="f-5d7cd57c" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> Dev Day運用事務局の皆様、ご協力ありがとうございました</span></p> <p class="footnote"><a href="#fn-63b52377" name="f-63b52377" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://blog.studysapuri.jp/entry/2022/12/19/darklaunch-ujihisa">https://blog.studysapuri.jp/entry/2022/12/19/darklaunch-ujihisa</a></span></p> <p class="footnote"><a href="#fn-7c0b9ad0" name="f-7c0b9ad0" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">本当に便利です。最高の開発体験をお求めの方はぜひ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ開発チームへ!</span></p> <p class="footnote"><a href="#fn-5dcac00d" name="f-5dcac00d" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://twitter.com/chaspy_/status/1672143398105673729?s=20">https://twitter.com/chaspy_/status/1672143398105673729?s=20</a></span></p> </div> tooooooooomy Sprint Planning をやめた話 hatenablog://entry/820878482945923536 2023-06-30T10:46:08+09:00 2023-06-30T10:46:08+09:00 小中新規開発グループ (a.k.a. tara チーム) の qsona です。 tara チームでは、スタディサプリ中学講座というプロダクトを開発しており、約1年前 (2022-02) に本リリースして以来、継続してプロダクト開発を続けています。 tara チームのプロダクト開発は、基本的にスクラムの手法にのっとる形で行っています。ビジネス的な境界により分けられた3つのスクラムチームが存在します。 スクラムの運用については、それぞれの現場において悩みごとが起きがちだと思いますが、tara チームでもご多分に漏れず、うまくいっていること・いっていないことが存在します。今回は、その3つのうちの1… <p>小中新規開発グループ (<a class="keyword" href="https://d.hatena.ne.jp/keyword/a.k.a.">a.k.a.</a> tara チーム) の qsona です。</p> <p>tara チームでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座というプロダクトを開発しており、約1年前 (2022-02) に本リリースして以来、継続してプロダクト開発を続けています。</p> <p>tara チームのプロダクト開発は、基本的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>の手法にのっとる形で行っています。ビジネス的な境界により分けられた3つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>チームが存在します。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>の運用については、それぞれの現場において悩みごとが起きがちだと思いますが、tara チームでもご多分に漏れず、うまくいっていること・いっていないことが存在します。今回は、その3つのうちの1つのチームである「学習コアチーム」において存在した、Sprint Planning に関する (あるいはそこから掘り出された) 課題と、それに対してどう対処したかについて書きたいと思います。</p> <p>なお、本記事中の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>に関する用語は、なるべく<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>ガイド(2020年版)に沿った意味として利用しています。</p> <h2 id="課題-Sprint-Planning-が効果的でない">課題: Sprint Planning が効果的でない</h2> <p>私たちのチームでは、Sprint の期間を2週間とし、基本的には<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>ガイドに則るような形で継続的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">イテレーション</a>を回していました。その中で、「Sprint Planning が効果的に行えていない」という課題に直面しました。</p> <p>以下、その課題について説明していきます。</p> <h3 id="我々の-Sprint-Planning-で行っていること">我々の Sprint Planning で行っていること</h3> <p>まず前提として私たちは、Product Backlog の Refinement は、おおむね Sprint Planning とは別の場で、事前に行っています。ここでの Refinement とは、以下のような内容を指します:</p> <ul> <li>Product Backlog Item (PBI) の作成と、大まかな優先順位付け <ul> <li>基本的に「ユーザーストーリー」形式で作成し、誰にどんな利益があるのかを明示する</li> <li>いわゆる INVEST を指針としている</li> </ul> </li> <li>PBI に対する見積もり <ul> <li>プランニングポーカーを利用し、相対見積もりを行う</li> <li>PBI の洗練化を兼ねる (曖昧な点を減らす)</li> </ul> </li> <li>見積もりを利用して、優先度を調整する <ul> <li>例: 簡単なタスクと予想して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%C3%A5%AF%A5%ED%A5%B0">バックログ</a>の一番上に入れていたが、見積もりを行った結果大きいと判明したので、優先度を下げる (あるいはその逆)</li> </ul> </li> </ul> <p>以上が完了しているという前提のもと、Sprint Planning を行います。</p> <p>Sprint Planning では、この Sprint で取り組む PBI の集合を定めます。</p> <p>基本的には、優先度が高い PBI を、この Sprint 内でチームとして達成できそうな分だけピックアップします。</p> <h3 id="Sprint-Planning-の課題-1-時間と労力がかかる">Sprint Planning の課題 (1): 時間と労力がかかる</h3> <p>Sprint で取り組む PBI をピックアップする上で、もっとも単純なやり方は、以下のような手法です。</p> <ul> <li>これまでの傾向から、2週間で達成できる合計ポイントの予測値を定める。</li> <li>Product Backlog の上から(=優先度の高い順に)PBIをピックアップする。合計ポイントが予測値を超える手前まで入れる。</li> </ul> <p>上に書いたようなやり方で良ければ、非常に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%C5%AA">機械的</a>な作業のように思えます。</p> <p>しかし、実際には以下のような理由でそう簡単ではなく、チームの時間・労力がかかっていました。</p> <h4 id="待ち時間が長くなりがちな-PBI-の存在">待ち時間が長くなりがちな PBI の存在</h4> <p>PBI のうちのいくつかは、他のチームとのコミュニケーションを通して進めていく必要があります。そういった PBI では、「待ち」の状態で数日間止まってしまうことがあります。</p> <p>タスクが待ち状態になる可能性が最初から見込まれる場合、実際の予測ベロシティよりも多くのタスクを積むなどを考慮する必要がありますが、この予測は難しく、Planning を難しくする一因になっていました。</p> <p>以下、そのような PBI の種類の一例を説明します。</p> <p>新規顧客層の開拓を見込んで、ビジネスのフィジビリティ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>が活発な時期などは、技術調査を行い、やりたいことを実現することができるのか、どれくらい難しいのか、代替手段はあるのか、などを提示するという任務があります。</p> <p>私たちのチームではこういったタスクも PBI として扱い見積もりポイントを振って進めています。この技術調査は、ビジネス上重要な意思決定の材料になるので、そのための十分な材料が集まったと判断されてから、完了することにしています。</p> <p>この完了条件については、合理性があり、納得感もある (ビジネスチームとの信頼関係は強いです) のですが、自チーム内だけで完了と判断できないため、待ちの時間が長くなりがちです。待ち状態であることを明確化すればオーバーヘッドは大きくありません。しかし、待ち状態のときは、別のタスクに着手する必要が出てきます。さらに、待ち状態のまま次のスプリントに持ち越されることがあります。</p> <h4 id="前スプリントからの繰り越し">前スプリントからの繰り越し</h4> <p>これはあまり<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>のプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス的にほめられたことではありませんが、現実問題としては、PBI がスプリント中に完了せずスプリントをまたぐことが多く発生していました。</p> <p>原因の一つは、一つ上で示した、待ち状態のままスプリント終了を迎えるタスクですが、それ以外にも、大きめの PBI などが繰り越されることが多くありました。</p> <p>繰り越しが多い状態でなるべく正確に予測して Planning するため、その繰り越しの分がどれくらいなのかの計算していました。例えば 8 ポイントの PBI が 3 ポイント分程度進捗していると考えて、今スプリントでは 5 ポイントとして扱う、というような具合です。</p> <p>個人的にはこの処理は煩雑すぎるのと、完了前の時点で進捗度合のポイント分を見積もるのはずれやすい (一般的に進捗を多く見積もりがち) ので、できればやめたいと思っていましたが、とはいえこの時は本当に繰り越しが多かったので、現実的には仕方ないとも感じていました。</p> <h4 id="チームメンバーのスキルセット">チームメンバーのスキルセット</h4> <p>チームメンバーのスキルセットや技術的育成といったトピックは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>ガイドで特に語られていませんが、実際には重要です。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>チームは、その目標を達成するための能力をすべて有している必要があり、私たちのチームもその点は十分考慮して編成させています。しかし当然ながら、メンバーごとに得意な領域やそうでもない領域はあります。例えば、Product Backlog の上位にあるアイテムがすべてバックエンド開発中心だった場合、フロントエンド領域を得意とするメンバーは十分に力を発揮することができません。</p> <p>自分の専門でない領域のタスクもカバーしていくことは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>の良いプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスの一つだと思います。しかし、チーム状況やメンバーの指向性によっては、専門領域に近いタスクに注力したほうがうまくいくこともあります。</p> <p>また、コードベースの健全な進化や、メンバーの技術的育成といった見地に立つと、ある開発者が、一定期間以上、特定の領域の開発を続けたほうがよい、ということがあります。特定の領域上のコードをいろんな人がひっきりなしに触るよりも、一人で長く触ったほうが、長期的視点を持って設計する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BB%A5%F3%A5%C6%A5%A3%A5%D6">インセンティブ</a>が生まれます。その結果、コードベースに良い設計が生まれ、また個人の視点でも設計力を伸ばすことができます。</p> <p>これらの理由により、優先度を入れ替えるべき場面がある、と個人的には考えています。すると、Planning でタスクを積むためにそういった点の考慮も必要でした。</p> <h4 id="-これらを2週間先まで見通して計画を立てる難しさ">... これらを、2週間先まで見通して計画を立てる難しさ</h4> <p>上で示した様々な事情があっても、「今日何をどこまでやるか?」を決める上ではある程度勘案して考えることができますが、2週間先まで決めるには十分不確実性が多く、難しすぎると感じていました。</p> <p>2週間がだめなら1週間にすればよいのでは? と思った方もいるかもしれません。実際このチームでは、Sprint Planning から1週間経過したときに、計画の内容を見直す、ということをやっていました。</p> <p>しかし、1週間にしてしまうと、よりスプリントをまたいで繰り越される PBI の割合が増えてしまいそうです。また、2週間単位の振り返りサイクルにはチームとして満足していたのもあり、単にスプリントを1週間にするのが良いとも思えませんでした。</p> <h3 id="Sprint-Planning-の課題-2-メリットが少ない">Sprint Planning の課題 (2): メリットが少ない</h3> <h4 id="Sprint-Goal-の確約のメリットが少ない">Sprint Goal の「確約」のメリットが少ない</h4> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>ガイドでは、Sprint で達成すべきゴールを定め「確約」することの意義が示されています。</p> <p>個人的な経験としても、Sprint Goal が大きな意味をなしたことがあります。一行で示されるような明確な Goal のもと、技術的に多岐に渡る PBI 群を、チームの結束のもと2週間でクリアしたという経験です。このように、チームを一致団結させるような目標設定ができると非常にハマると思います。</p> <p>しかし、最近の私たちのチーム開発では、新規開発・改善・調査などのタスクが満遍なく存在していて、それらを結束するようなゴールの制定というのは難しい状況でした。</p> <p>したがって Sprint Goal というのは単に、「取り組むべき PBI の集合」になりがちです。これを計画するのがそもそも難しいことは既に述べました。したがって、「確約」しようとするとより小さい集合になり、あまりチームをモチベートするような目標にはなり得ません。</p> <p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>ガイドではあまり語られてませんが、「確約」することには、プロダクトオーナーやチーム外の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%AF%A5%DB%A5%EB%A5%C0%A1%BC">ステークホルダー</a>に対して説明できるという意味もあるのではないかと思います。</p> <p>しかし、我々のチームにおいては、この意味での確約はそもそも必要性が薄いと感じていました。まず、2週間 (あるいは1週間や1ヶ月) でリリースしたい、というような、期日が厳しいタスクというのは、私たちのチームには多くありません。あるのは以下のような欲求です。</p> <ul> <li>一つ一つをできるだけ早くリリースすること (リードタイムを短くする)</li> <li>もっと長期的なスパンでビジネス計画を立てられるようにすることと、その計画に沿って開発が進むこと</li> </ul> <p>したがって、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>ガイドで示されている「確約」を行う意義はあまり大きくないと考えました。もっとも、タスクの期日等に関して一定の目標を持って取り組むことは仕事において大事だと思いますが、それは2週間単位での目標設定以外の方法でも実現できそうです。</p> <h4 id="イテレーションとリリースは分離されている"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C6%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">イテレーション</a>とリリースは分離されている</h4> <p>スプリントの成果物(インクリメント)単位でリリースを行なっている場合は、スプリントの計画=スプリント完了後のユーザーへの価値提供の計画となります。</p> <p>我々のチームではサービスのデプロイ作業を簡略化したり、E2E自動テストを充実させるなどにより、具体的には tara チーム全体でみると1日平均1回以上のデプロイを行なっています。したがって、もともとスプリントのサイクル単位でリリースを行っていたわけではなく、完了したものから順次リリースしていました。したがって、この意味においてもスプリントゴールは特に意味がありません。</p> <p>個人的な見解ですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>が広まった時代においては、今よりもデプロイのコストが高く、2週間に1回などの単位でまとめてリリースするような戦略が一般的だったのではないかと思います。現在でもクライアントサイドのネイティブアプリ等は一定そういった性質があります。その場合はよりスプリントという概念がしっくり当てはまるのですが、毎日デプロイするのが当然になっている現代では、スプリントというサイクルの価値がやや下がったのではないかと思います。</p> <h4 id="Sprint-Planning-がなくてもベロシティは計測できる">Sprint Planning がなくてもベロシティは計測できる</h4> <p>スプリントゴールの意義として、スプリント終了時の振り返りに利用できるという側面はありそうです。定めたゴールに対して (どれくらい) 達成できたのか、を知ることができます。</p> <p>しかし、2週間単位での計画が厳密でなくても、2週間分の成果を確認することは可能です。スプリントの振り返り時の観点としても、「計画を達成したか/達成しなかったか」ではなく、「達成したベロシティはいくつか/それは過去と比べてどうか」と考えることができます。</p> <p>ベロシティが計測できていれば、より長期の計画を立てることができます。例えば、チームのベロシティが2週間で平均15ポイントであれば、2ヶ月後には約60ポイント分の成果が期待できるといった具合です。これは必ずしもスプリントゴールを定めなくても可能です。</p> <h3 id="Sprint-Planning-の課題のまとめ">Sprint Planning の課題のまとめ</h3> <p>総じて言うと、私たちの Sprint Planning に対し、私は以下のような課題感を持っていました。</p> <ul> <li>難しい、多くの時間がかかっている</li> <li>意義が薄い</li> </ul> <p>...もし本当そうなのであれば、そのようなイベントはなくすべきです。ということで、この説についての検証に取り掛かりました。</p> <h2 id="課題に対する-Try">課題に対する Try</h2> <h3 id="Sprint-Planning-をやらない">Sprint Planning をやらない</h3> <p>以上のような問題提起をチームで行ってみたところ、チームのプロダクトマネージャーや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>マスターが持っていた課題感とかなり重なるところがあることがわかりました。</p> <p>まずプロダクトマネージャーは 「Sprint Planning の準備に時間がかかっている」という課題を持っていました。これはシンプルに Sprint Planning をなくせば解決します。</p> <p>次に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>マスターは「同時に並列で進む PBI の数が多すぎるのではないか (=仕掛かり制限を設けたい)」という課題を持っていました。これについては Sprint Planning が直接悪さをしているわけではありません。逆に、PBI の並列度が高いことにより、スプリントをまたぐ PBI の数が多くなり、Planning が難しくなる、という関係性にあります。</p> <p>ですが、目の前のものをチームとして日々着実に終わらせていく、ということが重要なのであり、そのためには、2週間先のゴールはむしろ不要なのではないか、と考えました。</p> <p>それであれば一旦 Sprint Planning はなくしてみよう、とチームで合意できました。</p> <h3 id="Product-Backlog-の整備は今まで通り行う">Product Backlog の整備は今まで通り行う</h3> <p>優先順位づけられたユーザーストーリーのリストである Product Backlog はこれまで通り非常に有用です。これを整備するイベントである Product Backlog Refinement についても、これまで通り、必要に応じて定期的に行うこととしました。</p> <h3 id="Daily-のミーティングで必要に応じて新たに取り組む-PBI-をピックアップし必要に応じて計画する">Daily のミーティングで、必要に応じて新たに取り組む PBI をピックアップし、必要に応じて計画する</h3> <p>Sprint Planning でそのスプリント分の PBI を定めて計画する代わりに、Daily のミーティングを利用することにしました。</p> <p>具体的には、手が空いた人がいた場合、以下のいずれかを行います:</p> <ul> <li>Product Backlog の上位の方にある Product Backlog Item (PBI) を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンする <ul> <li>PBI を自分に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンした人は PBI Lead として、その PBI の完了までリードする責務を持つ</li> </ul> </li> <li>すでに他の人がリードしている PBI を手伝う (PBI に紐づくタスクを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンする)</li> </ul> <p>ここで、新規の PBI に着手するときは、チームでその内容や完了条件を確認しています。その大きさや複雑度によっては、タスク分解を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンされた人に任せず、チームで議論するかもしれません。</p> <p>つまり、もともと Sprint Planning で2週間分の計画を行っていたかわりに、Daily のミーティングを利用して PBI 単位での計画を行っているということができます。</p> <p>またこのタイミングで、PBI Lead の役割を明文化しました。一言でいえば PBI をリリースまで持っていくための責任を持つ人で、PBI に関するタスクを全て行う必要はありませんが、タスク分解や進行管理、リリースの段取りなどはこの人が行うものとしました。</p> <h3 id="Sprint-Retrospective-は引き続き2週間単位で行う">Sprint Retrospective は引き続き2週間単位で行う</h3> <p>Sprint Retrospective は、今まで通り、2週間サイクルで最後の金曜日に行います。</p> <ul> <li>2週間分のベロシティの確認</li> <li>チームの振り返り (<a class="keyword" href="https://d.hatena.ne.jp/keyword/KPT">KPT</a>)</li> </ul> <p>この振り返りの内容や周期に関してはチームとして全く不満がなく、うまく回っていたため、当然に継続することになりました。</p> <p>なお、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%C6%A1%BC%A5%AF%A5%DB%A5%EB%A5%C0%A1%BC">ステークホルダー</a>への成果物の説明は、tara チーム全体で週1回のプロダクトに関わる人が集まる会の中で行われています。</p> <h2 id="振り返り">振り返り</h2> <p>以上の Try を行ってから約9ヶ月が経ちました。(9ヶ月間ブログ記事を完成させずに放置してしまっただけとも言いますw)</p> <p>現在においても、このチームでは Sprint Planning は廃止されたままになっています。</p> <p>9ヶ月の間も、チームでは定期的な振り返りを通して新たな課題に対処していっていますが、結果として Sprint Planning を復活したいという声は一度も起きなかったので、Sprint Planning をなくすことは少なくとも現場における現実解だったと考えています。</p> <p>平均的なベロシティの推移を追っても、Sprint Planning 廃止以降に下がったということはなく、むしろ上がっています。上がった要因はこれ以外の改善活動によるものが大きく、Sprint Planning 廃止によってベロシティが上がったとは言えません。が、少なくとも、「Sprint Goal を立てなくなったことにより求心力が下がりベロシティが下がった」というようなマイナスの事柄は起きませんでした。</p> <p>一方で、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>において Sprint Goal をうまく定められているときに比較すると、計画性の面では弱さがあるかもしれません。基本的に PBI 単位での計画を毎度しっかりやれれば理論的には変わらないはずと思っていますが、現実的には、Daily ミーティングでどこまで時間をとってやるのか迷うなど、やや運用に難しさがあります。</p> <h2 id="考察とまとめ">考察とまとめ</h2> <p>今回の改善は、Sprint Planning をうまくやるのが難しいという課題から出発したものですが、結果的にその後の状況は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B8%A5%E3%A5%A4%A5%EB">アジャイル</a>における「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%F3%A5%D0%A5%F3%CA%FD%BC%B0">カンバン方式</a>」として整理されている方法に似通っていることに後から気づきました。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%F3%A5%D0%A5%F3%CA%FD%BC%B0">カンバン方式</a>について調べていると、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a> vs カンバン」のように別物として整理している記事を多く見つけましたが、それはどうもあまり意味がないように見えます。たとえば、「カンバンでは作業を単体でいつでもリリースできるが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>はスプリントの完了後にまとめてリリースする」という言説は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>をやりながらでも毎日リリースできるので、やや時代遅れな考えといえるでしょう。</p> <p>つまり、私たちとしてやるべきことは、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>」か「カンバン」かあるいはその他の何かを一つ選択するだけではなく、その中の個々の手法を学んだりその背景を理解し、チーム状況に合わせて選択的に取り入れることなのではないでしょうか。</p> <p>たとえば「カンバン」方式を一旦採択するとしても、Product Backlog を整備することや、一定期間の振り返りをチームで行うことは、基本的には有用なプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスなはずです。</p> <p>こと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>においては、"それは正しく<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>のプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスを実践すれば解決する" というような論調もよくみますし、この記事にも一定そのような反応はあるでしょう。しかし、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%E9%A5%E0">スクラム</a>はどんな場面でも万能で使える<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B6%E4%A4%CE%C3%C6%B4%DD">銀の弾丸</a>ではありませんし、チームのことは最終的にはそのチームの人にしかわかりません(この記事でも残念ながら書ききれていないコンテキストがたくさんあります)。</p> <p>私たちが直面した課題は、みなさんのチームにおいての課題と全く同じであることはないでしょうが、「プ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスの背景を理解しつつ、実際の課題に適応させるために適切にアレンジする」という選択を持ってよいと私は考えており、それにはソフトウェアの設計をするのと同じような面白さもあります。この記事が背中を押すきっかけになればよいな、と思っています。</p> quipper-ja Jetpack Composeでスポットライト機能を実装する hatenablog://entry/820878482942784880 2023-06-26T08:00:00+09:00 2023-06-26T08:00:26+09:00 こんにちは、Androidエンジニアの@morux2です。本記事ではJetpack Composeでスポットライト機能を実装する方法を紹介します。 はじめに スポットライトは、特定の要素を目立たせることでユーザーの行動を促す機能です。スタディサプリ中学講座のオンボーディング画面にも採用されており、現在カスタムViewからの移行を進めています。 スタディサプリ中学講座のオンボーディング 今回は実装を3つのステップに分けて紹介します。 実装の3ステップ 画面全体を半透明の黒いViewで覆う スポットライトを当てたい要素の長方形の座標を取得する 取得した座標に沿って黒いViewを切り抜く 参考にさせ… <p>こんにちは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a>エンジニアの@morux2です。本記事では<a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Composeでスポットライト機能を実装する方法を紹介します。</p> <h3 id="はじめに">はじめに</h3> <p>スポットライトは、特定の要素を目立たせることでユーザーの行動を促す機能です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座のオンボーディング画面にも採用されており、現在カスタムViewからの移行を進めています。</p> <p><figure class="figure-image figure-image-fotolife" title="スタディサプリ中学講座のオンボーディング"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20230619/20230619141110.png" width="270" height="600" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座のオンボーディング</figcaption></figure></p> <p>今回は実装を3つのステップに分けて紹介します。</p> <h5 id="実装の3ステップ">実装の3ステップ</h5> <ol> <li>画面全体を半透明の黒いViewで覆う</li> <li>スポットライトを当てたい要素の長方形の座標を取得する</li> <li>取得した座標に沿って黒いViewを切り抜く</li> </ol> <p>参考にさせていただいた記事は<a href="https://medium.com/codex/how-to-accomplish-dynamic-absolute-positioning-in-androids-jetpack-compose-afa14f0e8dea">こちら</a>になります。</p> <h3 id="スポットライト機能の実装">スポットライト機能の実装</h3> <h5 id="1-画面全体を半透明の黒いViewで覆う">1. 画面全体を半透明の黒いViewで覆う</h5> <p>まず<a href="https://developer.android.com/jetpack/compose/graphics/draw/overview">Canvas</a>を使用して半透明の黒いViewを作成します。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Composable</span> <span class="synType">fun</span> Spotlight() { Canvas(modifier = Modifier.fillMaxSize()) { drawRect(Color.Black.copy(alpha = <span class="synConstant">0.8f</span>)) } } </pre> <h5 id="2-スポットライトを当てたい要素の座標を取得する">2. スポットライトを当てたい要素の座標を取得する</h5> <p>次に<a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/OnGloballyPositionedModifier">OnGloballyPositionedModifier</a>をスポットライトを当てたい要素に対して使用します。今回は<a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/layout/package-summary#extension-functions-summary">boundsInRoot</a>でルートのComposableを基準にした長方形の座標を取得しています。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Composable</span> <span class="synType">fun</span> SampleScreen() { <span class="synType">var</span> targetRect <span class="synStatement">by</span> remember { mutableStateOf&lt;Rect?&gt;(<span class="synConstant">null</span>) } <span class="synType">Target</span>( modifier = Modifier.onGloballyPositioned { coordinates <span class="synType">-&gt;</span> targetRect = coordinates.boundsInRoot() } ) } <span class="synIdentifier">@Composable</span> <span class="synType">fun</span> <span class="synType">Target</span>(modifier: Modifier = Modifier) { Text( modifier = modifier, text = <span class="synConstant">&quot;Hello Android&quot;</span> ) } </pre> <h5 id="3取得した座標に沿って黒いViewを切り抜く">3.取得した座標に沿って黒いViewを切り抜く</h5> <p>最後に<a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/Canvas#summary">clipPath</a>を用いて切り抜きます。<a href="https://developer.android.com/reference/kotlin/androidx/compose/ui/graphics/ClipOp">ClipOp.Difference</a>を指定することで領域が減算されます。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Composable</span> <span class="synType">fun</span> Spotlight(targetRect: Rect) { Canvas(modifier = Modifier.fillMaxSize()) { <span class="synType">val</span> spotlightPath = Path().apply { addRect(targetRect) } clipPath( path = spotlightPath, clipOp = ClipOp.Difference ) { drawRect(Color.Black.copy(alpha = <span class="synConstant">0.8f</span>)) } } } </pre> <p>この時、Pathの指定によって楕円や角丸のスポットライトを当てることも可能です。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synType">val</span> spotlightPath = Path().apply { addOval(targetRect) } <span class="synType">val</span> spotlightPath = Path().apply { addRoundRect( RoundRect( rect = targetRect, cornerRadius = CornerRadius(<span class="synConstant">16</span>.dp.toPx()) ) ) } </pre> <h5 id="完成">完成!</h5> <p>あとはスポットライトを重ねてあげれば完成です!</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Composable</span> <span class="synType">fun</span> SampleScreen() { <span class="synType">var</span> targetRect <span class="synStatement">by</span> remember { (mutableStateOf&lt;Rect?&gt;(<span class="synConstant">null</span>)) } Box(modifier = Modifier.fillMaxSize()) { <span class="synType">Target</span>( modifier = Modifier.onGloballyPositioned { targetRect = it.boundsInRoot() } ) targetRect?.let { Spotlight(targetRect = it) } } } </pre> <p><figure class="figure-image figure-image-fotolife" title="完成イメージ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20230619/20230619145804.png" width="292" height="600" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>完成イメージ</figcaption></figure></p> <h3 id="スポットライトが当たっている領域だけを操作可能にする">スポットライトが当たっている領域だけを操作可能にする</h3> <p>現状の実装ではスポットライトの裏側の要素をタップできるようになっているので、ユーザーが意図しない行動を取れてしまいます。そこで、スポットライトの内側のみタップを有効にする処理を加えていきます。</p> <p><a href="https://developer.android.com/reference/kotlin/androidx/compose/foundation/gestures/package-summary#extension-functions-summary">detectTapGestures</a>を用いてタップされた座標を取得し、それがスポットライトの長方形の座標の内側かどうか計算することで実現可能です。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Composable</span> <span class="synType">fun</span> Spotlight(targetRect: Rect) { Canvas(modifier = Modifier .fillMaxSize() .pointerInput(<span class="synType">Unit</span>) { detectTapGestures(onTap = {offset <span class="synType">-&gt;</span> <span class="synStatement">if</span> (targetRect.contains(offset)) { <span class="synComment">// スポットライトがクリックされた時の動作を指定</span> } }) }) { <span class="synComment">// 以下省略</span> } } </pre> <p><figure class="figure-image figure-image-fotolife" title="領域内タップ時のみトーストを表示"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20230619/20230619145350.gif" width="292" height="600" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>領域内タップ時のみトーストを表示</figcaption></figure></p> <h3 id="スポットライトに装飾やアニメーションをつける">スポットライトに装飾やアニメーションをつける</h3> <p>ここからはスポットライトに装飾やアニメーションを加えて動きをリッチにしていきます。 <figure class="figure-image figure-image-fotolife" title="完成イメージ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/morux2/20230619/20230619152329.gif" width="292" height="600" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>完成イメージ</figcaption></figure></p> <h5 id="スポットライトが当たっている要素から相対的な位置に装飾を配置する">スポットライトが当たっている要素から相対的な位置に装飾を配置する</h5> <p>スポットライトを注目させるために文字や矢印を配置したいケースがあるでしょう。そんな時は<a href="https://developer.android.com/jetpack/compose/layout#layout-modifier">LayoutModifier</a>を利用します。配置したい装飾の幅と高さ・スポットライト領域の座標を組み合わせて計算をすることで、相対的な位置を指定することができます。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Composable</span> <span class="synType">fun</span> Spotlight(targetRect: Rect) { Box { Canvas(modifier = Modifier.fillMaxSize()){ <span class="synComment">// 以下省略</span> } GuideLabel(targetRect = targetRect) } } <span class="synIdentifier">@Composable</span> <span class="synType">fun</span> GuideLabel(targetRect: Rect) { Text( modifier = Modifier.layout { measurable, constraints <span class="synType">-&gt;</span> <span class="synType">val</span> placeable = measurable.measure(constraints) layout(placeable.width, placeable.height) { placeable.placeRelative( <span class="synComment">// スポットライトを基準にラベルをセンタリング</span> x = (targetRect.topLeft.x <span class="synStatement">+</span> (targetRect.width <span class="synStatement">-</span> placeable.width) <span class="synStatement">/</span> <span class="synConstant">2</span>).toInt(), <span class="synComment">// スポットライトの上に16dpのpaddingを空ける</span> y = targetRect.topLeft.y.toInt() <span class="synStatement">-</span> (placeable.height <span class="synStatement">+</span> <span class="synConstant">16</span>.dp.toPx().toInt()) ) } }, text = <span class="synConstant">&quot;Click Here&quot;</span>, color = Color.White ) } </pre> <h5 id="スポットライトをフェードインさせる">スポットライトをフェードインさせる</h5> <p><a href="https://developer.android.com/jetpack/compose/animation?hl=ja#animatedvisibility">AnimatedVisibility</a>を用いると簡単にフェードインのアニメーションを実現できます。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Composable</span> <span class="synType">fun</span> SampleScreen() { <span class="synType">var</span> targetRect <span class="synStatement">by</span> remember { (mutableStateOf&lt;Rect?&gt;(<span class="synConstant">null</span>)) } Box(modifier = Modifier.fillMaxSize()) { <span class="synType">Target</span>( modifier = Modifier.onGloballyPositioned { targetRect = it.boundsInRoot() } ) targetRect.let { AnimatedVisibility( visible = it <span class="synStatement">!=</span> <span class="synConstant">null</span>, enter = fadeIn(tween(<span class="synConstant">5000</span>)) ) { Spotlight(targetRect = it) } } } } </pre> <h5 id="スポットライト領域をアニメーションで広げる">スポットライト領域をアニメーションで広げる</h5> <p><a href="https://developer.android.com/jetpack/compose/animation?hl=ja#animate-as-state">animateSizeAsState</a>関数を使ってスポットライト領域をアニメーションさせてみます。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synIdentifier">@Composable</span> <span class="synType">fun</span> Spotlight(targetRect: Rect) { <span class="synType">var</span> startAnimation: <span class="synType">Boolean</span> <span class="synStatement">by</span> remember { mutableStateOf(<span class="synConstant">false</span>) } <span class="synType">val</span> animatedSize: Size <span class="synStatement">by</span> animateSizeAsState( targetValue = <span class="synStatement">if</span> (startAnimation) targetRect.size <span class="synStatement">else</span> Size.Zero, animationSpec = tween(<span class="synConstant">1000</span>), label = <span class="synConstant">&quot;spotlight&quot;</span> ) <span class="synComment">// 中心から放射状に広がるアニメーション</span> <span class="synType">val</span> deltaX = animatedSize.width <span class="synStatement">/</span> <span class="synConstant">2</span> <span class="synType">val</span> deltaY = animatedSize.height <span class="synStatement">/</span> <span class="synConstant">2</span> <span class="synType">val</span> animatedRect = Rect( left = targetRect.center.x <span class="synStatement">-</span> deltaX, top = targetRect.center.y <span class="synStatement">-</span> deltaY, right = targetRect.center.x <span class="synStatement">+</span> deltaX, bottom = targetRect.center.y <span class="synStatement">+</span> deltaY ) LaunchedEffect(targetRect) { startAnimation = <span class="synConstant">true</span> } Canvas(modifier = Modifier.fillMaxSize()) { <span class="synType">val</span> spotlightPath = Path().apply { addRect(animatedRect) } clipPath( path = spotlightPath, clipOp = ClipOp.Difference ) { drawRect(Color.Black.copy(alpha = <span class="synConstant">0.8f</span>)) } } } </pre> <h3 id="さいごに">さいごに</h3> <p>いかがでしたでしょうか?個人的には想定していたよりも手軽に見通しよく実装できたと思っています。ぜひ皆さまも試してみてください。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fmorux2%2FComposeSpotlightSample" title="GitHub - morux2/ComposeSpotlightSample" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://github.com/morux2/ComposeSpotlightSample">github.com</a></cite></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリでは、一緒に最高のプロダクトを作っていってくれる仲間を募集しています! 少しでもご興味がある方はこちらのページからご連絡ください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbrand.studysapuri.jp%2Fcareer%2Fcategory%2Fengineer%2F%23openPositions" title="キャリア | スタディサプリ BRAND SITE" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://brand.studysapuri.jp/career/category/engineer/#openPositions">brand.studysapuri.jp</a></cite></p> morux2 早期ミスマッチ解消のために、職務経歴書のガイドを公開しました hatenablog://entry/820878482943392842 2023-06-22T08:00:00+09:00 2023-06-22T13:16:28+09:00 こんにちは、Web Engineer の @wozaki です。 今回は、採用プロセスの改善として、職務経歴書に記載いただきたいことを公開した背景をご紹介します。 概要 職務経歴書に、採用チームとして期待する情報が不足していることがある 不足すると、以下の課題が発生することがある 書類選考は通過するが、その後の選考でミスマッチと分かる (経歴書が充足していたら、より早期にミスマッチが分かったかもしれない) 面接の前に経歴に踏み込んだ質問を設計できずに、面接時間内でマッチしているか情報を引き出す難易度が上がる 既存の対策として、情報の追記をお願いすることがある 新たな対策として、記載いただきたい… <p>こんにちは、Web Engineer の <a href="https://github.com/wozaki">@wozaki</a> です。</p> <p>今回は、採用プロセスの改善として、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%A6%CC%B3%B7%D0%CE%F2%BD%F1">職務経歴書</a>に記載いただきたいことを公開した背景をご紹介します。</p> <h2 id="概要">概要</h2> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%A6%CC%B3%B7%D0%CE%F2%BD%F1">職務経歴書</a>に、採用チームとして期待する情報が不足していることがある</li> <li>不足すると、以下の課題が発生することがある <ul> <li>書類選考は通過するが、その後の選考でミスマッチと分かる (経歴書が充足していたら、より早期にミスマッチが分かったかもしれない)</li> <li>面接の前に経歴に踏み込んだ質問を設計できずに、面接時間内でマッチしているか情報を引き出す難易度が上がる</li> </ul> </li> <li>既存の対策として、情報の追記をお願いすることがある</li> <li>新たな対策として、記載いただきたいことを <a href="https://github.com/quipper/handbook/blob/master/interview-guide-ja.md#%E6%9B%B8%E9%A1%9E%E9%81%B8%E8%80%83">ガイドとして公開</a> することにした</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="記載いただきたいこと"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/w/wozaki/20230621/20230621153743.png" width="1200" height="789" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>記載いただきたいこと</figcaption></figure></p> <h2 id="早期ミスマッチ解消の必要性">早期ミスマッチ解消の必要性</h2> <p>Web Engineer の採用は競争が激化している肌感があります。 応募者の方々にとっても、様々な企業の中から限られた時間でマッチしている企業を選ぶことは大変だと思います。</p> <p>このような状況において、応募者と採用チーム<strong>双方に重要なのが早期のミスマッチ解消</strong>です。</p> <p>ミスマッチの解消は、採用プロセス改善の重要テーマとして捉えています。<a href="#f-36d5335f" name="fn-36d5335f" title="採用面接ガイドを公開したときもミスマッチを回避したい思いがありました。https://blog.studysapuri.jp/entry/2018/09/01/interview-guide">*1</a></p> <blockquote><p>引用: <a href="https://github.com/quipper/handbook/blob/master/interview-guide-ja.md">スタディサプリ Web Engineer 採用プロセスガイド</a></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの採用チームは「ミスマッチこそが採用活動における最大の失敗」と考えます。採用プロセスを通じてこれを避け、応募者と<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ双方にとって最良の結果を得ることを目指します</p></blockquote> <h2 id="職務経歴書に情報が不足する際に発生する課題"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%A6%CC%B3%B7%D0%CE%F2%BD%F1">職務経歴書</a>に情報が不足する際に発生する課題</h2> <p>課題を2つ説明します。</p> <h3 id="1-ミスマッチが分かるタイミングが遅れる">1. ミスマッチが分かるタイミングが遅れる</h3> <p>Web Engineer の採用プロセスは以下の順で進みます。書類選考はミスマッチを評価する一番最初の選考プロセスです。<a href="#f-fc0a4b20" name="fn-fc0a4b20" title="基本的にこのプロセスですが、その他検査が入る可能性もあります。">*2</a></p> <ol> <li>書類選考</li> <li>(任意) カジュアル面談</li> <li>コードテスト</li> <li>一次面接</li> <li>二次面接</li> <li>最終面接</li> </ol> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%A6%CC%B3%B7%D0%CE%F2%BD%F1">職務経歴書</a>に情報がやや不足している場合に「<a href="https://brand.studysapuri.jp/career/position/web-application-engineer/">Job Description</a> には当てはまりそうな為、お会いした上で詳細を確認させて頂く」と評価することがあります。 各選考プロセスで得られる情報と、評価観点が異なるので妥当な場合が多いですが「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%A6%CC%B3%B7%D0%CE%F2%BD%F1">職務経歴書</a>が充足していたら、より早期にミスマッチが分かったかも...?」と思うケースもあります。</p> <h3 id="2-面接で情報を引き出す難易度が上がる">2. 面接で情報を引き出す難易度が上がる</h3> <p>採用チームは、面接の限られた時間内で応募者を知るために、面接前にペアで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%A6%CC%B3%B7%D0%CE%F2%BD%F1">職務経歴書</a>を拝見し、質問を設計して面接に臨みます。</p> <p>事前に質問を設計できない場合、面接官は会話の中で前提情報をキャッチアップし、期待する情報を引き出すための質問を組み立てる必要があり、マッチしているか評価する情報を時間内に引き出す難易度が上がります。</p> <h2 id="対策">対策</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%A6%CC%B3%B7%D0%CE%F2%BD%F1">職務経歴書</a>に情報が不足している場合の対策について説明します。</p> <h3 id="既存の対策-応募者に追記リクエストする">既存の対策: 応募者に追記リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トする</h3> <p>以下のような依頼を応募者に送っています。 追記いただいた情報を参考に面接に臨みます。</p> <pre class="code" data-lang="" data-unlink>面接の限られた時間をお互いに有効に使うことを目的として、可能であれば職務経歴書を加筆いただけますでしょうか。 以下観点を参考に、Web アプリケーション開発者として主体的に課題を解決した点を、箇条書き程度で加筆いただきたいです。面接で議論を深堀りたい1プロジェクトで構いません。 - 何を解決するプロジェクトなのか - どのような役割で関わったのか - システムの全体像 - どのような技術的な課題があったのか - どのように主体的に解決に取り組んだのか - 結果はどうなったか</pre> <h3 id="新規対策-ガイドとして公開する">新規対策: ガイドとして公開する</h3> <p>追記リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを改善する議論の中で、<a href="https://github.com/kuranari">@kuranari</a> さんから 「もっと事前に応募者と期待を合わせたい。ガイドで公開するといいかも」とアイディアが出て、良さそうということで公開に至りました。</p> <p><a href="https://github.com/quipper/handbook/blob/master/interview-guide-ja.md#%E6%9B%B8%E9%A1%9E%E9%81%B8%E8%80%83">https://github.com/quipper/handbook/blob/master/interview-guide-ja.md#%E6%9B%B8%E9%A1%9E%E9%81%B8%E8%80%83</a></p> <h2 id="応募者にとって負担では">応募者にとって負担では?</h2> <p>はい。応募者にとって負担が増えるかもしれません。</p> <p>しかしながら、 他社の選考でも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%A6%CC%B3%B7%D0%CE%F2%BD%F1">職務経歴書</a>に期待する情報はそれほど違わないので応用できる、つまり、<strong>より多くの企業と効率よく早期ミスマッチ解消に利用できるメリット</strong>もあるのではと想像しています。(違うかもしれません..)</p> <p>ご理解とご協力をお願いいたします。</p> <h2 id="おわりに">おわりに</h2> <p>採用チームでは、応募者と<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ双方にとって、より良い採用プロセスを作るために、定期的にふりかえりを実践しています。 得られた知見はブログなどで共有していきますので、今後ともよろしくお願いします。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに興味を持った方は、まずはカジュアル面談からでもお話を聞きに来てもらえると嬉しいです。 <a href="https://brand.studysapuri.jp/career/category/engineer#openPositions">https://brand.studysapuri.jp/career/category/engineer#openPositions</a></p> <div class="footnote"> <p class="footnote"><a href="#fn-36d5335f" name="f-36d5335f" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">採用面接ガイドを公開したときもミスマッチを回避したい思いがありました。<a href="https://blog.studysapuri.jp/entry/2018/09/01/interview-guide">https://blog.studysapuri.jp/entry/2018/09/01/interview-guide</a></span></p> <p class="footnote"><a href="#fn-fc0a4b20" name="f-fc0a4b20" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">基本的にこのプロセスですが、その他検査が入る可能性もあります。</span></p> </div> wozaki 「スタディサプリ中学講座」における最近の Jetpack Compose 関連の改善 hatenablog://entry/820878482938952637 2023-06-19T09:00:00+09:00 2023-06-19T09:00:03+09:00 はじめに こんにちは。「スタディサプリ中学講座」 で Android エンジニアをしている @maxfie1d です。「スタディサプリ中学講座」 では Jetpack Compose を最大限に活用しています。現在アプリ全体の 7 割程度の UI は Jetpack Compose で構築されています。 Jetpack Compose は 2021 年 7 月の正式リリースから間もなく 2 年が経とうとしており、様々なアップデートを重ねてきました。「スタディサプリ中学講座」 では Jetpack Compose のアップデートを継続的にプロジェクトに取り入れています。この記事では 「スタディサ… <h2 id="はじめに">はじめに</h2> <p>こんにちは。「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座」 で <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> エンジニアをしている <a href="https://twitter.com/maxfie1d">@maxfie1d</a> です。「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座」 では <a href="https://developer.android.com/jetpack/compose?hl=ja">Jetpack Compose</a> を最大限に活用しています。現在アプリ全体の 7 割程度の UI は <a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose で構築されています。</p> <p><a href="https://android-developers.googleblog.com/2021/07/jetpack-compose-announcement.html">Jetpack Compose は 2021 年 7 月の正式リリース</a>から間もなく 2 年が経とうとしており、様々なアップデートを重ねてきました。「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座」 では <a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose のアップデートを継続的にプロジェクトに取り入れています。この記事では 「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座」 における最近の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose 関連の改善をご紹介します。</p> <h2 id="Mutipreview-annotations">Mutipreview annotations</h2> <p><a href="https://developer.android.com/jetpack/compose/tooling/previews#preview-multipreview">Multipreview annotations</a> は複数の <code>@Preview</code> annotation を 1 つにまとめるもので、コンポーザブル関数に追加することで異なるプレビューを一度にすべて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%F3%A5%C0%A5%EA%A5%F3%A5%B0">レンダリング</a>することができます。</p> <p>「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座」 は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%DE%A5%DB">スマホ</a>と<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%D6%A5%EC%A5%C3%A5%C8">タブレット</a>の両方の端末で使用することができます。この際注意したいのが、特定の端末サイズにおける表示崩れです。筆者の経験上、表示崩れが発生しやすいのは小さな端末か大きな端末のいずれかです。これらの端末で表示崩れが生じないことを実機や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%DF%A5%E5%A5%EC%A1%BC%A5%BF">エミュレータ</a>で一つずつ確認することは大変ですが、Multipreview annotations を活用することで <a class="keyword" href="https://d.hatena.ne.jp/keyword/Android%20Studio">Android Studio</a> 上で簡単に各端末サイズ上での表示を一度に確認できるようになりました。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink><span class="synComment">// Multipreview annotations の例</span> <span class="synIdentifier">@Preview</span>(device = Devices.PHONE,showBackground = <span class="synConstant">true</span>) <span class="synIdentifier">@Preview</span>(device = <span class="synConstant">&quot;spec:shape=Normal,width=350,height=600,unit=dp,dpi=300&quot;</span>,showBackground = <span class="synConstant">true</span>) <span class="synType">annotation</span> <span class="synType">class</span> JuniorHighScreenPreview </pre> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/maxfieldwalker/20230605/20230605155917.png" width="1096" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="Compose-Material-3">Compose Material 3</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose の導入時から Material 2 を使用してきましたが、最近の新規開発画面においては <a href="https://m3.material.io/">Material 3</a> を導入し始めています。Material 3 は <a href="https://m3.material.io/styles/color/dynamic-color/overview">Dynamic color</a> などの特徴的な機能が注目されがちですが、各種<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を含め全体的に改良が重ねられ Material 2 よりも使い勝手がよくなっていると感じます。</p> <p>また <a href="https://developer.android.com/jetpack/androidx/releases/compose-material3#1.1.0">最近リリースされた Compose Material 3 1.1.0</a> でほぼすべての<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が実装されたので、Material 2 と比べて実装が追いついていない部分はほぼなくなったと感じています。Material 3 の今後のアップデートにもとても期待しています。</p> <p>ちなみに筆者のお気に入りの Material 3 の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>は、<a href="https://m3.material.io/components/bottom-sheets/overview">Bottom sheets</a>, <a href="https://m3.material.io/components/top-app-bar/overview">Top app bars</a>, <a href="https://m3.material.io/components/cards/overview">Cards</a>, <a href="https://m3.material.io/components/carousel/overview">Carousel</a> です。</p> <h2 id="Compose-Navigation">Compose Navigation</h2> <p>これまでは <a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose を UI 構築そのものにフォーカスして使用してきたため、Activity や Fragment はそのまま残されていました。最近になってアプリの 7 割程度の UI が <a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose に置き換わってきたこともあり <a href="https://developer.android.com/jetpack/compose/navigation?hl=ja">Compose Navigation</a> を導入し始めました。</p> <p>Compose Navigation は実際に使っていくといくつか悩みどころが出てくるライブラリではないかと思うのですが、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座」 では以下のように解決しています。</p> <h3 id="遷移アニメーション">【遷移アニメーション】</h3> <p><a href="https://google.github.io/accompanist/navigation-animation/">Accompanist の Navigation Animation</a> を使用しています。最近リリースされた <a href="https://developer.android.com/jetpack/androidx/releases/navigation#2.7.0-alpha01">Navigation 2.7.0-alpha01</a> で遷移アニメーション機能が統合されることがアナウンスされたため、近々 Accompanist は不要になりそうです。</p> <h3 id="引数の型">【引数の型】</h3> <p>原則 <code>String</code> や <code>Int</code> といったプリミティブ型の値に制限しています。Serializable/Parcelable といった複雑なデータ構造を引数とすることは <a href="https://developer.android.com/guide/navigation/navigation-pass-data?hl=ja#supported_argument_types">公式ガイドでも非推奨</a>となっているためです。</p> <h3 id="型安全性">【型安全性】</h3> <p>公式ガイドを参考に型安全性を意識した遷移を実装しています。具体的には、<a href="https://developer.android.com/guide/navigation/navigation-type-safety?hl=ja#navigate-destination">各画面への遷移関数を <code>NavController</code> のエクステンションとして実装</a>し、<a href="https://developer.android.com/guide/navigation/navigation-type-safety?hl=ja#arguments-wrapper">引数はラッパーを作成してアクセスする</a>ようにしています。</p> <h2 id="Font-padding">Font padding</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Android">Android</a> では歴史的経緯からテキストの上下に Font padding と呼ばれる小さな空白が存在します。この Font padding は <a href="https://developer.android.com/jetpack/compose/text#includefontpadding_and_lineheight_apis">将来的にデフォルトで無効化されることがドキュメントに記されています</a>。フォントによって Font padding の影響は異なりそれほど気にならないこともあるのですが、デザイナーの作成した <a class="keyword" href="https://d.hatena.ne.jp/keyword/Figma">Figma</a> や <a class="keyword" href="https://d.hatena.ne.jp/keyword/iOS">iOS</a> と見た目をできるだけ一致させるために Font padding を無効化することにしました。</p> <p>具体的には以下のように Font padding を無効化した <code>TextStyle</code> を用意し、これをアプリ内のすべてのテキストの <code>TextStyle</code> のベースとしています。</p> <pre class="code lang-kotlin" data-lang="kotlin" data-unlink>TextStyle( fontFamily = fontFamily, lineHeight = <span class="synConstant">1</span>.em, lineHeightStyle = LineHeightStyle( alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None ), platformStyle = PlatformTextStyle( includeFontPadding = <span class="synConstant">false</span> <span class="synComment">// &lt;-- Disable font padding</span> ) ) </pre> <p>Font padding について詳しくはこちらの資料が大変参考になります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmedium.com%2Fandroiddevelopers%2Ffixing-font-padding-in-compose-text-768cd232425b" title="Fixing Font Padding in Compose Text" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://medium.com/androiddevelopers/fixing-font-padding-in-compose-text-768cd232425b">medium.com</a></cite></p> <h1 id="まとめ">まとめ</h1> <p>「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座」 における最近の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose 関連の改善をご紹介しました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose は頻繁にアップデートが行われています。新しい機能はより効率的で使いやすくなっており、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>をスムーズに進めることができます。「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリ中学講座」 では生産性を最大限に引き出すために、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Jetpack">Jetpack</a> Compose のアップデートを継続的に取り入れていきたいと思います。</p> maxfieldwalker 新しいチームメンバーとしてオンボーディング体制について語りたい hatenablog://entry/820878482935485751 2023-06-12T08:00:00+09:00 2023-06-12T08:00:13+09:00 はじめまして、小中高決済基盤開発グループの @tacumai です。 ぼくは2023年4月にスタディサプリの開発組織にジョインした、いわゆる新規参画者です。ちょうどこのブログを書いてる時期が参画から1ヶ月ほど経ったタイミングなので、初心をこのブログに残したいと思います。 この記事で一番伝えたいことは アプリケーション開発経験が薄くとも、技術的な側面でもドメイン知識の側面でも手厚くサポートしてくれる土壌がここにはあります。安心してうちにきてください!! です。 以下、なぜぼくがそう思ったのかをまとめました。 そもそもスタディサプリに入った経緯 ぼくは2023年3月まで、ホットペッパーグルメ等の開… <p>はじめまして、小中高決済基盤開発グループの @tacumai です。<br/> ぼくは2023年4月に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発組織にジョインした、いわゆる新規参画者です。ちょうどこのブログを書いてる時期が参画から1ヶ月ほど経ったタイミングなので、初心をこのブログに残したいと思います。</p> <p>この記事で一番伝えたいことは <em>アプリケーション開発経験が薄くとも、技術的な側面でも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知識の側面でも手厚くサポートしてくれる土壌がここにはあります。安心してうちにきてください!!</em> です。</p> <p>以下、なぜぼくがそう思ったのかをまとめました。</p> <h2 id="そもそもスタディサプリに入った経緯">そもそも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに入った経緯</h2> <p>ぼくは2023年3月まで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DB%A5%C3%A5%C8%A5%DA%A5%C3%A5%D1%A1%BC">ホットペッパー</a>グルメ等の開発・運用をしている飲食事業領域にて、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>インフラに関する業務を担っていましたが、この度4月から<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに異動しました。<br/> というのも、これまで<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/GCP">GCP</a>といった<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>インフラを使って<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>の様々なプロダクトの設計・開発・運用に携わらせてもらったのですが、ソフトウェアエンジニアとしてアプリケーションを開発する能力が乏しいと感じていたからです。<br/> さらに将来的には教育課題を解決するサービス作りにいずれ取り組みたいと考えていたため、当時の上長に相談したところ紹介をもらい部署異動を決意しました。</p> <h2 id="入ってみて感動したチームのあたりまえ">入ってみて感動したチームの「あたりまえ」</h2> <p>まずぼくは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Ruby">Ruby</a>を使った開発経験は1年チョットで、アプリケーション開発をしてたと言っても4年くらい前の話で記憶もあやふやです。感覚的な部分は残りつつもほぼ初心者と言っても過言ではない状態でした(そして今もそうです)。 さらには決済についての専門知識もなく、「技術的にも領域的にも初心者」といった状態です。<br/> そんなスキルセットでこれから開発業務をやっていけるか不安で仕方なかったのですが、その心配は杞憂に終わりました。なぜならぼくの思っていた以上に、チームオンボーディングが手厚かったからです。理由はたくさんあるのですが、ここでは主な3つを紹介します。</p> <h3 id="1-質問しやすい雰囲気環境づくり">1. 質問しやすい雰囲気・環境づくり</h3> <p>新しいチームに入ってまもないころ、定例で既存メンバー間での業務に関するやりとりを、ぽかーんと聞くだけの時間ってありませんでしたか? <br/> まだ担当したことのない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>や機能について他メンバーが会話しているのを横で聞こうと、最初は頑張っても時間が経つにつれ分からない概念が多々出てきて「もう何もわからない...」になって聞くのを止めることって多いですよね!?<br/> 特にリモートワークだとそういった状況になりやすいのですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリの開発チームのいいところは「定例のあとでフォローする時間・会を設けてくれるところ」だと思います。</p> <p>「他のメンバーの時間を奪っちゃうのでは...」<br/> 「このレベルの質問していいのかな...」</p> <p>と色々考えるタイプのぼくはこのサポートにとても助けられています。 うちではこのような入りたてのメンバーに対して、メンバーが理解・納得するまで丁寧に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D3%A5%B8%A5%CD%A5%B9%A5%ED%A5%B8%A5%C3%A5%AF">ビジネスロジック</a>や仕様について解説してくれることも多く、日々勉強させてもらっています。</p> <h3 id="2-メンターや上長との1on1によるサポート">2. メンターや上長との1on1によるサポート</h3> <p>うちでは新規参画者に対して最初の2〜3週間ほど、メンターをつけてオンボーディングサポートをすることになっています。 この仕組みがまた良いのです。 毎日気になっていること・つまづいているところを聞いてくれますし、些細なことにも応じてくれます。</p> <p>例えば業務を行うにあたり、開発環境のセットアップ等に時間をとられることが最初のうちはあると思うのですが、一人でドツボにはまって時間を消費してしまった...といった体験も相談できてすぐに解決に運べたのはメンターのサポートのおかげだと思っています。</p> <p>さらには上長との1on1も設けられており、チーム文化の共有やシステムの歴史的背景なんかを聞ける場として活用しています。</p> <h3 id="3-積極的なペアプログラミングによるドメイン知識と技術知識の補強">3. 積極的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DA%A5%A2%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0">ペアプログラミング</a>による<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知識と技術知識の補強</h3> <p>いざ業務に入ることになると通常は「まずはやってみて」という形で業務を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%B5%A5%A4">アサイ</a>ンされることも多いかと思います。</p> <p>仕様や概念が複雑かつ範囲の広いシステムだと、どんなに小さくて簡単な変更作業でも、最初のうちは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>にストレスを受けますよね。</p> <p>この点でもうちは手厚くて、まだ携わったことがない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>や機能の改修・開発になると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>エキスパートな方が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DA%A5%A2%A5%D7%A5%ED">ペアプロ</a>を行なってくれます。 ここでも仕様を解説しながら、コードを読みつつ、作業を進める、といった形で疑問点を取り除きながら作業を進めます。</p> <p>以上が、ぼくが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに入って衝撃を受けたオンボーディングの体制でした。 ここまで丁寧にメンバーの立ち上がりを設計している組織はなかなかないんじゃないかと思えるほど洗練されていて毎日感動しています。</p> <p>この記事を読んで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに興味を持ってくれた方には、過去の新規参画メンバーが書いたブログも参考にしてください! 彼らは今となっては<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリで大活躍していてぼくも教えてもらってばかりです! - <a href="https://blog.studysapuri.jp/entry/2022/11/22/what-surprised-me">チームにジョインして驚いた3つのこと</a> - <a href="https://blog.studysapuri.jp/entry/2019/03/27/080000">Quipper、外から見るか?中から見るか?</a></p> <h2 id="最後に補足">最後に補足</h2> <p>このブログ記事の趣旨とはズレますが、うちの組織異動制度について少し紹介します。</p> <p>冒頭で触れましたが、ぼくは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C3%E6%C5%D3%BA%CE%CD%D1">中途採用</a>で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに入ったのではなく、社内異動で参画しました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>では事業領域間の異動・ロールの変更を支援する仕組みが充実しています。</p> <p>例えば、旅行事業領域でデザイナーをやっていたが、自分の関心と今後のキャリアを考え、転職事業領域のエンジニアになるため、開発組織に異動するといったことが可能です。</p> <p>ぼくの例では「基盤開発からアプリケーション開発に移って、プロダクトの上から下までを担えるソフトウェアエンジニアになりたいです」と意思表明することで、(基盤開発からアプリケーション開発という僅かなロールチェンジはありつつ)ソフトウェアエンジニアというロールは変えず、事業領域を飲食領域から教育領域に変更しました。</p> <p>もちろん組織やビジネスの状況に依存する部分もありますが、多くの事業領域で多くのロールの方々がいる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>ならではの、とても良い制度だと思います。</p> <p>そんな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%AF%A5%EB%A1%BC%A5%C8">リクルート</a>および<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%BF%A5%C7%A5%A3">スタディ</a>サプリに興味を持ってくださった方へ!<br/> We are hiring!!</p> <p><a href="https://brand.studysapuri.jp/career/">採用ページ</a></p> tacumai