Cybozu Inside Out | サイボウズエンジニアのブログ 2024-03-18T12:00:00+09:00 cybozuinsideout Hatena::Blog hatenablog://blog/8454420450104491317 サイボウズはtry! Swift Tokyo 2024 にスポンサー協賛します! hatenablog://entry/6801883189090935763 2024-03-18T12:00:00+09:00 2024-03-18T12:26:09+09:00 こんにちは! サイボウズ モバイルエンジニアの森嶋です。 3月22日(金)から、try! Swift Tokyo 2024が開催されます。 try! Swift Tokyoは、iOSアプリやmacOSアプリ、Swift on Server、SwiftWASMなど、Swiftに関する知識を共有するための国際カンファレンスで、実に5年ぶりの開催となります。 日頃からお世話になっている Swift関連技術のコミュニティをともに盛り上げるべく、サイボウズはtry! Swift Tokyo 2024に「GOLD」「STUDENT SCHOLARSHIP」スポンサーとして協賛します。 サイボウズでは、ki… <p class="p1">こんにちは!</p> <p class="p1">サイボウズ モバイルエンジニアの<a href="https://x.com/m_orishi" target="_blank">森嶋</a>です。</p> <p class="p2"> </p> <p class="p1">3月22日(金)から、<a href="https://tryswift.jp/" target="_blank">try! Swift Tokyo 2024</a>が開催されます。</p> <p>try! Swift Tokyoは、iOSアプリやmacOSアプリ、Swift on Server、SwiftWASMなど、Swiftに関する知識を共有するための国際カンファレンスで、実に5年ぶりの開催となります。</p> <p>日頃からお世話になっている Swift関連技術のコミュニティをともに盛り上げるべく、サイボウズはtry! Swift Tokyo 2024に<strong>「GOLD」「STUDENT SCHOLARSHIP」</strong>スポンサーとして協賛します。</p> <p> </p> <p>サイボウズでは、kintone や サイボウズ Office、Garoonなどのモバイルアプリをリリースしており、SwiftUI や Swift Concurrency、Swift Package によるMulti module構成やTCA の導入、Observationなどなど新しい技術の取り込みを積極的に行なっています。<br />try! Swift Tokyo 2024で最新の知見が共有されるのを楽しみにしています!</p> <p class="p2"> </p> <p class="p1">また、当日は会場でブース出展を行っております。</p> <p class="p1">弊社のデザイナーやモバイルエンジニア達がブースに立っておりますので是非お立ち寄りください。</p> <p class="p1">また、現地参加するサイボウズの社員は下記のTシャツを着用していますので、お気軽にお声がけください!</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240312/20240312134630.png" width="375" height="365" loading="lazy" title="" class="hatena-fotolife" itemprop="image" /></p> <p class="p1">こちらのシャツデザインに至る経緯などは、弊社デザイナーのnoteにまとめられておりますのでこちらも是非見てみてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Foyssi%2Fn%2Fn27e0a61f3db6" title="try! Swift Tokyo 2024 に参加するので、サイボウズTシャツをデザインしました|おごし" 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://note.com/oyssi/n/n27e0a61f3db6">note.com</a></cite></p> <p class="p2"> </p> <p class="p1">当日は様々な方と交流できることを心から楽しみにしております!</p> <p class="p1">一緒にtry! Swift Tokyo 2024を盛り上げていきましょう。</p> <p class="p2"> </p> <p class="p1"><strong>さいごに</strong></p> <p class="p1">サイボウズでは「チームワークあふれる社会を創る」という理念を実現するべく、iOS エンジニアを募集中です!</p> <p class="p1">詳しくは下記をご参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fios-engineer.html" title="iOSエンジニアキャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/career/ios-engineer.html">cybozu.co.jp</a></cite></p> yellow-sabotech 「あの人誰だっけ…?」をなくす!社内イベントでアイコン入り+大きめの名札のススメ hatenablog://entry/6801883189086769290 2024-03-13T08:00:00+09:00 2024-03-13T08:00:01+09:00 実際に作ったアイコン入り名札 こんにちは!hokatomoといいます。福岡拠点に所属で、開発系の社内イベントの運営や協賛を担当しています。 「開運冬まつり」という開発・運用に携わる人向けの社内イベントを開催した際、交流のきっかけとして大きめの名札を作ったところ大好評でした! 参考:開運冬まつりについての記事 blog.cybozu.io <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/y/yellow-sabotech/20240312/20240312075611.png" alt="&#x5B9F;&#x969B;&#x306B;&#x4F5C;&#x3063;&#x305F;&#x30A2;&#x30A4;&#x30B3;&#x30F3;&#x5165;&#x308A;&#x540D;&#x672D;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実際に作ったアイコン入り名札</figcaption></figure></p> <p>こんにちは!<a href="https://twitter.com/tomoko_and">hokatomo</a>といいます。福岡拠点に所属で、開発系の社内イベントの運営や協賛を担当しています。</p> <p>「開運冬まつり」という開発・運用に携わる人向けの社内イベントを開催した際、交流のきっかけとして大きめの名札を作ったところ大好評でした!</p> <p> 参考:開運冬まつりについての記事 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2024%2F03%2F06%2F000000" title="リモートワークが中心だからこそチームや職能を超えた交流を作るためにオフラインで大規模社内イベントを開催してみた - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2024/03/06/000000">blog.cybozu.io</a></cite></p> <ul class="table-of-contents"> <li><a href="#はがきサイズの名札を作った理由">はがきサイズの名札を作った理由</a></li> <li><a href="#作ってみたら予想以上に好評だった">作ってみたら予想以上に好評だった</a><ul> <li><a href="#参加者からの声">参加者からの声</a></li> </ul> </li> <li><a href="#次回やるならチャレンジしたいこと">次回やるならチャレンジしたいこと</a></li> <li><a href="#余談">余談</a><ul> <li><a href="#内製した理由">内製した理由</a></li> <li><a href="#必要なもの200人分想定">必要なもの(200人分想定)</a></li> <li><a href="#ハマったところ">ハマったところ</a><ul> <li><a href="#社内の複合機で印刷するためレーザープリンター用の用紙が必須だけど意外と売ってない">社内の複合機で印刷するため、レーザープリンター用の用紙が必須だけど意外と売ってない</a></li> <li><a href="#激安のネームホルダーを買ったらものすごい入れにくかった">激安のネームホルダーを買ったらものすごい入れにくかった</a></li> </ul> </li> </ul> </li> </ul> <h2 id="はがきサイズの名札を作った理由">はがきサイズの名札を作った理由</h2> <p>サイボウズでは、コミュニケーションの多くを自社サービスである kintone や Garoon というグループウェアの上で行っています。</p> <p>そこでは個性豊かなアイコン(顔写真やイラスト)と表示名(本名またはニックネーム)を設定する文化があり、ミーティングで一緒にならない社員同士だと「アイコンと表示名は知っているが実際の顔は見たことが無い……」ということも。</p> <p>そこで、「人がたくさん集まったときに顔と名前とグループウェア上のアイコン(顔写真、イラスト等)を一致させて話しかけやすくしたい!」というのが名札を作った一番の理由です。</p> <h2 id="作ってみたら予想以上に好評だった">作ってみたら予想以上に好評だった</h2> <p><figure class="figure-image figure-image-fotolife" title="実際に作ったGaroonという製品のPMメンバーの名札"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240307/20240307142421.png" alt="&#x5B9F;&#x969B;&#x306B;&#x4F5C;&#x3063;&#x305F;Garoon&#x3068;&#x3044;&#x3046;&#x88FD;&#x54C1;&#x306E;PM&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x306E;&#x540D;&#x672D;" width="1000" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実際に作ったGaroonという製品のPMメンバーの名札</figcaption></figure></p> <p>とても評判がよく、ふりかえりコメントでも嬉しい声をたくさんいただきました。</p> <p>ちなみに、サイボウズでは名前入りの社員証を身につけているものの、その名前の印字が小さくパッと読むことが難しいです。 大きなアイコンと文字の名札があることで「(社員証を見ながら)あの人の名前…(チラッ)」と見ることがなくなったと思います。</p> <p>カンファレンスではよくあるものですが、社内イベントでもネームカード作成ぜひおすすめです。はがきサイズ大でぜひ!費用対効果抜群のアイテムでした。</p> <h3 id="参加者からの声">参加者からの声</h3> <blockquote><p>アイコンと名前入りの名札すごくよかったです。(今後も使いたいなと思って机に飾っています) グループウェアでやり取りしたあの人だ!というのが、遠くからでもすぐわかり声がかけやすかったです。</p></blockquote> <p>というこんな嬉しいコメントや</p> <blockquote><p>アイコン(顔写真やイラスト)の名札があるのはとてもよかったです! 面識のない人も多いので、名札があることでコミュニケーションが取りやすかったです。</p></blockquote> <p>といった形で企画の狙い通りのコメントがいただけて何よりでした。</p> <h2 id="次回やるならチャレンジしたいこと">次回やるならチャレンジしたいこと</h2> <ul> <li>英語表記をしたい(母国語が日本語以外のメンバーもいるので)</li> <li>デコれるようなステッカーを準備したい</li> </ul> <p>などを考えています。</p> <h2 id="余談">余談</h2> <h3 id="内製した理由">内製した理由</h3> <p>費用を考えると印刷会社に頼んでも良かった…とは思ったものの、直前まで印刷データ(参加者情報)が変わる可能性があったので自分たちで印刷したかったので内製にしました。</p> <h3 id="必要なもの200人分想定">必要なもの(200人分想定)</h3> <ul> <li>参加者のアイコン画像と名前を一覧にしたcsvデータ</li> <li>はがきサイズのレーザープリンター用紙、もしくはA4四割でミシン目が入っているレーザープリンター用紙(200枚約2,000円)</li> <li>はがきサイズが入るネームホルダー(200個約6,400円)</li> <li>印刷用のデータを作るためのソフト</li> </ul> <h3 id="ハマったところ">ハマったところ</h3> <h4 id="社内の複合機で印刷するためレーザープリンター用の用紙が必須だけど意外と売ってない">社内の複合機で印刷するため、レーザープリンター用の用紙が必須だけど意外と売ってない</h4> <p>電気店で用紙を買うときに「(会計しようとしている用紙は)インクジェット用ですけど大丈夫ですか?」と聞かれてハッとしました。 インクジェットの用紙は結構あるんですが、レーザープリンターになると途端に減るんですよね。</p> <p>意外と、レーザープリンター用かつはがきサイズ大(もしくはA4四割)って少ないんです。 発注したものが取り寄せになってしまったり、物が少ない印象だったので早めに発注しておくのがおすすめです。</p> <h4 id="激安のネームホルダーを買ったらものすごい入れにくかった">激安のネームホルダーを買ったらものすごい入れにくかった</h4> <p>薄型で激安のものにしたらネームカードをホルダーにいれるのがとにかく大変でした……。 厚みがあるほうがいいのかもしれないけどそうすると価格が高めになってしまったり、ネームホルダーは回収しない想定だったのでコストの兼ね合い的には良かったのかな、とも思っています。</p> yellow-sabotech JaSST'24 Tokyoの登壇者紹介(第2弾) hatenablog://entry/6801883189087349622 2024-03-11T11:28:38+09:00 2024-03-11T11:28:38+09:00 こんにちは。サイボウズのQAエンジニアの佐々木です。 前回こちらの記事でJaSST'24 Tokyoの協賛と登壇についてお伝えいたしましたが、 ありがたいことにJaSST'24 Tokyoではサイボウズからたくさんの登壇者がいるため、2回に分けて記事を書かせていただきました。 下記にすべての登壇の開始時間、登壇者、タイトルなどをまとめましたので、よろしければご覧ください。 ここからは登壇者、登壇内容の紹介です。 D3-2)事例セッション Track04​ 日時 3/14(木)15:00-15:30(30分) タイトル スプリント内で試験を完了させるには?アジャイル・スクラム開発に参加したQAエ… <p>こんにちは。サイボウズのQAエンジニアの佐々木です。 前回<a href="https://blog.cybozu.io/entry/2024/02/22/173000">こちら</a>の記事でJaSST'24 Tokyoの協賛と登壇についてお伝えいたしましたが、 ありがたいことにJaSST'24 Tokyoではサイボウズからたくさんの登壇者がいるため、2回に分けて記事を書かせていただきました。</p> <p>下記にすべての登壇の開始時間、登壇者、タイトルなどをまとめましたので、よろしければご覧ください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240307/20240307185705.png" alt="3&#x6708;14&#x65E5;(&#x6728;)&#x3068;3&#x6708;15&#x65E5;(&#x91D1;)&#x306E;&#x30B5;&#x30A4;&#x30DC;&#x30A6;&#x30BA;&#x306E;&#x30DF;&#x30CB;&#x30BB;&#x30C3;&#x30B7;&#x30E7;&#x30F3;&#x306E;&#x30BF;&#x30A4;&#x30E0;&#x30C6;&#x30FC;&#x30D6;&#x30EB;" width="1200" height="676" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240307/20240307185550.png" alt="3&#x6708;14&#x65E5;(&#x6728;)&#x3068;3&#x6708;15&#x65E5;(&#x91D1;)&#x306E;&#x30B5;&#x30A4;&#x30DC;&#x30A6;&#x30BA;&#x793E;&#x54E1;&#x304C;&#x767B;&#x58C7;&#x3059;&#x308B;&#x30BB;&#x30C3;&#x30B7;&#x30E7;&#x30F3;&#x306E;&#x30BF;&#x30A4;&#x30E0;&#x30C6;&#x30FC;&#x30D6;&#x30EB;" width="1200" height="677" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ここからは登壇者、登壇内容の紹介です。</p> <h4 id="D3-2事例セッション-Track04">D3-2)事例セッション Track04​</h4> <h5 id="日時">日時</h5> <p>3/14(木)15:00-15:30(30分) </p> <h5 id="タイトル">タイトル</h5> <p> <a href="https://jasst.jp/symposium/jasst24tokyo/details.html#D3-2">スプリント内で試験を完了させるには?アジャイル・スクラム開発に参加したQAエンジニアの悩みと対策</a></p> <h5 id="登壇者">登壇者</h5> <p>渡邉 豊幹(サイボウズ)​</p> <h5 id="セッション内容">セッション内容</h5> <blockquote><p>私の所属するチームはアジャイル・スクラム開発を導入しています。 しかしスプリントレビューまでに試験を完了できておらず、品質について迅速にフィードバックできていない課題がありました。 そこで開発チームで課題に取り組み、スプリント内で試験を完了しフィードバックを行えるよう工夫しました。 本セッションでは、取り組みとその結果についてご紹介します。</p></blockquote> <h4 id="B4テクノロジーセッション-Track02">B4)テクノロジーセッション Track02</h4> <h5 id="日時-1">日時</h5> <p>3/14(木) 16:00-17:00 (60分) </p> <h5 id="タイトル-1">タイトル</h5> <p><a href="https://jasst.jp/symposium/jasst24tokyo/details.html#B4">三社三様のQAのカタチ</a></p> <h5 id="登壇者-1">登壇者</h5> <p>阿部 将太郎(東急 URBAN HACKS)<br/> 菊一 則久(東急 URBAN HACKS)<br/> 岸 哲史(テクバン) <br/> 斉藤 裕希(サイボウズ) <br/> 佐々木 千絵美(サイボウズ)<br/> 水上 貴士(テクバン)</p> <h5 id="セッション内容-1">セッション内容</h5> <blockquote><p>事業会社・製品開発会社・テストベンダーの3社によるパネルディスカッションです。各社ごとに事業方針が全く異なる中で、QAチームや開発チームへのリソースへの向き合い方に対して、共感すること共感できないことをぶつけ合い、現場の生の声をお伝えさせていただきます。そしてジョブとしてのQAの現状とあるべき姿との差を、私たちがどのように解決しているのか、各社ならではの活動をお伝えできればと思います。</p></blockquote> <h4 id="E6企画セッション事前申し込み制Track05">E6)企画セッション(事前申し込み制)Track05</h4> <h5 id="日時-2">日時</h5> <p>3/15(金) 10:00-12:00(120分)</p> <h5 id="タイトル-2">タイトル</h5> <p><a href="https://jasst.jp/symposium/jasst24tokyo/details.html#E6">「価値あるソフトウェア」の”価値”ってなぁに?</a></p> <h5 id="登壇者-2">登壇者</h5> <p>安達 賢二(HBA)<br/> 永田 敦(サイボウズ)<br/> 吉澤 智美(日本電気)</p> <h5 id="セッション内容-2">セッション内容</h5> <blockquote><p>バグが多い、少ない/システム化する目的/ユーザーストーリーなど、さまざまな価値が存在する中、われわれが向き合うべき「価値あるソフトウェア」の”価値”とはいったい何なのか? 例えば「顧客満足を最優先し、価値のあるソフトウェアを早く継続的に提供する」を重視するAgile開発においてプロジェクト関係者は”価値”をどのように認識し、その結果何が起きているのか?そして、理想的にはソフトウェアの”価値”をどのように認識して実務を行うのがよいのか? Agile開発事例を通じて一緒に考える場にしたいと思います。</p></blockquote> <h4 id="D7-2事例セッション-Track04">D7-2)事例セッション Track04</h4> <h5 id="日時-3">日時</h5> <p>3/15(金) 13:30-14:00(30分)</p> <h5 id="タイトル-3">タイトル</h5> <p><a href="https://jasst.jp/symposium/jasst24tokyo/details.html#D7-2">サイボウズのQAエンジニア育成</a></p> <h5 id="登壇者-3">登壇者</h5> <p>斉藤 裕希(サイボウズ)</p> <h5 id="セッション内容-3">セッション内容</h5> <blockquote><p>サイボウズに中途入社し、配属されたチームで実施いただいた(私が受けた)オンボーディングについてお話します。 オンボーディングの内容と、入社前に感じていた3つの課題がどのように改善されたか、自分の気持ちがどのように変わっていったか等、入社した側の目線から紹介します。</p></blockquote> <h3 id="終わりに">終わりに</h3> <p>JaSST'24 Tokyoが間近に迫ってまいりました。</p> <p>登壇メンバーそれぞれが、参加者の皆様に楽しんでいただける内容を考えて準備しております。</p> <p>当日お楽しみいただければ幸いです。</p> yellow-sabotech リモートワークが中心だからこそチームや職能を超えた交流を作るためにオフラインで大規模社内イベントを開催してみた hatenablog://entry/6801883189086261455 2024-03-06T00:00:00+09:00 2024-03-06T09:19:41+09:00 開運冬まつり開催の様子 こんにちは!サイボウズのhokatomoといいます。福岡拠点に所属しており、開発系の社内イベントの運営や協賛をしています。 2020年以降、急速にリモートワークが広がり、サイボウズではこれを機に東京から地方へ移住する人も少なくありませんでした。そして現在でも、サイボウズで開発・運用に関わる多くのメンバーは日本各地のさまざまな場所で働いています。強制の出社日はなく、日々の業務では常にオンライン上でチーム内のコミュニケーションを取っている状況です。 そんな中で、ふと「斜め(チーム外)のコミュニケーションがコロナ前より減った気がする…」「チームや職能を超えたコミュニケーション… <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/y/yellow-sabotech/20240301/20240301105423.png" alt="&#x958B;&#x904B;&#x51AC;&#x307E;&#x3064;&#x308A;&#x958B;&#x50AC;&#x306E;&#x69D8;&#x5B50;" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>開運冬まつり開催の様子</figcaption></figure></p> <p>こんにちは!サイボウズのhokatomoといいます。福岡拠点に所属しており、開発系の社内イベントの運営や協賛をしています。</p> <p>2020年以降、急速にリモートワークが広がり、サイボウズではこれを機に東京から地方へ移住する人も少なくありませんでした。そして現在でも、サイボウズで開発・運用に関わる多くのメンバーは日本各地のさまざまな場所で働いています。強制の出社日はなく、日々の業務では常にオンライン上でチーム内のコミュニケーションを取っている状況です。 そんな中で、ふと「斜め(チーム外)のコミュニケーションがコロナ前より減った気がする…」「チームや職能を超えたコミュニケーションがもっとあったほうがチームワーク向上につながるはず!」と思った3人が集まり、 サイボウズの開発・運用に関わるメンバー向けの社内イベント「開運<a href="#f-0cab066b" id="fn-0cab066b" name="fn-0cab066b" title="開運とはスピリチュアルな意味ではなく、開発本部・クラウド基盤本部・情報システム本部・New Business Divisionの4本部を指しています。もともと、開発本部・運用本部を略して開運と呼んでいましたが、組織改変の影響で「運用」の名前がなくなったものの、「開運」という名称は残っています">*1</a> 冬まつり」を実施しました。</p> <p>個別のコンテンツ紹介や大規模な社内イベントを運営する際の細かいTipsは別の記事で書けたらと思います。</p> <ul class="table-of-contents"> <li><a href="#このイベントの目的と概要">このイベントの目的と概要</a></li> <li><a href="#なぜイベントをやろうと思ったか">なぜイベントをやろうと思ったか</a><ul> <li><a href="#チーム外のコミュニケーションが減ってしまった気がするので偶発的なコミュニケーションを生み新たな視点を得たい">チーム外のコミュニケーションが減ってしまった気がするので、偶発的なコミュニケーションを生み新たな視点を得たい</a></li> <li><a href="#Gathering-Day乱立問題の解消">"Gathering Day"乱立問題の解消</a></li> </ul> </li> <li><a href="#有志ではじめたイベントから熱量が生まれた">有志ではじめたイベントから熱量が生まれた</a></li> <li><a href="#開催結果">開催結果</a><ul> <li><a href="#寄せられた感想">寄せられた感想</a><ul> <li><a href="#イベント全体について">イベント全体について</a></li> <li><a href="#各職能ごとのイベントについて">各職能ごとのイベントについて</a></li> </ul> </li> </ul> </li> <li><a href="#次回に向けて">次回に向けて</a></li> </ul> <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/y/yellow-sabotech/20240301/20240301101602.png" alt="&#x958B;&#x904B;&#x51AC;&#x307E;&#x3064;&#x308A;&#x306E;&#x30ED;&#x30B4;&#x753B;&#x50CF;" width="545" height="247" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>開運冬まつりのロゴ画像</figcaption></figure></p> <p>開運冬まつりでは</p> <ul> <li>チームや職能を超えてコミュニケーションを取り新たな視点・刺激を得ること</li> <li>各チームのチームビルディングイベントを合わせてやることで出張者の負担を減らす</li> </ul> <p>を目的とし、</p> <p>コンテンツは</p> <ul> <li>さまざまな職能やチーム、ノンジャンルのLTイベント</li> <li>懇親会</li> <li>OST(Open Space Technology)</li> <li>職能別のチームビルディングイベント</li> </ul> <p>を4日間にわたってサイボウズ東京オフィスにて開催しました。</p> <p>結果、現地参加140人以上が参加したイベントとなりました!</p> <h2 id="なぜイベントをやろうと思ったか">なぜイベントをやろうと思ったか</h2> <h3 id="チーム外のコミュニケーションが減ってしまった気がするので偶発的なコミュニケーションを生み新たな視点を得たい">チーム外のコミュニケーションが減ってしまった気がするので、偶発的なコミュニケーションを生み新たな視点を得たい</h3> <p>サイボウズでは場所問わずさまざまなところに同僚がいるので、チーム内のコミュニケーションはZoomなどで取っています。</p> <p>チーム外になると途端にそれが難しくなってしまったり、業務で絡むポイントがないと、全くコミュニケーションを取らなくなってしまう現状があります。 そうなると、いざ連絡したいというときにハードルが上がってしまったり、やっている取り組みに気付けなかったり……。</p> <p>横断的なイベントをやることで、職能やチームを超えて交流をしコミュニケーションや相談をしやすい土壌を作ったり、それぞれの取り組みを知り<strong>新たな視点やつながりを作りたい!</strong>と思ったのが目的の1つです。</p> <h3 id="Gathering-Day乱立問題の解消">&quot;Gathering Day&quot;乱立問題の解消</h3> <p>サイボウズには<a href="https://cybozu.backstage.cybozu.co.jp/n/n0c8c63e003ac">チームビルディングの制度</a>があり、出張費や開催費用をまかなえます。 その制度を使ってGathering Dayと称し、職能単位や開発のチーム単位で1日がかりのチームビルディングを半期に1回程度するのが盛んです。 サイボウズでは各地にメンバーがいるので、場所の都合を考えるとほぼ東京オフィスでの開催となるのですが、複数のチームを兼務していたり、地方在住だと何度も出張をすることになるメンバーもいます。</p> <p>なので、「職能やチームビルディングのイベント+横断的なイベントをやることで移動の負担、日程調整のコストを削減」を目的その2としました。</p> <h2 id="有志ではじめたイベントから熱量が生まれた">有志ではじめたイベントから熱量が生まれた</h2> <p>このイベントの運営はモバイルエンジニアやフルスタックエンジニア、そして開発系のイベントの運営や協賛をしてる私、といった属性の全く異なる3人が集まり、 ちなみにそれぞれ東京・名古屋・福岡と離れているので、オンラインで半年間ほど準備を進めてきました。</p> <p>トップダウンでなにか依頼があったわけでもなく、頼まれたわけでもなく「もっとつながりを増やしたほうがいい!」から始まったイベント。 これを業務としてやることや「業務として参加してほしい」とマネージャーが後押ししてくれたり、周りの人たちに想いが伝わってさらなる熱量が生めたと実感しています。</p> <p>端的に言うと勝手に始めたことなのに、<strong>"やっていき"や"のっていき"精神で応援してくれる文化で良かった!</strong>と改めて感じました。</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/y/yellow-sabotech/20240305/20240305084540.png" alt="" width="1200" height="630" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>イベント開催中の様子</figcaption></figure></p> <ul> <li>現地参加人数:140人以上(全国各地から参加)</li> <li>フィードバック:190件ほどの登録</li> <li>派生イベントの開催:LTの再演、OSTを読み返す会が開催されたり、このイベントきっかけで動き出す施策などもあった</li> </ul> <p>という結果になりました。</p> <h3 id="寄せられた感想">寄せられた感想</h3> <h4 id="イベント全体について">イベント全体について</h4> <blockquote><p>開運冬まつり、最高でした! 個人的に各チームの取り組みや問題意識への理解や社内の新しい取り組みのキャッチアップといったところに課題を感じていたので、開運冬まつりはとてもよい機会となりました。 オフラインで集まってコミュニケーションをとる機会がなかなかないので、四半期に一回ぐらいのペースでこのイベントを開催してもいいのではないかと思います。</p></blockquote> <h4 id="各職能ごとのイベントについて">各職能ごとのイベントについて</h4> <blockquote><p>これまで職能単位などでバラバラにコストをかけて開催されていた Gathering イベントが開運冬まつりでまとめて開催され、さらに職能を超えたコラボレーションまでプラスされたので、とても高コスパなイベントだと思いました!</p></blockquote> <p>など、チームを超えたコミュニケーションを取ったことで新たな視点を得ることに価値を感じてくれたり、職能を超えた良さも感じてくれた声が多数ありました。</p> <h2 id="次回に向けて">次回に向けて</h2> <p>反省点は山ほどあるのですが、まだまだできることや生み出せる価値がたくさんあるので夏も開催したい!と考えています。</p> <p>それぞれのコンテンツや仕掛けなどの紹介はまた別の記事で紹介していきます。 読んでいただきありがとうございました。</p> <p>もし、似たような取り組みをやってるよ〜!とか、そういった業務をやっているよー!という方、お話できたら嬉しいです!! <a href="https://twitter.com/tomoko_and">X</a>などでご連絡ください。</p> <div class="footnote"> <p class="footnote"><a href="#fn-0cab066b" id="f-0cab066b" name="f-0cab066b" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">開運とはスピリチュアルな意味ではなく、開発本部・クラウド基盤本部・情報システム本部・New Business Divisionの4本部を指しています。もともと、開発本部・運用本部を略して開運と呼んでいましたが、組織改変の影響で「運用」の名前がなくなったものの、「開運」という名称は残っています</span></p> </div> yellow-sabotech スクラム開発に初めて飛び込んでみた見習い QA の話 hatenablog://entry/6801883189082857172 2024-03-05T11:14:43+09:00 2024-03-05T11:14:43+09:00 スクラム開発に初めて飛び込んでみた見習い QA の話 こんにちは!kintone フロントエンド刷新プロジェクト(通称フロリア)の Mira チーム にて QA エンジニアをしているはば丸と申します。 この記事では他部署から異動して初めてスクラム開発に参加した見習い QA が体験したことや感じたことを紹介したいと思います。 サイボウズのフロントエンド開発に興味がある方に、チームの雰囲気が伝われば幸いです。 目次 スクラム開発に初めて飛び込んでみた見習い QA の話 スクラム開発って何? カンバン方式な部署から異動して感じたこと 新人に優しいタスクピックアップ制度 QA がスクラムチームに参加す… <h1 id="スクラム開発に初めて飛び込んでみた見習い-QA-の話">スクラム開発に初めて飛び込んでみた見習い QA の話</h1> <p>こんにちは!kintone フロントエンド刷新プロジェクト(通称<a href="https://blog.cybozu.io/entry/2022/02/04/171154">フロリア</a>)の <a href="https://blog.cybozu.io/entry/2022/04/14/110000">Mira チーム</a> にて QA エンジニアをしているはば丸と申します。</p> <p>この記事では他部署から異動して初めてスクラム開発に参加した見習い QA が体験したことや感じたことを紹介したいと思います。<br/> サイボウズのフロントエンド開発に興味がある方に、チームの雰囲気が伝われば幸いです。</p> <hr /> <p>目次</p> <ul class="table-of-contents"> <li><a href="#スクラム開発に初めて飛び込んでみた見習い-QA-の話">スクラム開発に初めて飛び込んでみた見習い QA の話</a><ul> <li><a href="#スクラム開発って何">スクラム開発って何?</a></li> <li><a href="#カンバン方式な部署から異動して感じたこと">カンバン方式な部署から異動して感じたこと</a></li> <li><a href="#新人に優しいタスクピックアップ制度">新人に優しいタスクピックアップ制度</a></li> <li><a href="#QA-がスクラムチームに参加すると良いこと">QA がスクラムチームに参加すると良いこと</a></li> <li><a href="#終わりに">終わりに</a></li> </ul> </li> </ul> <hr /> <h2 id="スクラム開発って何">スクラム開発って何?</h2> <p><a href="https://scrumguides.org/docs/scrumguide/v2020/2020-Scrum-Guide-Japanese.pdf">スクラム公式ガイド</a> には、</p> <blockquote><p>スクラムとは、複雑な問題に対応する適応型のソリューションを通じて、⼈々、チーム、組織が価値を⽣み出すための軽量級フレームワークである。</p></blockquote> <p>と定義されています。おぉ、なんだか凄そうです。</p> <blockquote><p>簡単に⾔えば、スクラムとは次の環境を促進するためにスクラムマスターを必要とするものである。</p> <ol> <li>プロダクトオーナーは、複雑な問題に対応するための作業をプロダクトバックログに並べる。</li> <li>スクラムチームは、スプリントで選択した作業を価値のインクリメントに変える。</li> <li>スクラムチームとステークホルダーは、結果を検査して、次のスプリントに向けて調整する。</li> <li>繰り返す</li> </ol> </blockquote> <p>いくつかスクラム用語が出てきたのでまとめてみます。公式ガイド内には各用語の定義が詳しく記載されています。また <a href="https://blog.cybozu.io/entry/2023/07/26/170000#%E5%BD%B9%E5%89%B2%E5%88%86%E6%8B%85">弊社シニアスクラムマスターの記事</a> も参考になりますので合わせて参照ください。</p> <p>スクラムチームには2人の重要人物がいます。チームの方向性を決める<strong>プロダクトオーナー</strong>と、チームを健全に保つ<strong>スクラムマスター</strong>です。</p> <p>また<strong>スプリント</strong>とは開発工程における期間の単位であり、計画・開発・日次見直し・レビュー・調整を含んだ一定期間の区切りです。1ヶ月以下に設定されることが推奨され Mira チームでは2週間にしています。</p> <p>1 スプリントは次のような流れで進みます。</p> <ol> <li>プロダクトオーナーが目標達成に向けたタスクを並べる(プランニング)</li> <li>それぞれのメンバーにタスクを割り振り取り組む</li> <li>目標を達成出来たか全員で確認(レビュー)して調整し、次のスプリントに進む</li> </ol> <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/y/yellow-sabotech/20240213/20240213152059.png" alt="&#x7C21;&#x7565;&#x5316;&#x3057;&#x305F;&#x30B9;&#x30D7;&#x30EA;&#x30F3;&#x30C8;&#x306E;&#x6D41;&#x308C;&#xFF08;&#x30B9;&#x30D7;&#x30EA;&#x30F3;&#x30C8;&#x30D7;&#x30E9;&#x30F3;&#x30CB;&#x30F3;&#x30B0;&rarr;&#x958B;&#x767A;&rarr;&#x30B9;&#x30D7;&#x30EA;&#x30F3;&#x30C8;&#x30EC;&#x30D3;&#x30E5;&#x30FC;&#xFF09;" width="906" height="396" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>簡略化したスプリントの流れ</figcaption></figure></p> <h2 id="カンバン方式な部署から異動して感じたこと">カンバン方式な部署から異動して感じたこと</h2> <p>私は元々別部署であるインフラチームにいたのですが、そこでは基本的にカンバン方式で仕事を進めてきました。例えば機材故障に対処するための入れ替え作業や他チームからの依頼対応など、積まれたタスクを期限内にいかに速やかに終わらせるかを考えていました。</p> <p>スクラム開発ではスプリントレビューで目標を達成出来たかが明確になります。うまくいかなかったスプリントでは、何が起きたかチームで振り返り、NextAction の設定が行われます。例えば、「技術的に不確定要素が多いタスクをスプリントに投入してしまい、予想以上に時間を取られてスプリントの目標が達成できなかった」といった振り返りがあった場合、次回から「不確定要素が大きいタスクに関しては時間を決めてスパイク(事前調査)として扱う」といった Action が設定されます。<br/> このように自分達でスプリントの目標を決め頻繁に振り返り改善していくスピード感にとても驚きました。</p> <p>またスプリント毎に少人数でのミーティングが開かれるので新人の私でも発言しやすい雰囲気があり、参加してからあっという間にチームに溶け込むことが出来ました。</p> <h2 id="新人に優しいタスクピックアップ制度">新人に優しいタスクピックアップ制度</h2> <p>Mira チームは <a href="https://blog.cybozu.io/entry/2022/06/08/141944">スウォーミング</a> を取り入れています。これは優先度の高いタスクを割り当てられたメンバーがキャプテンとなって、何か困ったことがあったらチームメンバーを招集できる権利を持つ仕組みで「タスクピックアップ制度」と呼んで実践しています。</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/y/yellow-sabotech/20240213/20240213152501.png" alt="&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x53D7;&#x3051;&#x5165;&#x308C;&#x6E96;&#x5099;&#x30BF;&#x30B9;&#x30AF;&#x62C5;&#x5F53;&#x8005;&#x306B;&#x30AD;&#x30E3;&#x30D7;&#x30C6;&#x30F3;&#x304C;&#x6E21;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;&#x69D8;&#x5B50;" width="451" height="110" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>メンバー受け入れ準備タスク担当者にキャプテンが渡されている様子</figcaption></figure></p> <p>このやり方は短期的には開発速度向上に結び付かないかもしれません。しかしチームを健全に発展させ将来のチーム開発力を向上させるための良い投資になっているようです。<br/> 実際に新しいメンバーがこの制度を活用し、熟練エンジニアの協力を得てタスクを進めていく様子を何度も目撃しました。</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/y/yellow-sabotech/20240213/20240213152545.png" alt="&#x300C;&#x518D;&#x3073;&#x30BF;&#x30B9;&#x30AF;&#x30D4;&#x30C3;&#x30AF;&#x30A2;&#x30C3;&#x30D7;&#x5236;&#x5EA6;&#x306B;&#x6075;&#x307E;&#x308C;&#x305F;&#x300D;&#x3068;&#x3044;&#x3046;&#x6700;&#x8FD1;&#x30B8;&#x30E7;&#x30A4;&#x30F3;&#x3057;&#x305F;&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x3055;&#x3093;&#x306E;&#x58F0;" width="453" height="342" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>タスクピックアップ制度を活用した新メンバーの声</figcaption></figure></p> <h2 id="QA-がスクラムチームに参加すると良いこと">QA がスクラムチームに参加すると良いこと</h2> <p>部署によっては QA がスクラムチームに入らず、開発担当と品質保証担当が明確に分けられているところもあるようです。取り組む課題の種類によって最適なチーム構成は変わっていくので一概には言えませんが、Mira チームにおいては QA がスクラムに参加して良かったことがありました。<br/> それは、スプリントの目標やそれを決めた背景を把握しながら試験を進められることです。</p> <p>スプリントの目標はその年度の全体目標や他チームの進捗の兼ね合いなどを考慮して、メンバーと話し合いながら最終的にプロダクトオーナーが決定します。開発と品質保証が分離しているとなぜチームがその目標を掲げていて、なぜ各タスクの試験を言われた期限までに完了させる必要があるのかが分からなくなります。<br/> 前提とされていた背景状況が変化しタスクの優先度が変わっていても、柔軟に試験作業の分担を変えることは難しいでしょう。</p> <p>それに対して Mira では日々のミーティングに QA が深く参加するので、背景や進捗状況などを常に把握しながら作業に取り組みます。優先すべきタスクがどれかは自然と決まりますし、状況が変わればその都度担当者と話し合って仕事の進め方を決められます。</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/y/yellow-sabotech/20240213/20240213152654.png" alt="&#x76EE;&#x6A19;&#x9054;&#x6210;&#x3092;&#x559C;&#x3076;&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x305F;&#x3061;&#x306E;&#x30C1;&#x30E3;&#x30C3;&#x30C8;&#x753B;&#x9762;" width="577" height="367" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>目標達成を喜ぶメンバーたち</figcaption></figure></p> <h2 id="終わりに">終わりに</h2> <p>最後まで読んで頂きありがとう御座いました。今回は初めてスクラム開発に参加した際に体験したことや感じたことを紹介してみました。<br/> スクラム開発に関する更に詳しい情報はスクラムカテゴリの他の記事にも記載されていますので是非ご覧ください(私も絶賛勉強中です)。</p> <p><strong>サイボウズでは QA エンジニアを募集しています。</strong> 皆様のエントリーをお待ちしております!<br/> <a href="https://cybozu.co.jp/recruit/entry/career/qa-engineer.html">QA エンジニアキャリア採用 募集要項 | 採用情報 | サイボウズ株式会社</a></p> yellow-sabotech サイボウズは JaSST'24 Tokyo で協賛&登壇します! hatenablog://entry/6801883189084693568 2024-02-22T17:30:00+09:00 2024-02-22T17:30:00+09:00 こんにちは、QAエンジニアの久保田です。 サイボウズは 2024年3月14日(木)〜15日(金)に開催されるJaSST'24 Tokyoに、ゴールドスポンサーとして協賛します。 また、スポンサーセッションによる登壇とミニセッションがありますので、本記事ではその紹介をさせてください。 サイボウズでは「チームワークあふれる社会を創る」という企業理念のもと、kintoneやGaroon、サイボウズ Office、メールワイズなどチームを支えるサービスを開発しています。 その中でサイボウズのQAエンジニアは開発プロセス全体を通して品質を高める活動を行なっています。 そんな私たちが日々の活動でお世話にな… <p>こんにちは、QAエンジニアの久保田です。</p> <p>サイボウズは 2024年3月14日(木)〜15日(金)に開催される<a href="https://jasst.jp/symposium/jasst24tokyo.html">JaSST'24 Tokyo</a>に、ゴールドスポンサーとして協賛します。 また、スポンサーセッションによる登壇とミニセッションがありますので、本記事ではその紹介をさせてください。</p> <p>サイボウズでは「チームワークあふれる社会を創る」という企業理念のもと、kintoneやGaroon、サイボウズ Office、メールワイズなどチームを支えるサービスを開発しています。 その中でサイボウズのQAエンジニアは開発プロセス全体を通して品質を高める活動を行なっています。 そんな私たちが日々の活動でお世話になっているソフトウェアテストのコミュニティに、少しでも貢献できればと考えています。</p> <h2 id="テクノロジーセッションで登壇します">テクノロジーセッションで登壇します!</h2> <p>テクノロジーセッションにて「<a href="https://www.jasst.jp/symposium/jasst24tokyo/details.html#D2-1">既存プロセスからの脱却と変化に適応するために必要なこと</a>」というタイトルで登壇します。</p> <h5 id="日時">日時</h5> <p>3月14日(木) 13:00-13:30</p> <h5 id="登壇者">登壇者</h5> <p>とうま</p> <h5 id="内容紹介">内容紹介</h5> <p>kintone開発チームではLeSS (Lerge-Scale Scrum) を適用して、複数のチームで開発しています。 大規模なほど変化は起こりにくくなるもので、長らく既存のQAプロセスを踏襲する傾向が続いていました。 しかし、ここ数年でQAプロセスを大きく改善し、変化に柔軟に適応しやすくなりました。 改善を促進させるために行った取り組みと、その結果どのような変化が起こったかを紹介します。</p> <h2 id="ミニセッションも行います">ミニセッションも行います!</h2> <p>今回の JaSST'24 Tokyo では、各セッションの合間に行われるミニセッションがあります。 サイボウズも下記のセッションを予定しています。</p> <p>※ミニセッションの日時・内容は一部変更になる可能性があります</p> <h3 id="サイボウズQAの紹介">サイボウズQAの紹介</h3> <h5 id="日時-1">日時</h5> <p>3月14日(木)14:00~14:10<br/> 3月15日(金)14:00~14:10<br/> (1回目と2回目は同じ内容の配信になります。)</p> <h5 id="登壇者-1">登壇者</h5> <p>斉藤 裕希</p> <h5 id="内容紹介-1">内容紹介</h5> <p>サイボウズのQAエンジニアの組織体制、業務、サイボウズQAのユニークなところについてお話しします。 また、社内の品質保証に関するコミュニティ活動の「ミネルヴァ」についてもご紹介します。</p> <h3 id="販売管理オペレーターが開発チームの一員となった話">販売管理オペレーターが開発チームの一員となった話</h3> <h5 id="日時-2">日時</h5> <p>3月14日(木)14:15~14:25<br/> 3月15日(金)14:15~14:25<br/> (1回目と2回目は同じ内容の配信になります。)</p> <h5 id="登壇者-2">登壇者</h5> <p>浜田 じゅり</p> <h5 id="内容紹介-2">内容紹介</h5> <p>営業アシスタントが開発チームの一員となったきっかけ、業務内容、その影響などをお話しします。 同じ社内でも感じた文化・雰囲気の違いなどもお話しできればと思います。</p> <h3 id="試験仕様書の英語化をやってみたら試験仕様書の本質が見えてきた">試験仕様書の英語化をやってみたら試験仕様書の本質が見えてきた</h3> <h5 id="日時-3">日時</h5> <p>3月14日(木)15:30~15:40<br/> 3月15日(金)15:30~15:40<br/> (1回目と2回目は同じ内容の配信になります。)</p> <h5 id="登壇者-3">登壇者</h5> <p>臼井 新</p> <h5 id="内容紹介-3">内容紹介</h5> <p>Garoonチームでは日本と海外のチームメンバー誰もが読みやすく理解しやすい試験仕様書を目指して、試験仕様書の英語化と英文の添削を進めています。 今回のセッションでは、この活動を通じて見えてきた「試験仕様書で何をどのように伝えるべきか」についてお話しします。</p> <h3 id="主体的な活動で巨大な影響範囲のテストを乗りこなしていく話">主体的な活動で巨大な影響範囲のテストを乗りこなしていく話</h3> <h5 id="日時-4">日時</h5> <p>3月14日(木)15:45~15:55<br/> 3月15日(金)15:45~15:55<br/> (1回目と2回目は同じ内容の配信になります。)</p> <h5 id="登壇者-4">登壇者</h5> <p>福元 春輝</p> <h5 id="内容紹介-4">内容紹介</h5> <p>私が所属するチームでは、Garoonを新インフラへ移行しています。 Garoon全体が影響範囲となるため、QAのタスクではGaroonの機能だけでなく、Garoonを取り巻く他サービスとの関連を把握する必要があります。 全体像の把握やテスト設計漏れを防ぐために、どのような取り組みを行ったのかを紹介します。</p> <h2 id="終わりに">終わりに</h2> <p>テクノロジーセッション、ミニセッションともにDiscordで質問や感想を受け付けておりますので、皆さまのコメントお待ちしています。 興味のある方はぜひご参加ください。 JaSST'24 Tokyo で皆様と交流できることを楽しみにしています!</p> <p>また、私たちは「チームワークあふれる社会を創る」という理念のもと、日々チームワークを支えるソフトウェアの開発に取り組んでおり、現在一緒に働くQAエンジニアの仲間を募集しています。</p> <p>少しでもご興味を持っていただいた方は下記採用ページをご覧ください。</p> <p><a href="https://cybozu.co.jp/recruit/entry/career/qa-engineer.html">QA&#x30A8;&#x30F3;&#x30B8;&#x30CB;&#x30A2;&#x30AD;&#x30E3;&#x30EA;&#x30A2;&#x63A1;&#x7528; &#x52DF;&#x96C6;&#x8981;&#x9805; | &#x63A1;&#x7528;&#x60C5;&#x5831; | &#x30B5;&#x30A4;&#x30DC;&#x30A6;&#x30BA;&#x682A;&#x5F0F;&#x4F1A;&#x793E;</a></p> yellow-sabotech kintone モバイルチームのチームビルディング開催レポート hatenablog://entry/6801883189081071310 2024-02-22T17:00:00+09:00 2024-02-22T17:00:04+09:00 こんにちは!kintoneのAndroidエンジニア、トニオ(@tonionagauzzi)です。 今回は、先日サイボウズ東京オフィスで開催された「kintoneモバイルチームのチームビルディング(以後、チムビル)」の開催レポートを公開します! 私たちについて kintoneモバイルチームは、サイボウズのプロダクトであるkintoneのAndroid版とiOS版を開発しているチームです。 AndroidとiOSでチームは分かれていますが、一部のスクラムイベントは合同で行なっています。 開催の背景 サイボウズには、対面でのチームビルディング活動に対して補助を行う対面型チームビルディング支援制度(… <p>こんにちは!kintoneのAndroidエンジニア、トニオ(<a href="https://twitter.com/tonionagauzzi">@tonionagauzzi</a>)です。<br /> 今回は、先日サイボウズ東京オフィスで開催された「kintoneモバイルチームのチームビルディング(以後、チムビル)」の開催レポートを公開します!</p> <h2 id="私たちについて">私たちについて</h2> <p>kintoneモバイルチームは、サイボウズのプロダクトであるkintoneのAndroid版とiOS版を開発しているチームです。<br /> AndroidとiOSでチームは分かれていますが、一部のスクラムイベントは合同で行なっています。</p> <h2 id="開催の背景">開催の背景</h2> <p>サイボウズには、対面でのチームビルディング活動に対して補助を行う<strong>対面型チームビルディング支援制度</strong>(チムビル制度)があり、全社的にチムビルという文化が普及しています。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.backstage.cybozu.co.jp%2Fn%2Fn0c8c63e003ac" 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://cybozu.backstage.cybozu.co.jp/n/n0c8c63e003ac">cybozu.backstage.cybozu.co.jp</a></cite></p> <p>チムビルにはさまざまな目的や効果がありますが、私たちが必要としていたのは、<strong>お互いをよく知ること</strong>でした。<br /> 参加者11名のうち4名が2022年以降に入社した新しいメンバーでした。また、3名が2023年になってから他のプロダクトからの異動もしくは他のプロダクトとの兼務となったメンバーでした。さらにPMも、前年の後半に待望のモバイル専任PMとして就任したばかりでした。</p> <p><figure class="figure-image figure-image-fotolife" title="kintoneモバイルのメンバー構成"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240206/20240206182554.png" alt="kintone&#x30E2;&#x30D0;&#x30A4;&#x30EB;&#x306E;&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x69CB;&#x6210;" width="1200" height="790" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>kintoneモバイルのメンバー構成</figcaption></figure></p> <p>そのため、まだお互いのことをよく知らない状態で仕事をしており、以下のような意見があがりました。</p> <ul> <li>まだ対面で話したことがない人がいて、会話する際に話しにくさを感じている</li> <li>暗黙知が少なく、意思決定をするときに細かい確認や認識のすり合わせが都度必要になっている</li> <li>大勢のミーティングの中で発言しない人がいて、何を考えているのか知りたい</li> </ul> <p>このような経緯から、まずはお互いのことをよく知ることが開発プロセス改善において重要だと捉え、一部の有志がチムビルを企画しました。</p> <p>開発メンバーは全国に点在していますが、この2日間は東京オフィスに集まりました。</p> <h2 id="イベント概要">イベント概要</h2> <ul> <li>開催日:2023年11月14日(火)、15日(水)</li> <li>場所:サイボウズ東京オフィス</li> <li>参加者:11名 <ul> <li>内訳 <ul> <li>プロダクトマネージャー1名</li> <li>モバイルエンジニア6名</li> <li>QAエンジニア3名</li> <li>デザイナー1名</li> </ul> </li> </ul> </li> </ul> <h2 id="プログラム">プログラム</h2> <h3 id="1日目">1日目</h3> <ul> <li>アイスブレイク1:共通点探し</li> <li>アイスブレイク2:ウソ・ホントゲーム</li> <li>マシュマロタワー</li> <li>懇親会</li> </ul> <h3 id="2日目">2日目</h3> <ul> <li>わがままカード</li> <li>Open Space Technology(OST)</li> </ul> <h2 id="各プログラムのダイジェスト">各プログラムのダイジェスト</h2> <h3 id="アイスブレイク1-共通点探し">アイスブレイク1: 共通点探し</h3> <p>共通点探しとは、2人ペアになって共通点を見つけるアイスブレイクです。</p> <p>参考:<a href="https://teambuilding-seminar.com/getsamepoint/">https://teambuilding-seminar.com/getsamepoint/</a></p> <p>2人1組となって紙とペンを1つずつ持ち、制限時間(10分)以内に<strong>2人の間で共通すること</strong>をできるだけ見つけて書き出します。<br /> 今回は参加者が11人と奇数だったので、3人の組もありました。</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/y/yellow-sabotech/20240217/20240217225038.jpg" alt="&#x5171;&#x901A;&#x70B9;&#x63A2;&#x3057;&#x306E;&#x767A;&#x8868;&#x306E;&#x69D8;&#x5B50;" width="1200" height="900" 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/y/yellow-sabotech/20240217/20240217225118.jpg" alt="&#x3068;&#x3042;&#x308B;&#x30C1;&#x30FC;&#x30E0;&#x306E;&#x5171;&#x901A;&#x70B9;&#x63A2;&#x3057;&#x7D50;&#x679C;" width="900" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:480px" itemprop="image"></span><figcaption>とあるチームの共通点探し結果</figcaption></figure></p> <p>どのペアが一番多くの共通点を見つけるか、白熱しました。<br /> これを最初にやったので、自己紹介も兼ねていて盛り上がりました。</p> <h3 id="アイスブレイク2ウソホントゲーム">アイスブレイク2:ウソ・ホントゲーム</h3> <p>参考:<a href="https://ikusa.jp/202002037681">https://ikusa.jp/202002037681</a></p> <p>ウソ・ホントゲームは、自己紹介に1つだけ嘘を盛り込み、他の人がそれを当てるアイスブレイクです。</p> <p>参加者全員が紙とペンを持ち、自分のことを4つ書きます。<strong>4つのうちの1つは嘘</strong>です。<br /> 書き終えたら順番を決め、1人目が発表して、他の参加者はどれが嘘なのかを話し合って当てます。<br /> これを人数分繰り返します。</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/y/yellow-sabotech/20240217/20240217225241.jpg" alt="&#x30A6;&#x30BD;&#x30FB;&#x30DB;&#x30F3;&#x30C8;&#x30B2;&#x30FC;&#x30E0;&#x306E;&#x30EB;&#x30FC;&#x30EB;&#x8AAC;&#x660E;&#x306E;&#x69D8;&#x5B50;" width="984" height="738" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p>嘘か本当かわかりにくいものも仕込まれていて、それぞれの人の意外な一面を知ることができ、盛り上がりました。</p> <h3 id="マシュマロタワー">マシュマロタワー</h3> <p>以下のものだけを使い、<strong>一番高くマシュマロを立てたチームが勝つ</strong>ゲームです。</p> <ul> <li>マシュマロ:1つ</li> <li>乾燥パスタ:20本(1.7mm)</li> <li>ひも:90cm</li> <li>マスキングテープ:90cm</li> <li>はさみ:1つ</li> </ul> <p>3〜4人1チームで、制限時間は作戦タイムも含めて18分間です。</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/y/yellow-sabotech/20240217/20240217225315.jpg" alt="&#x30DE;&#x30B7;&#x30E5;&#x30DE;&#x30ED;&#x30BF;&#x30EF;&#x30FC;&#x5B9F;&#x8DF5;&#x306E;&#x69D8;&#x5B50;" width="984" height="738" 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/y/yellow-sabotech/20240217/20240217225400.jpg" alt="&#x4ECA;&#x56DE;&#x3044;&#x3061;&#x3070;&#x3093;&#x9AD8;&#x304B;&#x3063;&#x305F;&#x30DE;&#x30B7;&#x30E5;&#x30DE;&#x30ED;&#x30BF;&#x30EF;&#x30FC;&#x306F;&uarr;&#x3053;&#x308C;&#x3060;&#xFF01;" width="675" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span><figcaption></figcaption>今回いちばん高かったマシュマロタワーは↑これだ!</figure></p> <center>最高記録は<span style="font-size: 200%">58cm</span>でした!</center> <p><figure class="figure-image figure-image-fotolife" title="1位になったチーム"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240222/20240222100226.jpg" alt="1&#x4F4D;&#x306B;&#x306A;&#x3063;&#x305F;&#x30C1;&#x30FC;&#x30E0;" width="675" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span><figcaption>1位になったチーム</figcaption></figure></p> <p>このチームは1回目の組み立てではマシュマロを立てることができませんでした。<br /> ふりかえって再設計し、</p> <ul> <li>ひもでパスタを反らせるアイデアを加えた</li> <li>当初狙っていた高さよりも少し下げた</li> </ul> <p>といった試みを経て見事成功しました👏<br /> どのチームも、スクラムの検査と適応を重ねるような進め方をしていたのが印象的でした。</p> <p>ちなみに、世界記録は99cmだそうです。</p> <h3 id="わがままカード">わがままカード</h3> <p>参考:<a href="https://shop.cybozu.co.jp/products/4769252638799">https://shop.cybozu.co.jp/products/4769252638799</a></p> <p>わがままカードは、<a href="https://teamwork.cybozu.co.jp/">サイボウズチームワーク総研</a>が作った<strong>価値観を考えさせられるカードゲーム</strong>です。</p> <p>遊び方は以下です。</p> <ol> <li>カードを5枚ずつ配る</li> <li>残ったカードは山札にする</li> <li>順番を決める</li> <li>順番が来た人は、山札からカード1枚引く</li> <li>引いたカードを含め、自分の価値観からいちばん遠いカードを表向きに捨てる</li> <li>4〜5を、1人5回ずつ繰り返す</li> <li>手元に残った5枚のカードを参加者に見せる</li> </ol> <p>それが終わったら感想戦です。</p> <ul> <li>自分が捨てた価値観、残した5つの価値観の理由を順番に言う</li> <li>他の人が捨てた価値観、残した5つの価値観の理由を質問する</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/y/yellow-sabotech/20240217/20240217225520.jpg" alt="&#x308F;&#x304C;&#x307E;&#x307E;&#x30AB;&#x30FC;&#x30C9;&#x5B9F;&#x8DF5;&#x306E;&#x69D8;&#x5B50;" width="1200" height="675" 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/y/yellow-sabotech/20240217/20240217225524.jpg" alt="&#x305D;&#x308C;&#x305E;&#x308C;&#x304C;&#x6368;&#x3066;&#x305F;&#x3001;&#x81EA;&#x5206;&#x306E;&#x4FA1;&#x5024;&#x89B3;&#x304B;&#x3089;&#x9060;&#x3044;&#x30AB;&#x30FC;&#x30C9;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>それぞれが捨てた、自分の価値観から遠いカード</figcaption></figure></p> <p>それぞれの働く価値観や大切にしているものを知るきっかけになりました。<br /> どれも捨てられないけど…と言いながらカードを選んでいた人も多く、各自の考えを整理するきっかけにもなったのではないかと思います。</p> <h3 id="Open-Space-TechnologyOST">Open Space Technology(OST)</h3> <p>参考:<a href="https://schoo.jp/biz/column/1709">https://schoo.jp/biz/column/1709</a></p> <p>オープン・スペース・テクノロジーとは、参加者が議論や追求したいことを自らの意思で提案し、そのテーマに興味がある人たちが集まって意見を出し合うワークショップです。<br /> 世界各国で導入されており、さまざまな問題に対し成果をあげています。</p> <p>以下がOSTの4つの原則です。</p> <ol> <li>ここにやってきた人は、誰もが適任者である</li> <li>何が起ころうと、それが起こるべき唯一のことである</li> <li>いつ始まろうと、始まった時が適切な時である</li> <li>いつ終わろうと、終わった時が終わりの時なのである</li> </ol> <p>最初に、<strong>テーマ出し</strong>を15分間行います。話し合いたいことを紙に書き、書けた人から参加者の前で発表します。<br /> 発表し終えたら、どのターンにどの部屋で話したいかを宣言します。</p> <p><figure class="figure-image figure-image-fotolife" title="OSTにエントリーされたテーマ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225651.jpg" alt="OST&#x306B;&#x30A8;&#x30F3;&#x30C8;&#x30EA;&#x30FC;&#x3055;&#x308C;&#x305F;&#x30C6;&#x30FC;&#x30DE;" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>OSTにエントリーされたテーマ</figcaption></figure></p> <p>テーマ出しが終わったら、<strong>部屋を分かれます</strong>。<br /> それぞれテーマを出した人がホストとなります。セッションの内容を大きな紙かホワイトボードに書きながら、<strong>ホストが中心となって話し合いを進めます</strong>。</p> <p><figure class="figure-image figure-image-fotolife" title="テスト駆動開発について話したOSTセッション"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225726.jpg" alt="&#x30C6;&#x30B9;&#x30C8;&#x99C6;&#x52D5;&#x958B;&#x767A;&#x306B;&#x3064;&#x3044;&#x3066;&#x8A71;&#x3057;&#x305F;OST&#x30BB;&#x30C3;&#x30B7;&#x30E7;&#x30F3;" width="1200" height="675" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p>セッションに興味がない、もしくは貢献できないと感じたら、<strong>他のセッションに自由に移動してもかまいません</strong>。</p> <p>セッションの成果物は後で参加者全員に共有します。</p> <p>今回のOSTでは、プロダクトや開発手法のことからモチベーションやコミュニケーションのこと、四国に新幹線を通すにはどうすればよいかといった個性的な話し合いも行われました。<br /> ここで話し合ったことは数ヶ月後にも脳裏に残っていて、「そういえばチムビルのOSTで話したけど…」みたいに普段の会話でも活躍しています。</p> <h2 id="感想">感想</h2> <p>チムビルを終えて、以下の感想が寄せられました。</p> <ul> <li>オフラインで開催できたので席が近い人とじっくり話せてよかった</li> <li>ウソ・ホントゲームが盛り上がって面白かった。またやりたい</li> <li>マシュマロタワーは1回目失敗して、2回目で良くなったりチームによって違いが出てて面白かった</li> <li>QAさんなどとの距離が近づいて、振り返りなどでお互い遠慮が減り、仕事がしやすくなった</li> <li>次はライターやローカライズの人とやってみたい</li> <li>次はインセプションデッキとか難易度が高いワークをやってみたい</li> </ul> <h2 id="チームの変化">チームの変化</h2> <p>チムビル後の心境の変化を参加者にアンケートしました。</p> <p>得られた回答を平均して10点満点のスコアにし、それをグラフ化しました。</p> <p><figure class="figure-image figure-image-fotolife" title="Q1. 私は、強みを活かした役割を担い、弱みは補完してもらっている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225832.png" alt="Q1. &#x79C1;&#x306F;&#x3001;&#x5F37;&#x307F;&#x3092;&#x6D3B;&#x304B;&#x3057;&#x305F;&#x5F79;&#x5272;&#x3092;&#x62C5;&#x3044;&#x3001;&#x5F31;&#x307F;&#x306F;&#x88DC;&#x5B8C;&#x3057;&#x3066;&#x3082;&#x3089;&#x3063;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q2. 私は、チームが目指す理想についてよく話をしている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225836.png" alt="Q2. &#x79C1;&#x306F;&#x3001;&#x30C1;&#x30FC;&#x30E0;&#x304C;&#x76EE;&#x6307;&#x3059;&#x7406;&#x60F3;&#x306B;&#x3064;&#x3044;&#x3066;&#x3088;&#x304F;&#x8A71;&#x3092;&#x3057;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q3. .私は、メンバーに安心して質問したり考えを伝えたりできている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225841.png" alt="Q3. .&#x79C1;&#x306F;&#x3001;&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x306B;&#x5B89;&#x5FC3;&#x3057;&#x3066;&#x8CEA;&#x554F;&#x3057;&#x305F;&#x308A;&#x8003;&#x3048;&#x3092;&#x4F1D;&#x3048;&#x305F;&#x308A;&#x3067;&#x304D;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q4. 私は、メンバーに気軽に声をかけることができている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225846.png" alt="Q4. &#x79C1;&#x306F;&#x3001;&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x306B;&#x6C17;&#x8EFD;&#x306B;&#x58F0;&#x3092;&#x304B;&#x3051;&#x308B;&#x3053;&#x3068;&#x304C;&#x3067;&#x304D;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q5. 私は、ミスや失敗も共有することができている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225852.png" alt="Q5. &#x79C1;&#x306F;&#x3001;&#x30DF;&#x30B9;&#x3084;&#x5931;&#x6557;&#x3082;&#x5171;&#x6709;&#x3059;&#x308B;&#x3053;&#x3068;&#x304C;&#x3067;&#x304D;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q6. 私は、ノウハウを属人化せず組織として蓄積するようにしている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225857.png" alt="Q6. &#x79C1;&#x306F;&#x3001;&#x30CE;&#x30A6;&#x30CF;&#x30A6;&#x3092;&#x5C5E;&#x4EBA;&#x5316;&#x305B;&#x305A;&#x7D44;&#x7E54;&#x3068;&#x3057;&#x3066;&#x84C4;&#x7A4D;&#x3059;&#x308B;&#x3088;&#x3046;&#x306B;&#x3057;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q7. 私は、自分が必要だと思う情報は、速やかに充分に得られている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225902.png" alt="Q7. &#x79C1;&#x306F;&#x3001;&#x81EA;&#x5206;&#x304C;&#x5FC5;&#x8981;&#x3060;&#x3068;&#x601D;&#x3046;&#x60C5;&#x5831;&#x306F;&#x3001;&#x901F;&#x3084;&#x304B;&#x306B;&#x5145;&#x5206;&#x306B;&#x5F97;&#x3089;&#x308C;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q8. 私は、チームの「やるべきこと」に取り組めている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225907.png" alt="Q8. &#x79C1;&#x306F;&#x3001;&#x30C1;&#x30FC;&#x30E0;&#x306E;&#x300C;&#x3084;&#x308B;&#x3079;&#x304D;&#x3053;&#x3068;&#x300D;&#x306B;&#x53D6;&#x308A;&#x7D44;&#x3081;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q9. 私は、チームの業務を通して「できること」を増やせている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225911.png" alt="Q9. &#x79C1;&#x306F;&#x3001;&#x30C1;&#x30FC;&#x30E0;&#x306E;&#x696D;&#x52D9;&#x3092;&#x901A;&#x3057;&#x3066;&#x300C;&#x3067;&#x304D;&#x308B;&#x3053;&#x3068;&#x300D;&#x3092;&#x5897;&#x3084;&#x305B;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Q10. 私は、チーム内で自分の「やりたいこと」に取り組めている"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217225916.png" alt="Q10. &#x79C1;&#x306F;&#x3001;&#x30C1;&#x30FC;&#x30E0;&#x5185;&#x3067;&#x81EA;&#x5206;&#x306E;&#x300C;&#x3084;&#x308A;&#x305F;&#x3044;&#x3053;&#x3068;&#x300D;&#x306B;&#x53D6;&#x308A;&#x7D44;&#x3081;&#x3066;&#x3044;&#x308B;" width="1200" height="724" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption></figcaption></figure></p> <p>この結果から、1回のチムビルであらゆる良い効果が生まれて仕事がしやすくなったことがわかりました。</p> <p>とくに、<strong>メンバーに安心して質問したり考えを伝えたりできている</strong>と<strong>メンバーに気軽に声をかけることができている</strong>のスコアが大きく伸びました。<br /> これらは元々他の設問よりも低めのスコアだったにもかかわらず、チムビル後は他の設問よりも高いスコアとなりました。<br /> チームが普段もっとも課題に感じていたところをチムビルで解決できたと言えるのではないかと思います。</p> <p>実際、お互いの考えや強み弱みがわかったことで、チームワークにおける助け合いが以前よりも活発になったと感じています。<br /> ちなみに、どの設問においてもチムビル後にスコアが下がった人はいませんでした。<br /></p> <p>感想で寄せられたように、次回は違ったテーマで2回目のチムビルを開催したいと考えています。<br /> 今後もよりよいプロダクトづくりを目指して学びと実践を続けていきます!</p> <h2 id="おわりに">おわりに</h2> <p>サイボウズでは、「チームワークあふれる社会を創る」という理念のもと、日々チームワークを支えるソフトウェアの開発に取り組んでおり、一緒に働く仲間を募集しています。<br /> 少しでもご興味を持っていただいた方は下記採用ページをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%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://cybozu.co.jp/recruit/entry/">cybozu.co.jp</a></cite></p> yellow-sabotech QA Gathering Day 2024年2月 の開催レポート 〜社内のQAエンジニアの横のつながりを強化するために〜 hatenablog://entry/6801883189083683314 2024-02-20T10:09:24+09:00 2024-02-20T10:09:24+09:00 こんにちは!QAエンジニアの諌山です。先日サイボウズ東京オフィスで開催された「QA Gathering Day 2024年2月」の開催レポートを公開します。「QA Gathering Day」は、サイボウズのQAエンジニア系の職能のメンバーが集まる社内イベントで、今回は2回目の開催となりました。 1回目の記事はこちらです。 blog.cybozu.io イベント概要 開催日:2024年2月9日(金) 場所 :サイボウズ東京オフィス 参加者:約50名(QAエンジニア、プロダクトセキュリティエンジニア、クラウド基盤エンジニア(SET)) 目的 ①QAエンジニア系の職能メンバーの横のつながりを強化す… <p>こんにちは!QAエンジニアの諌山です。先日サイボウズ東京オフィスで開催された「QA Gathering Day 2024年2月」の開催レポートを公開します。「QA Gathering Day」は、サイボウズのQAエンジニア系の職能のメンバーが集まる社内イベントで、今回は2回目の開催となりました。<br/> 1回目の記事はこちらです。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2023%2F08%2F07%2F080000" title="QA Gathering Day 2023 の開催レポート 〜社内のQAエンジニアの横のつながりを強化するために〜 - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2023/08/07/080000">blog.cybozu.io</a></cite></p> <h2 id="イベント概要">イベント概要</h2> <ul> <li>開催日:2024年2月9日(金)</li> <li>場所 :サイボウズ東京オフィス</li> <li>参加者:約50名(QAエンジニア、プロダクトセキュリティエンジニア、クラウド基盤エンジニア(SET))</li> </ul> <h2 id="目的">目的
</h2> <p>①QAエンジニア系の職能メンバーの横のつながりを強化する<br/> ②チームごとの取り組みや、困っていること・お悩みを共有し、他メンバーに気づきを与える</p> <h2 id="プログラム">プログラム</h2> <ul> <li>新メンバーの紹介
</li> <li>アイスブレイク
</li> <li>OST(オープン・スペース・テクノロジー)
</li> <li>ランチ会🍱</li> </ul> <h2 id="各プログラムのダイジェスト">各プログラムのダイジェスト</h2> <p>ここからは各プログラムの内容と当日の様子を簡単にご紹介します。</p> <h4 id="新メンバー紹介">新メンバー紹介</h4> <p>まずはじめに、1回目のQA Gathering Day以降に入社した方の自己紹介が行われました。私も自己紹介したのですが、とても緊張しました。 <figure class="figure-image figure-image-fotolife"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240219/20240219163456.jpg" alt="&#x81EA;&#x5DF1;&#x7D39;&#x4ECB;&#x306E;&#x69D8;&#x5B50;" width="250" height="187" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span></figure></p> <h4 id="アイスブレイク">アイスブレイク</h4> <p>続いて、イベントの場を盛り上げるため、アイスブレイクを行いました。時間制限内に以下のお題に沿って全員が一列に並ぶというゲームでした。</p> <ul> <li>誕生日</li> <li>入社年次</li> <li>東京オフィスまでの通勤(所要)時間</li> </ul> <p>ゲームを進めていくと、同じ誕生日の方がいたり、いつの間にか入社年次が古いことに気づいた方、東京オフィスまでの通勤(所要)時間が4時間程度かかる方など、いろいろな発見もあり、とても盛り上がりました。</p> <h4 id="OSTオープンスペーステクノロジー">OST(オープン・スペース・テクノロジー)</h4> <p>アイスブレイクできたところで、OSTが始まりました。OSTは、話したいテーマ・解決したい課題を参加者が持ち寄り、参加者同士で話し合う場です。<br/> 最初に話したいテーマ・課題を紙に書いて、ホワイトボードに貼っていったのですが、あっという間に話したいテーマを書いた紙で埋め尽くされました。</p> <p><figure class="figure-image figure-image-fotolife"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240220/20240220092534.jpg" alt="OST&#x3067;&#x4E0A;&#x304C;&#x3063;&#x305F;&#x30C6;&#x30FC;&#x30DE;&#x4E00;&#x89A7;" width="250" height="187" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span></figure></p> <p>上がったテーマの例:
<br/> 「自動テストにどう関わっているか」<br/> 「ドキュメント作成と管理どうやっていけばいいんだろう問題」<br/> 「キャリアの話がしたい」<br/> 「QA研究会やりませんか?」 <br/> 「サイボウズのおすすめ・好きなところ」<br/> 「健康自慢」<br/> 「マネージャーに物申す!」など</p> <p>QA業務に直接関係する話題からQA業務以外のことまで、幅広くテーマに上がりました。 この中で各自興味のあるテーマのグループに参加し、グループ内で議論しました。<br/> <figure class="figure-image figure-image-fotolife"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217213758.jpg" alt="OST&#x3067;&#x8B70;&#x8AD6;&#x3059;&#x308B;&#x69D8;&#x5B50;1" width="250" height="187" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span></figure> <figure class="figure-image figure-image-fotolife"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20240217/20240217213916.jpg" alt="OST&#x3067;&#x8B70;&#x8AD6;&#x3059;&#x308B;&#x69D8;&#x5B50;2" width="250" height="187" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span></figure> 参加者が自分の関心のある議題に参加することで、意見が活発に出され、新しいアイディアやNext Actionが生まれやすかったです。また、議論していく中で、普段関わることが少ない他のチームの方を知れたり、他チームでの困り事等も共有でき、横の繋がりが強化されました。</p> <h4 id="ランチ会">ランチ会</h4> <p>OSTの後は、所属チームが異なるなどできるだけ関わりの少ないメンバー同士の5人程度のグループを作り、親睦を深めるためのランチ会を行いました。私のグループでは趣味の話で盛り上がり、ヴァイオリンが弾ける人、ピアノが弾ける人、エレクトーンが弾ける人が揃っており、演奏会を開催できそうなグループでした。また、今まで関わったことがないと思っていたけど、実は仕事でお世話になっていた、という発見もあり、ランチ会でもさらに横の繋がりができました。</p> <h2 id="感想">感想</h2> <p>私はキャリア入社で、今回初めてQA Gathering Day に参加したのですが、まず、OSTで出たテーマの数に驚きました。トップダウンの企業では、受け身になり、多くのテーマを出すことや議論することが難しいのではないかと思います。サイボウズでは<a href="https://cybozu.co.jp/recruit/about/philosophy/">「自立と議論」の文化/Culture</a>に則って、一人一人が主体的に動き、議論することを大切にしています。今回のOSTでも、活発な意見によって新しいアイディアを見つけたり、主体的に動くことでモチベーションが高まりました。また、お互いの意見を尊重し、楽しみながら横の繋がりを広げることができました。</p> <h2 id="最後に">最後に</h2> <p>サイボウズではQAエンジニア系職能のメンバーを募集しています。興味のある方はサイボウズの採用情報をご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fqa-engineer.html" title="QAエンジニアキャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/career/qa-engineer.html">cybozu.co.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fset.html" title="クラウド基盤エンジニア(SET)キャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/career/set.html">cybozu.co.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fnewgrad%2Fsecurity-engineer2025.html" title="プロダクトセキュリティエンジニア職 2025年卒 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/newgrad/security-engineer2025.html">cybozu.co.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fnewgrad%2Fqa-engineer2025.html" title="QAエンジニア(品質保証エンジニア)職 2025年卒 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/newgrad/qa-engineer2025.html">cybozu.co.jp</a></cite></p> yellow-sabotech PHPカンファレンス関西2024に協賛しました & PHPerKaigi 2024に協賛します! hatenablog://entry/6801883189083451129 2024-02-15T18:44:39+09:00 2024-02-15T18:44:39+09:00 お疲れ様です、Garoon開発チーム所属の酒井(@sakay_y)です。 先日開催されたPHPカンファレンス関西2024の協賛報告と、来月開催されるPHPerKaigi 2024の宣伝をさせてください。 PHPカンファレンス関西2024 2024.kphpug.jp ブーススポンサーとして協賛しました! サイボウズブースも完成!#phpkansai pic.twitter.com/LkBMtxP82y— SAKAI Yasuharu (@sakay_y) 2024年2月11日 ブースではノベルティを配らせてもらったり、アンケートに協力していただいたり、色んな人と交流することができました。 昨日… <p>お疲れ様です、Garoon開発チーム所属の酒井(<a href="https://x.com/sakay_y">@sakay_y</a>)です。 先日開催されたPHPカンファレンス関西2024の協賛報告と、来月開催されるPHPerKaigi 2024の宣伝をさせてください。</p> <h2 id="PHPカンファレンス関西2024">PHPカンファレンス関西2024</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2024.kphpug.jp%2F" title="PHPカンファレンス関西2024" 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://2024.kphpug.jp/">2024.kphpug.jp</a></cite></p> <p>ブーススポンサーとして協賛しました!</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">サイボウズブースも完成!<a href="https://twitter.com/hashtag/phpkansai?src=hash&amp;ref_src=twsrc%5Etfw">#phpkansai</a> <a href="https://t.co/LkBMtxP82y">pic.twitter.com/LkBMtxP82y</a></p>&mdash; SAKAI Yasuharu (@sakay_y) <a href="https://twitter.com/sakay_y/status/1756491995437310085?ref_src=twsrc%5Etfw">2024年2月11日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>ブースではノベルティを配らせてもらったり、アンケートに協力していただいたり、色んな人と交流することができました。</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">昨日サイボウズブースで実施したアンケートの結果です! <a href="https://twitter.com/hashtag/phpkansai?src=hash&amp;ref_src=twsrc%5Etfw">#phpkansai</a> <a href="https://t.co/XgUryjJjep">pic.twitter.com/XgUryjJjep</a></p>&mdash; SAKAI Yasuharu (@sakay_y) <a href="https://twitter.com/sakay_y/status/1756846400250560994?ref_src=twsrc%5Etfw">2024年2月12日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>個人的には、意外と週5で出社されている方が多いなと思いました。これが東京や別の地方だとどうなるのかなというのは気になりますね。</p> <p>あとは、サイボウズから2名(私も採択していただけました!)登壇しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffortee.jp%2Fphpcon-kansai2024%2Fproposal%2F65343fcd-ae18-4378-a32a-6577e546be52" title="はじめてのOSSコントリビュート" 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://fortee.jp/phpcon-kansai2024/proposal/65343fcd-ae18-4378-a32a-6577e546be52">fortee.jp</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffortee.jp%2Fphpcon-kansai2024%2Fproposal%2F30494f3c-b928-43b8-a617-f8c4a6992e08" title="20年の歴史を持つプロダクトの開発チームの変革:技術広報の本質と効果" 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://fortee.jp/phpcon-kansai2024/proposal/30494f3c-b928-43b8-a617-f8c4a6992e08">fortee.jp</a></cite></p> <h2 id="PHPerKaigi-2024">PHPerKaigi 2024</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fphperkaigi.jp%2F2024%2F" title="PHPerKaigi 2024" 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://phperkaigi.jp/2024/">phperkaigi.jp</a></cite></p> <p>ゴールドプラン+スポンサーセッションで協賛いたします!</p> <p>スポンサーセッションはこちら!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffortee.jp%2Fphperkaigi-2024%2Fproposal%2F89879dc7-5e21-4cf6-b5bf-a629dc61279d" title="帰ってきた「完成度低いの歓迎LT大会」(PHPerKaigi出張版)" 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://fortee.jp/phperkaigi-2024/proposal/89879dc7-5e21-4cf6-b5bf-a629dc61279d">fortee.jp</a></cite></p> <p>昨年も同様の企画をさせてもらいましたが、今年もやります!</p> <p>GaroonチームのメンバーからLT登壇もあります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffortee.jp%2Fphperkaigi-2024%2Fproposal%2Fccdbcea0-f8a6-4114-b29b-93d14fc33572" title="OSSの脆弱性との向き合い方" 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://fortee.jp/phperkaigi-2024/proposal/ccdbcea0-f8a6-4114-b29b-93d14fc33572">fortee.jp</a></cite></p> <p>PHPerKaigiでも私達Garoon開発チームと交流してくださると嬉しいです。<br/> よろしくお願いします!</p> <p>#KANSEIDOHIKUI</p> cybozuinsideout 自作SSDについてキオクシアの方に伺いました hatenablog://entry/820878482962360398 2024-01-11T16:06:04+09:00 2024-01-11T16:06:04+09:00 こんにちは。サイボウズ・ラボの内田( @uchan_nos )です。 SSD の自作という活動について、キオクシア株式会社の社員(元キオクシアを含む)にお話を伺う機会がありましたので、ご紹介します。 インタビューの様子は、電子の森ラジオ(電子工作とプログラミング系ポッドキャスト)にて配信しています。 エピソード 019 自作SSD 打ち合わせの様子 普段の収録と異なり、今回は企業からオフィシャルな立場で収録に来てくださるということで、事前に打ち合わせを行いました。 収録に向けた打ち合わせの様子 弊社の東京オフィスにある会議室での打ち合わせの様子です。 写真の左から順に、内田(サイボウズ・ラボ)… <p>こんにちは。サイボウズ・ラボの内田( @uchan_nos )です。 SSD の自作という活動について、キオクシア株式会社の社員(元キオクシアを含む)にお話を伺う機会がありましたので、ご紹介します。</p> <p>インタビューの様子は、電子の森ラジオ(電子工作とプログラミング系ポッドキャスト)にて配信しています。 <a href="https://podcasters.spotify.com/pod/show/electronic-wood/episodes/019-SSD-e2e9shb">エピソード 019 自作SSD</a></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/u/uchan_nos/20230828/20230828112457.jpg" alt="&#x30E9;&#x30DC;&#x5185;&#x7530;&#x3001;&#x30AD;&#x30AA;&#x30AF;&#x30B7;&#x30A2;&#x306E;&#x65E5;&#x4E0B;&#x3055;&#x3093;&#x3068;&#x7C73;&#x6FA4;&#x3055;&#x3093;&#x304C;&#x30C6;&#x30FC;&#x30D6;&#x30EB;&#x3092;&#x56F2;&#x3093;&#x3067;&#x6253;&#x3061;&#x5408;&#x308F;&#x305B;&#x3066;&#x3044;&#x308B;" width="800" height="403" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>収録に向けた打ち合わせの様子</figcaption></figure></p> <p>弊社の東京オフィスにある会議室での打ち合わせの様子です。 写真の左から順に、内田(サイボウズ・ラボ)、日下様(キオクシア)、米澤様(キオクシア)です。</p> <p>事前の打ち合わせでは、主にどのような話をするかと、出してはいけない話題について確認しました。 企業の看板を背負って出演することの責任をひしひしと感じました。 キオクシアからは広報担当の方もいらしており、緊張しました。</p> <p><figure class="figure-image figure-image-fotolife" title="SSD やフラッシュメモリに詳しくなれそうなお土産"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/uchan_nos/20230828/20230828112432.jpg" alt="SSD &#x540C;&#x4EBA;&#x8A8C;&#x3084;&#x30D5;&#x30E9;&#x30C3;&#x30B7;&#x30E5;&#x30E1;&#x30E2;&#x30EA;&#x89E3;&#x8AAC;&#x6F2B;&#x753B;&#x3001;&#x30B7;&#x30EA;&#x30B3;&#x30F3;&#x30A6;&#x30A7;&#x30CF;&#x30FC;&#x3046;&#x3061;&#x308F;" width="800" height="521" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>SSD やフラッシュメモリに詳しくなれそうなお土産</figcaption></figure></p> <p>打ち合わせに際してお土産をいただきました。 紙の書籍やシリコンウェハーを模したうちわなど、とても豪華でした。 (うちわは本物のウェハーと同じ大きさなので、バックパックに入らず、まだ自席に置いてあります)</p> <p>SSD Doujinshi 1/2 のダウンロードは<a href="https://www.kioxia.com/ja-jp/business/ssd/solution/doujinshi.html">「SSD Doujinshi」 SSD同人誌のご紹介とダウンロードのご案内 | KIOXIA - Japan (日本語)</a> から可能です。</p> <h2 id="初代の自作-SSD-と自作-CPU">初代の自作 SSD と自作 CPU</h2> <p>打ち合わせでは、いくつかの自作 SSD の実機を見せていただきました。 これらは放送の中で登場しますので、放送と合わせてお楽しみください。</p> <p><figure class="figure-image figure-image-fotolife" title="初代の自作SSDモジュール"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/uchan_nos/20230828/20230828112437.jpg" alt="&#x30B3;&#x30F3;&#x30C8;&#x30ED;&#x30FC;&#x30E9;&#x57FA;&#x677F;&#x3068;NAND&#x578B;&#x30D5;&#x30E9;&#x30C3;&#x30B7;&#x30E5;&#x30E1;&#x30E2;&#x30EA;&#x3092;&#x8F09;&#x305B;&#x305F;&#x57FA;&#x677F;&#x304C;3&#x5C64;&#x306B;&#x30B9;&#x30BF;&#x30C3;&#x30AF;&#x3055;&#x308C;&#x305F;&#x81EA;&#x4F5C;SSD&#x30E2;&#x30B8;&#x30E5;&#x30FC;&#x30EB;" width="800" height="519" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>初代の自作SSDモジュール</figcaption></figure></p> <p>初代の自作 SSD は 3 層構造です。全体で SSD を構成します。 一番上にある青い基板が NAND 型フラッシュメモリを搭載した基板で、これ自体では SSD ではありません。 NAND 型フラッシュメモリにコントローラを加えることで、全体として SSD として振る舞います。</p> <p>下層の緑基板がコントローラの役割を果たすマイコン基板(Airio-Base)で、NAND 型フラッシュメモリを制御します。 不良ブロックへのアクセスを代替ブロックへ振り向ける仕組みなどを、このコントローラが実現します。</p> <p>中央の赤い基板は、そのマイコン基板と外部をつなぐインターフェース(I/F)回路です。 パソコン用の SSD であれば、インターフェースは SATA や PCIe(NVMe)などですが、この I/F 回路はアドレスバスとデータバスを提供するものになっています。</p> <p><figure class="figure-image figure-image-fotolife" title="自作SSDを活用するための自作CPU基板"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/uchan_nos/20230828/20230828112442.jpg" alt="&#x30ED;&#x30B8;&#x30C3;&#x30AF;IC&#x3092;&#x8907;&#x6570;&#x7D44;&#x307F;&#x5408;&#x308F;&#x305B;&#x305F;&#x81EA;&#x4F5C;CPU&#x57FA;&#x677F;" width="800" height="520" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>自作SSDを活用するための自作CPU基板</figcaption></figure></p> <p>これは、自作 SSD を記憶装置として使う「TD8」という名の自作 CPU です。 「CPU の創りかた」の TD4 を基にし、レジスタ幅を 8 ビットに拡張したものだそうです。</p> <p>TD4 では、CPU が実行するためのプログラムを DIP スイッチを並べた「ROM」に格納するのですが、TD8 では自作 SSD に保存するのです。 先ほどの赤い基板(I/F 回路)がアドレスバスとデータバスを提供することで、通常の ROM と同じインターフェースで自作 SSD にアクセス可能になっています。 SSD Doujinshi p.23あたりに説明があります。</p> <h2 id="3-代目の自作-SSD">3 代目の自作 SSD</h2> <p>SSD Doujinshi 2 p.31 で登場する 3 代目の SSD です。 初代と 2 代目は、マイコン基板の拡張ボードとして作られていましたが、3 代目は NAND 型フラッシュメモリとマイコンを同一基板に載せた構成になりました。 見た目を M.2 SSD っぽくしたところがコダワリだそうです。</p> <p><figure class="figure-image figure-image-fotolife" title="最新の自作SSDモジュール"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/uchan_nos/20230828/20230828112452.jpg" alt="&#x30B3;&#x30F3;&#x30C8;&#x30ED;&#x30FC;&#x30E9;IC&#x3068;NAND&#x578B;&#x30D5;&#x30E9;&#x30C3;&#x30B7;&#x30E5;&#x30E1;&#x30E2;&#x30EA;&#x3092;&#x540C;&#x4E00;&#x57FA;&#x677F;&#x306B;&#x8F09;&#x305B;&#x305F;M.2&#x578B;&#x81EA;&#x4F5C;SSD&#x30E2;&#x30B8;&#x30E5;&#x30FC;&#x30EB;" width="800" height="486" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>最新の自作SSDモジュール</figcaption></figure></p> <p>放送でも話していますが、一連の自作 SSD は SSD というよりも USB メモリと言う方が近いです。 パソコンの USB 端子に接続することで、USB マスストレージとして見える仕組みだからです。</p> <p>SSD と USB メモリは、どちらも NAND 型フラッシュメモリとコントローラを搭載したものです。 主な違いはパソコンとの接続方法で、SSD は SATA や PCIe(NVMe)など、USB メモリは USB で接続して SCSI プロトコルで読み書きする、というような違いがあります。 ただ、NAND 型フラッシュメモリにコントローラを付与したもの、という大枠の構造は同じであり、より興味を持ってもらうために「自作 SSD」と呼称しているということでした。</p> <p>確かに「自作 USB メモリ」というと、かっこいいガワを自作して、オリジナルな見た目の USB メモリを作るという解釈をしてしまいそうです。 その点「自作 SSD」は、見た目よりも中身に焦点が当たる名前だなあと感じました。</p> <h2 id="世界初の-NAND-型フラッシュメモリを利用した-SSD">世界初の NAND 型フラッシュメモリを利用した SSD</h2> <p>NAND 型フラッシュメモリは、キオクシアの前身である東芝によって 1987 年に発明されました。 その世界初の NAND 型フラッシュメモリを利用して作成された自作 SSD がこちらです。</p> <p><figure class="figure-image figure-image-fotolife" title="世界初のNAND型フラッシュメモリを載せた自作SSD"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/uchan_nos/20230828/20230828112447.jpg" alt="DIP&#x30D1;&#x30C3;&#x30B1;&#x30FC;&#x30B8;&#x306E;NAND&#x578B;&#x30D5;&#x30E9;&#x30C3;&#x30B7;&#x30E5;&#x30E1;&#x30E2;&#x30EA;IC&#x3092;&#x8F09;&#x305B;&#x305F;&#x57FA;&#x677F;&#x304C;Arduino&#x30DC;&#x30FC;&#x30C9;&#x306B;&#x633F;&#x3055;&#x3063;&#x3066;&#x3044;&#x308B;" width="800" height="530" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>世界初のNAND型フラッシュメモリを載せた自作SSD</figcaption></figure></p> <p>まさか、このフラッシュメモリくんは Arduino に接続されるなんて思ってなかったでしょう。 時代を超えて繋がる物語、感動です。</p> <h2 id="SSD-Doujinshi-の推しの章">SSD Doujinshi の推しの章</h2> <p>収録に参加されたお三方(日下様、米澤様、村口様)に、SSD Doujinshi 1/2 から推しの章を紹介いただきました。 詳しい紹介は放送を聞いていただくとして、ここで手短にご紹介します。</p> <ul> <li>米澤様:SSD Doujinshi 2 p.30「かんがえる SSD」 <ul> <li>推しポイント:p.30 左下のスクリーンショットに本人が登場しているところ。</li> </ul> </li> <li>日下様:SSD Doujinshi 2 p.44「ECC でデータの誤りを訂正してみた」 <ul> <li>推しポイント:読むだけで、自分でも誤り訂正できるじゃんと思わせてくれるところ。</li> </ul> </li> <li>村口様:OpenStreetMap の記事 <ul> <li>推しポイント:OpenStreetMap は個人でも入手できるビッグデータで、しかも地図なので有用性が高いところ。</li> </ul> </li> </ul> <h2 id="第2回-自作CPUを語る会について">第2回 自作CPUを語る会について</h2> <p>収録から公開まで大分時間が掛かってしまいました。 そのため、番組中で既に終了したイベント「第2回 自作CPUを語る会」についての告知があることをご了承ください。 2024 年 6 月頃に第 3 回を開催予定ですので、お楽しみに。</p> uchan_nos GitHub Actionsのcomposite actionを使ってinternalリポジトリのファイルを配布する hatenablog://entry/6801883189074138388 2024-01-11T00:00:00+09:00 2024-01-11T17:19:14+09:00 クラウド基盤本部Cloud Platform部の pddg です。この前までチームだったんですが部になったらしいです。 引き続き精力的に cybozu.com のインフラ基盤の移行に取り組んでいます。 今回はKubernetesマニフェストのバリデーションのための仕組みを検討していたときに発見した、GitHub Actionsのちょっとハックっぽい、もしかしたら便利かもしれない手法について紹介したいと思います。 TL; DR 背景 テナントごとに分散しているマニフェスト kubeconformによるバリデーション テナントもNecoが使っているスキーマ定義ファイルを使いたい! internal… <p>クラウド基盤本部Cloud Platform部の <a href="https://github.com/pddg">pddg</a> です。この前までチームだったんですが部になったらしいです。<br/> 引き続き精力的に cybozu.com のインフラ基盤の移行に取り組んでいます。</p> <p>今回はKubernetesマニフェストのバリデーションのための仕組みを検討していたときに発見した、GitHub Actionsのちょっとハックっぽい、もしかしたら便利かもしれない手法について紹介したいと思います。</p> <ul class="table-of-contents"> <li><a href="#TL-DR">TL; DR</a></li> <li><a href="#背景">背景</a><ul> <li><a href="#テナントごとに分散しているマニフェスト">テナントごとに分散しているマニフェスト</a></li> <li><a href="#kubeconformによるバリデーション">kubeconformによるバリデーション</a></li> <li><a href="#テナントもNecoが使っているスキーマ定義ファイルを使いたい">テナントもNecoが使っているスキーマ定義ファイルを使いたい!</a></li> </ul> </li> <li><a href="#internalリポジトリのclone">internalリポジトリのclone</a><ul> <li><a href="#トークンの作り方">トークンの作り方</a></li> </ul> </li> <li><a href="#GitHub-Actionsのカスタムアクション">GitHub Actionsのカスタムアクション</a><ul> <li><a href="#GITHUB_ACTION_PATH-には何が入っている">GITHUB_ACTION_PATH には何が入っている?</a></li> </ul> </li> <li><a href="#ファイルを配布するアクションを作る">ファイルを配布するアクションを作る</a><ul> <li><a href="#パラメータでバージョンを選べるようにする">パラメータでバージョンを選べるようにする</a></li> <li><a href="#副次的に得られた効果">副次的に得られた効果</a></li> <li><a href="#セキュリティに関する考慮">セキュリティに関する考慮</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="TL-DR">TL; DR</h2> <ul> <li>Internalリポジトリにcomposite actionを実装してorg/enterprise内に対して利用を許可すると、利用する側のワークフロー内ではそのInternalリポジトリがクローンされる</li> <li>composite actionにおいて指定箇所へファイルをコピーするようなジョブを実装することで、そのorg/enterprise内であればInternalリポジトリのファイルを特別な認証なしで配布できる</li> </ul> <h2 id="背景">背景</h2> <p>cybozu.com では新しいインフラ基盤としてKubernetesを採用し、<a href="https://blog.cybozu.io/entry/2019/09/05/090000">Necoプロジェクト</a>と称して精力的に整備を進めてきました。<a href="https://blog.cybozu.io/entry/ever-lasting-neco">Necoプロジェクト自体は完了</a>ということになりましたが、今もNecoチームによる開発・運用が精力的に行われています。現在は実際に複数のテナントチームが様々なサービスを展開し、運用するようになってきました。</p> <h3 id="テナントごとに分散しているマニフェスト">テナントごとに分散しているマニフェスト</h3> <p>Necoでは<a href="https://argoproj.github.io/cd/">ArgoCD</a>を採用しており、<a href="https://blog.cybozu.io/entry/2020/02/04/110000">テナントが自由にアプリケーションを展開できる仕組み</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/p/pudding_info/20240111/20240111100500.png" width="1200" height="629" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>マニフェストリポジトリの関係性</figcaption></figure></p> <p>このように、テナントチームのマニフェストはクラスタを管理するNecoチームのマニフェストリポジトリ(以降neco-apps<a href="#f-b542ff21" id="fn-b542ff21" name="fn-b542ff21" title="以前neco-appsリポジトリはpublicリポジトリでしたが、現在は管理上の都合によりprivateなorgのinternalなリポジトリになっています。">*1</a>)とは異なる独立したリポジトリで管理されています。よって、テナントチームから見たとき実際のクラスタのKubernetesのバージョンや適用されているカスタムコントローラのバージョンなどはNecoチームのリリースノートを追うことでしか把握できない状態になっていました。クラスタに実際に適用される前に実施したいCI上でのマニフェストのバリデーションにおいて、これらの情報を適切に把握しなければ正しいバリデーションが実施できないという問題が生じていました。</p> <h3 id="kubeconformによるバリデーション">kubeconformによるバリデーション</h3> <p>Kubernetesマニフェストのバリデーションができるツールの一つに<a href="https://github.com/yannh/kubeconform">kubeconform</a>というツールがあります。適切なスキーマ定義ファイルを用意することで、Kubernetesのカスタムリソースも含めてスキーマに沿ったパラメータが設定されているかを検証できます<a href="#f-64b7c335" id="fn-64b7c335" name="fn-64b7c335" title="[https://github.com/yannh/kubeconform?tab=readme-ov-file#CustomResourceDefinition-CRD-Support]">*2</a>。このスキーマ定義ファイルは以下のような方法で用意することが出来るようになっています。</p> <ul> <li>CRDs-catalog から取得する <ul> <li><a href="https://github.com/datreeio/CRDs-catalog">https://github.com/datreeio/CRDs-catalog</a> というリポジトリに有志によっていくつかのカスタムリソースについてスキーマ定義ファイルが収集されている</li> </ul> </li> <li>CRDからスキーマ定義ファイルを生成する <ul> <li>kubeconformが提供する<a href="https://github.com/yannh/kubeconform/blob/v0.6.4/scripts/openapi2jsonschema.py">openapi2jsonschema.py</a>を使って、CRDのyaml形式のマニフェストから、kubeconform用のスキーマ定義ファイルを生成できる</li> </ul> </li> </ul> <p>neco-appsには実際のk8sクラスタに適用されているCRDを後者の方法で変換したスキーマ定義ファイルがコミットされており、kubeconformを使ったマニフェストのバリデーションが出来るようになっています。</p> <h3 id="テナントもNecoが使っているスキーマ定義ファイルを使いたい">テナントもNecoが使っているスキーマ定義ファイルを使いたい!</h3> <p>各テナントチームがkubeconformによるバリデーションをCI上で行いたいと考えたとき、カスタムリソースのマニフェストが含まれていると以下のいずれかの方法をとることが考えられます。</p> <ul> <li>CRDs-catalog からスキーマ定義ファイルを取得して利用する <ul> <li>カスタムリソースごとに詳細にバージョンを指定できないので、実際のクラスタに適用されているものと異なるかもしれない</li> <li>Necoチームが独自のパッチを適用していたりすると異なるものになっているかもしれない</li> <li>ここに登録されていない独自のCRDが存在するかもしれない</li> </ul> </li> <li>バリデーションせずスキップする <ul> <li><code>-ignore-missing-schemas</code>というフラグを付けることでスキーマ定義ファイルがないリソースのバリデーションをスキップできる</li> <li>適用時にエラーになる可能性が残る</li> </ul> </li> <li>実際のクラスタからCRDを取得して変換する <ul> <li>Necoは<a href="https://github.com/cybozu-go/meows">meows</a>というコントローラを使ってGitHub Actionsのself-hosted runnerを運用している。</li> <li>このrunnerからならCRDを取得し、スキーマ定義ファイルを生成すればクラスタの状態と一致したスキーマ定義ファイルを生成できる</li> <li>本番環境のクラスタ上では動作していないため、開発環境のクラスタのスキーマ定義ファイルしか生成できない</li> </ul> </li> <li>neco-appsをクローンして使う <ul> <li>今適用中の定義が確実に手に入る</li> <li>ブランチを選ぶことで、本番環境のものや開発環境のものを自由に選べる</li> </ul> </li> </ul> <p>neco-appsにコミットされている定義ファイルを参照するのが最も確実であり、この方法を取れないか検討したのですが、GitHub Actionsの仕様により一つの課題が浮かび上がってきたのでした。</p> <h2 id="internalリポジトリのclone">internalリポジトリのclone</h2> <p>GitHub Enterprise Cloudのリポジトリには3種類の公開範囲があります。</p> <ul> <li>public:orgの外にも公開される</li> <li>internal:enterprise内のユーザであれば誰でも読み取り権限を持つ</li> <li>private:enterprise内のユーザのうち明示的に権限が設定されたユーザのみが読み書きできる</li> </ul> <p>neco-appsはこのうちinternalなリポジトリとして運用されており、同enterprise内のユーザであれば、つまりcybozuのエンジニアであれば誰でも閲覧することができます。一方で、GitHub Actions内からinternalなリポジトリをcloneする場合、<code>secrets.GITHUB_TOKEN</code>では権限が足りません。これはこのトークンがそのCIを実行しているリポジトリに対する権限のみに限定されているためで、internalなリポジトリをcloneするためには権限を持つユーザがPersonal Access Token(PAT)を発行する必要があります。</p> <p>このように、enterprise内のユーザから見ると誰でもcloneできるのに、GitHub Actionsの中から実行するとデフォルトではcloneできないということになっています<a href="#f-3b307d0b" id="fn-3b307d0b" name="fn-3b307d0b" title="これは少し不便な気がしますが、CI環境が侵害された際の影響範囲を限定しやすいという意味では良い仕様なのかも知れません。Go ModulesのようなGitの仕組みに依存したパッケージ管理の仕組みと相性が悪いのはなんとかしたいところです。">*3</a>。</p> <h3 id="トークンの作り方">トークンの作り方</h3> <p>GitHub Actionsでもデフォルトの <code>secrets.GITHUB_TOKEN</code> とはまた別のトークンを設定すればクローンできることは分かったので、その生成・管理方法について検討してみました。</p> <ul> <li>ユーザのPATを使う <ul> <li>pros <ul> <li>すぐ実現できる</li> </ul> </li> <li>cons <ul> <li>ユーザの異動・休職・退職の際に作業が発生する</li> <li>PAT(classic)は権限が広すぎる</li> </ul> </li> </ul> </li> <li>ロボットアカウントのPATを使う <ul> <li>pros <ul> <li>すぐ実現できる</li> </ul> </li> <li>cons <ul> <li>各テナントチームのロボットアカウントをneco-appsに招待してもらう必要がある</li> <li>PAT(classic)は権限が広すぎる</li> </ul> </li> </ul> </li> <li>GitHub Appを作る <ul> <li>Contents: Readの権限が付いたGitHub Appを作って各リポジトリにインストールする <ul> <li><a href="https://docs.github.com/en/apps/using-github-apps/about-using-github-apps">https://docs.github.com/en/apps/using-github-apps/about-using-github-apps</a></li> </ul> </li> <li>pros <ul> <li>ユーザに依存しない短命なトークンを利用できる</li> </ul> </li> <li>cons <ul> <li>このappをインストールしたリポジトリ間は相互に参照できるようになってしまう</li> <li>権限範囲が別のリポジトリごとに異なるGitHub Appを作らねばならず、手間が大きい</li> </ul> </li> </ul> </li> </ul> <p>どれも一長一短で、そもそもinternalなリポジトリとして公開しているのだからもう少し簡単に参照できるようになってほしい……<br/> という気持ちがありしばらく見なかったことにして寝かせていました。</p> <h2 id="GitHub-Actionsのカスタムアクション">GitHub Actionsのカスタムアクション</h2> <p>リポジトリに所定の形式で記述した <code>action.yaml</code> を配置することで、GitHub Actions上でそのリポジトリを <code>uses</code> して使うことが出来るようになります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fja%2Factions%2Fcreating-actions" 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/actions/creating-actions">docs.github.com</a></cite></p> <p>登場当初はこれはpublicなリポジトリでしか使えず、privateないしinternalなリポジトリにあるactionを配布する方法にはいくつかの工夫が必要でした<a href="#f-5fec5d3c" id="fn-5fec5d3c" name="fn-5fec5d3c" title="[https://caddi.tech/archives/3737:embed:cite]">*4</a>。2022/12/14にprivateリポジトリからのactionやreusable workflowの共有に関してアップデートがあり、共有するactionを配置しているリポジトリに設定を行うことで、同一org内もしくはenterprise内のリポジトリのCIからは自由に <code>uses</code> できるようになりました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.blog%2Fchangelog%2F2022-12-14-github-actions-sharing-actions-and-reusable-workflows-from-private-repositories-is-now-ga%2F" title="GitHub Actions - Sharing actions and reusable workflows from private repositories is now GA" 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.blog/changelog/2022-12-14-github-actions-sharing-actions-and-reusable-workflows-from-private-repositories-is-now-ga/">github.blog</a></cite></p> <p>composite action(日本語では複合アクション)として作成したカスタムアクション内では<code>GITHUB_ACTION_PATH</code>という環境変数<a href="#f-cadd54ae" id="fn-cadd54ae" name="fn-cadd54ae" title="[https://docs.github.com/ja/actions/learn-github-actions/variables#default-environment-variables]">*5</a>が用意されています。この環境変数の指すパスにアクセスする事で、作成したcomposite action内からアクションの共有元のリポジトリの中身にアクセス出来ます。</p> <p>おや?もしかしてcompositeアクションにしてしまうとinternalなリポジトリの中身を簡単にorg/enterprise内に配布することができるのでは?</p> <h3 id="GITHUB_ACTION_PATH-には何が入っている"><code>GITHUB_ACTION_PATH</code> には何が入っている?</h3> <p><code>GITHUB_ACTION_PATH</code> というものがあることは分かりましたが、その実際の中身は自明ではありませんでした。実際に実験してみるのが早いと言うことで<a href="https://github.com/pddg/actions-playground">適当なリポジトリ</a>を用意し、<code>testcomposite/action.yaml</code>を作って以下の様に設定してみました。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Test composite action <span class="synIdentifier">runs</span><span class="synSpecial">:</span> <span class="synIdentifier">using</span><span class="synSpecial">:</span> <span class="synConstant">&quot;composite&quot;</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash <span class="synIdentifier">run</span><span class="synSpecial">:</span> | set -x ls -la <span class="synConstant">&quot;${GITHUB_ACTION_PATH}&quot;</span> ls -la <span class="synConstant">&quot;${GITHUB_ACTION_PATH}/../&quot;</span> cd <span class="synConstant">&quot;${GITHUB_ACTION_PATH}&quot;</span> <span class="synType">&amp;&amp;</span> git log --oneline || <span class="synConstant">true</span> </pre> <p>使う側では以下の様に書くだけで使えます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Test Composite Action <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synSpecial">[</span>push<span class="synSpecial">]</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">run</span><span class="synSpecial">:</span> <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">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> pddg/actions-playground/testcomposite@main </pre> <p>実行結果は以下の様になりました。</p> <p><figure class="figure-image figure-image-fotolife" title="Test Composite Actionの実行ログ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/pudding_info/20240111/20240111100934.png" width="831" height="450" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Test Composite Actionの実行ログ</figcaption></figure></p> <ul> <li><code>GITHUB_ACTION_PATH</code> は <code>action.yaml</code> が存在するディレクトリへのパスである</li> <li><code>GITHUB_ACTION_PATH</code> から相対パスを使って辿ることで、composite actionを提供するリポジトリ内の他のファイルにアクセス出来る</li> <li><code>.git</code> が存在しないため過去の履歴を辿ったりすることはできない</li> </ul> <p>ということが言えそうです。これを踏まえて、internalなリポジトリ内のファイルを配布するアクションを作成してみます。</p> <h2 id="ファイルを配布するアクションを作る">ファイルを配布するアクションを作る</h2> <p>注意点は以下の二つです。</p> <ul> <li><code>GITHUB_ACTION_PATH</code> は <code>action.yaml</code> を含むディレクトリへのパスであり、リポジトリのルートディレクトリではない</li> <li>composite actionが実行される際のカレントディレクトリは、 <code>uses</code> している側のリポジトリのルートディレクトリである</li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Install CRD schemas <span class="synIdentifier">inputs</span><span class="synSpecial">:</span> <span class="synIdentifier">dest</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Destination path to install. Creates the directory if it does not exist.&quot;</span> <span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">runs</span><span class="synSpecial">:</span> <span class="synIdentifier">using</span><span class="synSpecial">:</span> <span class="synConstant">&quot;composite&quot;</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">DEST</span><span class="synSpecial">:</span> ${{ inputs.dest }} <span class="synIdentifier">run</span><span class="synSpecial">:</span> | <span class="synComment"> # 相対パスでリポジトリのルートパスを指定</span> <span class="synComment"> # ここでは.github/actions/install-crd-schemas/action.yamlとして作ったので以下の様にする</span> root_dir=&quot;${GITHUB_ACTION_PATH}/../../../&quot; <span class="synComment"> # 指定されたインストール先のディレクトリがなければ作る</span> mkdir -p <span class="synConstant">&quot;${DEST}&quot;</span> <span class="synComment"> # ファイルをコピーする</span> cp -f ${root_dir}/test/crd-schemas/json-schemas/*.json <span class="synConstant">&quot;${DEST}&quot;</span> </pre> <p>書いてみると非常にシンプルですね。そしてリポジトリのSettings -> Actions -> GeneralからAccessに関する設定を開き、<code>Accessible from repositories in the '{{ your org name }}' organization</code>または<code>Accessible from repositories in the '{{ your company name }}' enterprise</code>にチェックを入れて保存すれば完了です<a href="#f-acd76b6f" id="fn-acd76b6f" name="fn-acd76b6f" title="ここで公開範囲を適切に選択しないと意図しないリポジトリなどから参照される可能性があることに注意してください。Enterprise全体で利用可能にして良いかどうかはセキュリティに関して十分な検討が必要であると考えます。">*6</a>。</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">kubeconform</span><span class="synSpecial">:</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> example.com/your_repo/.github/actions/install-crd-schemas@main <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">dest</span><span class="synSpecial">:</span> /tmp/crd-schemas </pre> <h3 id="パラメータでバージョンを選べるようにする">パラメータでバージョンを選べるようにする</h3> <p>指定したブランチ、タグ、コミットの時点のファイルを指定したディレクトリへインストールできるようにはなりましたが、不便な点が一つあります。<br/> 本番環境向け、開発環境向けなどで使い分ける際に明示的にブランチの指定が必要で、ワークフローの再利用が難しいということです。<code>uses</code>句でのブランチの指定には変数などが使えない<a href="#f-6f5ad395" id="fn-6f5ad395" name="fn-6f5ad395" title="[https://github.com/orgs/community/discussions/25246:embed:cite]">*7</a>ため、どれを <code>uses</code> するか静的に決める必要があります。<code>if</code> などで分岐することはできますが、使う側での設定が必要なので各チームごとに同じような分岐処理を書くことになります。これを少し楽にするラッパーのようなactionを作ります。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Setup CRD schemas <span class="synIdentifier">inputs</span><span class="synSpecial">:</span> <span class="synIdentifier">k8s-env</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'Which environment to use for validation'</span> <span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">type</span><span class="synSpecial">:</span> string <span class="synComment"> # Available values are &quot;latest&quot;, &quot;stage&quot;, and &quot;prod&quot;.</span> <span class="synIdentifier">default</span><span class="synSpecial">:</span> stage <span class="synIdentifier">outputs</span><span class="synSpecial">:</span> <span class="synIdentifier">crd-dir</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Path to the dir that contains CRD schemas&quot;</span> <span class="synIdentifier">value</span><span class="synSpecial">:</span> /tmp/crd-schemas-${{ inputs.k8s-env }} <span class="synIdentifier">runs</span><span class="synSpecial">:</span> <span class="synIdentifier">using</span><span class="synSpecial">:</span> composite <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> example.com/your_repo/.github/actions/install-crd-schemas@main <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ inputs.k8s-env == <span class="synConstant">'latest'</span> }} <span class="synIdentifier">id</span><span class="synSpecial">:</span> latest <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">dest</span><span class="synSpecial">:</span> /tmp/crd-schemas-${{ inputs.k8s-env }} <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> example.com/your_repo/.github/actions/install-crd-schemas@stage <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ inputs.k8s-env == <span class="synConstant">'stage'</span> }} <span class="synIdentifier">id</span><span class="synSpecial">:</span> stage <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">dest</span><span class="synSpecial">:</span> /tmp/crd-schemas-${{ inputs.k8s-env }} <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> example.com/your_repo/.github/actions/install-crd-schemas@prod <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ inputs.k8s-env == <span class="synConstant">'prod'</span> }} <span class="synIdentifier">id</span><span class="synSpecial">:</span> prod <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">dest</span><span class="synSpecial">:</span> /tmp/crd-schemas-${{ inputs.k8s-env }} </pre> <p>これで利用側は以下の様なreusable workflowを記述し、triggerの設定に応じてパラメータを切り替えるだけで環境を選択できます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Check manifests <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">workflow_call</span><span class="synSpecial">:</span> <span class="synIdentifier">inputs</span><span class="synSpecial">:</span> <span class="synIdentifier">k8s-env</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'Which environment to use for validation'</span> <span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">false</span> <span class="synIdentifier">type</span><span class="synSpecial">:</span> string <span class="synIdentifier">default</span><span class="synSpecial">:</span> stage <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">lint</span><span class="synSpecial">:</span> <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">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> example.com/your_repo/.github/actions/setup-crd-schemas@main <span class="synIdentifier">id</span><span class="synSpecial">:</span> setup-crd-schemas <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">k8s-env</span><span class="synSpecial">:</span> ${{ inputs.k8s-env }} <span class="synStatement">- </span><span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">CRD_DIR</span><span class="synSpecial">:</span> ${{ steps.setup-crd-schemas.outputs.crd-dir }} <span class="synIdentifier">run</span><span class="synSpecial">:</span> | <span class="synComment"> # install kubeconform here</span> wget ~~ <span class="synComment"> # run kubeconform</span> kubeconform \ -strict \ -schema-location <span class="synConstant">&quot;${CRD_DIR}/{{.ResourceKind}}-{{.Group}}-{{.ResourceAPIVersion}}.json&quot;</span> \ -schema-location default \ ./manifests </pre> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Check manifests (stage) <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">pull_request</span><span class="synSpecial">:</span> <span class="synIdentifier">branches</span><span class="synSpecial">:</span> <span class="synStatement">- </span>main <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">run</span><span class="synSpecial">:</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> example.com/your_repo/.github/workflows/check_manifests.yaml@main <span class="synIdentifier">secrets</span><span class="synSpecial">:</span> inherit <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">neco-env</span><span class="synSpecial">:</span> stage </pre> <p>prod用のブランチに対しては異なる設定を用意して切り替えます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Check manifests (prod) <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">pull_request</span><span class="synSpecial">:</span> <span class="synIdentifier">branches</span><span class="synSpecial">:</span> <span class="synStatement">- </span>prod <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">run</span><span class="synSpecial">:</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> example.com/your_repo/.github/workflows/check_manifests.yaml@main <span class="synIdentifier">secrets</span><span class="synSpecial">:</span> inherit <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">neco-env</span><span class="synSpecial">:</span> prod </pre> <p><code>workflow_dispatch</code> の <code>inputs</code> で <code>type: choice</code> を使うことで、任意のブランチに対して任意の環境におけるバリデーションを、ユーザはプルダウンから選択するだけで実行できるようになります。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Check manifests (manual) <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">workflow_dispatch</span><span class="synSpecial">:</span> <span class="synIdentifier">inputs</span><span class="synSpecial">:</span> <span class="synIdentifier">k8s-env</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">'which neco environment to use'</span> <span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">type</span><span class="synSpecial">:</span> choice <span class="synIdentifier">default</span><span class="synSpecial">:</span> <span class="synConstant">'prod'</span> <span class="synIdentifier">options</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'stage'</span> <span class="synStatement">- </span><span class="synConstant">'prod'</span> <span class="synStatement">- </span><span class="synConstant">'latest'</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">run</span><span class="synSpecial">:</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> example.com/your_repo/.github/workflows/check_manifests.yaml@main <span class="synIdentifier">secrets</span><span class="synSpecial">:</span> inherit <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">neco-env</span><span class="synSpecial">:</span> ${{ inputs.k8s-env }} </pre> <p><figure class="figure-image figure-image-fotolife" title="workflow_dispatchによってGitHubのUIから手動でジョブを実行できる"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/pudding_info/20240111/20240111101025.png" width="1200" height="433" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>workflow_dispatchによってGitHubのUIから手動でジョブを実行できる</figcaption></figure></p> <h3 id="副次的に得られた効果">副次的に得られた効果</h3> <p>この方法は単純にリポジトリのcloneを許可する場合と比較して、認証周り以外にも一つ利点があると考えています。共有したいファイルをホストしている側が、リポジトリの構造について考慮しなければならない点が減っていることです。</p> <p>単純にリポジトリのcloneを許可してテナントが自由にリポジトリ内のファイルを見るようにしてしまうと、そのリポジトリの管理側はディレクトリ構造を容易には変更できなくなってしまいます。利用しているチームに周知し、タイミングを合わせて変更したり、移行期間を設けて変更してもらうなどの手段が必要となります。</p> <p>GitHub Actionsのインターフェースを挟むことで、各テナントはcomposite actionを通してしかリポジトリのファイルへのアクセスができません<a href="#f-f614ef8c" id="fn-f614ef8c" name="fn-f614ef8c" title="厳密には GITHUB_ACTION_PATH が示すであろうパスにアクセスすれば自由に触れますが、それは自己責任でしょう。">*8</a>。リポジトリの構造の変更時にcomposite actionの操作も対応させることで、ユーザに影響を与えることなくリポジトリ構造の変更が実現できます。</p> <p>もし下記のようなセキュリティに関する考慮の結果、トークンを使って個別に認証する必要があるケースでも、cloneした後に <code>uses: ./your_repo/.github/actions/install-crd-schemas</code> のように実行してもらってcomposite action経由でファイルを配布することで同様の効果を得られます。</p> <h3 id="セキュリティに関する考慮">セキュリティに関する考慮</h3> <p><code>Accessible from repositories in the ~~</code>の設定を有効にすると、GitHub Actionsではorg内やenterprise内の他のinternal/privateリポジトリから実質参照し放題になってしまいます<a href="#f-8a1d752c" id="fn-8a1d752c" name="fn-8a1d752c" title="設定の説明にある&quot;Access is allowed only from private or internal repositories&quot;という記載が示すとおり、これらの設定を有効にしてもpublicリポジトリから参照されることはありません。">*9</a>。internalなリポジトリの場合、そのenterprise内のinternalもしくはprivateのリポジトリから、privateなリポジトリの場合、そのenterprise内のprivateなリポジトリからアクセス可能になっています。特定のリポジトリには限定できないため、意図しないリポジトリからのアクセスも許可してしまいます。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Fenterprise-cloud%40latest%2Frepositories%2Fmanaging-your-repositorys-settings-and-features%2Fenabling-features-for-your-repository%2Fmanaging-github-actions-settings-for-a-repository%23allowing-access-to-components-in-an-internal-repository" title="Managing GitHub Actions settings for a repository - GitHub Enterprise Cloud 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/en/enterprise-cloud@latest/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#allowing-access-to-components-in-an-internal-repository">docs.github.com</a></cite></p> <p>internalリポジトリはともかく、privateリポジトリでこの設定を有効化することは難しいでしょう。そのorgに自由にリポジトリを作成できる権限があれば、勝手にprivateリポジトリを作って <code>uses</code> すれば中身が見えてしまいます<a href="#f-af9e5a8c" id="fn-af9e5a8c" name="fn-af9e5a8c" title="privateリポジトリの存在を知らなければそういうこともできない、というのはそうかもしれませんが、それに依存したセキュリティにすべきではないでしょう。">*10</a>。また、internalなリポジトリであっても、指定した特定のファイルだけではなくリポジトリ内のファイル全体へのアクセスが可能になっていることから、ほかのリポジトリにおいてCI環境が侵害された場合などについて考慮が必要となります。重要な情報がリポジトリに含まれている場合は同様に許可できないでしょう。その場合は前述したトークンを使って配布する方法やdeploy keyにread onlyの権限を付与するなどで閲覧範囲を限定して緩和するなどの対応が考えられます。</p> <h2 id="まとめ">まとめ</h2> <p>composite actionを作ることで、internalリポジトリにあるファイルを同一org/enterprise内の他のリポジトリへトークンなしで配布できることがわかりました。セキュリティ面の考慮が必要であり無条件で採用できるものではありませんが、便利に使える側面もあります。</p> <p>Cybozuの新インフラ基盤では各テナントチームが裁量を持ち、独立して開発・運用する体制が徐々に整えられています。Cloud Platform部ではその新インフラ基盤への移行を支える仕事に興味のある方を募集中です。ぜひご応募ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fcloudplatform.html" title="クラウド基盤エンジニア(SRE/Cloud Platform)キャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/career/cloudplatform.html">cybozu.co.jp</a></cite></p> <hr /> <div class="footnote"> <p class="footnote"><a href="#fn-b542ff21" id="f-b542ff21" name="f-b542ff21" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">以前neco-appsリポジトリはpublicリポジトリでしたが、現在は管理上の都合によりprivateなorgのinternalなリポジトリになっています。</span></p> <p class="footnote"><a href="#fn-64b7c335" id="f-64b7c335" name="f-64b7c335" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://github.com/yannh/kubeconform?tab=readme-ov-file#CustomResourceDefinition-CRD-Support">https://github.com/yannh/kubeconform?tab=readme-ov-file#CustomResourceDefinition-CRD-Support</a></span></p> <p class="footnote"><a href="#fn-3b307d0b" id="f-3b307d0b" name="f-3b307d0b" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">これは少し不便な気がしますが、CI環境が侵害された際の影響範囲を限定しやすいという意味では良い仕様なのかも知れません。Go ModulesのようなGitの仕組みに依存したパッケージ管理の仕組みと相性が悪いのはなんとかしたいところです。</span></p> <p class="footnote"><a href="#fn-5fec5d3c" id="f-5fec5d3c" name="f-5fec5d3c" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text"><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcaddi.tech%2Farchives%2F3737" title="GitHub Actions で private リポジトリの action を共有する仕組み - CADDi Tech Blog" 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://caddi.tech/archives/3737">caddi.tech</a></cite></span></p> <p class="footnote"><a href="#fn-cadd54ae" id="f-cadd54ae" name="f-cadd54ae" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://docs.github.com/ja/actions/learn-github-actions/variables#default-environment-variables">https://docs.github.com/ja/actions/learn-github-actions/variables#default-environment-variables</a></span></p> <p class="footnote"><a href="#fn-acd76b6f" id="f-acd76b6f" name="f-acd76b6f" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text">ここで公開範囲を適切に選択しないと意図しないリポジトリなどから参照される可能性があることに注意してください。Enterprise全体で利用可能にして良いかどうかはセキュリティに関して十分な検討が必要であると考えます。</span></p> <p class="footnote"><a href="#fn-6f5ad395" id="f-6f5ad395" name="f-6f5ad395" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text"><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Forgs%2Fcommunity%2Fdiscussions%2F25246" title="Env variables in uses · community · Discussion #25246" 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/orgs/community/discussions/25246">github.com</a></cite></span></p> <p class="footnote"><a href="#fn-f614ef8c" id="f-f614ef8c" name="f-f614ef8c" class="footnote-number">*8</a><span class="footnote-delimiter">:</span><span class="footnote-text">厳密には GITHUB_ACTION_PATH が示すであろうパスにアクセスすれば自由に触れますが、それは自己責任でしょう。</span></p> <p class="footnote"><a href="#fn-8a1d752c" id="f-8a1d752c" name="f-8a1d752c" class="footnote-number">*9</a><span class="footnote-delimiter">:</span><span class="footnote-text">設定の説明にある"Access is allowed only from private or internal repositories"という記載が示すとおり、これらの設定を有効にしてもpublicリポジトリから参照されることはありません。</span></p> <p class="footnote"><a href="#fn-af9e5a8c" id="f-af9e5a8c" name="f-af9e5a8c" class="footnote-number">*10</a><span class="footnote-delimiter">:</span><span class="footnote-text">privateリポジトリの存在を知らなければそういうこともできない、というのはそうかもしれませんが、それに依存したセキュリティにすべきではないでしょう。</span></p> </div> pudding_info フロントエンドリアーキテクチャ(部分刷新)を素早く終わらせるために取り組んだこと hatenablog://entry/6801883189066385950 2023-12-25T12:00:00+09:00 2023-12-25T13:22:39+09:00 ※この記事は Cybozu Frontend Advent Calendar 2023 の 25 日目の記事です ! こんにちは、サイボウズでスクラムマスターとして働いている村田です。2023 年 8 月から新規機能を開発するチームに移動し、週3日(火水木)勤務で専任スクラムマスターとして活動しています。それ以前の約 1 年間は、kintone のヘッダーを React 化するチームでスクラムマスターとエンジニアの役割を兼務していました。 今回は以前所属していた kintone のヘッダーを React 化するチームで、フロントエンドリアーキテクチャ(部分刷新)を素早く終わらせるために取り組ん… <p>※この記事は  <a href="https://adventar.org/calendars/9255">Cybozu Frontend Advent Calendar 2023</a>  の 25 日目の記事です !</p> <p>こんにちは、サイボウズでスクラムマスターとして働いている<a href="https://twitter.com/kuroppe1819">村田</a>です。2023 年 8 月から新規機能を開発するチームに移動し、週3日(火水木)勤務で専任スクラムマスターとして活動しています。それ以前の約 1 年間は、kintone のヘッダーを React 化するチームでスクラムマスターとエンジニアの役割を兼務していました。</p> <p>今回は以前所属していた kintone のヘッダーを React 化するチームで、フロントエンドリアーキテクチャ(部分刷新)を素早く終わらせるために取り組んだことを紹介します。</p> <h2 id="プロジェクトの概要">プロジェクトの概要</h2> <p>kintone の全画面のヘッダー領域を Closure Library から React へ置き換える部分刷新を試みていました。プロジェクトゴールは次の2つです。</p> <ul> <li>kintone の全画面のヘッダー領域が刷新されている</li> <li>移譲先のチームが刷新後のヘッダー領域の開発を行っている</li> </ul> <p>当初の予定では2023年末までかかる見込みでしたが、2023年7月中にゴールを達成しました。</p> <p>刷新完了までのスケジュールは以下の通りです。</p> <table> <thead> <tr> <th> 期間 </th> <th> ゴール </th> <th> 概要 </th> </tr> </thead> <tbody> <tr> <td> 2022/01 ~ 2022/06 </td> <td> ページ単位の React 化を行うチームへ React 化したヘッダーを提供する </td> <td> <a href="https://blog.cybozu.io/entry/2022/11/18/120000">kintone の共通ヘッダー部分の React 化に責任を持つチームの紹介!</a> </td> </tr> <tr> <td> 2022/06 ~ 2023/06 </td> <td> kintone の全画面のヘッダー領域を刷新する </td> <td> <a href="https://blog.cybozu.io/entry/2023/08/29/101907">React 化した共通ヘッダーを kintone の全ページに適用しました!</a> </td> </tr> <tr> <td> 2023/06 ~ 2023/07 </td> <td> ヘッダー領域のコードのオーナーシップを他チームへ移譲する </td> <td> コード移譲先のチームメンバーにドキュメントの提供や説明会を実施。テスト&amp;CIの整備やライブラリアップデートも行う。 </td> </tr> </tbody> </table> <p>リリースの流れや技術的な詳細については次のブログ記事をご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2023%2F08%2F29%2F101907" title="React 化した共通ヘッダーを kintone の全ページに適用しました! - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2023/08/29/101907">blog.cybozu.io</a></cite></p> <p>この活動で得られた学びを Spotify で配信しているので、お時間があればこちらも聞いてみてください。</p> <p><iframe style="border-radius: 12px" width="100%" height="152" title="Spotify Embed: #2 部分的なReact刷新をやり遂げたAppShellというチームについて" frameborder="0" allowfullscreen allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy" src="https://open.spotify.com/embed/episode/5ve5hGEXH58dpBgubGWFzL?utm_source=oembed"></iframe><cite class="hatena-citation"><a href="https://open.spotify.com/episode/5ve5hGEXH58dpBgubGWFzL">open.spotify.com</a></cite></p> <h2 id="刷新スピードを上げるために取り組んだこと">刷新スピードを上げるために取り組んだこと</h2> <p>このプロジェクトにおいて、フロントエンド刷新を加速するために実施した 5 つの取り組みを紹介します。</p> <h3 id="クロスファンクショナルな小さいチームで-1-つのことに集中する">クロスファンクショナルな小さいチームで 1 つのことに集中する</h3> <p>小規模なチームはコミュニケーションのオーバーヘッドが少なく、調整もしやすいため俊敏に行動することが可能です。</p> <p>ヘッダー領域を刷新するチームでは最終的に 4 人体制で、以下の役割を担ってスクラムで開発をしていました。</p> <ul> <li>プロダクトオーナー兼エンジニア: 1人</li> <li>スクラムマスター兼エンジニア: 1人</li> <li>エンジニア: 1人</li> <li>QA: 1人</li> </ul> <p>このチームでは 1 人で解決できない問題に直面すると、すぐに Slack のハドルで集まって問題解決に向き合い、解決後はハドルを切ってそれぞれの業務に戻るという流れが自然に行われていました。チームメンバーが少ないことから、合意に至るまでの時間が短く、迅速な意思決定が可能でした。</p> <p>もちろん、最初からこのような動きができたわけではありません。</p> <p>私がスクラムマスター兼エンジニアとしてチームに入った 2022/06 の状況は、「ページ単位の React 化を行うチームへ React 化したヘッダーを提供する」ゴールを達成した直後で、燃え尽き症候群のような雰囲気が漂っていました。さらに、多数のチームメンバーがゴール達成のタイミングで別のプロジェクトに移ることが決まっており、ほとんどが新しいメンバーで「kintone の全画面のヘッダー領域を刷新する」ゴールに向かう必要がありました。</p> <p>ほぼゼロからの再スタートを余儀なくされた状況の中で、チームが安定して成果を生み出せる状態へ素早く移行するために 2 つのことを実施しました。1つはチームが達成すべきゴールの設定、もう1つは目標達成に向けた進捗の確認とふりかえりです。</p> <p><iframe id="talk_frame_1028794" class="speakerdeck-iframe" src="//speakerdeck.com/player/d2e15a2dadc14615aeab2e907942dc0b" 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/kuroppe1819/timunocheng-chang-wocu-sutamenihurikaerinogai-shan-niben-qi-texiang-kihe-tutahua">speakerdeck.com</a></cite></p> <p>これらの取り組みは、初期段階で目標に対する共通の理解を深め、その後のチームメンバー同士の信頼関係を強化することに繋がりました。</p> <h3 id="チームで独立したデプロイを可能にする">チームで独立したデプロイを可能にする</h3> <p>チームが管理しているコードの外に影響することなく更新をデプロイできるようになると、他チームとの調整作業がなくなり、リードタイムが短縮できます。</p> <p>ページ単位の React 化を行うチームへ React 化したヘッダーを提供していた頃は、ヘッダーを npm パッケージとして配布していました。npm パッケージを配布する場合におけるデプロイでは、パッケージを利用しているすべてのチームにバージョンの更新を依頼し、その依頼が完了するまで更新が反映されるのを待つ必要がありました。</p> <p>また、Closure Library を使用しているページでは npm などのパッケージマネージャーを使用したライブラリを import するのに一工夫必要で、React 化された画面と Closure Tools を用いて開発された画面それぞれで異なるデプロイ方法を行う必要がありました。</p> <p>更新のデプロイをチームのコントロール下において素早くリリースできるようにしたかったため、全画面刷新に踏み切ったタイミングで npm による配布を廃止し、ヘッダー領域の JavaScript ファイルを HTML で読み込む形式に変更しました。</p> <p><figure class="figure-image figure-image-fotolife" title="React 化したヘッダーを全ページへ適用"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231213/20231213100848.jpg" width="1200" height="421" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>React 化したヘッダーを全ページへ適用</figcaption></figure></p> <p>これにより、基本的にチーム外への依頼が不要になり、自由にデプロイすることが可能になりました。「基本的に」と書いたのは、共通ヘッダーとページコンテンツ間で利用しているインターフェースを変更する場合にはチーム間の調整が必要になるためです。現状ではヘッダーとページコンテンツ間は疎結合になっており、インターフェースの変更はほとんど行われません。</p> <p>ヘッダーとページコンテンツの結合方法はマイクロフロントエンドを参考に <a href="https://developer.mozilla.org/ja/docs/Web/API/CustomEvent">Custom Event</a> で実装しました。マイクロフロントエンドの実践にあたって検討した様子もブログで公開しているのでご一読いただければと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2022%2F12%2F21%2F110000" title="新卒でマイクロフロントエンドを経験してみて - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2022/12/21/110000">blog.cybozu.io</a></cite></p> <h3 id="刷新する価値のある機能を絞る">刷新する価値のある機能を絞る</h3> <p>当たり前の話ですが、刷新する機能が少なければ少ないほど素早く刷新することができます。</p> <p>ヘッダー領域を刷新するときに、この機能は本当に刷新する価値があるのかチームメンバーで議論しました。価値のある部分を見極めて刷新対象を絞ったことで、当初の計画よりも3~4ヶ月前倒しで kintone の全画面に刷新後のヘッダーを適用することが可能となりました。</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/y/yellow-sabotech/20231219/20231219100313.png" width="1200" height="780" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>全画面リリースに向けた準備</figcaption></figure></p> <p>刷新のスコープから除外した機能については、React で実装したコードから Closure Library で実装された機能を Custom Event を使って呼び出しています。理想は機能を削除することでしたが、諸々の理由で自チームで機能を削除を実現することは難しく、他チームの協力を仰いで優先順位を上げて取り組むこともこの時点では困難であったため、このような方法で刷新のスコープから除外しました。</p> <p>この段階では機能削除を諦めましたが、移譲先のチームで引き続き検討しています。</p> <h3 id="早すぎる最適化を避ける">早すぎる最適化を避ける</h3> <p>ゴール達成に直接影響せず、全体に及ぼす影響がほとんどないものは、問題が顕在化してから取り組めば良いと考えています。</p> <p>サイズの大きいライブラリの重複読み込み解消や、インターフェース部分のコードを切り出して共通化を図るといったタスクを今回の刷新のスコープから意図的に除外しました。これらは部分刷新が増えると、ユーザー体験が著しく低下したり、実装時の認知不可が高まる問題を抱えています。しかし、この段階で部分刷新を行っているチームは我々のみであり、最適化を図るには時期尚早だと判断しました。</p> <p>パフォーマンスよりもチーム間の独立性を重視し、重複読み込みを許容しました。インターフェースに関しても Custom Event のイベント名にどの領域から飛ばされてきたイベントなのか判別できる prefix を付け、後に差し替えるタイミングで影響範囲を最小限にするために Custom Event をラップする層を作ってそれを呼び出す形にしました。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">// 例: header/event-listener.js</span> header.eventlistener.addShowNotifierListener = <span class="synIdentifier">function</span> () <span class="synIdentifier">{</span> <span class="synStatement">document</span>.addEventListener(<span class="synConstant">'header:show-notifier'</span>, <span class="synIdentifier">function</span> (evt) <span class="synIdentifier">{</span> ... <span class="synIdentifier">}</span> <span class="synIdentifier">}</span> </pre> <h3 id="不具合を検出してから修正までのフィードバックループを短くする">不具合を検出してから修正までのフィードバックループを短くする</h3> <p>不具合を早期に検出することで、手戻りに要する時間を短縮できます。</p> <p>kintoneのフロントエンドを刷新する前は、多くのテストが E2E で自動化されていました。自動化されたテストが全くない状態よりは良いですが、E2E Test の増加に伴い不安定なテスト(Flaky Test)の数も増え、さらにテスト実行時間の増加によるフィードバックの遅れが開発生産性を低下させていました。そのため、刷新のタイミングで QA 観点で作成したテスト仕様書をベースに、Integration Test の実装を増やし、E2E Test の実装を最小限にしました。</p> <p>最終的な E2E Test と Integration Test の件数を以下に示します。</p> <table> <thead> <tr> <th>テストの種類</th> <th>テストの件数</th> </tr> </thead> <tbody> <tr> <td>E2E</td> <td>22件</td> </tr> <tr> <td>Integration</td> <td>352件</td> </tr> </tbody> </table> <p>補足:Unit Test はエンジニア判断で実装しています。中には Integration Test と重複する項目もありますが、重複を省いてもテストの実行時間短縮には効果がなく、QA と調整する時間を省ける方が開発効率の向上に繋がると判断したため重複を許容しました。最終的な Unit Test の件数は374件になりました。</p> <p>この結果、ヘッダー領域における不具合を検出してから修正にかかる時間を大きく短縮することができました。環境依存ではありますが筆者のローカル開発環境で Integration Test と Unit Test の合計726件のテストを実行するのにかかる時間は約23秒です。</p> <p>エンジニアと QA 間で協力して Integration Test を実装した様子もブログで公開しているのでもしよければご一読ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2022%2F12%2F20%2F110000" title="エンジニアとの距離が近くなっていいことたくさんだったQAの話 - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2022/12/20/110000">blog.cybozu.io</a></cite></p> <h2 id="まとめ">まとめ</h2> <ul> <li>小さいチームで1つのことに集中する</li> <li>チームで独立して顧客に価値提供できるデプロイパイプラインを作る</li> <li>顧客に提供している価値を見極めリアーキテクチャの規模を小さくする</li> <li>早すぎる最適化を避ける</li> <li>不具合を検出してから修正までのフィードバックループを短くする</li> </ul> <p>これらが素早いリアーキテクチャを実現するための鍵です。※素早くリアーキテクトする手段はもちろんこれだけではないと思います</p> <p>特段目新しいことは 1 つも述べていませんが、大規模なアプリケーションでリアーキテクチャに取り組んでいる方々の参考になる情報があれば嬉しく思います。</p> <h2 id="あとがき">あとがき</h2> <p>2023 年は「kintone の全画面のヘッダー領域を刷新する!」というゴールに一直線に向かうために、多くの「やらないこと」を決めた 1 年でした。この活動を通して自分自身のリアーキテクチャに対する姿勢も大きく変化しました。</p> <p>リアーキテクチャを始めた初期の頃は刷新へのモチベーションが高く、とにかく技術を磨いてスピードを出すことに意識が向いていました。しかし、時間が経過するにつれて刺激が減少し、その結果、徐々にモチベーションも下がっていきました。同じ環境で 1 度下がったモチベーションを高めるのは難しいです。</p> <p>人のモチベーションが下がりきる前にゴールを達成する。そのために、技術を磨く以外の方法で刷新のスピードを上げることにも向き合いました。</p> <p>リアーキテクチャでは技術力の向上は基本ですが、それだけでは不十分だと思います。顧客価値を生まないコードを負債と見なし、除外することの重要性を痛感しました。これは特に大規模なアプリケーションにおいて効果的だと思います。</p> <p>そして、機能やテストを削除し、代替手段を提案するには、アプリケーションが顧客にどのような解決策を提供しているかを理解する必要があります。理解がなければ、仕様をそのまま踏襲して再現する以外に方法がないのです。</p> <p>リアーキテクチャの文脈においても、迅速に目標を達成するためには、チームが顧客のニーズを深く理解していることが不可欠であるということを強く実感しました。以前は、リアーキテクチャを技術に特化し、その分野に全力を尽くす精鋭たちの取り組みだと考えていました。しかし、このプロジェクトを経て、リアーキテクチャにおいてもドメインの理解が重要であることを学びました。</p> <p>価値を生み出している要素を理解していない場合、変更可能な範囲が狭くなります。その結果、スピードを上げることが難しくなります。</p> <p>この学びを新しいチームに共有し、今後もリアーキテクチャに臨んでいきたいと思います。</p> yellow-sabotech サイボウズサマーインターン2023 プロダクトセキュリティコース 開催報告 hatenablog://entry/6801883189060870873 2023-12-25T12:00:00+09:00 2023-12-23T09:25:00+09:00 こんにちは!Cy-PSIRTの田口です。 本記事はサマーインターンシップ2023 プロダクトセキュリティコースの開催報告です。 今年も去年に続き、8月と9月に全2ターム、フルリモートでインターンシップを開催しました。 概要 プロダクトセキュリティコースは第1タームを8月28日〜31日、第2タームを9月11日〜14日で開催し、それぞれ4名のインターン生にご参加いただきました。 インターンでは、普段Cy-PSIRTが行っている業務をもとにコンテンツを構成しており、以下のような内容をインターン生の皆さんに体験していただきました。 製品理解・ハンズオン 脆弱性検証 脆弱性評価 外部通報対応 Cy-PS… <p>こんにちは!Cy-PSIRTの田口です。<br> 本記事はサマーインターンシップ2023 プロダクトセキュリティコースの開催報告です。 今年も<a href="https://blog.cybozu.io/entry/2022/10/11/080000">去年</a>に続き、8月と9月に全2ターム、フルリモートでインターンシップを開催しました。</p> <h2 id="概要">概要</h2> <p>プロダクトセキュリティコースは第1タームを8月28日〜31日、第2タームを9月11日〜14日で開催し、それぞれ4名のインターン生にご参加いただきました。 インターンでは、普段Cy-PSIRTが行っている業務をもとにコンテンツを構成しており、以下のような内容をインターン生の皆さんに体験していただきました。</p> <ul> <li>製品理解・ハンズオン</li> <li>脆弱性検証</li> <li>脆弱性評価</li> <li>外部通報対応</li> </ul> <p>Cy-PSIRTの具体的な業務内容については以下の記事で紹介しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2021%2F10%2F08%2F170000" title="Cy-PSIRTの紹介 - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2021/10/08/170000">blog.cybozu.io</a></cite></p> <p>また、コンテンツで触れる業務以外のCy-PSIRTが行なっている取り組みの紹介や、社内環境を一部お見せしながらチームの雰囲気や実際の業務の進め方を知っていただく時間も今年から新たに設けました。</p> <h2 id="製品理解ハンズオン">製品理解・ハンズオン</h2> <p>はじめに、サイボウズ製品やWebアプリケーションの脆弱性について学んでいただきました。 Cy-PSIRTの業務ではあらゆる場面で製品理解が重要になります。その第一歩として各製品の機能や使い方について簡単に説明を行いました。 その後、脆弱性の概要や原理について学び、ハンズオンを行なっていただきました。ハンズオンでは、過去サイボウズ製品に存在していた脆弱性(改修済み)を再現させ、脆弱性の挙動を確認していただきました。</p> <h2 id="脆弱性検証">脆弱性検証</h2> <p>この時間では、Cy-PSIRT内での脆弱性検証フローである以下の業務を順番に演習し、脆弱性検証の一連の流れを体験していただきました。</p> <ul> <li>要件確認</li> <li>試験仕様書の作成</li> <li>脆弱性検証</li> </ul> <p>まずは 要件確認を行なっていただきました。要件確認とは、開発チームが実施した要件を確認し脆弱性検証の要否を判断する業務です。Cy-PSIRTでは、通常、開発によって生じる製品の差分に対して脆弱性検証を実施しています。そのため、要件確認は脆弱性検証フローの中で最初に行う必要がある業務です。 演習では、要件の実例を挙げながら、脆弱性検証が必要となる判断基準や考え方を学んでいただきました。</p> <p><figure class="figure-image figure-image-fotolife" title="要件確認(第1ターム)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231215/20231215214841.png" alt="&#x8981;&#x4EF6;&#x78BA;&#x8A8D;&#x306E;&#x69D8;&#x5B50;(&#x7B2C;1&#x30BF;&#x30FC;&#x30E0;)" width="1200" height="549" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>要件確認(第1ターム)</figcaption></figure></p> <p>次に、要件確認によって脆弱性検証が必要であると判断した要件について試験仕様書を作成します。 検証対象のリクエストについて、パラメータ単位での検証項目、権限、その他検証観点を漏れがないよう洗い出し、kintoneアプリに書き出していただきました。</p> <p>試験仕様書を作成後、いよいよ検証に入ります。 作成した試験仕様書の内容をもとに、Burp Suiteを使いながら一つずつ検証を行います。 インターン生の中には、アジャイル開発における脆弱性検証や自動化に関心を持つ方、QAとPSIRTの試験範囲の違いに疑問を抱く方がいらっしゃいました。また、インターン期間中に脆弱性を見つけることを目標に掲げて検証に取り組む方もいらっしゃいました。インターン生は、検証やツールの使い方など技術的な面以外にも、脆弱性や品質保証に対する考え方など多くの気づきを得ることができたようです。</p> <h2 id="ランチ">ランチ</h2> <p>お昼はCy-PSIRTのメンバーとインターン生でランチをしました。 一緒にご飯を食べながら気軽に雑談することで、Cy-PSIRTメンバーもインターン生のことをより知ることができました。 インターン生の皆さんも、チームの雰囲気を知る機会になっていただけたら嬉しいです。</p> <p><figure class="figure-image figure-image-fotolife" title="ランチ(第1ターム)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231218/20231218155644.png" alt="&#x30E9;&#x30F3;&#x30C1;(&#x7B2C;1&#x30BF;&#x30FC;&#x30E0;)" width="1200" height="647" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ランチ(第1ターム)</figcaption></figure> <figure class="figure-image figure-image-fotolife" title="ランチ(第2ターム)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231218/20231218160013.png" alt="&#x30E9;&#x30F3;&#x30C1;(&#x7B2C;2&#x30BF;&#x30FC;&#x30E0;)" width="1200" height="628" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ランチ(第2ターム)</figcaption></figure></p> <h2 id="脆弱性評価">脆弱性評価</h2> <p>続いては、脆弱性評価の時間です。サイボウズでは、脆弱性の深刻度をスコアリングする手法の一つである<a href="https://www.ipa.go.jp/security/vuln/scap/cvssv3.html">CVSSv3</a>を用いて脆弱性評価を行なっています。インターン生の皆さんには、CVSSv3の考え方やスコアの付け方を学んでいただき、過去サイボウズ製品に存在していた脆弱性を評価していただきました。</p> <p><figure class="figure-image figure-image-fotolife" title="脆弱性評価(第2ターム)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231215/20231215214745.png" alt="&#x8106;&#x5F31;&#x6027;&#x8A55;&#x4FA1;(&#x7B2C;2&#x30BF;&#x30FC;&#x30E0;)" width="1200" height="664" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>脆弱性評価(第2ターム)</figcaption></figure></p> <p>後半では、インターン生の皆さんが算出したスコアや考え方についてお互いに共有しました。 他の人の意見を聞いて、想定するシナリオや脆弱性の捉え方が人によって異なることを実感していただけたと思います。 Cy-PSIRT内でも、各評価項目に対する考え方やスコアの付け方に関する議論がよくありますが、他の人の意見を聞いたり議論を行うことで、より公正な評価を行うことができます。 この時間を通して、スコアをつけることの難しさやシナリオの考え方など新たな学びを得る機会になっていれば嬉しいです。</p> <h2 id="外部通報対応">外部通報対応</h2> <p>最後のコンテンツは外部通報対応です。この時間では、サイボウズが運営している<a href="https://cybozu.co.jp/products/bug-bounty/">脆弱性報奨金制度</a>の対応業務を体験していただきました。 外部通報対応は、脆弱性の再現確認や検証、脆弱性評価、また製品に対する理解など、これまでの時間で体験した業務の知識が必要となる集大成です。 はじめに、脆弱性報奨金制度の仕組みや実施することの意義を学んでいただき、その後「トリアージ」「再現確認」「評価」の一連の流れを演習していただきました。 このコンテンツを通して、脆弱性報告の書き方について気づきを得た方やバグハンティングに興味を持っていただいた方もいました。脆弱性報奨金制度の運営側について知る良い機会になったのではないでしょうか。</p> <h2 id="その他の紹介">その他の紹介</h2> <p>最終日には、インターン生にCy-PSIRTの取り組みや普段の業務の雰囲気を知ってもらう時間を設けました。 Cy-PSIRTではコンテンツで紹介した業務の他にも、たくさんの活動や取り組みを行っています。 今回は以下の取り組みについて紹介しました。</p> <ul> <li>脆弱性検証の自動化</li> <li>モバイル検証</li> <li>インシデントハンドリング</li> <li>チーム内での勉強会</li> <li>WG / タスクフォース<a href="#f-5fea9175" id="fn-5fea9175" name="fn-5fea9175" title="Cy-PSIRTではチーム内の課題や問題意識をもとに有志で小さなチームを作り様々な活動を行っています。">*1</a></li> <li>チーム内イベント</li> <li>社外のセキュリティイベント/カンファレンス</li> </ul> <p>インターン生は自動化やモバイルなど検証に関わる部分に興味を示している方が多かったようです。<br> それぞれの取り組みについて、目的や背景、現状の運用について説明しました。 また、チーム内のイベントや社外イベントへの参加を通じた情報収集や交流についても簡単にご紹介しました。</p> <p>さらに、業務環境のスクリーンショットを一部紹介し、普段の業務の様子を知っていただきました。 脆弱性検証、評価などコンテンツで学んだ業務について、実際の運用や議論の様子をご覧いただきました。 また、開発チームとCy-PSIRTとのやりとりも紹介し、ユーザー企業のセキュリティチームとしての役割についても理解を深めていただきました。 こうした時間を通じて、業務のイメージやチームの雰囲気がより鮮明になったり、新たに得られる気づきがあれば嬉しいです。</p> <h2 id="成果報告会懇親会">成果報告会・懇親会</h2> <p>最終日の夕方は、kintoneアプリを使った形式で成果報告会を実施しました。 インターン生の皆さんには、「やったこと」「学び、気づき」「やってみたいこと」「感想」をベースに、各コンテンツについて事前に振り返りを記入していただきました。 成果報告会では、その振り返りの内容をもとに各コンテンツについての学びや感想を共有していただきました。</p> <p><figure class="figure-image figure-image-fotolife" title="成果報告会(第1ターム)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231219/20231219170816.png" alt="&#x6210;&#x679C;&#x5831;&#x544A;&#x4F1A;&#xFF08;&#x7B2C;1&#x30BF;&#x30FC;&#x30E0;&#xFF09;" width="1200" height="682" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>成果報告会(第1ターム)</figcaption></figure> <figure class="figure-image figure-image-fotolife" title="成果報告会(第2ターム)"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231219/20231219170924.png" alt="&#x6210;&#x679C;&#x5831;&#x544A;&#x4F1A;&#xFF08;&#x7B2C;2&#x30BF;&#x30FC;&#x30E0;&#xFF09;" width="1200" height="555" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>成果報告会(第2ターム)</figcaption></figure></p> <p>夜には懇親会を行いました。去年に続き、Cy-PSIRTメンバーに聞いてみたいことを事前に登録していただき、その内容をもとに雑談しながら夕食を食べました。 業務のことから業務以外の話題までとても盛り上がり、非常に楽しい時間となりました。</p> <h2 id="いただいた感想">いただいた感想</h2> <p>今回のインターンでは、第1タームと第2タームそれぞれ4名の方にご参加いただきました。 参加者からいただいた感想を一部ご紹介いたします。</p> <blockquote><p>メンターさん以外の方も含めた社員の皆さんや他のインターン生、青野さんともお話させていただく機会があり、チームワークを大切にするサイボウズの風土を感じられました。また、座学と演習のプログラム全体が繋がっていて、手を動かしながらCy-PSIRTや事業会社のセキュリティ業務について理解していけるインターンでした。参加して本当に良かったです。</p></blockquote> <hr /> <blockquote><p> 実際の業務の流れを体験することで、プロダクトセキュリティエンジニアという仕事への解像度が高くなりました。脆弱性評価や外部通報対応を通して、セキュリティの楽しさと難しさを再実感しましたし、メンターさんからのフィードバックをいただくことでより理解が深まりました。また、4日間でサイボウズの文化やチームワークの強さを知ることができたという意味でも、非常に参加して良かったと思いました。</p></blockquote> <hr /> <blockquote><p> 今回のインターンでは、サイボウズの「チームワーク」について特に注目して参加していました。参加する前は、チーム内での助け合いが活発なのかなと考えていたが、実際には、チームを超えて助け合いが行われていました。チーム内だけでなく、サイボウズ社内でチームワークを発揮し助け合ってチームワークを高められる最高のグループウェアを作る、という社風がとても素敵だと感じました。大変有意義な4日間でした。非常に楽しかったです。</p></blockquote> <h2 id="おわりに">おわりに</h2> <p>今年も去年に続き、オンラインでの開催となりましたが2タームとも無事に実施することができました。 4日間と短い間でしたが、会社やチームの雰囲気、プロダクトセキュリティについて知る良い機会となったと思います。 また、インターン生同士が積極的に交流を深める様子も見られ、メンターとして大変嬉しく思いました。</p> <p>インターンで用意されているコンテンツは、実際の業務をもとにした実践的な内容で構成されています。 ユーザー企業のセキュリティチームとしての役割や、Cy-PSIRTならではの業務についても学んでいただけたと思います。 インターン生の皆さんが、この経験や学びを今後の活動に役立ててくださることを願っています。今後の活躍を楽しみにしています!</p> <p>Cy-PSIRTでは一緒に働く仲間を募集中です。ご興味のある方は以下をご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fnewgrad%2Fsecurity-engineer2025.html" title="プロダクトセキュリティエンジニア職 2025年卒 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/newgrad/security-engineer2025.html">cybozu.co.jp</a></cite></p> <div class="footnote"> <p class="footnote"><a href="#fn-5fea9175" id="f-5fea9175" name="f-5fea9175" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">Cy-PSIRTではチーム内の課題や問題意識をもとに有志で小さなチームを作り様々な活動を行っています。</span></p> </div> yellow-sabotech モバイルエンジニアの入社後チーム体験でスクラムの理解が深まった話 hatenablog://entry/6801883189067024291 2023-12-16T08:00:00+09:00 2023-12-16T08:00:01+09:00 こんにちは。 モバイルエンジニアの臼井(@usuiat)です。 私は2023年9月にサイボウズに入社し、11月からGaroonモバイルのAndroidアプリの開発を担当しています。 サイボウズにモバイルエンジニアとして入社したメンバーは、正式配属前に、各開発チームを順に体験します。 この記事では、私が入社してから、各チームの体験を経てGaroonモバイルチームに配属が決まるまでの間に、体験したことや感じたことを紹介したいと思います。 サイボウズのモバイル開発に興味がある方に、チームの雰囲気が伝われば幸いです。 モバイルアプリの開発体制 サイボウズでは主に、kintone、サイボウズOffice… <p>こんにちは。 モバイルエンジニアの臼井(<a href="https://twitter.com/usuiat">@usuiat</a>)です。 私は2023年9月にサイボウズに入社し、11月からGaroonモバイルのAndroidアプリの開発を担当しています。</p> <p>サイボウズにモバイルエンジニアとして入社したメンバーは、正式配属前に、各開発チームを順に体験します。 この記事では、私が入社してから、各チームの体験を経てGaroonモバイルチームに配属が決まるまでの間に、体験したことや感じたことを紹介したいと思います。 サイボウズのモバイル開発に興味がある方に、チームの雰囲気が伝われば幸いです。</p> <h2 id="モバイルアプリの開発体制">モバイルアプリの開発体制</h2> <p>サイボウズでは主に、kintone、サイボウズOffice、Garoonの3つの製品のモバイルアプリを開発していて、それぞれにiOSアプリとAndroidアプリがあります。</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/u/usuiat/20231215/20231215140753.png" alt="&#x30E2;&#x30D0;&#x30A4;&#x30EB;&#x30A2;&#x30D7;&#x30EA;&#x306E;&#x958B;&#x767A;&#x4F53;&#x5236;" width="1200" height="629" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>モバイルアプリの開発体制</figcaption></figure></p> <p>サイボウズにはモバイルエンジニアが、iOSとAndroid合わせて20人ほど所属しています。 iOSとAndroidのエンジニアはそれぞれ3つずつのチームに分かれており、一つのチームが一つのアプリを担当する体制になっています。</p> <p>製品開発チームは、iOS/Androidエンジニアの他に、PM、デザイナー、QA、テクニカルライター、リサーチャーなどのメンバーで構成されます。 これらのメンバーで、スクラム形式でアプリを開発しています。</p> <p>開発体制を図にすると、マトリクスっぽい形になっています。 縦の開発チームに対して、モバイルエンジニアは横のつながりを持っていて、iOS/Androidの技術のキャッチアップを一緒に行ったり、各製品開発で困っていることを相談しあったりしています。</p> <h2 id="チーム体験とは">チーム体験とは</h2> <p>サイボウズに入社したモバイルエンジニアは、最終的な配属決定の前に、各チームを体験する期間が用意されています。 体験期間は標準で1チームあたり3週間ですが、状況により柔軟に決めています。 サイボウズでは1週間のスプリントでスクラム開発をしているので、各チームを3スプリントずつ体験することになります。 入社から2ヶ月程度をかけて3チームを順番に体験し、その後に最終的な配属チームが決まります。</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/u/usuiat/20231215/20231215140856.png" alt="&#x30C1;&#x30FC;&#x30E0;&#x4F53;&#x9A13;&#x306E;&#x65E5;&#x7A0B;" width="1200" height="188" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>チーム体験の日程</figcaption></figure></p> <p>私の場合は9月1日に入社し、9月の前半はサイボウズ全体やモバイル職能全体のオンボーディングを受け、9月中旬に開催されたDroidKaigiをエンジョイし、その後チーム体験が始まりました。 チーム体験はサイボウズOffice → kintone → Garoonの順に実施しました。 そして、11月下旬に、Garoonチームに配属が決まりました。</p> <p>チーム体験の期間中は、基本的に正規メンバーと同じように開発に携わります。 スクラムイベントにも参加します。 ソースコードの実装やレビューも初日から行います。 とはいえドメイン知識など分からないところも多いので、基本はペアプロやモブプロで色々と教えてもらいながらの作業が多いです。</p> <p>また、体験期間は、アプリそのものの開発だけでなく、kintone、Office、Garoonといった製品自体についての理解を深めるための期間でもあります。 色々な社内資料を読み込んで勉強したり、PMやデザイナーと話をする機会を作ってモバイルエンジニア以外の職能の業務を理解したりすることにも力を入れました。</p> <p>では、ここからは私が各チームの体験で感じたことを簡単に振り返っていきます。 なお、私はAndroidエンジニアなので、以降はAndroidの3チームの紹介になりますが、入社から配属までの流れはiOSも基本的に同じです。</p> <h2 id="サイボウズ-Office-チーム体験">サイボウズ Office チーム体験</h2> <p>サイボウズOfficeは、主に中小企業向けのグループウェア製品で、メールやスケジュールなどたくさんの機能があります。 モバイルアプリでも一通りの機能を使うことができます。 現状はWebViewベースの画面も多いのですが、優先度の高い機能から順に、Jetpack Composeを用いたネイティブUIに置き換えを進めています。</p> <p><figure class="figure-image figure-image-fotolife" title="サイボウズOffice Androidアプリ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/usuiat/20231215/20231215140941.png" alt="&#x30B5;&#x30A4;&#x30DC;&#x30A6;&#x30BA;Office Android&#x30A2;&#x30D7;&#x30EA;" width="1200" height="634" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>サイボウズOffice Androidアプリ</figcaption></figure></p> <p>私は前職がウォーターフォール型の開発だったので、スクラム開発を体験するのはサイボウズOfficeチームが初めてでした。 スクラムイベントがどんなものなのかよくわかっていない状態でしたが、実際に参加しながら覚えていきました。</p> <p>初めてのスクラム開発を体験して一番感動したのは、毎週何らかの進捗が出るスピード感でした。 また、振り返りを行って自分たちでプロセスを改善していく仕組みが機能していることにも感動しました。 年単位のウォーターフォール開発の現場から移ってきた私にはとても新鮮でした。</p> <p>私が体験したときは、スケジュール機能の実装を進めているところでした。 私はJetpack ComposeのUI作成がもともと得意だったので、UI部分は積極的に実装に参加できました。 一方でAPIとの連携やログイン処理などの知識が足りない部分は、モブプロでいろいろ教えてもらいながらの参加になりました。</p> <h2 id="kintone-チーム体験">kintone チーム体験</h2> <p>kintoneは、色々な業務を効率化する「アプリ」を作れるサービスです。 kintoneモバイルでは、kintone上の「アプリ」の内容を閲覧したり、レコードを追加したり、コメントでチームメンバーとコミュニケーションを取ったりできます。</p> <p><figure class="figure-image figure-image-fotolife" title="kintone Androidアプリ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/usuiat/20231215/20231215141158.png" alt="kintone Android&#x30A2;&#x30D7;&#x30EA;" width="1200" height="632" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>kintone Androidアプリ</figcaption></figure></p> <p>kintoneモバイルチームのスクラムの取り組み方は、Officeチームとはかなり違ったので、Officeチームの体験でスクラムについてだいたい分かった気でいた私は、少し戸惑いました。 しかしこれが、チームの状況やメンバーの考え方によって、スクラムにはいろいろな取り組み方があって良いんだと気づくきっかけになりました。</p> <p>kintoneモバイルチームは、開発プロセスの改善に積極的なチームでした。</p> <p>kintoneモバイルはサイボウズのモバイルアプリの中でも歴史が長いアプリです。 その影響もあって、モジュール間の依存が複雑に絡み合うなど、スピーディーな開発が難しい状態になってしまっていました。 この状況を改善するためのソースコードの構成変更を、スプリントの中でタスクとして取り組みました。</p> <p>また、アプリのリリースのあるべき姿についても議論が行われていました。 リリースサイクルを早めることは、プロダクトの価値を素早くユーザーに提供することにつながります。 しかし、高速なリリースのためには回帰テストの工数を削減する必要があります。 また、マーケティング的に意味のある機能追加を含むリリースと、バグ修正などの軽微なリリースの扱いをどうやって分けるかという課題もあります。 そういった検討事項を洗い出して、kintoneアプリにとって最適なリリースの仕方はどのようなものか、チームで議論しました。</p> <h2 id="Garoon-チーム体験">Garoon チーム体験</h2> <p>Garoonは、主に100人以上の大企業向けのグループウェア製品です。 機能的にはサイボウズOfficeと似ている部分もありますが、多言語対応されていたり、システム管理者用の機能が充実しているなどの違いがあります。 Garoonモバイルアプリは、この記事で紹介した3つのアプリの中では一番新しいアプリで、これからどんどんと機能を追加していくフェーズです。</p> <p><figure class="figure-image figure-image-fotolife" title="Garoon Androidアプリ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/u/usuiat/20231215/20231215141153.png" alt="Garoon Android&#x30A2;&#x30D7;&#x30EA;" width="1200" height="632" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Garoon Androidアプリ</figcaption></figure></p> <p>Garoonモバイルチームは、3チームの中で唯一、iOSとAndroidが足並みを揃えて機能開発を進めています。 ひとつのスクラムチームにiOSとAndroidのモバイルエンジニアが参加し、リファインメントやプランニングも一緒に行います。 もちろん、細かい部分でOSによる差異は出てきます。 しかし全体的なUXを考えたときに、Garoonモバイルとして望ましいユーザー体験を、OSによらずに提供できるように、iOSとAndroidで一緒に議論をしています。</p> <p>また、スプリントレビューに開発チーム外のステークホルダーを呼んでインタビューしたのも印象に残る出来事でした。 Garoonの営業担当者に、開発中の新しい画面の印象や使い勝手について聞きました。 Garoonモバイルアプリが想定しているユーザー像に近い人から意見を出してもらうことによって、開発チーム内では意見が割れていた仕様について納得して決めることができたり、アプリの使用シーンが具体的にイメージできるようになったりといった収穫がありました。 そして何より、開発に対するモチベーションが上がりました。</p> <h2 id="そして配属決定">そして配属決定</h2> <p>Garoonチームの体験期間が終わりに近づいた11月中旬、モバイルエンジニアのマネージャーと、配属について相談することになりました。 私の希望を伝えて、チーム側の受け入れ希望とのすり合わせを行う場です。</p> <p>数日前からソワソワしはじめ、希望を伝えるときはとても緊張しました。 この先のキャリアの方向性を決める重要な選択です。 チームに貢献できるか、私自身が成長できるか、プロダクトに共感できるか、などいろいろ考えました。 配属先の決定をマネージャーに一任することもできたのですが、せっかく「自立と議論」を大切にするサイボウズに入ったのだから、自分の意思をしっかり伝えたいと考えました。</p> <p>そして私からは、Garoonチームに参加したいという希望を伝えました。 マネージャーは各チームに対して事前に、私を受け入れたいかどうかをヒアリングしてくれていて、Garoonチーム側も私を受け入れたいといってくれていたため、すんなりと配属が決まりました。</p> <p>私がGaroonを選んだ理由は、私が加わることによってチームに起きる変化が、一番大きそうだったからです。</p> <p>現状のGaroonモバイルアプリは、いったんWebViewで一通りの機能を提供していますが、これからはiOS/Androidネイティブ実装の割合を増やして、モバイルならではの使いやすいアプリに進化させていこうとしています。 そのタイミングで、私の得意とするUI周りの知見を活かせると良いなと思っています。</p> <p>また、人数的な事情もありました。 これまでのGaroonチームのAndroidエンジニアは2人体制でした。 iOSエンジニアは3人体制だったので、私がチームに加わることによって、今後もiOSとAndroidが足並みを揃えて開発していければ良いなという思いがありました。</p> <h2 id="チーム体験で得られたもの">チーム体験で得られたもの</h2> <p>3つのチームを体験することによって得られた学びの中で一番大きかったのは、スクラムの取り組み方に唯一の正解はなく、チームの数だけやり方がある、ということに気づけたことです。 前述したように、私はサイボウズに入社して初めてスクラム開発に取り組みました。 もし最初からどこかのチームに配属されていたら、そのチームのスクラムのやり方が絶対的な唯一の正解だと勘違いしていたと思います。 しかし実際は、3つのチームはそれぞれのやり方でスクラムに取り組んでいて、何を重要課題として考えるかもチームそれぞれでした。 教科書的に「正しいスクラム」を求めるのではなく、自分たちで議論して自分たちの正解を探すことが大事なのだと気づけました。</p> <p>また、Androidエンジニアはもちろん、一緒に開発したPM、デザイナー、QAなど、各チームのメンバーと面識ができたことも大きな収穫でした。 サイボウズはオープンな組織なので、自分のチーム以外の情報もたくさん目に入ってきます。 チーム体験でお世話になった方々の発言を見かけると、他チームの出来事でも、今までより身近に感じるようになりました。 自社の複数のプロダクトに興味を持てるようになったという意味で、自分にとってプラスになったと感じています。</p> <h2 id="終わりに">終わりに</h2> <p>あっという間のチーム体験期間でしたが、各チーム3週間ずつの期間を確保してもらえたおかげで、じっくりと各チームを体験することができました。 自分にとっても、そしてたぶんモバイル開発チーム全体にとっても、よい経験にできたと思います。 配属も無事に決まったので、これからしっかりとプロダクトに向き合って、Garoonモバイルの価値を高めていきたいと思います!</p> usuiat 持続可能なフロントエンドのテストコードを書くために大切にしていること hatenablog://entry/6801883189062610358 2023-12-13T12:37:01+09:00 2023-12-13T12:37:01+09:00 こんにちは、kintone フロントエンドリアーキテクチャプロジェクト(フロリア)でエンジニアをしているはた丸です。 昨今は何かとSDGsが話題になる世の中なので、フロントエンドのテストコードも持続可能なものを作っていきたいと考えています。 この記事ではフロリアのチームの1つであるMiraチームがテストを自動化するうえで意識していることをお伝えします。 自分のチームに自動テストを導入できるか不安な方、導入後に継続できるか心配な方に寄り添えると嬉しいです。 はじめに、Miraチームの概要やテスト手法の全体像について知りたい方は、次の記事をご覧ください。 目次 📝 前提 ♻️ Integratio… <p>こんにちは、<a href="https://blog.cybozu.io/entry/2022/02/04/171154">kintone フロントエンドリアーキテクチャプロジェクト(フロリア)</a>でエンジニアをしているはた丸です。</p> <p>昨今は何かとSDGsが話題になる世の中なので、フロントエンドのテストコードも持続可能なものを作っていきたいと考えています。</p> <p>この記事ではフロリアのチームの1つである<a href="https://blog.cybozu.io/entry/2022/04/14/110000">Miraチーム</a>がテストを自動化するうえで意識していることをお伝えします。</p> <p>自分のチームに自動テストを導入できるか不安な方、導入後に継続できるか心配な方に寄り添えると嬉しいです。</p> <p>はじめに、Miraチームの概要やテスト手法の全体像について知りたい方は、次の記事をご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2022%2F10%2F17%2F110000" title="フロントエンド刷新プロジェクトを成功に導くためのテスト手法の紹介 - Cybozu Inside Out | サイボウズエンジニアのブログ" 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></p> <hr /> <p>目次</p> <ul class="table-of-contents"> <li><a href="#-前提">📝 前提</a></li> <li><a href="#️-Integrationテストをがんばる">♻️ Integrationテストをがんばる</a><ul> <li><a href="#a11yを意識した実装をしておく">a11yを意識した実装をしておく</a></li> <li><a href="#ヘルパー関数を用意する">ヘルパー関数を用意する</a></li> <li><a href="#QAとエンジニアで協調する">QAとエンジニアで協調する</a></li> </ul> </li> <li><a href="#️-E2Eテストをがんばりすぎない">♻️ E2Eテストをがんばりすぎない</a></li> <li><a href="#おわりに">おわりに</a><ul> <li><a href="#サイボウズではフロントエンドエンジニアを募集しています">サイボウズではフロントエンドエンジニアを募集しています。</a></li> </ul> </li> </ul> <hr /> <h2 id="-前提">📝 前提</h2> <p>フロリアでは主に React と <a href="https://testing-library.com/docs/react-testing-library/intro/">Testing Library</a> を利用しており、記事内で紹介する記述には同様の技術スタックを前提としたものがあります。</p> <h2 id="️-Integrationテストをがんばる">♻️ Integrationテストをがんばる</h2> <p>MiraチームではTesting Trophyの考え方に沿ってテストの方針を決めており、Integrationテストの比重を大きくしています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftestingjavascript.com" title="Testing JavaScript with Kent C. Dodds" 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></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkentcdodds.com%2Fblog%2Fstatic-vs-unit-vs-integration-vs-e2e-tests" title="Static vs Unit vs Integration vs E2E Testing for Frontend Apps" 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></p> <p>Testing Trophyではテストの重要性や優先順位をトロフィーの形で表現しており、その中で開発コストに対する品質保証面のコスパが優れているIntegrationテストの重要性について紹介されています。</p> <p>詳細については上記ブログをご覧いただきたく、ここではIntegrationテストの自動化に効率的に取り組むために、Miraチームで体感したTipsをご紹介します。</p> <h3 id="a11yを意識した実装をしておく">a11yを意識した実装をしておく</h3> <p>Integrationテストの実装時に苦労することの1つに、画面の奥底にある要素を取得するためにコードや画面をいったりきたりしながら試行錯誤を繰り返す、ということがあります。</p> <p>しかし、機能実装時にあらかじめa11yを意識して構築しておけば、Testing Libraryで推奨される <code>getByRole</code> による要素取得を活用できるようになります。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">// ダイアログが表示されている</span> <span class="synStatement">const</span> dialog = screen.getByRole(<span class="synConstant">'dialog'</span>) expect(dialog).toBeInTheDocument(); <span class="synComment">// ダイアログの中身の表示確認</span> <span class="synStatement">const</span> title = within(dialog).getByRole(<span class="synConstant">'heading'</span>, <span class="synIdentifier">{</span> level: 2 <span class="synIdentifier">}</span>); expect(title).toHaveTextContent(<span class="synConstant">'cybozu'</span>); <span class="synStatement">const</span> closeButton = within(dialog).getByRole(<span class="synConstant">'button'</span>, <span class="synIdentifier">{</span> name: <span class="synConstant">'close'</span> <span class="synIdentifier">}</span>); expect(closeButton).toBeInTheDocument(); </pre> <h3 id="ヘルパー関数を用意する">ヘルパー関数を用意する</h3> <p>といっても、それでも取得するのに苦労する要素はあります。そんなときはヘルパー関数を用意しましょう。苦労するのは、はじめに頑張ったあなただけで十分です。</p> <p>同様にページ共通で使う要素なども積極的に切り出しておきましょう。</p> <p>このような共通化や再利用性の向上は、後述のQAとの協調においても助けになります。</p> <pre class="code lang-javascript" data-lang="javascript" data-unlink><span class="synComment">/** Notificationを得る */</span> <span class="synStatement">export</span> <span class="synStatement">const</span> getNotifier = () =&gt; screen.getByTestId(<span class="synConstant">'Notifier'</span>); <span class="synComment">/** Notificationの閉じるボタンを得る */</span> <span class="synStatement">export</span> <span class="synStatement">const</span> getNotifierCloseBtn = () =&gt; within(getNotifier()).getByRole(<span class="synConstant">'button'</span>, <span class="synIdentifier">{</span> name: <span class="synConstant">'close'</span> <span class="synIdentifier">}</span>); </pre> <h3 id="QAとエンジニアで協調する">QAとエンジニアで協調する</h3> <p>フロリアでは、QAと実装者が協力する形でIntegrationテストの設計・実装・レビューを進めています。逆にUnitテストにおいては、単一のコンポーネントやhooksを対象とするため、実装者の裁量に一任しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2023%2F07%2F03%2F162424" title="結合テストの自動化にQAはどうかかわっていったか - Cybozu Inside Out | サイボウズエンジニアのブログ" 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></p> <p>上記ブログに詳細なプロセスが書かれていますが、特に重要と考えるのは</p> <ul> <li>機能実装前にIntegrationテストの設計(ケース策定)が完成している</li> <li>テスト設計にエンジニアも参加している</li> <li>テストコードレビューにQAも参加している</li> </ul> <p>の3点です。フロリアでは実装から不具合発見までのフィードバックを早くすることを目標にし、手戻りを減らすことを心がけています。 そのためにはエンジニアが実装と同時にIntegrationテストの自動化に取り組み、実装直後にQAからフィードバックを得られる仕組みが大切だと考えています。</p> <h2 id="️-E2Eテストをがんばりすぎない">♻️ E2Eテストをがんばりすぎない</h2> <p>Miraチームでは、正常系の基本的なユースケースのフローを確認する「ハッピーパス」の方針で、E2Eテストを自動化しています。また、Integrationテストで充足すると考えられるE2Eのケースは排除し、E2Eテストをスリム化しています。</p> <p>もしかすると、これからテストの自動化に取り組もうとしているチームでは、「とりあえずE2Eテストからはじめよう」と考えている方がいるかもしれませんが、おすすめはできません。</p> <p>実はkintoneの開発でも、これまではE2Eでの自動テストとQAによる手動テストが主でしたが、いくつか課題がありました。</p> <ul> <li>実行に時間がかかるためCIを継続・反復しづらい</li> <li>実装してからフィードバックを得るまでに時間がかかる</li> <li>メンテナンスコストが高い</li> </ul> <p>これらに対する対策として、フロリアではIntegrationテストを重視する方針を立てています。</p> <p>「それでもチームナレッジを貯めるためにどうしてもブラウザテストから始めていきたい!」という場合はリグレッションテストなど、ケース数が少なく、将来的にも肥大化しづらいものを対象としてスタートしていくことをおすすめします。</p> <h2 id="おわりに">おわりに</h2> <p>今回紹介した内容の他にもフロリアでは様々なテストナレッジが溜まっています。テストカテゴリの他の記事もぜひご覧ください!</p> <h4 id="サイボウズではフロントエンドエンジニアを募集しています">サイボウズではフロントエンドエンジニアを募集しています。</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Ffront-end-engineer-kintone.html" title="フロントエンドエンジニア(kintoneアーキテクチャ刷新PJ)キャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fsenior-front-end-engineer-kintone.html" title="シニアフロントエンドエンジニア(kintoneアーキテクチャ刷新PJ)キャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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></p> yellow-sabotech 入社1ヶ月のQAエンジニアにフロントエンド刷新チームのテスト業務について聞いてみた! hatenablog://entry/6801883189062395842 2023-12-06T17:57:52+09:00 2023-12-06T17:57:52+09:00 こんにちは!kintone フロントエンド刷新プロジェクト(フロリア)のReactoneチームでQAエンジニアとして活動しているTsuneです。 本記事では、10月に配属された新メンバーのMiyakeさんに、入社から1ヶ月経ってみての率直な感想や現在の業務についてインタビューをしたので、紹介していきたいと思います! フロリアについてはこちらの記事をご覧ください。 blog.cybozu.io 自己紹介 まずは私たちの経歴や背景を簡単に紹介します。 聞き手:Tsune 2012年からkintoneQAに所属。2021年からはフロリアのReactoneチームに参加 趣味は温泉とドライブ 話し手:M… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231206/20231206144101.png" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは!kintone フロントエンド刷新プロジェクト(フロリア)のReactoneチームでQAエンジニアとして活動しているTsuneです。</p> <p>本記事では、10月に配属された新メンバーのMiyakeさんに、入社から1ヶ月経ってみての率直な感想や現在の業務についてインタビューをしたので、紹介していきたいと思います!</p> <p>フロリアについてはこちらの記事をご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2022%2F02%2F04%2F171154" title="kintone フロントエンドリアーキテクチャプロジェクトで大切にしていること - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2022/02/04/171154">blog.cybozu.io</a></cite></p> <h2 id="自己紹介">自己紹介</h2> <p>まずは私たちの経歴や背景を簡単に紹介します。</p> <h3 id="聞き手Tsune">聞き手:Tsune</h3> <ul> <li>2012年からkintoneQAに所属。2021年からはフロリアのReactoneチームに参加</li> <li>趣味は温泉とドライブ</li> </ul> <h3 id="話し手Miyake">話し手:Miyake</h3> <ul> <li>不動産(営業)→ 第三者検証(QA)→ 物流系SaaS(QA)→ サイボウズ(QA)</li> <li>福岡県在住の2児の父。ハイボールが好き</li> <li>テスト(UI/APIテスト、テスト自動化)、採用(人と出会うこと)が好き</li> </ul> <h2 id="入社から1ヶ月経って感じたこと">入社から1ヶ月経って感じたこと</h2> <p><strong>Tsune: 入社して1ヶ月が経ちましたが、サイボウズで働いてみてどのように感じましたか?</strong></p> <p><strong>Miyake:</strong> まず初めに感じたのは、入社時のオンボーディングや入社後のフォローが手厚くていいなーと思いました!<br/> 会社共通のオンボーディングは初めの1週間で行われ、Reactoneチームに入った後のOJTやフォローは6ヶ月かけて実施されるので、非常に手厚いほうだと思います。<br/> また、OJTのタスク一覧がkintone上で用意されているので、入ったばかりでも迷子にならずに進めていくことができました。</p> <p><figure class="figure-image figure-image-fotolife" title="ReactoneチームのOJTのタスク一覧"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231128/20231128163746.jpg" width="1200" height="696" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ReactoneチームのOJTのタスク一覧</figcaption></figure></p> <p><strong>Tsune: 一般的にオンラインでの入社は心細かったり、繋がりが作りにくいと聞きますが、大丈夫でしたか?</strong></p> <p><strong>Miyake:</strong> フルリモートでのオンボーディングは初めてだったので心配していましたが、蓋を開けたら全然問題なかったです。<br/> リモート上で雑談の機会も多かったし、フルリモートとはいえ一部のチームメンバーとは福岡オフィスで直接顔を合わせてカレーを食べに行きました(笑)<br/> そこで一気に心の距離的なものが近づいたので、業務上でわからないことが出てきても気軽に質問して解決することができました!</p> <p>次に感じたこととしては、雑談がしやすい環境でいいなーと思いました!<br/> オンボーディングの中で様々なチームの人と雑談を組んでくれていたからそう感じた部分もありますが、会社全体的に雑談を大切にしていると思います。<br/> 普段から雑談などでコミュニケーションをとっていることで、いざ何かしら議論が必要な状況になっても、感情的にならずに建設的な議論が可能になっていると感じました。</p> <p><strong>Tsune: サイボウズは資格や書籍の支援をはじめ、研修費やイベント参加費の支援など、様々な支援制度があります。Miyakeさんも制度を利用して、JaSST’23 Kyushuに参加されていましたが、どうでしたか?</strong></p> <p><strong>Miyake:</strong> 実をいうと、このようなQAイベントのリアル参加は初めてだったんですよね。<br/> せっかく九州で開催されるなら行きたいなーと思ってチームメンバーに相談したら、会社から参加費の補助も出るし自由に行っていいよーって言ってもらえて安心しました。<br/> 現地で他社のQAの方とテスト自動化の話とかいろいろとお話できたので行ってよかったと思います。</p> <h2 id="QAのお仕事について">QAのお仕事について</h2> <p><strong>Tsune: 前職のQA業務とギャップを感じた部分はありましたか?</strong></p> <p><strong>Miyake:</strong> これまで自社で作っている製品をテスト以外の業務で使うことはなかったのですが、サイボウズではkintoneやGaroonをフル活用して業務を行っているので、かなりレアなんじゃないかなと思います。<br/> 正直、入ったばかりのときは製品の使い方がわからず戸惑いましたね......<br/> ただ、直感的に操作してわかるようなUIになっているので、1週間もすれば普段の業務では問題なく使えるようになりましたね。</p> <p><strong>Tsune:</strong> 実際のテスト業務はとてもスムーズに進められているなと感じました。ユーザーの視点に立って感じる違和感も報告されていて、今までのQAの経験を活かして即戦力で活躍されているなと思います。何か意識していることはありますか?</p> <p><strong>Miyake:</strong> 正直ある程度QAの経験値(この機能は何のために存在しているんだろう、という視点)は必要だと思っています。<br/> ただ、逆に言うと「この機能は何のために存在しているんだろう」という視点さえ持っていれば全然やっていけると思います。みんなフォローしてくれるし安心してチームに入って業務ができています。</p> <p><strong>Tsune: 業務を進めていくことで困ったことはありましたか?</strong></p> <p><strong>Miyake:</strong> 初めて触る機能なので、テスト設計を行う際にどういうテスト観点があるかわからず初めは困りました。<br/> しかし、kintone上にテストケースがたまっていたのでテスト観点を流用することができ、スムーズに解決できました。<br/> 今後もテストケースを流用して少し修正するだけでテスト設計が完成する場面も多いと思うので、効率よくQA業務ができそうだと感じています。</p> <p>また、過去の不具合の履歴もためられているので、何か不具合を発見したときに過去の履歴を探して「あーこういう背景で改修しないまま残ってるんだー」と理解することができ、余分なコミュニケーションが発生しないので嬉しいですね。</p> <p><figure class="figure-image figure-image-fotolife" title="Reactoneチームのテストケース"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231128/20231128164001.jpg" width="1200" height="574" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Reactoneチームのテストケース</figcaption></figure></p> <h3 id="Reactoneチームのテストの進め方について">Reactoneチームのテストの進め方について</h3> <p><strong>Tsune: 実際にReactoneチームに入ってどのような業務を経験しましたか?</strong></p> <p><strong>Miyake:</strong> まだ右も左もわからない状況でしたが、各スクラムイベントに参加させてもらい、色々なことをやりました(笑)</p> <ul> <li>品質保証、品質を高める活動</li> <li>テスト計画</li> <li>テスト設計、実施</li> <li>テスト自動化への取り組み</li> </ul> <p><strong>Tsune:</strong> Reactoneチームは現在以下のような開発プロセスですが、テストやテスト自動化の進め方についてどう感じましたか?</p> <p><figure class="figure-image figure-image-fotolife" title="eactoneチームのテストの進め方"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231128/20231128164035.jpg" width="1200" height="353" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Reactoneチームのテストの進め方</figcaption></figure></p> <p>Reactoneチームの開発の流れについては、「<a href="https://blog.cybozu.io/entry/2023/11/10/131530">BARフロントえんどう#1</a>」に発表資料があるので、詳細はこちらをご覧ください。 <iframe id="talk_frame_1099526" class="speakerdeck-iframe" src="//speakerdeck.com/player/55d7af0067c24b45b5f7b577f1b8df53" 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/undefined_name/ensiniatoqatekorahosuruhurontoentoriakitekutiyakai-fa-noshi-li">speakerdeck.com</a></cite></p> <p><strong>Miyake:</strong> テストの進め方としては、現在1週間スプリントで開発を進めているため、小さく作って小さく出すことを意識してタスクが細分化されている!と感じました。<br/> 私は1週間スプリントは初体験だったのですが、タスクが細分化されていることにより手戻りも小さくテストを進めることができて好きでした。</p> <p>テスト自動化の進め方としては、エンジニアは「コーディング」QAは「テスト設計」という形でお互いの得意分野を分担できていていいなーと思いました。</p> <p><strong>Tsune: テストプロセスで改善したいところはありますか?</strong></p> <p><strong>Miyake:</strong> 現状、機能実装に自動テストの実装が完全に追い付いているわけではないので、QAでも自動テストの実装を巻き取れるようになりたいと思っています。<br/> わからないことがあればエンジニアからのサポートもあるので、コーディングの知識が乏しい私でもテスト自動化に関わっていけそうな気がしています!</p> <p><strong>Tsune:</strong> 今後は自動化テストを安定して運用していく為に、フローの改善やテストケースの整理も必要になるので、Miyakeさんのアイディアや強みも活かせるように話し合いながら進めていけたらいいなと思っています!</p> <h2 id="今後について">今後について</h2> <p><strong>Tsune: 最後に、今後やってみたいことについて教えてもらえますか?</strong></p> <p><strong>Miyake:</strong> 一言でいうと「QA組織が幸せな方向に進む活動」をやっていきたいですね。<br/> これまでは業界経験も浅く私自身のスキルを高めていくことで精一杯だったのですが、現在は少し視野も広くなったので組織がより良い方向に進むような活動をやっていきたいと思っています。<br/> 現状手探りではありますが、採用業務に関わってより良いQAの方に入社いただいたり、QAイベントの運営に関わってQAのスキル向上の手助けなどをできればいいなーーと思っています。</p> <p>また、外部発信などにも積極的に関わっていきたいです。<br/> これまではリソース不足もあり、プロダクト開発が最優先すぎてQA以外の業務に手を出す余裕がなかったんですよね......</p> <p><strong>Tsune:</strong> 素敵ですね!QA組織が活性化されていく未来にワクワクします。<br/> これから一緒に頑張っていきましょう!</p> <h2 id="おわりに">おわりに</h2> <p>最後まで読んでいただきありがとうございました。<br/> この記事を読んでサイボウズのQAに興味を持っていただけたら嬉しいです!<br/> 皆様のエントリーを楽しみにしております。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fqa-engineer.html" title="QAエンジニアキャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/career/qa-engineer.html">cybozu.co.jp</a></cite></p> yellow-sabotech 新卒フロントエンド職同期と「React のドキュメントを読む会」をやってみた hatenablog://entry/6801883189064036388 2023-12-06T09:06:56+09:00 2023-12-06T09:06:56+09:00 こんにちは!kintone フロントエンド刷新プロジェクト(通称フロリア)にてエンジニアをしている キム テフンです。今日は フロントエンド職で新卒入社後にできることの一例として、現在も継続して社内で開催している「React のドキュメントを読む会」の事例を紹介しつつ、面白かった React の学びを少しだけ共有したいと思います! ※ この記事は Cybozu Frontend Advent Calendar 2023 の 5 日目の小さなトリートになります :) 目次 React のドキュメントを読む会を始めたきっかけ 社内勉強会開催のハードル 社内勉強会の内容紹介 みんなでアウトプット 個… <p>こんにちは!kintone フロントエンド刷新プロジェクト(通称<a href="https://blog.cybozu.io/entry/2022/02/04/171154">フロリア</a>)にてエンジニアをしている キム テフンです。今日は <strong>フロントエンド職で新卒入社後にできることの一例</strong>として、現在も継続して社内で開催している「React のドキュメントを読む会」の事例を紹介しつつ、面白かった React の学びを少しだけ共有したいと思います!</p> <p>※ この記事は <a href="https://adventar.org/calendars/9255">Cybozu Frontend Advent Calendar 2023</a> の 5 日目の小さなトリートになります :)</p> <hr /> <p>目次</p> <ul class="table-of-contents"> <li><a href="#React-のドキュメントを読む会を始めたきっかけ">React のドキュメントを読む会を始めたきっかけ</a></li> <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="#BONUS面白かった学びの共有">BONUS:面白かった学びの共有</a><ul> <li><a href="#useRefのベースはuseState">useRefのベースはuseState?</a></li> <li><a href="#keyでstateをリセットする上級技">keyでstateをリセットする「上級技」</a></li> </ul> </li> <li><a href="#あとがき">あとがき</a></li> </ul> <hr /> <h2 id="React-のドキュメントを読む会を始めたきっかけ">React のドキュメントを読む会を始めたきっかけ</h2> <p>きっかけとしては、<a href="https://blog.cybozu.io/entry/2023/09/25/080000">DOGO(どうご)チーム</a>のまっつー(<a href="https://twitter.com/ryo_manba">@ryo-manba</a>)からの誘いでした。ドキュメントを読む会を開催することで周りを巻き込んでみんなで楽しく React の熟練度を高められそうだと思ったのと、「自立と議論」というサイボウズが大事にしている文化にも当てはまるような企画だと思い開催を決意しました!</p> <h2 id="社内勉強会開催のハードル">社内勉強会開催のハードル</h2> <p>「React のドキュメントを読む会」という名前の社内勉強会についての事例紹介ですが、便宜上以下「社内勉強会」と呼びます。</p> <p>社内勉強会の開催までの流れは以下の通りです。</p> <ol> <li>新卒メンバーでテーマ決め相談会(Zoom で雑談ベース)</li> <li>社内勉強会に時間を使うことに対し、各自所属チームメンバーからの了承を得る</li> <li>社内カレンダーに日程登録</li> <li>Slack チャンネルで社内勉強会の宣伝</li> </ol> <div style="text-align: center; margin-left: auto; margin-right: auto; width: 550px;"> <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/y/yellow-sabotech/20231204/20231204171026.png" alt="&#x307E;&#x3063;&#x3064;&#x30FC;&#x306E;&#x5E83;&#x544A;" width="1200" height="201" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>社内勉強会の宣伝をしているまっつー</figcaption></figure> </div> <p>このように、主催者側の社内勉強会開催のハードルはとても低めで、必要性さえあれば誰でも気軽に開催できる雰囲気があります。参加者側からも社内勉強会参加のために取るべきプロセスは特になくて、チームメンバーからの了承を得るくらいで良い流れです。</p> <p>開催のハードルはともかく、社内勉強会の質が気になってなかなか主催することを躊躇ってしまうかもしれませんが、以下のようにガッツリとした社内勉強会でなくても大丈夫です :)</p> <h2 id="社内勉強会の内容紹介">社内勉強会の内容紹介</h2> <p>私を含めた 3 人のフロントエンド職新卒が中心となって社内勉強会を進行しています。内容としては基本的に公式 React ドキュメントを読み通す流れになります。</p> <div style="text-align: center; margin-left: auto; margin-right: auto; width: 550px;"> <figure class="figure-image figure-image-fotolife" title="公式 React ドキュメント"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231204/20231204172249.png" alt="&#x516C;&#x5F0F;&#x30EA;&#x30A2;&#x30AF;&#x30C8;&#x30C9;&#x30AD;&#x30E5;&#x30E1;&#x30F3;&#x30C8;" width="1200" height="507" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>公式 React ドキュメント</figcaption></figure> </div> <p>その過程で、各々が思ったことや疑問を発信及び議論することで理解を深めることを目指します。</p> <div style="text-align: center; margin-left: auto; margin-right: auto; width: 550px;"> <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/y/yellow-sabotech/20231204/20231204172522.png" alt="&#x30B9;&#x30E9;&#x30C3;&#x30AF;&#x3067;&#x306E;&#x3084;&#x308A;&#x53D6;&#x308A;" width="1200" height="409" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>社内勉強会中、Slackでのやり取り</figcaption></figure> </div> <p>参加条件として特に掲げているものはなく、事前準備も不要なふらっと参加できる勉強会です。(直接フロントエンド開発をせずとも React コードに興味を持つ方も気軽に参加してくれています)</p> <p>最小開催人数は 3 人に指定していますが、最大参加人数に制限はなく、平均の参加人数は 4~5 人です。個人的には、参加者が意見を出し合いやすいちょうど良い人数だと感じました。</p> <p>社内勉強会の進行と合わせて、参加者でアウトプットもしてきました。</p> <h3 id="みんなでアウトプット">みんなでアウトプット</h3> <div style="text-align: center; margin-left: auto; margin-right: auto; width: 550px;"> <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/y/yellow-sabotech/20231204/20231204172614.png" alt="&#x793E;&#x5185;&#x52C9;&#x5F37;&#x4F1A;&#x306E;&#x30CE;&#x30FC;&#x30C8;" width="1200" height="552" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>社内勉強会の共有ノート</figcaption></figure> </div> <p>3人の固定メンバーで、React ドキュメントからの学び、参加者からの補足説明資料、参加者の声、などをノートにサクッとまとめています。ノートにまとめることは良い復習にはなりますが、アウトプットとして少し物足りなかったり、もっと生産的なものを残したい場合には以下のように追加でアクションを起こしました。</p> <h3 id="個人でアウトプット">個人でアウトプット</h3> <p>私は社内勉強会のノートで復習するときに感じる疑問を追加で学習し、あとで個人ノートにまとめたりしていましたが、メンバーの中には React 公式ドキュメントの翻訳を改善する形でアウトプットしていました!</p> <div style="text-align: center; margin-left: auto; margin-right: auto; width: 550px;"> <figure class="figure-image figure-image-fotolife" title="React公式ドキュメントのコントリビュート🎉"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231204/20231204173110.png" alt="&#x30EA;&#x30A2;&#x30AF;&#x30C8;&#x30B3;&#x30F3;&#x30C8;&#x30EA;&#x30D3;&#x30E5;&#x30FC;&#x30B7;&#x30E7;&#x30F3;" width="1200" height="421" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>React公式ドキュメントのコントリビュート🎉</figcaption></figure> </div> <p>このように、社内勉強会からのアウトプットは持続的なところに重点を置きつつ、物足りないところは各々で埋めるような形にすれば、社内勉強会の質が気になって開催を躊躇うといった心配はなくなると思います。</p> <p>社内勉強会の紹介はここで終了なのですが、せっかくなので最後に私が社内勉強会で学んで面白かった内容2つを紹介します :)</p> <h2 id="BONUS面白かった学びの共有">BONUS:面白かった学びの共有</h2> <h3 id="useRefのベースはuseState"><code>useRef</code>のベースは<code>useState</code>?</h3> <p>※ 参考:<a href="https://ja.react.dev/learn/referencing-values-with-refs#how-does-use-ref-work-inside">useRef の内部動作</a></p> <p>よく input field の<code>onChange</code>が毎回トリガーされないように<code>setState</code>の代わりに<code>useRef</code>を使ったりすると思います。<code>useRef</code>はコンポーネントの再レンダリングを起こさず情報を記憶したい場合に使う「避難ハッチ」とも呼ばれたりしますね。しかし、なんと<code>useRef</code>は以下のように実装されているのとほとんと同じであるとのことです!</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">function</span> useRef<span class="synStatement">(</span>initialValue<span class="synStatement">)</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">[</span>ref<span class="synStatement">,</span> unused<span class="synIdentifier">]</span> <span class="synStatement">=</span> useState<span class="synStatement">(</span><span class="synIdentifier">{</span> current: initialValue <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synStatement">return</span> ref<span class="synStatement">;</span> <span class="synIdentifier">}</span> </pre> <p>このように、<code>useRef</code>は最初のレンダーで<code>{ current: initialValue }</code>を返しますが、それからもレンダーが発生するたびに同じオブジェクトを返す構造になってます。加えて、<code>useRef</code>は常に再レンダリングを起こさず情報を記憶するので、セッターを返さないことが納得できますね。</p> <p>React の理念の 1 つである <strong>「イミュータビリティ」から離れるための「避難ハッチ」である<code>useRef</code>が</strong>、<strong>実は「イミュータビリティ」理念に最も忠実な<code>useState</code>をベースに実装されているのと同然</strong>であることから皮肉を感じた面白い学びでした!</p> <h3 id="keyでstateをリセットする上級技"><code>key</code>で<code>state</code>をリセットする「上級技」</h3> <p>※ 資料:<a href="https://ja.react.dev/learn/preserving-and-resetting-state#option-2-resetting-state-with-a-key">key で state をリセットする</a></p> <div style="text-align: center; margin-left: auto; margin-right: auto; width: 400px;"> <figure class="figure-image figure-image-fotolife" title="Reactのレンダーツリー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231204/20231204173414.png" alt="&#x540D;&#x8A00;" width="894" height="76" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231204/20231204173635.png" alt="&#x30EC;&#x30F3;&#x30C0;&#x30FC;&#x30C4;&#x30EA;&#x30FC;" width="1162" height="594" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Reactのレンダーツリー</figcaption></figure> </div> <p>上記の画像のように React はコンポーネント構造を「レンダーツリー」としてビルドすることで、各<code>state</code>の所属コンポーネントを把握及び紐付けます。紐付けられた<code>state</code>は、所属するコンポーネントと生死を共にすることになります。</p> <p>こういった特徴によって、<strong>同じ位置の同じコンポーネントへの再レンダー</strong>は<code>state</code>が維持されますが、<strong>同じ位置の異なるコンポーネントへの再レンダー</strong>は<code>state</code>がリセットされてしまいます。しかし、ここで<strong>同じ位置の同じコンポーネントにも関わらず state をリセットさせる</strong>「上級技」を Mira チーム担当のコンポーネントで活用していることを学びました!</p> <div style="text-align: center; margin-left: auto; margin-right: auto; width: 400px;"> <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/y/yellow-sabotech/20231205/20231205190754.gif" alt="&#x8AAC;&#x660E;&#x306E;&#x30A4;&#x30E1;&#x30FC;&#x30B8;&#x4F8B;" width="272" height="150" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>コンポーネントのイメージ例(ミント色)</figcaption></figure> </div> <p>上のイメージ例のように、コンポーネントの位置は常に同じですが、ボタンが選択されるたびに <code>fade-out</code> アニメーションをさせるための <code>state</code> をリセットさせないといけません。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> Component <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> ... <span class="synStatement">return</span> <span class="synStatement">(</span> ... <span class="synStatement">&lt;</span>MintMessage key<span class="synStatement">=</span><span class="synIdentifier">{</span>uniqueKey<span class="synIdentifier">}</span> /<span class="synStatement">&gt;</span> ... </pre> <p>そこで上記のように<code>key</code>を使って「特定のメッセージコンポーネント」として React に識別させることで、同じ位置の異なるコンポーネントとして認知するように強制しています。<code>key</code>を使う際には一意性のことをお忘れなく!</p> <p>React レンダーツリーの特徴を実際に所属しているチームで活用していることがわかった面白い学びでした。</p> <h2 id="あとがき">あとがき</h2> <p>面白かった内容の共有で少し話の流れが逸れかけたところで、締めの挨拶になります。紹介したように新卒メンバーが主体的に社内勉強会を企画できた背景には、開催のためのシンプルなプロセスも大きく関係しています。サイボウズには「実務で必要な学びが欲しい、でも 1 人では眠くなりそうだからガッツリでなくともみんなで楽しく勉強する場が欲しい!」などの場合に気軽に社内勉強会を開催できるような環境が整っています。</p> <p>この記事が、新卒でも主体的に動くことで成長の場をいくらでも設けられるサイボウズのフロントエンド職に興味を持っていただくきっかけとなれれば幸いです :)</p> <p>以上、フロントエンド職の新卒ができることの一例である「React のドキュメントを読む会」を紹介させて頂きました!🫡</p> yellow-sabotech サイボウズ Office のフロントエンド刷新におけるアクセシビリティ改善の取り組み hatenablog://entry/6801883189062643636 2023-12-01T08:00:00+09:00 2023-12-22T11:38:57+09:00 こんにちは。DOGO(どうご)チームのまっつー(@ryo-manba)です。DOGO チームでは、「サイボウズ Office」 のフロントエンド刷新を行っています。この記事では、フロントエンド刷新を進めていく中で取り組んでいるアクセシビリティ改善について紹介します。 ※ この記事は Cybozu Frontend Advent Calendar 2023 の 1日目の記事です。 DOGO とは 刷新前の問題点 アクセシビリティ改善の取り組み 輪読会の実施 OSS をフル活用したコンポーネント実装 改善事例:DatePicker Poca11y チームとの協力 おわりに DOGO とは DOGO… <p>こんにちは。DOGO(どうご)チームのまっつー(<a href="https://twitter.com/ryo_manba">@ryo-manba</a>)です。DOGO チームでは、「サイボウズ Office」 のフロントエンド刷新を行っています。この記事では、フロントエンド刷新を進めていく中で取り組んでいるアクセシビリティ改善について紹介します。</p> <p>※ この記事は <a href="https://adventar.org/calendars/9255">Cybozu Frontend Advent Calendar 2023</a> の 1日目の記事です。</p> <ul class="table-of-contents"> <li><a href="#DOGO-とは">DOGO とは</a></li> <li><a href="#刷新前の問題点">刷新前の問題点</a></li> <li><a href="#アクセシビリティ改善の取り組み">アクセシビリティ改善の取り組み</a><ul> <li><a href="#輪読会の実施">輪読会の実施</a></li> <li><a href="#OSS-をフル活用したコンポーネント実装">OSS をフル活用したコンポーネント実装</a></li> <li><a href="#改善事例DatePicker">改善事例:DatePicker</a></li> <li><a href="#Poca11y-チームとの協力">Poca11y チームとの協力</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="DOGO-とは">DOGO とは</h2> <p>DOGO は、サイボウズ Office のフロントエンドを刷新するプロジェクトです。独自のスクリプト言語で書かれた MPA(Multi-Page Application)を、Next.js の App Router に画面単位で置き換えています。現在、エンジニア7名と QA 2名の計9名で進行中です。詳細は以下のリンクからご確認いただけます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2023%2F09%2F25%2F080000" title="サイボウズOfficeのフロントエンド刷新(DOGOプロジェクト) をやってます! - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2023/09/25/080000">blog.cybozu.io</a></cite></p> <p><iframe id="talk_frame_1108295" class="speakerdeck-iframe" src="//speakerdeck.com/player/32e120aa3fa94fa7bc3cc1e2480d8e07" 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/mugi_uno/next-dot-js-app-router-deno-mpa-hurontoendoshua-xin">speakerdeck.com</a></cite></p> <p>これまでに6画面のリリースが完了しており、その過程でアクセシビリティの改善にも取り組んでいます。</p> <h2 id="刷新前の問題点">刷新前の問題点</h2> <p>サイボウズ Officeのフロントエンド刷新にあたり、アクセシビリティの向上は重要な課題でした。これまで横断的なアクセシビリティの向上を目的とする <a href="https://blog.cybozu.io/entry/2022/04/25/110000">Poca11y(ポカリ)チーム</a>を中心に多くの改善がなされてきました。しかし、セマンティックなマークアップや W3C が推奨するキーボードサポート、WAI-ARIA 属性の設定など、さらなる改良の余地が残されています。そこで、刷新の際にこれらの要素も改善することを目標に掲げました。</p> <p>しかし、刷新の主目的は全画面の迅速な置き換えであるため、一つの画面に割ける時間は限られています。加えて、DOGO チームは少人数で構成され、kintone のように<a href="https://blog.cybozu.io/entry/2022/03/16/141935">デザインシステム専門チーム</a> のサポートはありませんでした。このため、少人数でも効率的に進められるアクセシビリティ改善の取り組みが求められました。</p> <h2 id="アクセシビリティ改善の取り組み">アクセシビリティ改善の取り組み</h2> <h3 id="輪読会の実施">輪読会の実施</h3> <p>チームメンバーの前提知識の統一と理解を深めるために、<a href="https://gihyo.jp/book/2023/978-4-297-13366-5">Webアプリケーションアクセシビリティ</a>の輪読会を開催しました。この輪読会は週に1回行われ、事前に指定された章を読み進める形式で進行しました。各回では、ファシリテーターがその章の概要をおさらいした後、参加者が感想や考えを共有しました。</p> <p>重要なのは、具体的な Tips よりも、どのような状態がアクセシブルであるか、どのような状況で製品が使用されるかに関する共通の理解を深めることでした。個人的に特に印象に残ったのは、一時的な障害によってもユーザー体験がどのように変わるかについての認識です。例えば、視覚に関連しては「メガネを忘れた場合」、聴覚に関連しては「Bluetooth イヤホンの充電が切れた場合」といったシナリオを通じて、アクセシビリティは一部の人々だけでなく、広範なユーザーに影響を及ぼすことを理解しました。このような共通認識がチーム内で形成されたことは、プロジェクトにとって大きな前進だったように感じます。</p> <h3 id="OSS-をフル活用したコンポーネント実装">OSS をフル活用したコンポーネント実装</h3> <p>少人数のチームでアクセシビリティの改善に効率的に取り組むために、OSS の Headless UI ライブラリの導入を検討しました。Headless UI ライブラリとは見た目に関するスタイルは提供せず、主にアクセシビリティ対応のためのロジックを提供するライブラリです。具体的には <a href="https://www.radix-ui.com/">Radix UI</a> や <a href="https://headlessui.com/">Headless UI</a> のようなライブラリがあります。それらの Headless UI ライブラリ群と DOM 以外の振る舞いとアクセシビリティを担う <a href="https://react-spectrum.adobe.com/react-aria/">React Aria</a> や <a href="https://zagjs.com/">Zag.js</a> の Pros/ Cons を分析し、ADR(Architecture Decision Record)を基にチーム内で議論しました。最終的に、柔軟なカスタマイズが可能な Hooks 形式の React Aria の導入を決定しました。</p> <p>導入にあたり、チーム内で React Ariaのドキュメントや実装に基づく勉強会も実施しました。これにより、一定の学習コストはかかるものの、それ以上に得られる恩恵のほうが大きいと判断し、一部のコンポーネントのみではなく、全面的な導入に踏み切ることができました。</p> <p>さらに、React Aria の Hooks を利用した Headless UI ライブラリである <a href="https://react-spectrum.adobe.com/react-aria/react-aria-components.html">React Aria Components</a> がリリース候補(RC)に達したこともあり<a href="#f-1a782b7c" id="fn-1a782b7c" name="fn-1a782b7c" title="2023年11月末時点での React Aria Components のメンテナーの X のポストによると、2023 年中には stable の v1 をリリース予定">*1</a>、React Aria Components を使用したコンポーネントの置き換えも進行中です。まだ RC 段階の OSS ライブラリを採用するに至った背景には、単に OSS を利用するだけでなく、積極的に upstream への貢献を目指すというチームの方針に基づくものでした。現に利用を始めてから半年程度で<a href="https://github.com/adobe/react-spectrum/pulls?q=is%3Apr+author%3Aryo-manba+is%3Aclosed+">8件ものコントリビューション</a>を行っています。</p> <h3 id="改善事例DatePicker">改善事例:DatePicker</h3> <p>改善事例として、DatePickerコンポーネントを新たに実装しました。刷新前の画面では table を用いたスタイル調整が主で、キーボード操作や WAI-ARIA 属性の設定が不十分でした。例えば、日付選択のためのキーボード操作が Tab キーに限定されていたり、エスケープキーでカレンダーダイアログを閉じることができないなど、ユーザビリティの観点から改善の余地がありました。</p> <p>React Aria の導入により、<a href="https://www.w3.org/WAI/ARIA/apg/">APG</a>(W3Cが提供するアクセシビリティ向上のためのガイドライン)に沿ったキーボードサポートを実現することが可能になりました。具体的には、十字キーによる日付セルの移動やエスケープキーを使ってダイアログを閉じる機能などが追加され、より直感的に操作ができるように改善されています。</p> <p>加えて、適切な WAI-ARIA を施すことでアクセシブルなコンポーネントの実現を図りました。これにより、DatePicker の見た目はこれまでと大きな変更はないものの、さまざまなユーザーが操作しやすい形に改善されました。</p> <p><figure class="figure-image figure-image-fotolife" title="DatePicker とChrome DevTools の Accessibility ペインで表示されるアクセシビリティツリー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/ryo_manba/20231129/20231129160843.png" alt="DatePicker &#x3068; Chrome DevTools &#x306E; Accessibility &#x30DA;&#x30A4;&#x30F3;&#x3067;&#x8868;&#x793A;&#x3055;&#x308C;&#x308B;&#x30A2;&#x30AF;&#x30BB;&#x30B7;&#x30D3;&#x30EA;&#x30C6;&#x30A3;&#x30C4;&#x30EA;&#x30FC;" width="927" height="456" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>DatePicker とChrome DevTools の Accessibility ペインで表示されるアクセシビリティツリー</figcaption></figure></p> <p>前述の Poca11yチームからのフィードバックを受けてさらなる改善点を発見し、React Aria に対しても貢献することができました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fadobe%2Freact-spectrum%2Fpull%2F5166" title="Update aria-expanded to use true or false values by ryo-manba · Pull Request #5166 · adobe/react-spectrum" 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/adobe/react-spectrum/pull/5166">github.com</a></cite></p> <h3 id="Poca11y-チームとの協力">Poca11y チームとの協力</h3> <p>Poca11y チームとの連携も大きな支えとなっています。社内でいつでも相談できる環境があり、アプリケーション固有の事情で特殊なマークアップが必要となった場合や、よりアクセシブルにするための改善方法などの相談に乗ってもらい、チームワークでより良い刷新を行うことができています。</p> <p>具体的な事例では、読み込み状態を表示する Loading Indicator の実装があります。 アクセシビリティを考慮した実装について調べてみると、<code>role</code>、<code>aria-busy</code>、<code>aria-live</code> などさまざまな WAI-ARIA 属性が候補として挙がりました。特に <code>aria-busy="true"</code> と <code>aria-live="polite"</code> の組み合わせ、または <code>role="alert"</code> と <code>aria-live="assertive"</code> のいずれかの使用について検討していました。</p> <p>しかし、どちらの実装についても確信が持てなかったため、Poca11y チームに相談しました。その結果、<code>aria-busy</code> 属性はローダーに対してではなく、更新中の要素に対して使用すべきであること、また <code>aria-live="assertive"</code> は緊急性の高い情報に適しているため、Loading Indicator には適さないことが分かりました。これらの助言を受け、最終的に <code>role="status" aria-live="polite"</code> を用いた実装に落ち着き、より優れたユーザー体験を提供することに繋がりました。</p> <p><figure class="figure-image figure-image-fotolife" title="Poca11y チームからのやり取りの様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/ryo_manba/20231129/20231129161040.png" alt="Poca11y &#x30C1;&#x30FC;&#x30E0;&#x3068;&#x306E;&#x3084;&#x308A;&#x53D6;&#x308A;&#x306E;&#x69D8;&#x5B50;" width="1200" height="1188" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Poca11y チームとのやり取りの様子</figcaption></figure></p> <p>このようにアクセシビリティの専門チームにより、改善案を提案してもらえるのは、DOGO チームにとっても大きな学びの機会です。アクセシビリティに関するトレンドや手法を継続的に学ぶことができ、メンバーのスキルアップにも繋がっています。</p> <h2 id="おわりに">おわりに</h2> <p>DOGO チームでは刷新にあたり、単なるフレームワークの置き換えに留まらず、アクセシビリティの改善を始めとした製品をより良く、使いやすくするための取り組みを行っています。また現在 QA がアクセシビリティのチェックシートを作成しており、チーム全体でアクセシビリティ改善の取り組みを進めていく予定です。小さなチームではありますが、今後も継続的な改善を目指しています。</p> <p>興味のある方は、ぜひ以下の募集要項からご応募ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Ffront-end-engineer-office.html" title="フロントエンドエンジニア(サイボウズOfficeフロントエンド刷新PJJ)キャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/career/front-end-engineer-office.html">cybozu.co.jp</a></cite></p> <p>▼Cybozu Frontend Advent Calendar はこちら</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F9255" title="Cybozu Frontend Advent Calendar 2023 - Adventar" 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://adventar.org/calendars/9255">adventar.org</a></cite></p> <div class="footnote"> <p class="footnote"><a href="#fn-1a782b7c" id="f-1a782b7c" name="f-1a782b7c" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">2023年11月末時点での React Aria Components のメンテナーの <a href="https://twitter.com/devongovett/status/1729189833531363474?s=20">X のポスト</a>によると、2023 年中には stable の v1 をリリース予定</span></p> </div> ryo_manba 新卒一年目エンジニアがkintoneの新機能をプロトで提案してリリースまでした話 hatenablog://entry/6801883189059026490 2023-11-23T08:00:00+09:00 2023-11-23T08:00:02+09:00 こんにちは!kintone新機能開発チームでWebエンジニアをやっている立山です。 今回は、エンジニアの私がkintoneの新機能を提案し、それを実際にプロダクトに組み込むまでの経験についてお話しします。 この記事を通じて、新しいアイデアを提案し、実現に近づけるためのステップを共有したいと思います。 特に、新しいものを作りたいと考えているエンジニアの方々に向けて、私の経験が役立てば嬉しいです。 新機能提案の経緯 きっかけ プロト実装 バックログアイテム化 見積もりの難しさ 受け入れ条件の定義の難しさ デザインブラッシュアップ・ユーザーテスト 開発チームで着手・リリース プロト開発の魅力 好奇心… <p>こんにちは!kintone新機能開発チームでWebエンジニアをやっている立山です。 今回は、エンジニアの私がkintoneの新機能を提案し、それを実際にプロダクトに組み込むまでの経験についてお話しします。</p> <p>この記事を通じて、新しいアイデアを提案し、実現に近づけるためのステップを共有したいと思います。 特に、新しいものを作りたいと考えているエンジニアの方々に向けて、私の経験が役立てば嬉しいです。</p> <ul class="table-of-contents"> <li><a href="#新機能提案の経緯">新機能提案の経緯</a><ul> <li><a href="#きっかけ">きっかけ</a></li> <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></li> <li><a href="#開発チームで着手リリース">開発チームで着手・リリース</a></li> </ul> </li> <li><a href="#プロト開発の魅力">プロト開発の魅力</a><ul> <li><a href="#好奇心とモチベーション駆動の開発">好奇心とモチベーション駆動の開発</a></li> <li><a href="#実物をもとに検討を進められる">実物をもとに検討を進められる</a></li> <li><a href="#プロト開発時の注意点">プロト開発時の注意点</a></li> </ul> </li> <li><a href="#活動を行えた背景">活動を行えた背景</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="新機能提案の経緯">新機能提案の経緯</h2> <p>まず、今回提案した「関数」や「フィールドコード」の入力を補助する機能 について簡単に説明したいと思います。 kintoneには、フィールドの値や関数を組み合わせることで別の値を算出することができる、計算式という機能があります。 この計算式によって、勤怠時間の管理や、消費税額の計算などが行えます。</p> <p>しかし、従来の計算式の設定では、手入力によって、関数名や文法に誤りが生じないように気を付ける必要がありました。 ユーザーによっては複雑な計算式を組む場合もあり、人的ミスが起こりやすい機能の一つでした。</p> <p>今回提案した「関数」や「フィールドコード」の入力を補助する機能は、入力した文字に応じてフィールドや関数を推測し、補完する機能です。 これによって、kintoneの計算式に慣れていない人でも簡単に計算式を設定することができるようになりました。</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/y/yellow-sabotech/20231115/20231115174009.png" alt="&#x300C;&#x95A2;&#x6570;&#x300D;&#x3084;&#x300C;&#x30D5;&#x30A3;&#x30FC;&#x30EB;&#x30C9;&#x30B3;&#x30FC;&#x30C9;&#x300D;&#x306E;&#x5165;&#x529B;&#x3092;&#x88DC;&#x52A9;&#x3059;&#x308B;&#x6A5F;&#x80FD;&#xFF08;&#x30D8;&#x30EB;&#x30D7;&#x3088;&#x308A;&#xFF09;" width="1200" height="454" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>「関数」や「フィールドコード」の入力を補助する機能(ヘルプより)</figcaption></figure></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fkintone.cybozu.co.jp%2Fupdate%2Fmain%2F2023-11.html" title="kintone(キントーン)- 主なアップデート(2023年11月) | サイボウズの業務改善プラットフォーム" 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://kintone.cybozu.co.jp/update/main/2023-11.html">kintone.cybozu.co.jp</a></cite></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/y/yellow-sabotech/20231115/20231115180017.png" alt="&#x30EA;&#x30EA;&#x30FC;&#x30B9;&#x307E;&#x3067;&#x306E;&#x51FA;&#x6765;&#x4E8B;&#x3092;&#x6642;&#x7CFB;&#x5217;&#x9806;&#x306B;&#x8868;&#x793A;&#x3057;&#x305F;&#x56F3;" width="1200" height="296" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>リリースまでの流れ</figcaption></figure></p> <h3 id="きっかけ">きっかけ</h3> <p>この機能を作ろうと思ったきっかけは、ユーザーからのフィードバックを開発チームで共有する会でした。 この時、計算式に関するフィードバック(e.g. 使える関数がわかりづらい、フィールドを参照するための情報を逐一確認する必要があり手間を感じるなど)が多くある印象を受けました。</p> <p>同時に、エクセルのように関数が補完される機能があればこれらの計算式が難しいという問題は解決するかもしれないと思いました。</p> <h3 id="プロト実装">プロト実装</h3> <p>割と簡単に作れるんじゃないかと思い、早速プロト作りに着手しました。 この時は「入力中の文字列を含む関数、フィールドを候補表示・補完する機能によって入力が楽になる」というざっくりした仮説で作成し始めました。</p> <p>ただ、やってみると意外と難しかったです。 そもそもジョインしたばかりなので開発言語に慣れてなかったのはかなり大きなハードルでした。 また、入力中のさまざまな操作(入力、項目の選択、カーソル移動など)をモデル化し、適切に候補表示するための実装は結構複雑になりました。</p> <p>出来上がったものは、まず、プロダクトマネージャー(PM)に見せました。 実用面でのフィードバックも受けることができ、それを通じて機能の解像度が向上しました。</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/y/yellow-sabotech/20231122/20231122120334.gif" alt="&#x30D7;&#x30ED;&#x30C8;&#x6642;&#x70B9;&#x306E;&#x5B9F;&#x88C5;&#x3067;&#x52D5;&#x4F5C;&#x3059;&#x308B;&#x69D8;&#x5B50;" width="889" height="410" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>プロト時点の実装</figcaption></figure></p> <p>この段階で、候補の表示だけでなく、入力状況(関数内の引数の位置、使えるフィールドのデータ型など)に応じて候補を絞り込む機能が必要であることに気付くことができました。 しかし、この時点ではその実装案が思いつかなかったため、プロトの作成を一時中断しました。</p> <p>2ヶ月くらい経って、入力状況に応じて候補を絞り込む実装案が思いつき、開発を再開しました。 すぐにプロトを作り、この時はチームにフランクに共有し、PMに見せる運びとなりました。 再度PMのフィードバックを受け、正式にバックログアイテム(=開発チームとして取り組む可能性があるタスク)として登録することになりました。</p> <h3 id="バックログアイテム化">バックログアイテム化</h3> <p>この段階では、機能概要を説明し、タスクの工数を見積もり、チームとして着手するかどうかの判断材料を集めることが目的となります。 しかし、エンジニアがプロト提案をしてバックログアイテム化するという前例の少ないケースだったため、このプロセスは難しかったです。</p> <h4 id="見積もりの難しさ">見積もりの難しさ</h4> <p>プログラムとしてどのような実装が残っているかを自分では把握できていた一方で、この機能がユーザーにどのように利用されるのかや、詳細な仕様の部分がまだ明確に定まっていない状況でした。 そのため、他の開発メンバーにとっては、残り何をやるべきか、工数の予測がしにくくなってしまっていたと思います。 さらに、機能のコードを自分だけが把握していた状況も、予測のしづらさを高めてしまったと思います。</p> <h4 id="受け入れ条件の定義の難しさ">受け入れ条件の定義の難しさ</h4> <p>受け入れ条件とは、バックログアイテムの達成条件とも言い換えられます。 通常受け入れ条件は、「ユーザーの特定の操作に対して期待するシステムの挙動」を示すもので、具体的な操作に関する期待値を明示的に記述します。 簡単な例で言えば、「◯◯ボタンを押したら、××と表示される」といった形式です。</p> <p>しかし今回の機能のように、様々な操作状況に関連する場合、それらすべてに対する受け入れ条件を簡潔にまとめることは難しいものでした。</p> <p>最終的には、受け入れ条件という形で進めるのではなく、プロトを触りながら増やしたい機能・減らしたい機能を考えていき、徐々に作り上げていきましょうという進め方になりました。</p> <h3 id="デザインブラッシュアップユーザーテスト">デザインブラッシュアップ・ユーザーテスト</h3> <p>このプロセスはPM主導で行なっていただきました。 まず、デザインチームと連携し、ユーザーインターフェースの配置や文言のブラッシュアップを行い、見た目や使い勝手を向上させるための改良が行われました。</p> <p>その後、ブラッシュアップされたデザインを元に、ユーザーテストを実施しました。 このテストでは、実際にカスタマーサポートのチームのメンバーに新機能を試してもらい、現場目線での使い勝手や問題点の把握を行いました。</p> <p>上記のブラッシュアップを経て、新機能に組み込むべき機能や改良点が確定していきました。 このプロセスがなければ、ユーザーにとって価値のある機能として進化させることはできなかったと思います。</p> <h3 id="開発チームで着手リリース">開発チームで着手・リリース</h3> <p>前述のプロセスを経て、ようやく開発チームでの着手となりました。 この段階では、実装の細かい部分を詰めたり、製品コードとしての品質向上が主要な目標でした。</p> <p>特に時間を費やしたのは、仕様書の作成でした。 各操作に対する期待結果を仕様書に落とし込む作業をチームで行なったことで、この機能の属人化を防ぐことができました。</p> <p>最終的に、開発チームで完成させ、製品としてリリースすることができました。</p> <h2 id="プロト開発の魅力">プロト開発の魅力</h2> <p>以上が今回の提案の流れでしたが、ここでは、今回新機能を提案できた経験を振り返り、プロト開発の魅力と、その際の注意点について考えたことをお伝えしたいと思います。</p> <h3 id="好奇心とモチベーション駆動の開発">好奇心とモチベーション駆動の開発</h3> <p>今回のプロト開発に着手した際、好奇心とモチベーションが私の最大の推進力でした。 思いついたアイデアをもとに「これを作りたい!」という強い意志を持って取り組むことで、開発スピードが向上し、難しい技術的課題に挑戦する意欲も高まりました。 ものづくりが好きな私にとって、プロト開発は新たな刺激となり、日々の業務に活気を与えてくれました。</p> <p>また、出来上がったものを共有して、ポジティブなフィードバックを受けられると、モチベーションが一層高まりました。 このようなフィードバックループは、アイデアが実用化される過程で不可欠なもので、開発を持続させる重要な要素でした。</p> <h3 id="実物をもとに検討を進められる">実物をもとに検討を進められる</h3> <p>プロトがあると、その実用性や効果を直接感じることができるため、説得力が増すと思います。 また、実際に動くものがあることで、リアルな状況を想定して機能の検討を進めることができます。 その結果、着想時には思いもよらなかった良いものが生まれる可能性もあります。</p> <p>特に今回の提案では、バックログアイテム化された時点で多くの不明瞭なポイントが存在しましたが、プロトがあったおかげで、機能が「なんとなく良さそう」と感じ取ってもらうことができました。 その直感が強かったからこそ、デザインブラッシュアップやユーザーテストを経ることで製品の目指す姿を定義するというプロセスで進められたのだと思います。</p> <h3 id="プロト開発時の注意点">プロト開発時の注意点</h3> <p>エンジニアとして、ものづくりは楽しいですが、注意すべき点もあります。 それは、時間をかけず、過度に作り込まないことです。 これによって、アイデアを高める作業をより効率的に行えます。 また、仮にアイデアがイイ線行っていないとわかったとしても撤退時の時間的・精神的なダメージを小さく抑えられます。</p> <p>そのためには、検証すべき仮説を持つことが不可欠です。 例えば今回のプロジェクトでは以下のような基本的な仮説を立てました。</p> <ol> <li><p>コンボボックスを実現すれば計算式入力が楽になる</p></li> <li><p>入力位置に基づくフィールド・関数の絞り込みが行えると計算式入力がもっと楽になる</p></li> </ol> <p>これらの仮説はとてもざっくりしたものですが、達成すべきことを意識するのに重要なものでした。 これを意識するのを忘れるとすぐに作り込みに走ってしまうので注意が必要です。</p> <p>そして、ある程度できた時点でフィードバックをもらうのも重要です。 仮説検証ができるだけでなく、作っているものの方向性を早期に修正できるからです。 一人で考えているだけでは、アイデアが限定的になりがちで、自分でも気づかぬうちに作り込みの方に進んでいる可能性があります。 人に話すことによって思わぬアイデアが生まれ、不要な作り込みに時間をかけることを避けられます。</p> <h2 id="活動を行えた背景">活動を行えた背景</h2> <p>今回のような活動ができたのには外部的な要因もいくつかあります。</p> <p>まず、kintone開発チームには、日常のタスクの時間以外に、個々で勉強会をしたり、コードの改善活動などに自由に使える探求時間があることです。 このような時間があることで、新しいアイデアやプロジェクトに集中できる環境が整っていました。</p> <p>また、今回提案した機能がフロントエンドに閉じていたこともうまくいった要因だと思います。 万が一不具合が起きたとしても影響が小さく抑えられることがわかっていたため、PM的にもGoを出しやすい機能だったのだと思います。</p> <h2 id="まとめ">まとめ</h2> <p>今回、エンジニアからの新機能の提案という通常の開発プロセスとは異なる進め方をしたことはとても良い経験となりました。 この経験を通して、改めてサイボウズがさまざまな技術的な挑戦をしやすい環境であることを実感しました。 今回の私の経験がアイデアの実現に向けて、少しでも役立つことを願っています。</p> yellow-sabotech JavaScript のグローバルオブジェクトに立ち向かう hatenablog://entry/6801883189055791295 2023-11-21T09:18:27+09:00 2023-11-21T09:18:27+09:00 こんにちは kintone のフロントエンド刷新プロジェクトでフロントエンドエンジニアをしている Nokogiri です 現在 kintone ではフロントエンドのレガシーコードの刷新に取り組んでいます。 このプロジェクトでグローバルスコープの JS オブジェクト(以降グローバルオブジェクトと呼びます)と向き合ってきた過程について紹介したいと思います。 同じようなグローバルオブジェクトの取り扱いに困っている人の助けになれば幸いです。 kintone フロントエンド刷新プロジェクト(フロリア)とは kintone の画面初期表示用データ取得方法の現状 kintone のフロントエンドのデータ取得… <p>こんにちは kintone のフロントエンド刷新プロジェクトでフロントエンドエンジニアをしている <a href="https://twitter.com/nkgrnkgr">Nokogiri</a> です</p> <p>現在 kintone ではフロントエンドのレガシーコードの刷新に取り組んでいます。 このプロジェクトでグローバルスコープの JS オブジェクト(以降グローバルオブジェクトと呼びます)と向き合ってきた過程について紹介したいと思います。 同じようなグローバルオブジェクトの取り扱いに困っている人の助けになれば幸いです。</p> <ul class="table-of-contents"> <li><a href="#kintone-フロントエンド刷新プロジェクトフロリアとは">kintone フロントエンド刷新プロジェクト(フロリア)とは</a></li> <li><a href="#kintone-の画面初期表示用データ取得方法の現状">kintone の画面初期表示用データ取得方法の現状</a></li> <li><a href="#kintone-のフロントエンドのデータ取得の課題">kintone のフロントエンドのデータ取得の課題</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="#課題に対してどのように検討し対策を試してきたか">課題に対してどのように検討し対策を試してきたか?</a><ul> <li><a href="#REST-API-などの非同期通信を使って値を取得する方法に変更する">REST API などの非同期通信を使って値を取得する方法に変更する</a></li> <li><a href="#参照専用の関数を作りそれ以外からの直接アクセスを禁止する">参照専用の関数を作り、それ以外からの直接アクセスを禁止する</a></li> </ul> </li> <li><a href="#課題を踏まえてのアプローチ">課題を踏まえてのアプローチ</a><ul> <li><a href="#react-dom-の-render-関数-実行時に-component-の-props-としてグローバルスコープ-JS-オブジェクトを渡す">react-dom の render 関数 実行時に component の props としてグローバルスコープ JS オブジェクトを渡す</a></li> <li><a href="#グローバルオブジェクトの参照箇所を-ReduxStore-と-ReactContextProvider-初期化時に限定する">グローバルオブジェクトの参照箇所を ReduxStore と ReactContextProvider 初期化時に限定する</a></li> <li><a href="#テストや-Storybook-用の-MockProvider-を準備する">テストや Storybook 用の MockProvider を準備する</a></li> <li><a href="#グローバルオブジェクトを-Zod-を使って-validate-する">グローバルオブジェクトを Zod を使って validate する</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> <li><a href="#サイボウズではフロントエンドエンジニアを募集しています">サイボウズではフロントエンドエンジニアを募集しています</a></li> </ul> <h2 id="kintone-フロントエンド刷新プロジェクトフロリアとは">kintone フロントエンド刷新プロジェクト(フロリア)とは</h2> <p>kintone フロントエンドリアーキテクチャプロジェクト(フロリア)は kintone のフロントエンド部分を刷新するプロジェクトです。次の 3 点をゴールに掲げています。</p> <ul> <li>全てのページが React によって表示されている ​</li> <li>フロントエンドが技術的にもチーム的にも分割されている ​</li> <li>ユーザー体験に関する指標に対する計測が行われており、チームの関心ごとになっている</li> </ul> <p>フロリアについて詳しく知りたい方はぜひ以下のブログも読んでみてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2022%2F02%2F04%2F171154" title="kintone フロントエンドリアーキテクチャプロジェクトで大切にしていること - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2022/02/04/171154">blog.cybozu.io</a></cite></p> <h2 id="kintone-の画面初期表示用データ取得方法の現状">kintone の画面初期表示用データ取得方法の現状</h2> <p>kintone では画面初期用データの多くをサーバーサイドのテンプレートエンジンで <code>window</code> 領域にグローバルオブジェクトとして埋め込む形でクライアント側に渡しています。</p> <p>例)一般的なサーバーサイドのテンプレートエンジンでのグローバルオブジェクト埋め込み</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">script</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;text/javascript&quot;</span><span class="synIdentifier">&gt;</span> <span class="synSpecial"> </span><span class="synComment">/*&lt;![CDATA[*/</span> <span class="synSpecial"> </span><span class="synStatement">window</span><span class="synSpecial">.userData = </span><span class="synComment">/*[[${data}]]*/</span><span class="synSpecial"> </span><span class="synConstant">&quot;{ userName: 'hoge', userAge: 10, ... }&quot;</span><span class="synSpecial">;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">script</span><span class="synIdentifier">&gt;</span> </pre> <p>例)フロントエンドでのデータ取得</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> User <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> <span class="synIdentifier">{</span> userName<span class="synStatement">,</span> userAge <span class="synIdentifier">}</span> <span class="synStatement">=</span> <span class="synSpecial">window</span>.userData<span class="synStatement">;</span> <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span>UserName is : <span class="synIdentifier">{</span>userName<span class="synIdentifier">}</span><span class="synStatement">&lt;</span>/div<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>div<span class="synStatement">&gt;</span>UserAge is : <span class="synIdentifier">{</span>userAge<span class="synIdentifier">}</span><span class="synStatement">&lt;</span>/div<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/div<span class="synStatement">&gt;</span> <span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> </pre> <p>kintone では歴史的経緯から先述したグローバルオブジェクトを使った方法でサーバーサイドとのデータのやり取りをしていました。 開発者としてはこのグローバルオブジェクトを使ったデータのやり取りに課題を感じています。</p> <h2 id="kintone-のフロントエンドのデータ取得の課題">kintone のフロントエンドのデータ取得の課題</h2> <p>おもに課題として感じているのは次のような点です。</p> <ul> <li>メンテナンス性が悪い</li> <li>型安全ではない</li> <li>名前空間が汚染される</li> <li>意図しない参照が発生する</li> </ul> <h3 id="メンテナンス性が悪い">メンテナンス性が悪い</h3> <p>実装上どのファイルからでも自由にアクセスできるためコードの可読性と保守性を低下させます。 特にテストや Storybook で事前に準備すべきデータがわかりにくくなるため扱いづらいという問題もあります。</p> <h3 id="型安全ではない">型安全ではない</h3> <p>フロントエンドの TypeScript から扱うためには、任意の型を自前でつける必要があるため型安全ではないという問題があります。</p> <h3 id="名前空間の汚染される">名前空間の汚染される</h3> <p>異なるスクリプトやライブラリなどにおける名前空間の衝突を引き起こす可能性があります。</p> <h3 id="意図しない参照が発生する">意図しない参照が発生する</h3> <p>Chrome 拡張など他のスクリプトからアクセス可能であり不用意にアクセスされてしまう可能性があります。 特に kintone ではユーザーが自分で JavaScript を記述してスクリプトを実行可能であり開発側として意図せずアクセスされるリスクが高いです。</p> <h2 id="課題に対してどのように検討し対策を試してきたか">課題に対してどのように検討し対策を試してきたか?</h2> <p>フロリアのプロジェクトではいくつかの方法で対処をしてきました</p> <ul> <li>REST API などの非同期通信を使って値を取得する方法に変更する</li> <li>参照専用の関数を作り、それ以外からの直接アクセスを禁止する</li> </ul> <p>検討したそれぞれの案について、試した方法と試してみて感じた課題について紹介します。</p> <h3 id="REST-API-などの非同期通信を使って値を取得する方法に変更する">REST API などの非同期通信を使って値を取得する方法に変更する</h3> <p>この方法は先述したデメリットを全て解消できる良い方法ではあるものの 「表示速度面でパフォーマンスが悪くなりユーザー体験が今よりも悪くなる」「既にユーザーに提供している API に互換性がなくなる」という問題があります。</p> <p>非同期処理は同期処理に比べて画面が表示されるまでの待ち時間が長くなってしまいユーザー体験が悪くなるデメリットがあります。SSR を導入するという選択肢もありますが kintone では現在フロントエンドだけでなくインフラ基盤刷新の過渡期でもあるため、フロリアではフロントエンド基盤の範囲を超えた刷新を選択していません。</p> <p>kintone には <a href="https://cybozu.dev/ja/kintone/docs/js-api/list/">kintone JavaScript API</a> というものがあります。 これは kintone ユーザーが自分で記述した JavaScript から kintone を操作するための API です。 この API では現在<em>同期的にデータを参照</em>するものがあり、非同期通信に変更することで API の互換性がなくなってしまうという課題があります。</p> <p>kintone というプロダクトはこういった API を利用したカスタマイズが魅力の一つでもあり、多くのユーザーにご利用いただていることもあって簡単に互換性をなくしたアップデートができるわけではありません。</p> <h3 id="参照専用の関数を作りそれ以外からの直接アクセスを禁止する">参照専用の関数を作り、それ以外からの直接アクセスを禁止する</h3> <p>グローバルオブジェクトにアクセスできるものを一部の関数に限定することでコードの可読性を上げることはできます。 TypeScript を採用して関数経由でアクセスするルールにすることで関数の戻り値を指定でき型の安全性も担保できました。</p> <p>しかしこの専用関数はコンポーネントや Hooks のどこからアクセスされるのか理解するのが難しいという問題は残ります。 結局実際にテストや Storybook で事前に <code>window</code> 領域に値を注入してから起動するのですが、何が必要かわからないため既存の画面の全てのグローバルオブジェクト を注入してから開始するという運用に落ち着きました。</p> <p>例)</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// アクセサー</span> <span class="synStatement">export</span> <span class="synType">const</span> getGlobalUserData <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synSpecial">window</span>.userData<span class="synStatement">;</span> <span class="synStatement">export</span> <span class="synType">const</span> getGlobalCompanyData <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synSpecial">window</span>.companyData<span class="synStatement">;</span> </pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synComment">// 子供のコンポーネントからグローバルオブジェクトにアクセスしている場合、何が必要なのかわからない</span> <span class="synType">const</span> Story <span class="synStatement">=</span> <span class="synStatement">&lt;</span>Page /<span class="synStatement">&gt;;</span> </pre> <h2 id="課題を踏まえてのアプローチ">課題を踏まえてのアプローチ</h2> <p>課題を踏まえてグローバルオブジェクトの取り扱いについて、別のアプローチを検討し試しています。</p> <h3 id="react-dom-の-render-関数-実行時に-component-の-props-としてグローバルスコープ-JS-オブジェクトを渡す">react-dom の render 関数 実行時に component の props としてグローバルスコープ JS オブジェクトを渡す</h3> <p><a href="https://nextjs.org/">Next.js</a> の <code>getSeverSideProps</code> や <code>getStaticProps</code> から着想を得ました。 Next.js ではページ表示前に prefetch した値を props として渡すことで 非同期通信処理なしでサーバーから取得した値やビルド時に設定した値を渡すことができます。</p> <p>これに倣い、以下のような実装で <code>Page</code> コンポーネント描画前にグローバルオブジェクトを props として渡すようにしました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synStatement">import</span> <span class="synIdentifier">{</span> Page <span class="synIdentifier">}</span> <span class="synStatement">from</span> <span class="synConstant">&quot;./Page&quot;</span><span class="synStatement">;</span> <span class="synType">const</span> container <span class="synStatement">=</span> <span class="synSpecial">document</span>.getElementById<span class="synStatement">(</span><span class="synConstant">&quot;root&quot;</span><span class="synStatement">);</span> <span class="synType">const</span> root <span class="synStatement">=</span> createRoot<span class="synStatement">(</span>container<span class="synConstant">!</span><span class="synStatement">);</span> <span class="synComment">// Page以下のコンポーネントからはアクセスを禁止しrender時のpropsでのみ渡すようにする</span> root.render<span class="synStatement">(&lt;</span>Page <span class="synIdentifier">{</span>...getServerSideProps<span class="synStatement">()</span><span class="synIdentifier">}</span> /<span class="synStatement">&gt;);</span> <span class="synType">const</span> getServerSideProps <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synStatement">return</span> <span class="synIdentifier">{</span> userName: <span class="synSpecial">window</span>.userData.userName<span class="synStatement">,</span> userAge: <span class="synSpecial">window</span>.userData.userAge<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> </pre> <h3 id="グローバルオブジェクトの参照箇所を-ReduxStore-と-ReactContextProvider-初期化時に限定する">グローバルオブジェクトの参照箇所を ReduxStore と ReactContextProvider 初期化時に限定する</h3> <p>ページのトップレベルコンポーネントに props として渡すだけだと、子供のコンポーネントへのバケツリレーが多く発生してしまいます。 そこで画面の GUI の表示に必要な値で更新する値は ReduxStore に、静的な値は ReactContext 経由で子供のコンポーネントに渡すことにしました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink> <span class="synType">const</span> PageContents <span class="synStatement">=</span> <span class="synStatement">()</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// ReduxStoreにselector経由でアクセスしたり、useContextでアクセスする</span> ... <span class="synIdentifier">}</span> <span class="synType">const</span> Page <span class="synStatement">=</span> <span class="synStatement">(</span>props: ServerSideProps<span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// ReduxStoreの初期</span> <span class="synType">const</span> store <span class="synStatement">=</span> createReduxStore<span class="synStatement">(</span>props<span class="synStatement">);</span> <span class="synComment">// ContextValueの初期化</span> <span class="synType">const</span> initialContextValue <span class="synStatement">=</span> createReactContextInitialValue<span class="synStatement">(</span>props<span class="synStatement">);</span> <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>ReduxProvider store<span class="synStatement">=</span><span class="synIdentifier">{</span>store<span class="synIdentifier">}</span><span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>ReactContext.Provider value<span class="synStatement">=</span><span class="synIdentifier">{</span>initialContextValue<span class="synIdentifier">}</span><span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>PageContents<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/ReactContext.Provider<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/ReduxProvider<span class="synStatement">&gt;</span> <span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> </pre> <h3 id="テストや-Storybook-用の-MockProvider-を準備する">テストや Storybook 用の MockProvider を準備する</h3> <p>Redux や Context を使っても子供のコンポーネントがどのグローバルオブジェクトにアクセスしているかわかりにくいことには変わりありません。 とはいえグローバルオブジェクトにアクセスできるのは初期化時のみに限定しているので、テストや Storybook では MockProvider を用意することでグローバルオブジェクトを個別に準備しなくてもよいようにしました。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> FAKE_PROPS <span class="synStatement">=</span> <span class="synIdentifier">{</span> userName: <span class="synConstant">&quot;hoge&quot;</span><span class="synStatement">,</span> userAge: <span class="synConstant">10</span><span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synComment">// グローバルオブジェクトのうちテスト時に上書きしたいものだけを上書きできるようにするため再起的にPartialな値を受け取る</span> <span class="synStatement">type</span> Props <span class="synStatement">=</span> NestedPartial<span class="synStatement">&lt;</span>ServerSideProps<span class="synStatement">&gt;;</span> <span class="synType">const</span> MockProvider <span class="synStatement">=</span> <span class="synStatement">(</span><span class="synIdentifier">{</span> props<span class="synStatement">,</span> children<span class="synStatement">,</span> <span class="synIdentifier">}</span>: <span class="synIdentifier">{</span> props: Props<span class="synStatement">;</span> children: React.ReactNode<span class="synStatement">;</span> <span class="synIdentifier">}</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synComment">// あらかじめデフォルト値をFAKE_PROPSとして準備し、引数のPropsとマージする</span> <span class="synType">const</span> merged <span class="synStatement">=</span> merge<span class="synStatement">(</span>FAKE_PROPS<span class="synStatement">,</span> props<span class="synStatement">);</span> <span class="synType">const</span> store <span class="synStatement">=</span> createReduxStore<span class="synStatement">(</span>merged<span class="synStatement">);</span> <span class="synType">const</span> initialContextValue <span class="synStatement">=</span> createReactContextInitialValue<span class="synStatement">(</span>merged<span class="synStatement">);</span> <span class="synStatement">return</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>ReduxProvider store<span class="synStatement">=</span><span class="synIdentifier">{</span>store<span class="synIdentifier">}</span><span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>ReactContext.Provider value<span class="synStatement">=</span><span class="synIdentifier">{</span>initialContextValue<span class="synIdentifier">}</span><span class="synStatement">&gt;</span> <span class="synIdentifier">{</span>children<span class="synIdentifier">}</span> <span class="synStatement">&lt;</span>/ReactContext.Provider<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/ReduxProvider<span class="synStatement">&gt;</span> <span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> </pre> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> Story <span class="synStatement">=</span> <span class="synStatement">(</span> <span class="synStatement">&lt;</span>MockProvider props<span class="synStatement">=</span><span class="synIdentifier">{{</span> <span class="synComment">// FAKE_PROPSとの差分だけ宣言する</span> userName: <span class="synConstant">&quot;fuga&quot;</span><span class="synStatement">,</span> <span class="synIdentifier">}}</span> <span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>PageContents /<span class="synStatement">&gt;</span> <span class="synStatement">&lt;</span>/MockProvider<span class="synStatement">&gt;</span> <span class="synStatement">);</span> </pre> <h3 id="グローバルオブジェクトを-Zod-を使って-validate-する">グローバルオブジェクトを Zod を使って validate する</h3> <p><code>getServerSideProps</code> で <code>window</code>領域にアクセスするときに型安全ではないという問題は依然として残りました。</p> <p>そのため<a href="https://github.com/colinhacks/zod">Zod</a> を利用して型安全に扱えるようにしています。</p> <p><a href="https://github.com/colinhacks/zod">Zod</a> は TypeScript の型を利用してデータを静的にチェックできるライブラリです。</p> <pre class="code lang-typescript" data-lang="typescript" data-unlink><span class="synType">const</span> ServerSidePropsSchema <span class="synStatement">=</span> z.<span class="synType">object</span><span class="synStatement">(</span><span class="synIdentifier">{</span> userName: z.<span class="synType">string</span><span class="synStatement">(),</span> userAge: z.<span class="synType">number</span><span class="synStatement">(),</span> <span class="synIdentifier">}</span><span class="synStatement">);</span> <span class="synType">const</span> parseServerSideProps <span class="synStatement">=</span> <span class="synStatement">(</span>serverSideProps: <span class="synType">unknown</span><span class="synStatement">)</span> <span class="synStatement">=&gt;</span> ServerSidePropsSchema.parse<span class="synStatement">(</span>serverSideProps<span class="synStatement">);</span> <span class="synStatement">export</span> <span class="synStatement">type</span> ServerSideProps <span class="synStatement">=</span> ReturnType<span class="synStatement">&lt;typeof</span> parseServerSideProps<span class="synStatement">&gt;;</span> <span class="synStatement">export</span> <span class="synType">const</span> getServerSideProps <span class="synStatement">=</span> <span class="synStatement">()</span>: ServerSideProps <span class="synStatement">=&gt;</span> <span class="synIdentifier">{</span> <span class="synType">const</span> data: <span class="synType">unknown</span> <span class="synStatement">=</span> <span class="synIdentifier">{</span> <span class="synComment">// @ts-ignore</span> userName: <span class="synSpecial">window</span>.userData.userName<span class="synStatement">,</span> <span class="synComment">// @ts-ignore</span> userAge: <span class="synSpecial">window</span>.userData.userAge<span class="synStatement">,</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> <span class="synStatement">return</span> parseServerSideProps<span class="synStatement">(</span>data<span class="synStatement">);</span> <span class="synIdentifier">}</span><span class="synStatement">;</span> </pre> <p>これで <code>getServerSideProps</code> したときに意図せずデータの型が変わった場合やプロパティが削除された場合などでエラーが発生するので開発中のバグに気づきやすくなります。</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/y/yellow-sabotech/20231103/20231103201836.png" width="1200" height="784" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>最終的なグローバルオブジェクトの取り扱い構造イメージ</figcaption></figure></p> <h2 id="おわりに">おわりに</h2> <p>グローバルオブジェクトの扱いについて試行錯誤の結果、テストでのモックがしやすくなり Storybook で必要なデータのみを用意することができるようになりました。 トレードオフとしてコードの複雑性が多少上がってしまいましたが、チームとしては元の状態よりもグローバルオブジェクトを扱いやすくなっており、今後も改善していきたいと思っています。</p> <p>刷新プロジェクトではレガシーコードの刷新をするために様々な工夫をしながら粛々と進めています。 今回の取り組みが同じようなレガシー刷新の助けになれば幸いです。</p> <h2 id="サイボウズではフロントエンドエンジニアを募集しています">サイボウズではフロントエンドエンジニアを募集しています</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Ffront-end-engineer-kintone.html" title="フロントエンドエンジニア(kintoneアーキテクチャ刷新PJ)キャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fsenior-front-end-engineer-kintone.html" title="シニアフロントエンドエンジニア(kintoneアーキテクチャ刷新PJ)キャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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></p> yellow-sabotech スクラムマスター専任と兼務の両方を経験してみて感じた違いと僕が専任を推す理由 hatenablog://entry/6801883189057321972 2023-11-15T11:15:00+09:00 2023-11-15T11:15:00+09:00 こんにちは、サイボウズでスクラムマスターとして働いている村田です。 2023年8月から kintone の新規機能を開発するチームに移動し、週3日(火水木)勤務で専任スクラムマスターとして活動しています。 それ以前の約1年間は、kintone のヘッダーを React 化するチームでスクラムマスターとエンジニアの役割を兼務していました。 活動紹介 blog.cybozu.io 以前所属していたチームはゴールを達成して現在は解散しています。この活動で得られた学びを Spotify で配信しているので、お時間があればこちらも聞いてみてください。 open.spotify.com 今回は前と今のチー… <p>こんにちは、サイボウズでスクラムマスターとして働いている<a href="https://twitter.com/kuroppe1819">村田</a>です。 2023年8月から kintone の新規機能を開発するチームに移動し、週3日(火水木)勤務で専任スクラムマスターとして活動しています。 それ以前の約1年間は、kintone のヘッダーを React 化するチームでスクラムマスターとエンジニアの役割を兼務していました。</p> <p>活動紹介 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2023%2F08%2F29%2F101907" title="React 化した共通ヘッダーを kintone の全ページに適用しました! - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2023/08/29/101907">blog.cybozu.io</a></cite></p> <p>以前所属していたチームはゴールを達成して現在は解散しています。この活動で得られた学びを Spotify で配信しているので、お時間があればこちらも聞いてみてください。</p> <p><iframe style="border-radius: 12px" width="100%" height="152" title="Spotify Embed: #2 部分的なReact刷新をやり遂げたAppShellというチームについて" frameborder="0" allowfullscreen allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy" src="https://open.spotify.com/embed/episode/5ve5hGEXH58dpBgubGWFzL?utm_source=oembed"></iframe><cite class="hatena-citation"><a href="https://open.spotify.com/episode/5ve5hGEXH58dpBgubGWFzL">open.spotify.com</a></cite></p> <p>今回は前と今のチームでスクラムマスター兼務と専任両方の経験を通して得た違いを共有したいと思います。</p> <h2 id="兼務時代">兼務時代</h2> <h3 id="スクラムマスターとエンジニアの兼務を始めた理由">スクラムマスターとエンジニアの兼務を始めた理由</h3> <p>エンジニアとスクラムマスターの兼務を始めたのは今から2年前です。当時の僕は<a href="https://dev-moyashi.hatenablog.com/entry/2021/12/17/141731">開発組織のチームのあり方に疑問を抱いていました</a>。</p> <p>そんなときに<a href="https://blog.cybozu.io/entry/2022/02/04/171154">フロントエンドリアーキテクチャPJ(フロリア)</a>という新しいチーム体制でのプロジェクトが始まり、1チームに1人スクラムマスターを配置することが決まりました。自分はチームの成果を最大化するために試してみたいアイデアを沢山持っていて、これらのアイデアを実際に試すにあたってスクラムマスターは最適な役割だったため立候補しました。とはいえ、いきなりスクラムマスターになるのはキャリアの不安がありましたし、できることならエンジニアとしてキャリアを積んでいきたい気持ちも強かったため、まずは兼務でスクラムマスターを始めてみることにしました。</p> <h3 id="兼務時代のふるまい">兼務時代のふるまい</h3> <p>エンジニアとスクラムマスターを兼務していました。割合はチームの状況に合わせて変えていました。スクラムマスター業が1割の週もあれば、9割の週もありました。スクラムマスターはチームに1人で代わりがいなかったため、自分の場合はスクラムマスター業を優先して、残った工数でエンジニア業に勤しむ動き方をしていました。</p> <p>兼務時代は目の前の課題解決に意識が向くことが多かったです。プレイヤーとしてのアウトプットも求められるため、スクラムマスターとしてチーム内の目の前のチームの阻害要因を解決したら、即エンジニアに転身してコードを書いていました。基本的にチーム内の活動に目がいきがちで、目の前の課題がチームの外の協力を得ないと進められない状況になったときに初めてチーム外との協力関係を築いていくような立ち回りをしていました。一個人の目線で解決した方が良いと感じた課題をチームに提案して共感が得られたら解決に向けて動き始めることが多かったです。</p> <h2 id="専任時代">専任時代</h2> <h3 id="スクラムマスター専任を始めた理由">スクラムマスター専任を始めた理由</h3> <p>フロリアで活動していたチームが解散し、新たに新規機能を開発するチームでスクラムマスターとして活動することになりました。前と同じ規模のチームで兼務として似たような仕事をしても得られるものが少ないと考えたため、思い切って専任に挑戦してみました。</p> <h3 id="専任時代のふるまい">専任時代のふるまい</h3> <p>自分が所属しているチームをより俯瞰的に観察するようになりました。今までコードを書くために使っていた時間をチームの中長期的なビジョンの実現に向けた活動や、チーム外との関係性強化に向けて動くことが多くなりました。兼務時代と違って自分が直接成果物を生み出す仕事をしなくなったので、個々のチームメンバーの意見を拾う場を整えたり、世の中や社内の他のスクラムチームの事例を持ち帰ってチームに伝えたり、チームの成果を最大化するために支援に集中する応援団長的なふるまいが強化されました。常にチームの中と外の状況を見て、何を解決するのが最も効果的か考えてチームメンバーに促すことが多くなりました。</p> <h2 id="スクラムマスター専任になって良かったこと悪かったこと">スクラムマスター専任になって良かったこと・悪かったこと</h2> <h3 id="良かったこと">良かったこと</h3> <h4 id="1-役割が明確になった">1. 役割が明確になった</h4> <p>専任として活動することで、「今、エンジニアとスクラムマスターどっちの役割で話してるんだっけ?」という混乱がなくなりました。兼務時代は「今はスクラムマスターの立場で話すんだけど…」という枕詞を使ってなるべくメンバーを混乱させないようにふるまっていました(この枕詞を使ったとしても聞いている側は混乱していたと思います)。また、一度エンジニアとして深く介入しすぎると、オーナーシップを他のメンバーに移すことが難しくなり、解決するまで自分がリードしてしまうことも何度もありました。専任になることで役割が明確になり、課題解決に向けて最期までメンバーを支援することに集中できるようになりました。自分がリードするのではなく、リードする人を育てるふるまいを意識しています。</p> <h4 id="2-手段に拘らなくなった">2. 手段に拘らなくなった</h4> <p>兼務時代は自分が実行役も担っていたため、どうしても納得できない手段に対しては反対意見を出さざるを得ませんでした。専任になってからは手段を開発メンバーに任せるようになったため、「なぜ今、我々がその課題を解決するのか」という目的を定めることに関心を寄せるようになりました。手段は常日頃から課題に向き合っているチームメンバーの方がより良い方法を知っていますし、仮に誤った選択だったとしたとしてもスクラムマスターが効果的なふりかえりの場を用意できていれば自ずと最適解に向けて収束していくと信じているので、取り返しのつくことはすべて任せるようにしています。</p> <p>スクラムマスターとして特に意識しているのは意思決定に必要な情報をチームの外から引っ張ってくることと、チームの活動が外から見えるようにすること(=スクラムの3本柱における透明性)です。場に情報を貯めて、メンバーが考えて実行する機会を増やして小さな意思決定を繰り返していけば、次第に大きなことも決められるチームに成長すると信じています。</p> <h3 id="悪かったこと">悪かったこと</h3> <h4 id="1-チームメンバーとの認識のズレが大きくなった">1. チームメンバーとの認識のズレが大きくなった</h4> <p>開発メンバーと同じ目線で物事を捉えることができなくなってしまったので、問題意識や課題感についてメンバーから共感を得て主体的に進めることが難しくなりました。自分とメンバーとの間に情報の非対称性がある状況で目的を伝えてもチームメンバーは納得してくれません。専任になってからは今まで以上に「よく観察すること」「よく話を聞いてから判断すること」が大事だと痛感するようになりました。まず対話の場を作り、お互いが持っている情報を共有し合って共通認識を作るプロセスを踏むことを意識しています。</p> <p>しかし、頭では理解しているつもりでも未熟者である故に色々と物申したくなる気持ちが煮えたぎり、ある日唐突に主観100%の意見をチームにぶつけて場をしらけさせてしまうことが度々あります。言うは易し行うは難しだなと日々感じています。</p> <h2 id="チーム同士の関係性にアプローチしたいなら専任を勧めたい">チーム同士の関係性にアプローチしたいなら専任を勧めたい</h2> <p>最初のとっかかりとして兼務を選ぶのは良い選択だと思います。スクラムマスターの所属している小さな1チームにアプローチするだけなら兼務でも十分責務を果たせると思いました(<a href="https://scrummasterway.com/scrummasterway-ja.html">ScrumMasterWayにおけるレベル1</a>)。</p> <p>もしチーム同士の関係性にアプローチすることを考えているなら専任をおすすめします。チーム同士の関係性にアプローチを試みたいなら、まず前提としてチームの外側で中立的な立場に立つことが求められます。チーム内の開発メンバーとしてのアウトプットが求められる状況下で、中立的な立場を維持しながら関係性にアプローチしていくのは難しいと判断しました(<a href="https://scrummasterway.com/scrummasterway-ja.html">ScrumMasterWayにおけるレベル2 or 3</a>)。</p> <p>大事なことは1つのチームの中で組織構造のレイヤーの異なる立場の役割を兼務しないことだと考えています。自分の場合はスクラムマスターとしてのアプローチの幅を広げながら、エンジニアのキャリアを積むことを理想としていたため、現在は週3日(火水木)サイボウズでスクラムマスター専任として働き、それ以外の日は業務委託で別の会社のエンジニアとして働く働き方を選択しています<a href="#f-20b391c7" id="fn-20b391c7" name="fn-20b391c7" title="社内規定に従い上長からの承認を得ています。">*1</a>。</p> <div class="footnote"> <p class="footnote"><a href="#fn-20b391c7" id="f-20b391c7" name="f-20b391c7" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">社内規定に従い上長からの承認を得ています。</span></p> </div> yellow-sabotech サイボウズサマーインターン 2023 報告 〜 プラットフォームコース hatenablog://entry/6801883189058412984 2023-11-15T08:00:00+09:00 2023-11-15T08:00:04+09:00 cybozu.com Cloud Platform 部の新井です。 Cloud Platform 部では現在、旧クラウド基盤上で動作している製品を、Kubernetes ベースの新基盤に移行させるためのコンポーネントを開発・運用しています。 今年 2023 年、Cloud Platform 部ではプラットフォームコースという新しいコースで、製品の新基盤移行に関われるインターンを開催し、平地さんに参加いただきました。 そしてインターンの中では、Envoy を用いた TLS のクライアント認証に関する技術検証を行なっていただきました。 ただし、単純なクライアント認証ではなく、 認証局が複数個あり、… <p>cybozu.com Cloud Platform 部の新井です。 Cloud Platform 部では現在、旧クラウド基盤上で動作している製品を、Kubernetes ベースの新基盤に移行させるためのコンポーネントを開発・運用しています。 今年 2023 年、Cloud Platform 部ではプラットフォームコースという新しいコースで、製品の新基盤移行に関われるインターンを開催し、<a href="https://github.com/csenet">平地さん</a>に参加いただきました。 そしてインターンの中では、Envoy を用いた TLS のクライアント認証に関する技術検証を行なっていただきました。 ただし、単純なクライアント認証ではなく、</p> <ul> <li>認証局が複数個あり、さらに動的に増減する</li> <li>コネクションごとに、クライアント証明書の検証に利用する認証局が異なる</li> </ul> <p>という要件を満たすような実装を行っていただきました。 本記事では、インターンの内容とその成果をご紹介します。</p> <h2 id="インターンの進め方">インターンの進め方</h2> <p>インターンの開催期間は、8/28(月)から 9/8(金)までの2週間で、オンラインでの開催でした。 1日目から2日目は人事のオリエンテーションや環境構築、インターンで取り組む課題の説明を行いました。 いくつか用意していた新基盤移行のための実タスクのうち、参加者の希望を聞いて、冒頭でも紹介した Envoy を用いたクライアント認証の技術検証タスクをやってもらうことにしました。</p> <p>ただ、メンターとしては、このタスクはかなり困難な挑戦になると予想していました。 というのも、Envoy は導入するかどうかすら未検討ということもあって、メンター陣が Envoy に詳しくなかったからです。 また、参加者自身も、Linux や Kubernetes に関する知識は持っていたのですが、TLS の仕様についてそれほど詳しいわけではありませんでした。 しかし、参加者自身が積極的に Envoy の調査・コーディングをして、その状況を報告してくれたので、その内容について議論するうちに、ともに Envoy の機能について理解を深められました。 また、TLS の仕様や社内における TLS の利用についてはメンターからレクチャーを行ったり、調査に行き詰まった時は、適宜 Zoom を繋いで一緒に悩んだりしながらタスクを進めました。 結果として、当初メンターが想定していた以上の成果が得られたと思います。</p> <p>その他、タスクを進める以外の業務としては、メンターが出席する会議に一緒に出てもらい、製品移行時のアーキテクチャを検討する議論や、基盤の運用に関する議論などを見てもらいました。</p> <h2 id="タスクの内容">タスクの内容</h2> <p>まずは、今回取り組んだタスクである「Envoy を用いた、TLS のクライアント認証の際に使用する認証局を動的に選択する方法の検証」が必要になった背景と関連用語について説明します。</p> <h3 id="TLS-におけるクライアント認証">TLS におけるクライアント認証</h3> <p>TLS では、X.509 証明書を用いたサーバ認証を行います。 サーバ認証では、クライアントは、サーバから証明書を受け取ると、今通信しているサーバは本当にその証明書の所有者か、その証明書は信頼された認証局(CA:Certificate Authority)から発行されたものかなどを検証します。 クライアント認証はその逆で、サーバがクライアント証明書を受け取り、その検証を行います。</p> <p>サーバ認証が必須なのに対し、クライアント認証は任意です。 言い換えると、クライアント認証を行わないときは、クライアントがサーバを一方的に認証し、クライアント認証を行うときは、クライアントとサーバが相互に認証します。 そのため、この方式を mutual TLS(mTLS)と呼びます。</p> <p><figure class="figure-image figure-image-fotolife" title="mTLS"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/a/arailly/20231113/20231113132641.png" alt="mTLS" width="641" height="339" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>mTLS</figcaption></figure></p> <h3 id="社内での利用">社内での利用</h3> <p>弊社のクラウド基盤である cybozu.com の内部に、クライアント認証を行うコンポーネントがあり、これを Kubernetes 基盤に移行する必要があります。 しかし、このコンポーネントのクライアント認証は少し特殊で、アクセス元の SNI(Server Name Indication)<a href="#f-e3e19dfa" id="fn-e3e19dfa" name="fn-e3e19dfa" title="TLS の拡張で、TLS のコネクション確立時に接続先のサーバ名をクライアントからサーバに伝えられます。">*1</a> ごとに、クライアント証明書を検証する CA を切り替えます。 さらに、アクセス元の SNI のバリエーションは動的に増減するので、その対応ルールもまた動的に更新しなければなりません。</p> <p>少しわかりにくいので、具体的に説明します。 例えば、Apple と Banana という 2種類のクライアントがいるとします。 Apple は apple.example.com という SNI でサーバにアクセスし、banana.example.com という SNI でサーバにアクセスします。 これらを認証するために、クライアント証明書を配布するのですが、Apple に配布する証明書は Apple CA から、Banana に配布する証明書は Banana CA からというように、SNI ごとに異なる CA から発行された証明書を配布します。 そのため、Apple からのリクエストが来た時には Apple CA の証明書を使って、提示されたクライアント証明書が Apple CA から発行されたものかどうかを検証し、Banana からのリクエストには、 Banana CA 証明書を使って検証しなければなりません。 さらに、将来 chocolate.example.com という SNI でアクセスしてくる新たなクライアントが現れるかもしれない、という状況です。</p> <p><figure class="figure-image figure-image-fotolife" title="cy-mtls"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/a/arailly/20231113/20231113132953.png" width="633" height="685" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>社内における mTLS の利用</figcaption></figure></p> <p>この仕組みを Kubernetes 基盤上でも提供し続けるためには、Nginx のようなロードバランサを単純には使えない<a href="#f-bed6d310" id="fn-bed6d310" name="fn-bed6d310" title="Nginx でも、SNI ごとにクライアント証明書を検証する CA を使い分けられます。しかし、SNI のバリエーションが増えた時、それに対応する CA をロードするには再起動が必要です。私たちのユースケースでは頻繁に SNI のバリエーションが増えることが予想されるため、そのたびに Nginx の再起動を行うのは現実的でないと判断しました。">*2</a>ので、他の選択肢を検討する必要がありました。 そこで、柔軟にプロキシの設定を変更できる Envoy について調査することにしました。</p> <h2 id="Envoy">Envoy</h2> <p>Envoy は、非常に柔軟な設定を書けるプロキシとして動作します。 それだけでなく、xDS API と呼ばれるインタフェース経由で、動的にその設定を変更できるのが大きな特徴です。 ここで、xDS API 経由で動的に設定を配信するサーバをコントロールプレーンと呼びます。 それに対して、受け取った設定を元に実際にプロキシとして動作するコンポーネントをデータプレーンと呼びます(この文脈では Envoy がデータプレーンに相当します)。</p> <p>本節では、シンプルかつ静的な Envoy の設定例を紹介した後、クライアント認証の設定例について説明します。</p> <h3 id="Envoy-の設定例">Envoy の設定例</h3> <p>まずは、<a href="https://www.envoyproxy.io/docs/envoy/latest/start/quick-start/configuration-static">Envoy の公式チュートリアル</a>に沿って、静的な Envoy の設定方法を紹介します。 また、<a href="https://www.envoyproxy.io/docs/envoy/latest/start/install">Installing Envoy</a> の手順で Envoy がインストールされていると仮定します。</p> <p>Envoy の静的な設定を書くのに必要なことは、listeners と clusters リソースを static_resources 内で指定することです。 設定例は以下の通りです。 また、設定の中でも重要な点、補足が必要と感じた点はコメントで補足を入れています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">static_resources</span><span class="synSpecial">:</span> <span class="synIdentifier">listeners</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> listener_0 <span class="synComment"> # Envoy がリッスンするアドレスとポートを指定</span> <span class="synIdentifier">address</span><span class="synSpecial">:</span> <span class="synIdentifier">socket_address</span><span class="synSpecial">:</span> <span class="synIdentifier">address</span><span class="synSpecial">:</span> 0.0.0.0 <span class="synIdentifier">port_value</span><span class="synSpecial">:</span> <span class="synConstant">10000</span> <span class="synIdentifier">filter_chains</span><span class="synSpecial">:</span> <span class="synComment"> # プロキシのルールを指定</span> <span class="synStatement">- </span><span class="synIdentifier">filters</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> envoy.filters.network.http_connection_manager <span class="synIdentifier">typed_config</span><span class="synSpecial">:</span> <span class="synConstant">&quot;@type&quot;</span><span class="synSpecial">:</span> type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager <span class="synIdentifier">stat_prefix</span><span class="synSpecial">:</span> ingress_http <span class="synIdentifier">access_log</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> envoy.access_loggers.stdout <span class="synIdentifier">typed_config</span><span class="synSpecial">:</span> <span class="synConstant">&quot;@type&quot;</span><span class="synSpecial">:</span> type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog <span class="synIdentifier">http_filters</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> envoy.filters.http.router <span class="synIdentifier">typed_config</span><span class="synSpecial">:</span> <span class="synConstant">&quot;@type&quot;</span><span class="synSpecial">:</span> type.googleapis.com/envoy.extensions.filters.http.router.v3.Router <span class="synIdentifier">route_config</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> local_route <span class="synIdentifier">virtual_hosts</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> local_service <span class="synComment"> # ワイルドカードが指定されているので、任意のドメインへのアクセスはこのルールに沿って処理される</span> <span class="synIdentifier">domains</span><span class="synSpecial">:</span> <span class="synSpecial">[</span><span class="synConstant">&quot;*&quot;</span><span class="synSpecial">]</span> <span class="synIdentifier">routes</span><span class="synSpecial">:</span> <span class="synComment"> # / という prefix が付いたリクエストは、www.envoyproxy.io にホストが書き換えられる</span> <span class="synStatement">- </span><span class="synIdentifier">match</span><span class="synSpecial">:</span> <span class="synIdentifier">prefix</span><span class="synSpecial">:</span> <span class="synConstant">&quot;/&quot;</span> <span class="synIdentifier">route</span><span class="synSpecial">:</span> <span class="synIdentifier">host_rewrite_literal</span><span class="synSpecial">:</span> www.envoyproxy.io <span class="synComment"> # cluster の設定は service_envoyproxy_io を利用</span> <span class="synIdentifier">cluster</span><span class="synSpecial">:</span> service_envoyproxy_io <span class="synIdentifier">clusters</span><span class="synSpecial">:</span> <span class="synComment"> # www.envoyproxy.io との接続に関する設定</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> service_envoyproxy_io <span class="synComment"> # DNS の設定</span> <span class="synIdentifier">type</span><span class="synSpecial">:</span> LOGICAL_DNS <span class="synComment"> # Comment out the following line to test on v6 networks</span> <span class="synIdentifier">dns_lookup_family</span><span class="synSpecial">:</span> V4_ONLY <span class="synIdentifier">load_assignment</span><span class="synSpecial">:</span> <span class="synIdentifier">cluster_name</span><span class="synSpecial">:</span> service_envoyproxy_io <span class="synComment"> # 実際に接続するアドレスの指定</span> <span class="synIdentifier">endpoints</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">lb_endpoints</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">endpoint</span><span class="synSpecial">:</span> <span class="synIdentifier">address</span><span class="synSpecial">:</span> <span class="synIdentifier">socket_address</span><span class="synSpecial">:</span> <span class="synIdentifier">address</span><span class="synSpecial">:</span> www.envoyproxy.io <span class="synIdentifier">port_value</span><span class="synSpecial">:</span> <span class="synConstant">443</span> <span class="synIdentifier">transport_socket</span><span class="synSpecial">:</span> <span class="synComment"> # TLS の設定</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> envoy.transport_sockets.tls <span class="synIdentifier">typed_config</span><span class="synSpecial">:</span> <span class="synConstant">&quot;@type&quot;</span><span class="synSpecial">:</span> type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext <span class="synIdentifier">sni</span><span class="synSpecial">:</span> www.envoyproxy.io </pre> <p>次に、この内容で動作確認を行います。 以下のコマンドにより、この設定ファイルを、Envoy に読み込ませて起動します。</p> <pre class="code" data-lang="" data-unlink>$ wget https://www.envoyproxy.io/docs/envoy/latest/_downloads/92dcb9714fb6bc288d042029b34c0de4/envoy-demo.yaml 2&gt; /dev/null $ ./envoy -c envoy-demo.yaml [2023-10-26 02:06:55.603][3223099][info][main] [source/server/server.cc:413] initializing epoch 0 (base id=0, hot restart version=11.104) ...</pre> <p>別の端末で Envoy にアクセスします。</p> <pre class="code" data-lang="" data-unlink>$ curl -i localhost:10000 HTTP/1.1 200 OK accept-ranges: bytes age: 345 cache-control: public,max-age=0,must-revalidate content-length: 15388 content-security-policy: frame-ancestors &#39;self&#39;; content-type: text/html; charset=UTF-8 date: Thu, 26 Oct 2023 02:07:42 GMT etag: &#34;9ba8c8215f215781dc8a29c164ee46a3-ssl&#34; server: envoy strict-transport-security: max-age=31536000 x-nf-request-id: 01HDMVSJSNGKQXG9ZRCAAJ9J0W x-envoy-upstream-service-time: 279 &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;title&gt;Envoy proxy - home&lt;/title&gt; ...</pre> <p>この結果から、<code>localhost:10000</code> でリッスンしている Envoy にアクセスすると、プロキシ先である Envoy proxy の公式ページからのレスポンスが返っていることがわかります。</p> <h2 id="タスクに対するアプローチ">タスクに対するアプローチ</h2> <p>本タスクは、あらかじめ方針が決まっていたわけではありませんでした。 そのため、インターン期間中に参加者自身に様々な機能について調査してもらい、実装方針を決めるところから取り組んでもらいました。 具体的には、ext_authz(External authorization filter)、SDS(Secret Discovery Service)などの機能を試してもらい、最終的に、LDS(Listener Discovery Service)を利用した方法がうまくいきそうだということがわかりました。 そして、参加者に要件を満たす LDS のコントロールプレーンを実装してもらいました。</p> <p>本章では、これらの機能について試したことと、最終的にうまくいった方法を説明します。</p> <h3 id="ext_authzExternal-authorization-filterの利用">ext_authz(External authorization filter)の利用</h3> <p><a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/ext_authz_filter#config-http-filters-ext-authz">ext_authz</a> は、認証を外部のサーバに切り出せる機能です。 Envoy には、<a href="https://www.envoyproxy.io/docs/envoy/latest/api-v3/service/auth/v3/external_auth.proto#authorization-service-proto">Authorization service</a> という API 仕様が決められています。 Envoy は、クライアントからのリクエストを受けると、この仕様に沿ってリクエストのメタデータを認証サーバに送信します。 よって、そのデータを元に認証を行い、結果を Authorization service API で送信するロジックを実装することにより、下図のように外部サーバによる認証を実現できます。</p> <p><figure class="figure-image figure-image-fotolife" title="ext_authz"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/a/arailly/20231113/20231113171027.png" width="944" height="381" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ext_authz</figcaption></figure></p> <p>この機能を使って、クライアントから送られてきた証明書とアクセス先の SNI を認証サーバに転送し、SNI に応じて検証に利用する CA を切り替えるロジックを実装するというアプローチを試しました。 しかし、この試みは失敗に終わりました。 元々、ext_authz は主に HTTP のレイヤにおける認証のための機能で、TLS のレイヤのものではありませんでした。 そのため、クライアント証明書を認証サーバに送れることは確認したのですが、TLS 自体は前段の Envoy で終端されてしまうため、後段の認証サーバは TLS のクライアント認証を行えませんでした。 また、TLS を認証サーバで終端するのも難しいことがわかりました。</p> <h3 id="SDSSecret-Discovery-Serviceの利用">SDS(Secret Discovery Service)の利用</h3> <p><a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/security/secret">SDS</a> は、証明書のようなシークレットを設定の中で静的に指定するのではなく、<a href="https://github.com/envoyproxy/envoy/blob/0e441fbe53b4fe530ba41b8f24541afd5a734d86/api/envoy/service/secret/v3/sds.proto">SDS API</a> 経由で動的に配信するサービスです。 この機能について、設定例を交えて説明します。</p> <p>例えば、<a href="https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ssl">Envoy における TLS の設定例</a>では、以下のように証明書を指定しています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">static_resources</span><span class="synSpecial">:</span> <span class="synIdentifier">listeners</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> listener_0 <span class="synIdentifier">address</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">socket_address</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">address</span><span class="synSpecial">:</span> 127.0.0.1, <span class="synIdentifier">port_value</span><span class="synSpecial">:</span> <span class="synConstant">10000</span><span class="synSpecial">}}</span> <span class="synIdentifier">filter_chains</span><span class="synSpecial">:</span> ...(省略)... <span class="synIdentifier">transport_socket</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> envoy.transport_sockets.tls <span class="synIdentifier">typed_config</span><span class="synSpecial">:</span> <span class="synComment"> # クライアントと Envoy の間の通信に関する設定</span> <span class="synConstant">&quot;@type&quot;</span><span class="synSpecial">:</span> type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext <span class="synIdentifier">common_tls_context</span><span class="synSpecial">:</span> <span class="synComment"> # サーバ証明書を指定</span> <span class="synIdentifier">tls_certificates</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">certificate_chain</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">filename</span><span class="synSpecial">:</span> <span class="synConstant">&quot;certs/servercert.pem&quot;</span><span class="synSpecial">}</span> <span class="synIdentifier">private_key</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">filename</span><span class="synSpecial">:</span> <span class="synConstant">&quot;certs/serverkey.pem&quot;</span><span class="synSpecial">}</span> <span class="synIdentifier">validation_context</span><span class="synSpecial">:</span> <span class="synComment"> # クライアント証明書の検証に使う CA 証明書を指定</span> <span class="synIdentifier">trusted_ca</span><span class="synSpecial">:</span> <span class="synIdentifier">filename</span><span class="synSpecial">:</span> certs/cacert.pem <span class="synIdentifier">clusters</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> some_service ...(省略)... </pre> <p>この例では、<code>static_resources</code>、つまり静的な設定として証明書のパスを記述しています。</p> <p>次に、SDS のドキュメントには、以下のような<a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/security/secret#example-two-sds-server">設定例</a>が紹介されています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">static_resources</span><span class="synSpecial">:</span> <span class="synIdentifier">listeners</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> listener_0 ...(省略)... <span class="synIdentifier">filter_chains</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">transport_socket</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> envoy.transport_sockets.tls <span class="synIdentifier">typed_config</span><span class="synSpecial">:</span> <span class="synConstant">&quot;@type&quot;</span><span class="synSpecial">:</span> type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext <span class="synIdentifier">common_tls_context</span><span class="synSpecial">:</span> <span class="synComment"> # サーバ証明書の設定</span> <span class="synIdentifier">tls_certificate_sds_secret_configs</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> server_cert <span class="synIdentifier">sds_config</span><span class="synSpecial">:</span> <span class="synIdentifier">resource_api_version</span><span class="synSpecial">:</span> V3 <span class="synIdentifier">api_config_source</span><span class="synSpecial">:</span> <span class="synComment"> # gRPC で設定をフェッチ</span> <span class="synIdentifier">api_type</span><span class="synSpecial">:</span> GRPC <span class="synIdentifier">transport_api_version</span><span class="synSpecial">:</span> V3 <span class="synIdentifier">grpc_services</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">envoy_grpc</span><span class="synSpecial">:</span> <span class="synComment"> # SDS サーバの指定</span> <span class="synIdentifier">cluster_name</span><span class="synSpecial">:</span> sds_server_mtls <span class="synComment"> # クライアント証明書を検証する際の設定</span> <span class="synIdentifier">validation_context_sds_secret_config</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> validation_context <span class="synIdentifier">sds_config</span><span class="synSpecial">:</span> <span class="synIdentifier">resource_api_version</span><span class="synSpecial">:</span> V3 <span class="synIdentifier">api_config_source</span><span class="synSpecial">:</span> <span class="synComment"> # gRPC で設定をフェッチ</span> <span class="synIdentifier">api_type</span><span class="synSpecial">:</span> GRPC <span class="synIdentifier">transport_api_version</span><span class="synSpecial">:</span> V3 <span class="synIdentifier">grpc_services</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">envoy_grpc</span><span class="synSpecial">:</span> <span class="synComment"> # SDS サーバの指定</span> <span class="synIdentifier">cluster_name</span><span class="synSpecial">:</span> sds_server_uds <span class="synIdentifier">clusters</span><span class="synSpecial">:</span> ...(SDS サーバの設定に続く)... </pre> <p>このように、Envoy に対してシークレットの設定を gRPC でフェッチするように指示し、シークレットの設定を配信するコントロールプレーンを用意することで、柔軟にシークレットの設定ができます。</p> <p>しかし、調査の結果、SDS は証明書の入れ替えなどの際に、その管理を自動化する目的で使われることが多い機能で、SNI ごとにロードする証明書を切り替えるといった機能は持っていませんでした。 そのため、当初の要件を満たせず、この機能も使えないという結論に至りました。</p> <h3 id="LDSListener-Discovery-Serviceの利用">LDS(Listener Discovery Service)の利用</h3> <p>LDS は、Envoy における listeners の設定を動的に配信できるサービスです。 まずは、もう一度静的な TLS の設定例を見てみます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">static_resources</span><span class="synSpecial">:</span> <span class="synIdentifier">listeners</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> listener_0 <span class="synIdentifier">address</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">socket_address</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">address</span><span class="synSpecial">:</span> 127.0.0.1, <span class="synIdentifier">port_value</span><span class="synSpecial">:</span> <span class="synConstant">10000</span><span class="synSpecial">}}</span> <span class="synIdentifier">filter_chains</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">filters</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> envoy.filters.network.http_connection_manager ...(省略)... <span class="synIdentifier">transport_socket</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> envoy.transport_sockets.tls <span class="synIdentifier">typed_config</span><span class="synSpecial">:</span> <span class="synConstant">&quot;@type&quot;</span><span class="synSpecial">:</span> type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext <span class="synIdentifier">common_tls_context</span><span class="synSpecial">:</span> <span class="synIdentifier">tls_certificates</span><span class="synSpecial">:</span> <span class="synComment"> # サーバ証明書の指定</span> <span class="synStatement">- </span><span class="synIdentifier">certificate_chain</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">filename</span><span class="synSpecial">:</span> <span class="synConstant">&quot;certs/servercert.pem&quot;</span><span class="synSpecial">}</span> <span class="synIdentifier">private_key</span><span class="synSpecial">:</span> <span class="synSpecial">{</span><span class="synIdentifier">filename</span><span class="synSpecial">:</span> <span class="synConstant">&quot;certs/serverkey.pem&quot;</span><span class="synSpecial">}</span> <span class="synIdentifier">validation_context</span><span class="synSpecial">:</span> <span class="synComment"> # クライアント証明書を検証する CA 証明書の指定</span> <span class="synIdentifier">trusted_ca</span><span class="synSpecial">:</span> <span class="synIdentifier">filename</span><span class="synSpecial">:</span> certs/cacert.pem ...(省略)... </pre> <p>コメントにも記載した通り、<code>static_resources.listeners</code> 以下の設定で、サーバ証明書とクライアント証明書を検証する CA を指定できます。 そして、<a href="https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener_components.proto#envoy-v3-api-msg-config-listener-v3-filterchain"><code>static_resources.listeners[].filter_chains[]</code></a> には、<a href="https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/listener/v3/listener_components.proto#envoy-v3-api-msg-config-listener-v3-filterchainmatch"><code>filter_chain_match.server_names</code></a> を指定することにより、SNI の値によって適用する設定を選択できそうだということがわかりました。 よって、SNI のバリエーションの数だけ filter を用意し、それぞれの中で使う CA を指定すれば、要件を満たせそうだと考えました。 これを実現するため、データプレーンには listeners 以下に記述されている内容を、LDS API 経由で受け取るような設定を記述します。 そして、LDS API に沿って、そのような設定を配信するようなコントロールプレーンを開発するというのが、今回の方針です。</p> <h3 id="実装">実装</h3> <p>実装は以下で公開しています。コントロールプレーンの実装には <a href="https://github.com/envoyproxy/go-control-plane">go-control-plane</a> を使いました。</p> <p><cite class="hatena-citation"><a href="https://github.com/cybozu-go/envoy-ca-selection-poc">github.com</a></cite><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fcybozu-go%2Fenvoy-ca-selection-poc" title="GitHub - cybozu-go/envoy-ca-selection-poc" 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></p> <h4 id="注意点">注意点</h4> <p>今回開催したインターンでは、参加者自身に調査してもらい、コードを書いて進めてもらいました。 しかし、社内向けに書いたコードを公開するにあたって、メンターによる修正作業を行う必要がありました。 その作業の関係上、リポジトリのコミットログにはメンターのアカウント名が含まれていますが、この成果の大部分はインターン参加者によるものだということを申し添えます。</p> <h4 id="システムの構成図">システムの構成図</h4> <p>以下に本システムの構成図を示します。 Envoy はコントロールプレーンから設定をフェッチしています。 その内容は以下の通りです。</p> <ul> <li>クライアントが Envoy にクライアント証明書とともにアクセスすると、SNI に応じてクライアント証明書を検証する CA を選択し、TLS を終端する。</li> <li>クライアント認証をパスしたリクエストは、Upstream に転送される。</li> <li>その際、プロキシされたリクエストには、<code>X-Forwarded-Host</code> ヘッダが含まれる。<code>X-Forwarded-Host</code> ヘッダの値には、クライアントがアクセスしたときのホスト名が入る。</li> </ul> <p>また、Upstream は、リクエストの <code>X-Forwarded-Host</code> の値を返すだけのサーバとして振る舞います。</p> <p><figure class="figure-image figure-image-fotolife" title="envoy-architecture"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/a/arailly/20231113/20231113171121.png" width="1173" height="371" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>LDS を用いたアーキテクチャ</figcaption></figure></p> <h4 id="コードの説明">コードの説明</h4> <p>リポジトリには、Envoy の設定ファイル、コントロールプレーンと Upstream のソースコード、そしてそれらをコンテナとして起動し、テストするための設定ファイルなどが含まれます。</p> <h5 id="Envoy-の設定ファイル">Envoy の設定ファイル</h5> <p>Envoy の設定ファイルはリポジトリ内の <a href="https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/config/envoy/envoy.yaml"><code>config/envoy/envoy.yaml</code></a> にあり、 go-control-plane の <a href="https://github.com/envoyproxy/go-control-plane/blob/main/sample/bootstrap-xds.yaml">sample/bootstrap-xds.yaml</a> を使いました。 内容としては、<code>dynamic_resources</code> に <code>lds_config</code> を、<code>api_config_source</code> に <code>api_type: GRPC</code> を指定することにより、LDS の設定を gRPC 経由で受け取れるようになっています。</p> <h5 id="コントロールプレーン">コントロールプレーン</h5> <p>コントロールプレーンの実装は go-control-plane の <a href="https://github.com/envoyproxy/go-control-plane/tree/main/internal/example">example</a> を参考にしました。 ディレクトリ構成は以下の通りです。</p> <pre class="code" data-lang="" data-unlink>tree control-plane control-plane ├── Dockerfile ├── go.mod ├── go.sum ├── logger.go ├── main │ └── main.go ├── README.md ├── repository.go ├── resource.go └── server.go</pre> <p><a href="https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/control-plane/main/main.go">main/main.go</a> はコントロールプレーンのエントリポイントで、コントロールプレーンとして振る舞う gRPC サーバを起動します。</p> <p><a href="https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/control-plane/server.go">server.go</a> は、gRPC サーバの設定を行います。</p> <p><a href="https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/control-plane/resource.go">resource.go</a> は最も重要なファイルで、データプレーンに配信する内容を生成します。 具体的には、上で説明したような、SNI の数だけ filter を用意し、それぞれで使用する CA の証明書を指定した設定を組み立てる処理を行います。 このファイルで定義した関数がサーバから呼ばれます。</p> <p>注意が必要なのは、filter 内で <a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/listeners/listener_filters/tls_inspector#config-listener-filters-tls-inspector">TLS Inspector</a> を有効化しておかないと、SNI を解析できないという点です。 本リポジトリでは、以下の行で有効化しています。</p> <p><a href="https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/control-plane/resource.go#L248-L251">https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/control-plane/resource.go#L248-L251</a></p> <p><a href="https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/control-plane/repository.go">repository.go</a> は、サーバ証明書やクライアント証明書を検証する CA の証明書を読み込みます。 このファイルで定義した関数が resource.go の関数から呼ばれます。</p> <h5 id="Upstream">Upstream</h5> <p><a href="https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/upstream/main.go">upstream/main.go</a> は、リクエストの <code>X-Forwarded-Host</code> ヘッダの値をレスポンスとして返すサーバを起動します。</p> <h3 id="動作確認">動作確認</h3> <p><a href="https://github.com/cybozu-go/envoy-ca-selection-poc/blob/main/README.md">README.md</a> に記載の通り、以下の手順で動作確認ができます。</p> <pre class="code" data-lang="" data-unlink>$ make cert $ make up $ make test</pre> <p><code>make cert</code> は、以下の証明書を発行します。</p> <ul> <li>ルート CA 証明書</li> <li>サーバ証明書(ルート CA から発行)</li> <li>Apple CA 証明書(<code>apple.example.com</code> に対応する CA。ルート CA から発行。)</li> <li>Banana CA 証明書(<code>banana.example.com</code> に対応する CA。ルート CA から発行。)</li> <li>Apple クライアント証明書(Apple CA から発行)</li> <li>Banana クライアント証明書(Banana CA から発行)</li> </ul> <p><code>make up</code> は、コントロールプレーン、データプレーン、Upstream コンテナを起動します。</p> <p><code>make test</code> は、<code>curl</code> を使って <code>apple.example.com</code> と <code>banana.example.com</code> に向けて GET リクエストを送ります。 以下はその結果です。</p> <pre class="code" data-lang="" data-unlink>$ make test ./scripts/test apple.example.com * Added apple.example.com:10000:127.0.0.1 to DNS cache * Hostname apple.example.com was found in DNS cache * Trying 127.0.0.1:10000... * TCP_NODELAY set * Connected to apple.example.com (127.0.0.1) port 10000 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: certs/apple.example.com/ca.pem CApath: /etc/ssl/certs * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 * ALPN, server did not agree to a protocol * Server certificate: * subject: C=US; ST=CA; L=San Francisco; CN=example.com * start date: Oct 24 05:16:00 2023 GMT * expire date: Oct 23 05:16:00 2024 GMT * subjectAltName: host &#34;apple.example.com&#34; matched cert&#39;s &#34;*.example.com&#34; * issuer: C=US; ST=CA; L=San Francisco; CN=Root CA * SSL certificate verify ok. &gt; GET / HTTP/1.1 &gt; Host: apple.example.com:10000 &gt; User-Agent: curl/7.68.0 &gt; Accept: */* &gt; * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * old SSL session ID is stale, removing * Mark bundle as not supporting multiuse &lt; HTTP/1.1 200 OK &lt; date: Tue, 24 Oct 2023 05:25:12 GMT &lt; content-length: 23 &lt; content-type: text/plain; charset=utf-8 &lt; x-envoy-upstream-service-time: 0 &lt; server: envoy &lt; * Connection #0 to host apple.example.com left intact apple.example.com:10000 ./scripts/test banana.example.com * Added banana.example.com:10000:127.0.0.1 to DNS cache * Hostname banana.example.com was found in DNS cache * Trying 127.0.0.1:10000... * TCP_NODELAY set * Connected to banana.example.com (127.0.0.1) port 10000 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * successfully set certificate verify locations: * CAfile: certs/banana.example.com/ca.pem CApath: /etc/ssl/certs * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Request CERT (13): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.3 (OUT), TLS handshake, Certificate (11): * TLSv1.3 (OUT), TLS handshake, CERT verify (15): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 * ALPN, server did not agree to a protocol * Server certificate: * subject: C=US; ST=CA; L=San Francisco; CN=example.com * start date: Oct 24 05:16:00 2023 GMT * expire date: Oct 23 05:16:00 2024 GMT * subjectAltName: host &#34;banana.example.com&#34; matched cert&#39;s &#34;*.example.com&#34; * issuer: C=US; ST=CA; L=San Francisco; CN=Root CA * SSL certificate verify ok. &gt; GET / HTTP/1.1 &gt; Host: banana.example.com:10000 &gt; User-Agent: curl/7.68.0 &gt; Accept: */* &gt; * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * TLSv1.3 (IN), TLS handshake, Newsession Ticket (4): * old SSL session ID is stale, removing * Mark bundle as not supporting multiuse &lt; HTTP/1.1 200 OK &lt; date: Tue, 24 Oct 2023 05:25:12 GMT &lt; content-length: 24 &lt; content-type: text/plain; charset=utf-8 &lt; x-envoy-upstream-service-time: 0 &lt; server: envoy &lt; * Connection #0 to host banana.example.com left intact banana.example.com:10000</pre> <p>この結果から、apple.example.com と banana.example.com への TLS 接続におけるクライアント認証が成功していて、Upstream からのレスポンスである、クライアントがアクセスしたホスト名が表示されていることがわかります。</p> <h3 id="今後取り組みたいこと">今後取り組みたいこと</h3> <p>時間の都合上、インターン期間中に Envoy の性能検証までは実施できませんでした。 運用環境では、SNI のバリエーションは非常に多数であり、それと同じだけの filter_chains を設定しても高速に動作するのか、また、設定のフェッチにどれくらいの時間がかかるのかなどについても調べる必要があります。 その他、Envoy が TLS アラートを返す際に、その挙動をカスタマイズできるのか、などについてもまだ調べられていません。</p> <p>今後、クライアント認証の基盤に使うソフトウェアを選定する際には、今回のインターンで得られた知見に加え、この辺りの情報を揃えた上で、本番環境に導入できるかどうか検討する予定です。</p> <h2 id="最後に">最後に</h2> <p>今回のプラットフォームコースのインターンでは、Envoy を用いた、TLS のクライアント認証の際に使用する認証局を動的に選択する方法の検証というテーマで実施しました。 インターン参加者自身で高速に試行錯誤のサイクルを回してくれたことによって、インターン期間中に PoC が動作するところまで進められました。 この成果により、現行基盤上のクライアント認証を行うコンポーネントを Kubernetes 基盤に移行できる可能性があるとわかり、また、Envoy に関する知見を深められました。 このインターンが、参加者にとっても有意義なものになっていればとても嬉しいです。</p> <p>最後になりますが、Cloud Platform 部では一緒に働いてくれる方を募集中です。 以下の募集要項を見て興味を持った方はぜひご応募ください。</p> <p><cite class="hatena-citation"><a href="https://cybozu.co.jp/recruit/entry/career/cloudplatform.html">cybozu.co.jp</a></cite><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fcloudplatform.html" title="クラウド基盤エンジニア(SRE/Cloud Platform)キャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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></p> <div class="footnote"> <p class="footnote"><a href="#fn-e3e19dfa" id="f-e3e19dfa" name="f-e3e19dfa" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">TLS の拡張で、TLS のコネクション確立時に接続先のサーバ名をクライアントからサーバに伝えられます。</span></p> <p class="footnote"><a href="#fn-bed6d310" id="f-bed6d310" name="f-bed6d310" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">Nginx でも、SNI ごとにクライアント証明書を検証する CA を使い分けられます。しかし、SNI のバリエーションが増えた時、それに対応する CA をロードするには再起動が必要です。私たちのユースケースでは頻繁に SNI のバリエーションが増えることが予想されるため、そのたびに Nginx の再起動を行うのは現実的でないと判断しました。</span></p> </div> arailly フロントエンドリアーキテクトをテーマに「BARフロントえんどう #1」を開催しました! hatenablog://entry/6801883189057359665 2023-11-10T13:15:30+09:00 2023-11-10T13:21:53+09:00 こんにちは。 フロントエンドエキスパートチームの BaHo です 2023 年 10 月 31 日(火)にサイボウズのフロントエンドエンジニア主催で第一回BARフロントえんどうを開催しました。 本記事では、当日の様子やセッションについて紹介します。 BAR フロントえんどうとは BARフロントえんどうは Web フロントエンドのトピックをテーマに、登壇者や参加者の間で情報を共有することで知見を深めてもらおうというコンセプトの勉強会です。 第一回は「フロントエンドリアーキテクト」をテーマとして開催しました。 セッション内容 今回はメインセッションとして Japan Node.js Associa… <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231110/20231110125543.png" width="1200" height="491" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは。 フロントエンドエキスパートチームの <a href="https://twitter.com/b4h0_c4t">BaHo</a> です</p> <p>2023 年 10 月 31 日(火)にサイボウズのフロントエンドエンジニア主催で第一回BARフロントえんどうを開催しました。 本記事では、当日の様子やセッションについて紹介します。</p> <h2 id="BAR-フロントえんどうとは">BAR フロントえんどうとは</h2> <p>BARフロントえんどうは Web フロントエンドのトピックをテーマに、登壇者や参加者の間で情報を共有することで知見を深めてもらおうというコンセプトの勉強会です。 第一回は「フロントエンドリアーキテクト」をテーマとして開催しました。</p> <h2 id="セッション内容">セッション内容</h2> <p>今回はメインセッションとして Japan Node.js Association の古川さん、株式会社出前館の白石さん、株式会社サイボウズの Nokogiri さんのお三方に発表していただきました。</p> <p>発表間に設けた質疑応答タイムでは多くの参加者から質問があり、どの発表も盛り上がっていました。</p> <h3 id="リアーキテクトと開発生産性について">リアーキテクトと開発生産性について</h3> <p>発表者: Japan Node.js Association 古川さん</p> <p>SPACE フレームワークを用いて開発生産性におけるリアーキテクトの位置付けを表現しつつ、リアーキテクトは開発生産性を上げるためだけの手段ではないという認識を広めるセッションでした。</p> <p><iframe id="talk_frame_1099592" class="speakerdeck-iframe" src="//speakerdeck.com/player/484e3afaf2ed4308af9eab12960e5cb3" width="710" height="532" style="aspect-ratio:710/532; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/yosuke_furukawa/riakitekutotokai-fa-sheng-chan-xing-nituite">speakerdeck.com</a></cite></p> <h3 id="エンジニアと-QA-でコラボするフロントエンドリアーキテクチャの開発事例">エンジニアと QA でコラボするフロントエンドリアーキテクチャの開発事例</h3> <p>発表者: サイボウズ株式会社 Nokogiri さん</p> <p>フロントエンド刷新プロジェクト「フロリア」での開発経験を通して、エンジニアと QA が職能を超えてコラボレーションできる可能性を見つけ、その体験について発表していただきました。</p> <p><iframe id="talk_frame_1099526" class="speakerdeck-iframe" src="//speakerdeck.com/player/55d7af0067c24b45b5f7b577f1b8df53" 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/undefined_name/ensiniatoqatekorahosuruhurontoentoriakitekutiyakai-fa-noshi-li">speakerdeck.com</a></cite></p> <h3 id="出前館-Web-フロントエンドにおけるリプレイスプロジェクトの取り組みと反省">出前館 Web フロントエンドにおけるリプレイスプロジェクトの取り組みと反省</h3> <p>発表者: 出前館株式会社 白石さん</p> <p>出前館の Web フロントエンドを PHP から Next.js へ刷新するまでの取り組みを技術選定をベースに発表していただきました。</p> <p><iframe id="talk_frame_1099945" class="speakerdeck-iframe" src="//speakerdeck.com/player/ccfded5388d54362ac73c63d384a8184" 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/demaecan/chu-qian-guan-webhurontoendoripureisupuroziekutonoqu-rizu-mitofan-sheng-nituite">speakerdeck.com</a></cite></p> <p><figure class="figure-image figure-image-fotolife" title="Nokogiriさんの発表の様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231109/20231109161215.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Nokogiriさんの発表の様子</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/y/yellow-sabotech/20231109/20231109161329.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>発表中の会場の様子</figcaption></figure></p> <h2 id="LT-タイム">LT タイム</h2> <p>今回 LT での発表者を募集し、5 名の方に 5 分間の LT をしていただきました。 LT タイムでは質疑応答の時間を設けていませんでしたが、その後の懇親会で LT の内容に関する情報交換も行われていました。</p> <p><figure class="figure-image figure-image-fotolife" title="LTの様子1"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231109/20231109161717.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>LTの様子1</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="LTの様子2"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231109/20231109161741.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>LTの様子2</figcaption></figure></p> <p>また、当日の様子は有志の方々が X(旧 Twitter)にて、ハッシュタグ <a href="https://twitter.com/search?q=%2523BAR%E3%83%95%E3%83%AD%E3%83%B3%E3%83%88%E3%81%88%E3%82%93%E3%81%A9%E3%81%86">#BARフロントえんどう</a> で実況してくれていましたので、当日の様子などを見えみたい方はぜひ閲覧していただければと思います。</p> <h2 id="今後について">今後について</h2> <p>第二回BARフロントえんどうを鋭意企画中です。 時期やテーマも含めて現在検討中ですが、決定次第 <a href="https://twitter.com/cybozuinsideout">SNS</a> や <a href="https://cybozu.connpass.com/">connpass</a> 等で告知予定です。 <a href="https://blog.cybozu.io/">Cybozu Inside Out</a> も含めてチェックしていただけると幸いです。</p> <p>最後になりましたが、これからもBARフロントえんどうをよろしくお願いします!</p> yellow-sabotech Garoonのインフラ移行を行うTsukimiチームの紹介 hatenablog://entry/6801883189049586546 2023-10-30T12:08:41+09:00 2023-10-30T12:08:41+09:00 こんにちは。 Garoon開発チームの洲崎です。 Garoon開発チームを紹介する全5回の記事も今回が最後です! Garoon開発 日本チーム全体 セキュリティ: ❄️Yukimiチーム❄️ リリース: 🌸Hanamiチーム🌸 パフォーマンス: 🚄Nozomiチーム🚄 インフラ: 🌙Tsukimiチーム🌙 <-- 今回はコレ! 今回はGaroonのインフラ移行するTsukimiチームの紹介です! Garoonのインフラ移行するTsukimiチーム <p>こんにちは。 Garoon開発チームの洲崎です。</p> <p>Garoon開発チームを紹介する全5回の記事も今回が最後です!</p> <ul> <li><a href="https://blog.cybozu.io/entry/2023/10/03/162616">Garoon開発 日本チーム全体</a></li> <li><a href="https://blog.cybozu.io/entry/2023/10/04/101916">セキュリティ: ❄️Yukimiチーム❄️</a></li> <li><a href="https://blog.cybozu.io/entry/garoon-four-keys-growth-team">リリース: 🌸Hanamiチーム🌸</a></li> <li><a href="https://blog.cybozu.io/entry/2023/10/23/111500">パフォーマンス: 🚄Nozomiチーム🚄</a></li> <li>インフラ: 🌙Tsukimiチーム🌙 &lt;-- 今回はコレ!</li> </ul> <p>今回はGaroonのインフラ移行するTsukimiチームの紹介です!</p> <p><figure class="figure-image figure-image-fotolife" title="Garoonのインフラ移行するTsukimiチーム"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231027/20231027173351.png" alt="Garoon&#x306E;&#x30A4;&#x30F3;&#x30D5;&#x30E9;&#x79FB;&#x884C;&#x3059;&#x308B;Tsukimi&#x30C1;&#x30FC;&#x30E0;" width="1200" height="670" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Garoonのインフラ移行するTsukimiチーム</figcaption></figure></p> <h1 id="何をするチームなの">何をするチームなの?</h1> <p>Tsukimiチームは「Neco移行」と「クラウド関連の作業」を行なっています。</p> <p>「Neco移行」とは自社で提供しているクラウド環境の基盤変更です。 全社的にKubernetesで動くNeco基盤に移行準備中で、Garoonもこの流れに乗ってNeco移行準備を行なっています。</p> <p>Necoについては次をご参照ください。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.cybozu.io%2Fentry%2F2019%2F09%2F05%2F090000" title="cybozu.com の新データセンター「Neco」が稼働開始 - Cybozu Inside Out | サイボウズエンジニアのブログ" 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.cybozu.io/entry/2019/09/05/090000">blog.cybozu.io</a></cite></p> <p>クラウド関連の機能に触れることが多いことから、「クラウド関連の作業」も行なっています。 具体的にはElasticsearch対応やトラブル対応などです。</p> <p>チームは9人(エンジニア6人, QAエンジニア3人)で構成されています。 必要な作業の検討から実装、テストまでを全てチーム内で完結させることができます。</p> <h1 id="Neco移行の難しさ">Neco移行の難しさ</h1> <p>Tsukimiチームは「Garoonのお引越し作業」をしています。単なる載せ替えと思いきや、意外と大変です。</p> <p>まず、コンテナでの提供になるのでGaroonが動くイメージを作らなければなりません。 外部リソースとの連携など、今までの動作環境とは異なる部分を変える必要があります。</p> <p>単純移植でもいいのですが、Neco時代に最適な技術選定も同時に行なっています。 現行クラウド基盤ではPHP-FPMを利用していますが、「もっと適したものがないのか」検討したりしました。 過去の技術選定時には無かった選択肢もあり、悩みました。</p> <p>他にもNeco移行では基盤変更以外にも大きな変化があります。 これからは我々ソフトウェア開発チームの対応範囲が広がり、よりインフラ的な領域も見ます。 製品知識を持ったメンバーが、製品都合に関わる全般の面倒を見た方が効率良いという観点からです。 これにより、今までにはインフラチームにお任せだったnginxなどのミドルウェアの設定などにも触れました。</p> <p>先の話にはなりますが、お客様に不都合なく現行VM基盤からNeco基盤への移行が求められます。</p> <p>このように今までの製品コードを主に扱ってきたエンジニアに「新しい知識と知恵」を求められるのがNeco移行の難しさです。</p> <h1 id="Neco移行の面白さ">Neco移行の面白さ</h1> <p>一方でこの難しさは面白さでもあります。 「今まで触れることの少なかった領域に触れられること」がNeco移行(Tsukimiチーム)の面白さとも言えます。 中でも特に面白かった経験が3つあります。</p> <p>まず、PHP構成の選定の経験です。 今選べる選択肢を並べて、メリット・デメリットを議論できました。 日々の業務ではあまり意識することのなかったPHPの構成について深く考えることができたのは良い経験でした。</p> <p>次に、運用寄りの技術に触れられた経験です。 今まではnginxやネットワークの設定などは運用メンバにお任せだったのですが、この度、自分達で設定することになりました。 既に動いている環境の設定を参照したりすることで、理解しやすかったです。</p> <p>最後に、新規でサービスを構築できた経験です。 Neco環境でGaroonを提供するにあたって、付随するサービスを新規開発する必要がありました。 こちらのサービスはGoで開発を進めています。 既存サービスの中に居ながら、新技術で新規開発できたのは良い経験でした。</p> <p>このように新しい領域に触れる機会の多いです。経験を通して自分の対応可能範囲が広くなることは嬉しいですね。</p> <h1 id="Tsukimiチームの今">Tsukimiチームの今</h1> <p>今、チーム内では大きく2つのプロジェクトが進行中です。</p> <h2 id="デプロイマイグレーションサービスの構築">デプロイ・マイグレーションサービスの構築</h2> <p>お客様が契約したときにはお客様が利用できるように、各種リソースの割り当てが必要になります。</p> <p>現行基盤でも同様の仕組がありますが、別チームが開発・運用してくれていました。 Neco移行後は製品の知識を持っているソフトウェア開発チームが開発・運用していきます。</p> <p>現在、この仕組をGoで構築しています。要件定義から実装まで自分達で行なっています。</p> <p>これに付随してお客様の全てのリソースをバージョンアップさせる仕組も作成しています。 Garoonには他のプロダクトと異なるバージョンアップ処理があり、検討の難所です。</p> <h2 id="非同期ジョブ実行の仕組の移行">非同期ジョブ実行の仕組の移行</h2> <p>Neco上で非同期ジョブを実行する仕組を構築しています。</p> <p>現在はオンプレ時代から引き継がれるScheduling serviceと呼ばれる仕組で非同期ジョブを実行しています。 Neco環境では事情があって動かしにくいので、Workerと呼ばれる別の仕組で実行しようとしています。 Workerは登録された非同期ジョブを順に実行してくれるミドルウェアです。</p> <p>非同期ジョブ実行の仕組は少々複雑になっています。 即時実行したい非同期ジョブはBackground Job daemonと呼ばれる他の仕組が仲介しているのです。</p> <p>Workerで実行するようにすると仲介が不要になります。 新基盤に移るタイミングで簡単化しようとしています。 ただ、これがなかなか難題でした… <figure class="figure-image figure-image-fotolife" title="Background Job daemonで仲介せずに実行する"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231027/20231027134015.png" alt="Background Job daemon&#x3067;&#x4EF2;&#x4ECB;&#x305B;&#x305A;&#x306B;&#x5B9F;&#x884C;&#x3059;&#x308B;" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Background Job daemonで仲介せずに実行する</figcaption></figure></p> <p>Background Job daemonで動いている非同期ジョブはリアルタイムで動き続けています。 アップデート中も絶えず非同期ジョブが追加・処理されるので以下のような課題を考える必要がありました。</p> <ul> <li>アップデート中に増える非同期ジョブはどのDBのテーブルに登録すべきか</li> <li>増えた非同期ジョブは新・旧どちらのロジックで動かすべきか</li> <li>移行に失敗した場合は復旧できるのか</li> </ul> <p>今回は段階に分けて細かくアップデートすることで、安全にアップデートできるようにしました。 また機会があれば紹介させてください。</p> <h1 id="Tsukimiチームの今後">Tsukimiチームの今後</h1> <p>Neco移行は「細々とした機能対応」や「監視体制の構築」のようにやることがあります。来年ぐらいまで続きそうです。 安定運用のための仕組づくりがメインになってきそうです。</p> <p>また、忘れちゃいけないのが「移行」です。 現在使っていただいているお客様にご不便かけないようにスムーズな切り替えも計画しないといけません。</p> <p>やることはいっぱいありますが、新基盤に切り替えた先には良いことがあります。 リソースを適切に配置できるようになり、お客様体験の向上するでしょう。 他のプロダクトとの独立性も上がるので、リリースしやすくなり開発体験も良くなるはずです。</p> <p>そんな新時代のGaroonを目指して、Tsukimiチームはこれからも頑張っていきます!</p> yellow-sabotech サイボウズサマーインターン 2023 報告 〜 Android コース 〜 hatenablog://entry/6801883189053068222 2023-10-27T17:00:00+09:00 2023-10-27T17:00:00+09:00 こんにちは!kintoneチーム所属のAndroidエンジニア、トニオ(@tonionagauzzi)です。 今回は、サイボウズのインターンシップにおけるAndroidアプリ開発コースについて紹介したいと思います! 概要 サイボウズでは毎年サマーインターンシップを開催しています。2023年度のサマーインターンシップでも、さまざまなコースを用意しました。 この記事では、Androidアプリ開発の2コースについて紹介します。 インターンの内容 Androidアプリ開発は、2種類のコースを用意しました。 Android 1週間コース Android 2週間コース Android 1週間コースには2名… <p>こんにちは!kintoneチーム所属のAndroidエンジニア、トニオ(<a href="https://twitter.com/tonionagauzzi">@tonionagauzzi</a>)です。<br /> 今回は、サイボウズのインターンシップにおけるAndroidアプリ開発コースについて紹介したいと思います!</p> <h2 id="概要">概要</h2> <p>サイボウズでは毎年サマーインターンシップを開催しています。2023年度のサマーインターンシップでも、さまざまなコースを用意しました。<br /> この記事では、Androidアプリ開発の2コースについて紹介します。</p> <h2 id="インターンの内容">インターンの内容</h2> <p>Androidアプリ開発は、2種類のコースを用意しました。</p> <ul> <li><a href="https://cybozu.co.jp/company/job/recruitment/intern/android1.html">Android 1週間コース</a></li> <li><a href="https://cybozu.co.jp/company/job/recruitment/intern/android2.html">Android 2週間コース</a></li> </ul> <p>Android 1週間コースには2名、Android 2週間コースには3名のインターン生にご参加いただきました。<br /> インターン生にはリモートワークで実際のプロジェクトに参加することで、実践的な経験を積んでいただきました。</p> <h2 id="Android-1週間コース">Android 1週間コース</h2> <p>まずはkintoneチームで行われたAndroid 1週間コースの内容です。</p> <h3 id="インターン中の開発の流れ">インターン中の開発の流れ</h3> <p>kintone Androidチームの業務は、主にモブプログラミング形式(以下、モブ)で行っています。<br /> 今回のインターンでは、インターン生2名と社員3名の合計5名で、Zoomを用いたモブを行いました。</p> <h3 id="インターン中のスケジュール">インターン中のスケジュール</h3> <p>大まかなタイムスケジュールは以下の通りでした。</p> <p><figure class="figure-image figure-image-fotolife" title="Android1週間コース時間割"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231024/20231024160435.png" alt="Android1&#x9031;&#x9593;&#x30B3;&#x30FC;&#x30B9;&#x6642;&#x9593;&#x5272;" width="1200" height="473" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Android1週間コース時間割</figcaption></figure></p> <h3 id="ミーティングへの参加">ミーティングへの参加</h3> <p>kintone Androidの開発は、1週間スプリントのスクラムベースで実施しており、インターン中もスクラム開発は行われています。<br /> インターン生には普段のスクラムイベントのうち、朝会(デイリースクラム)に出席していただきました。</p> <p>また、kintone開発チーム全体で実施しているスプリントレビューも見学していただきました。<br /> インターン生が参加したスプリントレビューは、各チームによる成果の説明のあと、プロジェクトマネージャーから次の3か月の開発計画が紹介されるタイミングでもありました。<br /> その日の夕会で感じたことを聞かせていただきました。<br /> そこで得られた感想は以下の通りです。</p> <ul> <li>成果を褒めながら進められていたのが良かった <ul> <li>もっと業務連絡っぽいと予想していた</li> </ul> </li> <li>開発計画の説明内容が、とても考えられたもので良いなと思った</li> </ul> <h3 id="社内イベントへの参加">社内イベントへの参加</h3> <p>メンターや同じチームのメンバーだけでなく、チーム外の人との交流の機会も設けました。<br /> kintoneのiOSエンジニア、QAエンジニア、PMとの雑談会、他プロダクトも交えたAndroidエンジニア同士の雑談会、懇親会、そして青野社長との雑談会も行われました。<br /> リモートワークにもかかわらず、コミュニケーションを多くとって大切にしているという印象を持っていただけたのではないかと思います。</p> <h3 id="取り組んだタスク">取り組んだタスク</h3> <p>私たちがインターンで取り組んだバックログアイテムは、ファイル添付時に<a href="https://developer.android.com/training/data-storage/shared/photopicker?hl=ja">写真選択ツール</a>を利用して画像や動画を添付できるようにする、というものです。</p> <h4 id="背景">背景</h4> <p>kintoneは、サイボウズが提供する業務アプリ開発プラットフォームで、ノーコードでお客様の用途に合わせた業務アプリの作成が可能です。<br /> その機能の1つとして、kintone内で作成したアプリやスペースなどに、写真や動画、ファイルを添付する機能があります。<br /> 添付操作の際、モバイルアプリでは以下のようなボトムシートが表示され、どの方法で添付するかを選択できます。</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/y/yellow-sabotech/20231024/20231024160513.png" alt="&#x65E2;&#x5B58;&#x306E;&#x6DFB;&#x4ED8;&#x30DC;&#x30C8;&#x30E0;&#x30B7;&#x30FC;&#x30C8;" width="1080" height="1080" loading="lazy" title="" class="hatena-fotolife" style="width:360px" itemprop="image"></span><figcaption>既存の添付ボトムシート</figcaption></figure></p> <ul> <li>カメラ…カメラが起動し、その場で撮った写真を添付する</li> <li>カメラ(ビデオ録画)…カメラが起動し、その場で撮った動画を添付する</li> <li>ファイル…デバイス内に保存されたファイルを選んで添付する</li> </ul> <h4 id="課題">課題</h4> <p>すでに撮影済みの写真や動画を選びたい場合、以下の課題が存在していました。</p> <ul> <li>写真や動画なのに「ファイル」の選択肢を選ぶ必要がある <ul> <li>一見わからず、誤ってカメラを起動して迷ってしまう</li> </ul> </li> <li>「ファイル」アプリを開いてから、目的の写真や動画へスムーズに辿り着けないことがある</li> </ul> <p>そのため、Androidの操作に詳しくない人には、やや使いにくい仕様になっていました。</p> <h4 id="取り組んだこと">取り組んだこと</h4> <p>Androidには撮影済みの写真や動画を選びやすい写真選択ツールが存在するため、その写真選択ツールをファイル添付時のボトムシートの選択肢に追加しました。<br /> メンターによる計画時は、選択肢を3つから4つに増やすだけの、技術的には大したことない作業だと考えていました。</p> <h4 id="問題発生">問題発生!</h4> <p>ところが、インターンが始まり着手してみると、既存のボトムシートには写真選択ツールを追加できないことが発覚しました!</p> <p><a href="https://developer.android.com/reference/android/content/Intent">Intent.createChooser</a>に指定する<a href="https://developer.android.com/reference/android/content/Intent#EXTRA_INTENT">EXTRA_INTENT</a>と<a href="https://developer.android.com/reference/android/content/Intent#EXTRA_INITIAL_INTENTS">EXTRA_INITIAL_INTENTS</a>の制限で、規定のIntentを1つ、追加のIntentを2つまでしか設定できないからです。<br /> すでに規定のIntentに「写真」が、追加のIntentに「動画」と「ファイル」の2つが設定済みです。前述の上限に引っかかっており、これ以上アイコンを並べられなかったのです。</p> <p>実際に選択肢を実装してみても、表示が変わりませんでした。<br /> これは想定外!となって、実現方法を再調査し、ボトムシートを自作することにしました。</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/y/yellow-sabotech/20231024/20231024160539.png" alt="&#x81EA;&#x4F5C;&#x306E;&#x6DFB;&#x4ED8;&#x30DC;&#x30C8;&#x30E0;&#x30B7;&#x30FC;&#x30C8;" width="1080" height="1080" loading="lazy" title="" class="hatena-fotolife" style="width:360px" itemprop="image"></span><figcaption>自作の添付ボトムシート</figcaption></figure></p> <p>インターン期間中は、その再調査からボトムシートのプロトタイプ実装までを、インターン生とメンターとの合同モブで行いました。<br /> メンターとしては予想外の展開でしたが、これによりインターン生はよりリアルな開発の現場事情を体験することができたのではないかと思います。</p> <h3 id="インターン生の感想">インターン生の感想</h3> <p>インターンに参加してくださった2名のインターン生からは、以下のような感想をいただきました。</p> <ul> <li>インターンで期待していたチーム開発だけでなく、メンターの方同士の意見の出し合いや思考のプロセスを知ることができた</li> <li>メンターの方達とさまざまな意見を出し合い、新たな問題に対する解決のプロセスを体験することができてよかった</li> <li>モブという学生としては馴染みのない体験によって、チームワークや自分の考えを説明する力の重要性を理解した</li> <li>プログラムのテスト工程等を体験できなかったのは残念</li> </ul> <h3 id="ふりかえり">ふりかえり</h3> <p>メンター側ではふりかえりを行い、来年に向けて以下のような意見が寄せられました。</p> <ul> <li>PMやQAなど、インターンで関わらない人たちの普段の業務内容も質問してもらえたので、有意義だった</li> <li>分報を書く暇もなかったので、もう少し自由時間を設けてもよい</li> <li>次はテストの話もちゃんと説明したい</li> <li>インターン生はドライバーだったので、慣れてきた4日目くらいにナビゲーターの経験ができるとよかった <ul> <li>ただし、4日目は締め切り間近で余裕がないことも考えられる</li> </ul> </li> </ul> <h3 id="まとめ">まとめ</h3> <p>kintone Androidチームのインターンでは、モブ形式で開発を行い、スクラムベースの1週間スプリントを実施しました。<br /> インターン生には朝会やスプリントレビューに参加していただき、普段の開発業務を経験しながら、社内イベントも楽しんでいただけたかなと思います。<br /> 取り組んだタスクは写真選択ツールの追加でしたが、予想外の制限によりボトムシートを自作することになりました。<br /> インターン生にはチーム開発や意見の出し合いを経験していただけたと思いますし、メンター側も有意義なふりかえりを行いました。</p> <h2 id="Android-2週間コース">Android 2週間コース</h2> <p>続いて、サイボウズOffice Androidチーム(以下、Office Android)で行われたAndroid 2週間コースの内容です。</p> <h3 id="インターン中の開発の流れ-1">インターン中の開発の流れ</h3> <p>Office Androidの業務も、主にモブで行っています。<br /> そのため、今回のインターンではインターン生3名と社員3名の合計6名で、Zoomを用いたモブを行いました。</p> <h3 id="インターン中のスケジュール-1">インターン中のスケジュール</h3> <p>大まかなタイムスケジュールは以下の通りでした。</p> <p><figure class="figure-image figure-image-fotolife" title="Android2週間コース時間割 1週目"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231024/20231024160606.png" alt="Android2&#x9031;&#x9593;&#x30B3;&#x30FC;&#x30B9;&#x6642;&#x9593;&#x5272; 1&#x9031;&#x76EE;" width="1200" height="419" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Android2週間コース時間割 1週目</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Android2週間コース時間割 2週目"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231024/20231024162055.png" alt="Android2&#x9031;&#x9593;&#x30B3;&#x30FC;&#x30B9;&#x6642;&#x9593;&#x5272; 2&#x9031;&#x76EE;" width="1200" height="415" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Android2週間コース時間割 2週目</figcaption></figure></p> <h3 id="ミーティングへの参加-1">ミーティングへの参加</h3> <p>Office Androidの開発でも、インターン生には朝会(デイリースクラム)に出席していただきました。<br /> さらに実際のスプリントプランニングやスプリントレビュー、見積もりなどを行うリファインメントも体験していただきました。</p> <h3 id="社内イベントへの参加-1">社内イベントへの参加</h3> <p>こちらのチームでも他プロダクトも交えたAndroidエンジニアとの雑談会、懇親会、そして青野社長との雑談会を行いました。</p> <h3 id="ハッカソン">ハッカソン</h3> <p>Android2週間コース独自のイベントとして、ハッカソンを開催しました。 目的は、インターン生にモブやJetpack Compose(以下、Compose)を使った開発に慣れていただくためです。<br /> ハッカソンの中で、2種類の課題をこなしていただきました。</p> <ol> <li>Composeの標準Componentを各自触ってみよう!(難易度:低)</li> <li>標準にないComponentを、モブで1から作ってみよう!(難易度:高)</li> </ol> <p>以下がインターン生に作ってもらった自作Componentです!</p> <p><figure class="figure-image figure-image-fotolife" title="リストの1項目を構成するListTile!"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231024/20231024160729.png" alt="&#x30EA;&#x30B9;&#x30C8;&#x306E;1&#x9805;&#x76EE;&#x3092;&#x69CB;&#x6210;&#x3059;&#x308B;ListTile&#xFF01;" width="1080" height="168" loading="lazy" title="" class="hatena-fotolife" style="width:360px" itemprop="image"></span><figcaption>リストの1項目を構成するListTile!</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="押すと拡張するExpansionTile!"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231024/20231024160758.png" alt="&#x62BC;&#x3059;&#x3068;&#x62E1;&#x5F35;&#x3059;&#x308B;ExpansionTile&#xFF01;" width="1080" height="272" loading="lazy" title="" class="hatena-fotolife" style="width:360px" itemprop="image"></span><figcaption>押すと拡張するExpansionTile!</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="Tinderのようにリストアイテムをスタック表示するStackedListPreview!"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231024/20231024160819.png" alt="Tinder&#x306E;&#x3088;&#x3046;&#x306B;&#x30EA;&#x30B9;&#x30C8;&#x30A2;&#x30A4;&#x30C6;&#x30E0;&#x3092;&#x30B9;&#x30BF;&#x30C3;&#x30AF;&#x8868;&#x793A;&#x3059;&#x308B;StackedListPreview&#xFF01;" width="908" height="908" loading="lazy" title="" class="hatena-fotolife" style="width:360px" itemprop="image"></span><figcaption>Tinderのようにリストアイテムをスタック表示するStackedListPreview!</figcaption></figure></p> <p>ハッカソンを実施してよかった点は、モブやComposeに早い段階で慣れるという目的を達成できたことでした。<br /> 一方、改善点として、ナビゲーターの役割分担が挙がりました。2人以上がナビゲーターを担当していると、誰が指示を出すかが明確でないことがありました。今回はそれに気づいた時点で、指示を出さない人はハウスキーパーに徹してもらうようにしました。<br /> 来年はより明確な役割分担を行い、チーム全体が円滑にコミュニケーションを取れるように改善していきたいと考えています。</p> <h3 id="取り組んだタスク-1">取り組んだタスク</h3> <p>2週間コースのインターンでは、Office Androidの実際のスケジュールを改修するバックログに取り組みました。<br /> 取り組んだバックログアイテムは、以下の内容です。</p> <ul> <li>予定にメモを登録/変更できるようにする</li> <li>時刻を表示しない終日予定を登録/削除できるようにする</li> </ul> <h4 id="背景-1">背景</h4> <p>Officeでは、最近モバイル版アプリ「サイボウズOffice新着通知」を「サイボウズOffice」へ名称変更し、リニューアルしました。<br /> モバイルからの操作を起点とする機能を追加し、業務をいつでもどこでも進められる環境を目指しています。<br /> 基本的な機能が揃ったことを受け、アプリ名を「サイボウズOffice」に変更しました。<br /> 今後も、働き方の多様化に対応し、モバイルひとつで業務を迅速に進められるツールとして、利用者の課題に対する最適なソリューションを提供し続ける予定です。</p> <p>詳細は以下のリンクをご覧ください。<br /> <a href="https://topics.cybozu.co.jp/news/2023/03/06-18409.html">https://topics.cybozu.co.jp/news/2023/03/06-18409.html</a></p> <p>Officeの持つさまざまなグループウェア機能の1つに、予定を登録してチームや組織で共有するスケジュール機能があります。</p> <h4 id="課題-1">課題</h4> <p>開始時刻と終了時刻が明確でない予定(たとえば、休みや終日外出など)を登録したくても、モバイルアプリでは開始日時と終了日時の入力欄に必ず時刻を入力する必要がありました。 また、訪問先などの詳細をメモとして登録したくても、予定にメモを添付する機能はありませんでした。</p> <h4 id="取り組んだこと-1">取り組んだこと</h4> <p>インターン生が参加したスプリントでは、スケジュールを登録する機能に対し、時刻情報の付かない日付だけの予定を登録できるようにしたり、予定にメモを登録したり後から変更できるようにしました。<br /> インターン生にはモブに参加し、Composeを用いて既存の予定作成画面と予定編集画面に対し、日付だけの予定に対応していただいたり、メモの入力欄を追加していただきました。</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/y/yellow-sabotech/20231024/20231024165030.png" alt="&#x65E5;&#x4ED8;&#x3060;&#x3051;&#x306E;&#x4E88;&#x5B9A;&#x304C;&#x767B;&#x9332;&#x53EF;&#x80FD;&#x306B;&#x306A;&#x3063;&#x305F;&#xFF01;" width="600" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:360px" 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/y/yellow-sabotech/20231024/20231024165043.png" alt="&#x30E1;&#x30E2;&#x306E;&#x5165;&#x529B;&#x304C;&#x53EF;&#x80FD;&#x306B;&#x306A;&#x3063;&#x305F;&#xFF01;" width="600" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:360px" itemprop="image"></span><figcaption>メモの入力が可能になった!</figcaption></figure></p> <h3 id="インターン生の感想-1">インターン生の感想</h3> <p>インターンに参加してくださった3名のインターン生からは、以下のような感想をいただきました。</p> <ul> <li>モブやスクラム開発、チームの雰囲気など期待していた以上の経験ができた</li> <li>モブやスクラム開発の知見を教えていただき、勉強になった</li> <li>インターンを経て、サイボウズという会社に対するイメージが変わり、フランクでフラットな職場であると実感した</li> <li>インターン生のみが交流する場があるとよかった。同世代のインターン生が普段どのようなことを行っているのか知りたいと思った</li> </ul> <h3 id="ふりかえり-1">ふりかえり</h3> <p>メンター側ではふりかえりを行い、来年に向けて以下のような意見が寄せられました。</p> <ul> <li>1スプリントのスクラムイベントをほぼすべて体験してもらえてよかった</li> <li>どのイベントを見学してもらうか、見学しないイベントにメンターの誰が行くかを直前に決めていた <ul> <li>あらかじめ決めておいたほうがよかった</li> </ul> </li> <li>インターン用のプロダクトバックログアイテムがもう少し難しくてもよかった</li> <li>カメラONでやったほうが、表情がわかるのでよかったかもしれない</li> <li>インターン生のみの雑談会、インターン中のランチ会で交流を増やしてもよかったかもしれない</li> </ul> <h3 id="まとめ-1">まとめ</h3> <p>Android2週間コースでは、Office Androidチームに参加してモブを行い、スクラムの各イベントにも参加していただきました。<br /> また、Composeに慣れることを目的としたハッカソンを開催し、Office Androidのスケジュール改修にも取り組んでいただきました。<br /> インターン生からはモブやスクラム開発の経験を高く評価していただけた一方で、インターン生同士の交流の場があると良いとの意見もいただきました。<br /> メンター側では、ツールの早期導入や役割分担の明確化、インターン生の交流機会の増加などの改善点を見つけ出しました。</p> <h2 id="おわりに">おわりに</h2> <p>インターンシップのはじめは新しいことばかりで大変だったかもしれませんが、皆さんが自律的にアイデアを出し、積極的に取り組む姿を見て、私たちは大いに刺激を受けました。<br /> この経験が皆さんの技術的な成長だけでなく、チームでのコミュニケーションの経験にも繋がったとしたら、私たちはとても嬉しいです。<br /> そして、この経験が皆さんの今後のキャリアにとって大きなプラスになることを心から願っています。<br /> この短期間で見せてくれた皆さんの成長と努力に感謝し、今後のさらなる活躍を期待しています!</p> yellow-sabotech Garoonのパフォーマンスを改善するNozomiチームの紹介 hatenablog://entry/6801883189051735376 2023-10-23T11:15:00+09:00 2023-10-23T11:15:00+09:00 Garoon開発チームのぱくとま (@pakutoma) です。 Garoon開発チームを紹介する全5回の記事も今回が4回目です! 前回:GaroonのFour Keysを改善するHanamiチームの紹介 今回は、Garoonのパフォーマンスを改善する新チーム、Nozomiチームを紹介します! Garoonをストレスなく使ってもらうためのチーム Nozomiチームは、Garoonのパフォーマンス改善に取り組むチームです。 Garoonをたくさんの人にストレスなく使ってもらうことを目標としています! パフォーマンス改善に取り組むNozomiチーム Garoonは、数万人規模の大企業でも活用してい… <p>Garoon開発チームのぱくとま (<a href="https://twitter.com/pakutoma">@pakutoma</a>) です。</p> <p>Garoon開発チームを紹介する全5回の記事も今回が4回目です!<br/> 前回:<a href="https://blog.cybozu.io/entry/garoon-four-keys-growth-team">GaroonのFour Keysを改善するHanamiチームの紹介</a></p> <p>今回は、Garoonのパフォーマンスを改善する新チーム、Nozomiチームを紹介します!</p> <h2 id="Garoonをストレスなく使ってもらうためのチーム">Garoonをストレスなく使ってもらうためのチーム</h2> <p>Nozomiチームは、Garoonのパフォーマンス改善に取り組むチームです。 Garoonをたくさんの人にストレスなく使ってもらうことを目標としています!</p> <p><figure class="figure-image figure-image-fotolife" title="パフォーマンス改善に取り組むNozomiチーム"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231019/20231019143054.png" alt="Nozomi&#x30C1;&#x30FC;&#x30E0;&#x306F;&#x30E1;&#x30F3;&#x30D0;&#x30FC;&#x304C;2&#x4EBA;&#x3067;&#x3001;&#x6027;&#x80FD;&#x6539;&#x5584;&#x6848;&#x306E;&#x7ACB;&#x6848;&#x30FB;&#x691C;&#x8A3C;&#x3084;&#x30D9;&#x30F3;&#x30C1;&#x30DE;&#x30FC;&#x30AF;&#x74B0;&#x5883;&#x306E;&#x6574;&#x5099;&#x3092;&#x3057;&#x3066;&#x3044;&#x307E;&#x3059;" width="1200" height="667" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>パフォーマンス改善に取り組むNozomiチーム</figcaption></figure></p> <p>Garoonは、数万人規模の大企業でも活用していただいています。 数万人のお客さまが毎朝一斉にメールチェックをしたり、一日のスケジュールを確認したりする時間帯がGaroonのピークタイムになります。 そんな時にGaroonがもっとサクサク動作するようになれば、チームワークがもっともっと良くなるはずですよね!</p> <p>いつでもストレスなく動くGaroonが提供できることを目指して、Nozomiチームは日々パフォーマンス改善に取り組んでいます。</p> <h2 id="出来たてほやほやのチームです">出来たてほやほやのチームです</h2> <p>Nozomiチームは、まだ活動開始から3ヶ月、メンバーも2人しかいない出来たてほやほやのチームです! 先日やっと一つ目のプロジェクトを終えられた、そんな立ち上げ真っただ中の段階です。</p> <p>まだ走りはじめたばかりのチームですので、チーム紹介の場をお借りしてNozomiチームを立ち上げた時の話をさせてもらおうと思います。</p> <p>Nozomiチームは今年で新卒入社二年目の私ぱくとまと、私が元々所属していたYukimiチームの先輩である赤間の2人で活動しているチームです。 チームの始まりは、性能問題に苦しめられた私が「性能チームを作りたいです!」とGaroonチーム全体に呼びかけたことでした。</p> <h2 id="初めて関わったプロジェクトで性能問題に苦しむ">初めて関わったプロジェクトで性能問題に苦しむ</h2> <p>2022年の11月末ごろ、私が所属するYukimiチームが抱えるGaroonのPHP 8.1移行プロジェクトは大詰めを迎えていました。修正が全て終わり、あとはリリースするだけというある日、事件が起こりました。</p> <p>プロダクトの性能検証で測定値の劣化が確認され、リリース基準に達しないことからリリースが中止になったのです。性能検証との長い戦いが始まりました。 当時のGaroonの性能検証は、リリース候補バージョンに大規模な負荷テストを実行し、その負荷テストにパスする仮想ユーザー数を過去のバージョンと比較するというものでした。</p> <p>この仕組みは製品の許容ユーザー数を見積もるためには適していますが、性能劣化の原因を探るためにはあまり役に立ちません。遅いリクエストが分かるだけで、そのリクエストのパラメータや呼び出すメソッド、時間のかかる処理などがこの性能検証からは分からないためです。それに、性能検証に利用するデータやシナリオはブラックボックス化しており、チームの誰も分からない状態でした。</p> <p>性能検証をパスするための1ヶ月ほど、目隠しで宝探しをするような気分で性能劣化の原因を探していました。見つかった原因はPHP 8.1から出るようになったログが大量にファイルに書き込まれていることだったのですが、良い性能検証の仕組みがあればもっと早く気がつけていたはずです。</p> <h2 id="性能問題を扱うチームがないチームを作ろう">性能問題を扱うチームがない!チームを作ろう!</h2> <p>年が明けた2023年1月、性能検証の仕組みに疑問を持った私はチームをまたいで多くのメンバーと話し、性能に関する課題を集めました。その結果、次のような課題を知ることができました。</p> <p>現状の性能検証に不満があるメンバーは少なくないこと。 それを改善出来ないのは、性能について詳しい人が少なく、性能問題を扱うチームも存在しないので良い仕組みを作れないためであること。</p> <p>何より、性能問題を扱うチームがないことは性能検証だけでなく、Garoonの性能問題の解決が難しいことや、大規模ユーザーの受け入れが難しいことにもつながっていること。</p> <p>これは大きな課題だ!と思いました。性能問題を扱うチームが出来れば、Garoonがもっと良くなるに違いありません!</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/y/yellow-sabotech/20231019/20231019150120.png" alt="&#x300C;&#x6027;&#x80FD;&#x30C1;&#x30FC;&#x30E0;&#x4F5C;&#x308A;&#x305F;&#x3044;&#xFF01;&#x65B0;&#x6625;&#x60C5;&#x5831;&#x5171;&#x6709;&#x3056;&#x3064;&#x3060;&#x3093;&#x300D;&#x3068;&#x3044;&#x3046;&#x30BF;&#x30A4;&#x30C8;&#x30EB;&#x306E;&#x30DF;&#x30FC;&#x30C6;&#x30A3;&#x30F3;&#x30B0;&#x3092;&#x8A2D;&#x5B9A;&#x3057;&#x305F;&#x30B0;&#x30EB;&#x30FC;&#x30D7;&#x30A6;&#x30A7;&#x30A2;&#x306E;&#x30B9;&#x30B1;&#x30B8;&#x30E5;&#x30FC;&#x30EB;&#x753B;&#x9762;" width="1200" height="274" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>そのまますぎるタイトルのミーティング</figcaption></figure></p> <p>ここからチームの立ち上げに向けて動き出しました。CIパイプラインの移行や新人の受け入れ、インターンシップの面接などをこなしながら準備を進め、7月にNozomiチームを立ち上げることができました。</p> <h2 id="まずはノウハウを蓄積する">まずはノウハウを蓄積する</h2> <p>Nozomiチームを立ち上げて最初にぶつかった壁が、タスクの優先順位でした。性能に関する課題はたくさんあるのにメンバーは2人だけです。取り組むこと・取り組まないことを決めないとタスクに押しつぶされてしまいます!</p> <p>タスクの優先順位決めと一言で言っても、メンバーそれぞれで優先したいことは違いますし、他のチームから求められる役割もあります。2ヶ月かけて議論した結論が以下のものです。</p> <p>「Garoonの性能問題のうち、短期的な解決が難しいものを数ヶ月かけて解決する」</p> <p>Nozomiチームでは、短期的な性能問題の対応や長期的な仕組み作りは一旦扱わないことにしたのです。 なぜなら今Nozomiチームに必要なことは問題の解決そのものではなく、そのためのノウハウを貯めることだったからです。</p> <p>短期的な性能問題の対応をするにはNozomiチームのリソースが足りませんし、長期的な仕組み作りに必要な性能に関するノウハウもNozomiチームにはまだ足りません。現在のリソースでノウハウを蓄積するには、数ヶ月で解決するサイズの問題が最適でした。</p> <h2 id="最初のプロジェクトが完了">最初のプロジェクトが完了</h2> <p>Nozomiチームのタスクの進め方を模索するために、スケジュールアプリのある機能のパフォーマンス改善に取り組みました。</p> <p>プロジェクトを始める際に『詳解システム・パフォーマンス』をメンバーで読み、内容について話し合って「性能劣化の真の原因を見つけてから改善すること」をプロジェクトの目標として決めました。</p> <p>この目標は、一つのデータから問題らしいものを見つけてすぐに改善に取り組むのではなく、データから立てた仮説に対して複数のデータからそれが真の原因であることを検証してから改善に取り組むというものです。仮説の検証には時間がかかりますが、検証せずに実装するより早く問題が修正できることも多いです。また、意味のない改善や最適化によってリソースを無駄にしたり、コードを複雑にしてしまうことが避けられます。</p> <p>決めた目標を元にプロジェクトを進めたところ、この機能のレイテンシを60倍も高速化できました。効果が大きい上に技術的にも面白い改善だったので、詳細はまたInside Outに投稿します!</p> <h2 id="今後のNozomiチーム">今後のNozomiチーム</h2> <p>タスクの優先順位も決まり、一つ目のプロジェクトの完了も経験したNozomiチームですが、まだまだやりたいことはたくさんあります。</p> <p>Nozomiチームの喫緊の課題はノウハウとリソースです。短期的にはGaroonの性能問題に向き合いつつノウハウを蓄積し、長期的にはリソースに余裕を作ってGaroonの性能が機能追加や修正のたびに上がっていくようなカルチャーや仕組み作りを目指しています!</p> <p>改善を続けていくGaroonとNozomiチームをよろしくお願いします!</p> yellow-sabotech サードパーティークッキーって関係あるのかい?ないのかい?どっちなんだい? hatenablog://entry/6801883189050880297 2023-10-20T08:00:00+09:00 2023-10-20T08:00:27+09:00 サードパーティ―クッキー、いつの間にか焼かれる立場になっていた。クッキーだけに。 な… 何を言っているのか… わからねーと思うが…… <p>ご覧いただきありがとうございます。 涼しくなってきましたが、皆様いかがお過ごしでしょうか?</p> <p>広告が関係ないシステムはサードパーティクッキーへの影響はないと思っていたので寝耳に水だったのですが、 社内システムにて、サードパーティクッキー規制影響するケースが判明しました。</p> <p>システムへの影響が大きいことが分かったので、現象および影響についてまとめます。</p> <p>みなさまの対応状況や誤った記述の指摘など、コメントやSNSコメントなどで教えていただけますと大変参考になります。</p> <p>サードパーティークッキー概説や規制の経緯はインターネット上に詳しく書かれたものがあるのでそちらを参照してください。</p> <h2 id="本格化するサードパーティクッキー規制">本格化するサードパーティ・クッキー規制</h2> <p>Firefoxではサードパーティクッキー規制がデフォルトでロールアウトされています。 Safariは以前よりサードパーティクッキーを規制していました。Chrome, Edgeでも規制がアナウンスされています。</p> <p>Chrome, Edge, Firefox, Safariの規制状況をまとめると次のようになります。</p> <ul> <li>Chrome: 2024 Q1から規制開始をアナウンス <a href="https://developer.chrome.com/ja/docs/privacy-sandbox/third-party-cookie-phase-out/">サードパーティ Cookie の段階的廃止への準備</a></li> <li>Edge: Chromeに同じ?</li> <li>Firefox: 規制済み <a href="https://blog.mozilla.org/en/mozilla/firefox-rolls-out-total-cookie-protection-by-default-to-all-users-worldwide/">Firefox rolls out Total Cookie Protection by default to more users worldwide</a></li> <li>Safari: 規制済み</li> </ul> <h2 id="どういう現象が報告されたの">どういう現象が報告されたの?</h2> <p>今回、社内で報告された内容は次のようなものです。ブラウザはFirefoxでした。</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/y/yellow-sabotech/20231018/20231018113635.png" width="658" height="221" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>現象が報告されたシステム構成図</figcaption></figure></p> <p>サードパーティクッキー規制前は、SAMLでのIdP→Aの間でSSOを行うと、iframeで埋め込まれたBのサイトへもSSOが自動的に行われていました。</p> <p>サードパーティクッキー規制後は、Bのサイトへの自動的SSOが失敗し、<code>X-Frame-Options: deny</code> エラーが出てしまいます。 Bではiframeで埋め込まれることを想定してX-Frame-Optionsは出力していません。</p> <p>何が起こったのでしょうか?</p> <p>これを説明するために<a href="https://developer.chrome.com/ja/docs/privacy-sandbox/chips/">Cookies Having Independent Partitioned State (CHIPS)</a> を紹介します。</p> <h2 id="CHIPSとは何か">CHIPSとは何か?</h2> <p>CHIPSは、サードパーティクッキー規制の仕様です。</p> <p>CHIPS以前は、クッキーはDomain, HttpOnly, Secureなどの属性に従って読み書きの許可が行われます。 この仕様はiframeで埋め込まれていても同様に読み書きの許可が得られれば同じように扱われていました。</p> <p>CHIPS以後は埋め込み元の情報も読み書きの許可の判断に利用されます。 ブラウザでiframeを利用せずに表示したときに設定されたクッキーとiframeで埋め込まれたサイトから設定されたクッキーは別物として扱われます。 この埋め込み元の情報が異なるクッキーの値は読み取ることができません。文字通りCookies Having Independent Partitioned Stateというわけです。</p> <p><figure class="figure-image figure-image-fotolife" title="CHIPS概念図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231018/20231018110829.png" width="311" height="371" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>CHIPS概念図</figcaption></figure></p> <p>報告された現象が起きるときクッキーは次のように扱われるようです:</p> <ul> <li>Aをブラウザで直接開いた時に設定されるクッキーは (<code>https</code>,<code>A</code>) という埋め込み情報を持ちます。</li> <li>iframeを使ってAに埋め込まれたBに設定されるクッキーは(<code>https</code>, <code>B</code>, <code>A</code>) という情報を持ち区別されます。(<code>https</code>, <code>A</code>)のクッキーの読み書きを行えません。</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/y/yellow-sabotech/20231018/20231018113639.png" width="338" height="399" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>埋め込み情報が異なるクッキーは読み書きできない</figcaption></figure></p> <p>報告された現象は次のように説明できます:</p> <ul> <li>AにログインするためにIdPとAのログインクッキーを生成 <ul> <li>Aのクッキー (<code>https</code>, <code>A</code>), IdPのクッキーを (<code>https</code>, <code>IdP</code>) とします</li> </ul> </li> <li>iframeで埋め込まれたBはログインセッションをもたないのでIdPにSAMLのリクエストを実行 <ul> <li>CHIPS以前はSAML処理の時にIdpリダイレクトで(<code>https</code>, <code>IdP</code>)が読むことができた</li> <li>CHIPS以後はiframeで埋め込まれるのでIdPのクッキー(<code>https</code>, <code>Idp</code>)を読めない。(<code>https</code>, <code>Idp</code>, <code>A</code>) として別のクッキーとして扱われる</li> </ul> </li> <li>Idpのログインクッキーは存在しないのでSAML処理を継続するために、ログイン画面にリダイレクトします。</li> <li>IdPのログイン画面は、<code>X-Frame-Options: deny</code> を出力しているのでX-Frameオプションエラーが発生する。</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/y/yellow-sabotech/20231018/20231018112930.png" width="860" height="423" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>サードパーティクッキー規制があると動かなくなるシステム構成</figcaption></figure></p> <p>CHIPSが適用されると問題視されていたサードパーティクッキーによる個人情報のトラッキングも防ぐことができます。</p> <p><figure class="figure-image figure-image-fotolife" title="おわかりいただけたであろうか。CHIPSによってクッキーのトラッキングを防げているのを"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/cybozuinsideout/20231019/20231019105856.png" width="844" height="401" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>おわかりいただけたであろうか。CHIPSによってクッキーのトラッキングを防げているのを</figcaption></figure></p> <h2 id="サードパーティクッキー廃止は関係あるのかいないのかいどっちなんだい">サードパーティ・クッキー廃止は関係あるのかい?ないのかい?どっちなんだい?</h2> <p>結論:広告が関係ないシステムも影響するかも</p> <p>クッキーを利用したシステム連携はSSOの常套手段なので、影響はあると考えるべきです。 例えば次のようなケースです:</p> <ul> <li>クロスドメインのサイトをiframeで埋め込みクッキーを利用してSSO</li> <li>Auth0のサイレント認証に似た認証の仕組みを導入しているシステム <a href="https://auth0.com/docs/authenticate/login/configure-silent-authentication">Configure Silent Authentication</a></li> </ul> <p>連携していないように見えるサイレント連携を見落としてしまうと、サードパーティクッキーの規制が本格化したときに トラブルになりそうなので、Chrome, Edgeで機能フラグをON/OFFしてテストを行うことをお勧めします。</p> <h2 id="Chrome-Edgeで機能フラグで提供されている">Chrome, Edgeで機能フラグで提供されている</h2> <p>Edge, ChromeはTest Third Party Cookie Phaseoutの機能フラグが提供されています。 こちらを利用して事前に技術検証を行うとよいでしょう。</p> <p>URLバーに次を入力すると機能フラグのON・OFFを制御できます。</p> <ul> <li>(Edge用) edge://flags/#test-third-party-cookie-phaseout</li> <li>(Google Chrome用) chrome://flags/#test-third-party-cookie-phaseout</li> </ul> <h2 id="サードパーティクッキー規制で動かないことが判明したどうすれば">サードパーティクッキー規制で動かないことが判明した。どうすれば...?</h2> <p>個別にサードパーティクッキーを許可する例外を設定していただく他ありません。</p> <p>デバイス管理ソリューションを採用している場合は設定テンプレートを利用するのが良さそうです。</p> <ul> <li><a href="https://github.com/mozilla/policy-templates">mozilla > policy-templates</a></li> <li><a href="https://learn.microsoft.com/ja-jp/deployedge/configure-microsoft-edge">Windows デバイスで Microsoft Edge ポリシー設定を構成する</a></li> <li><a href="https://www.chromium.org/administrators/policy-templates/">The Chromium Projects > Policy Templates</a></li> </ul> <h2 id="クッキーのPartitioned-属性について">クッキーのPartitioned 属性について</h2> <p>CHIPSの仕様ではPartitioned 属性も導入されています。これは将来的にiframeなどの埋め込みでクッキーを設定したければ Partitioned属性がオプトインしなければならないというものです。</p> <p>Firefoxはiframeなどで埋め込まれたサイトから設定されたクッキーはPartitioned 属性付きとして処理されるとしています。 <a href="https://hacks.mozilla.org/2021/02/introducing-state-partitioning/">Introducing State Partitioning</a></p> <p>将来的にPartitioned属性がついていないクッキーの場合、iframeからクッキーの設定は無視されるかもしれません。 実際に、Chrome, EdgeでPartitioned属性を機能フラグで有効化すると、iframeからPartitioned属性がないクッキーは無視されます。</p> <p>こちらも同様に機能フラグでテストを行うことができます。</p> <ul> <li>(Edge用) edge://flags/#partitioned-cookies</li> <li>(Google Chrome用) chrome://flags/#partitioned-cookies</li> </ul> <p>iframeで外部サイトから埋め込れるサービスは実装の修正が必要になる可能性は想定しておいた方がいいでしょう。</p> <p>比較的新しい仕様として次のようなものがあります。</p> <ul> <li><a href="https://developer.chrome.com/docs/privacy-sandbox/related-website-sets/">Related Website Sets</a></li> <li><a href="https://developer.chrome.com/ja/docs/privacy-sandbox/fedcm/">Federated Credential Management API </a></li> </ul> <p>各Idpベンダーの対応状況やブラウザの対応状況によってできる対策に差が生まれそうです。 サードパーティクッキー規制のブラウザ側の対応を見守っていきましょう。</p> <p>文責 <a href="https://github.com/yokotaso">@yokotaso</a></p> yellow-sabotech サイボウズは JaSST'23 Kyushu で協賛&登壇します! hatenablog://entry/6801883189051741292 2023-10-19T16:41:55+09:00 2023-10-19T16:41:55+09:00 こんにちは、QA(品質保証)エンジニアの斉藤です。 サイボウズは 2023年11月2日(木)に開催されるJaSST'23 Kyushuに、ゴールドスポンサーとして協賛します。 また、スポンサーセッションにて登壇いたしますので、本記事ではその紹介をさせてください。 協賛について サイボウズでは「チームワークあふれる社会を創る」という企業理念のもと、kintoneやGaroon、 サイボウズ Office、メールワイズなどチームを支えるサービスを開発しています。 その中でサイボウズのQAエンジニアは開発プロセス全体を通して品質を高める活動を行なっています。 そんな私たちが日々の活動でお世話になって… <p>こんにちは、QA(品質保証)エンジニアの斉藤です。</p> <p>サイボウズは 2023年11月2日(木)に開催される<a href="https://www.jasst.jp/symposium/jasst23kyushu.html">JaSST'23 Kyushu</a>に、ゴールドスポンサーとして協賛します。 また、スポンサーセッションにて登壇いたしますので、本記事ではその紹介をさせてください。</p> <h3 id="協賛について">協賛について</h3> <p>サイボウズでは「チームワークあふれる社会を創る」という企業理念のもと、kintoneやGaroon、 サイボウズ Office、メールワイズなどチームを支えるサービスを開発しています。 その中でサイボウズのQAエンジニアは開発プロセス全体を通して品質を高める活動を行なっています。 そんな私たちが日々の活動でお世話になっているソフトウェアテストのコミュニティに少しでも貢献できればという想いから、 2023年は東京、東北、関西、北海道に続いてJaSST'23 Kyushuでもスポンサーとして参加させていただくことになりました。</p> <h3 id="スポンサーセッションについて">スポンサーセッションについて</h3> <p>今回のJaSST'23 Kyushuではスポンサーセッションに登壇します。</p> <h4 id="タイトル">タイトル</h4> <p>サイボウズのQAエンジニアと社内勉強会チーム「ミネルヴァ」の紹介</p> <h4 id="日時">日時</h4> <p>11月2日(木) 15:00~15:50 (参考) <a href="https://www.jasst.jp/symposium/jasst23kyushu/timetable.html">タイムテーブル</a></p> <h4 id="発表者">発表者</h4> <p>斉藤 裕希</p> <h4 id="内容紹介">内容紹介</h4> <p>サイボウズのQAエンジニアの業務、体制、特徴などについてお話しします。 また、社内における勉強会開催を支援している「ミネルヴァ」についても紹介します。</p> <p>興味のある方はぜひご参加ください!</p> <h3 id="終わりに">終わりに</h3> <p>当日はサイボウズの社員が参加していますので、お気軽にお声がけください。 (社員はサイボウズのTシャツを着用しています)</p> <p>JaSST'23 Kyushuで皆様と交流できることを楽しみにしています。</p> <p>また、私たちは「チームワークあふれる社会を創る」という理念のもと、日々チームワークを支えるソフトウェアの開発に取り組んでおり、現在一緒に働くQAエンジニアの仲間を募集しています。 少しでもご興味を持っていただいた方は下記採用ページをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcybozu.co.jp%2Frecruit%2Fentry%2Fcareer%2Fqa-engineer.html" title="QAエンジニアキャリア採用 募集要項 | 採用情報 | サイボウズ株式会社" 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://cybozu.co.jp/recruit/entry/career/qa-engineer.html">cybozu.co.jp</a></cite></p> yellow-sabotech ソースコードのハッシュ値を利用したCIの高速化 hatenablog://entry/6801883189049747127 2023-10-17T13:41:38+09:00 2023-10-17T13:41:38+09:00 こんにちは、kintoneチームの川向です。 ソースコードハッシュ値計算ツールであるsverを導入してCIの高速化を行ったので、その紹介をさせてください。 この仕組みにより、通常は1時間かかるCIの実行時間が最善のケースでは20分程度に短縮可能になりました。 導入前の課題 解決方法の検討 sverを使ったテストのスキップによるCI高速化 kintoneでのsverの利用方法 sver設定ファイルの書き方 キャシュの保存先(GitHub Actions Cache、Amazon S3) sverを使ったジョブの書き方 sver情報生成ジョブ: ハッシュ生成とキャッシュの存在確認 ビルドジョブ: … <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231013/20231013095105.png" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>こんにちは、kintoneチームの川向です。 ソースコードハッシュ値計算ツールである<a href="https://github.com/mitoma/sver">sver</a>を導入してCIの高速化を行ったので、その紹介をさせてください。</p> <p>この仕組みにより、通常は1時間かかるCIの実行時間が最善のケースでは20分程度に短縮可能になりました。</p> <ul class="table-of-contents"> <li><a href="#導入前の課題">導入前の課題</a></li> <li><a href="#解決方法の検討">解決方法の検討</a></li> <li><a href="#sverを使ったテストのスキップによるCI高速化">sverを使ったテストのスキップによるCI高速化</a></li> <li><a href="#kintoneでのsverの利用方法">kintoneでのsverの利用方法</a><ul> <li><a href="#sver設定ファイルの書き方">sver設定ファイルの書き方</a></li> <li><a href="#キャシュの保存先GitHub-Actions-CacheAmazon-S3">キャシュの保存先(GitHub Actions Cache、Amazon S3)</a></li> <li><a href="#sverを使ったジョブの書き方">sverを使ったジョブの書き方</a><ul> <li><a href="#sver情報生成ジョブ-ハッシュ生成とキャッシュの存在確認">sver情報生成ジョブ: ハッシュ生成とキャッシュの存在確認</a></li> <li><a href="#ビルドジョブ-依存ファイル以外に依存しないことの確認">ビルドジョブ: 依存ファイル以外に依存しないことの確認</a></li> <li><a href="#テストジョブ-ジョブ成功後にキャッシュ保存">テストジョブ: ジョブ成功後にキャッシュ保存</a></li> <li><a href="#下流ジョブのifの書き方">下流ジョブのifの書き方</a></li> </ul> </li> </ul> </li> <li><a href="#結果">結果</a></li> <li><a href="#課題と今後の展開">課題と今後の展開</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="導入前の課題">導入前の課題</h2> <p>kintoneのCIの大まかな構成は以下のようになっています。</p> <p><figure class="figure-image figure-image-fotolife" title="kintoneのCI概要"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231012/20231012091742.jpg" alt="kintone&#x306E;CI&#x6982;&#x8981;&#x3002;&#x59CB;&#x3081;&#x306B;&#x30D0;&#x30C3;&#x30AF;&#x30A8;&#x30F3;&#x30C9;&#x3068;&#x30D5;&#x30ED;&#x30F3;&#x30C8;&#x30A8;&#x30F3;&#x30C9;&#x306E;&#x30D3;&#x30EB;&#x30C9;&#x3068;&#x5358;&#x4F53;&#x30C6;&#x30B9;&#x30C8;&#x304C;&#x5B9F;&#x65BD;&#x3055;&#x308C;&#x308B;&#x3002;&#x305D;&#x306E;&#x5F8C;E2E&#x30C6;&#x30B9;&#x30C8;&#x304C;&#x5B9F;&#x884C;&#x3055;&#x308C;&#x3001;&#x6700;&#x7D42;&#x7684;&#x306B;&#x30EA;&#x30EA;&#x30FC;&#x30B9;&#x5019;&#x88DC;&#x30A2;&#x30FC;&#x30AB;&#x30A4;&#x30D6;&#x306E;&#x30A2;&#x30C3;&#x30D7;&#x30ED;&#x30FC;&#x30C9;&#x304C;&#x884C;&#x308F;&#x308C;&#x308B;" width="1200" height="406" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>kintoneのCI概要</figcaption></figure></p> <p>E2EテストはAPIテストとSeleniumテストの2種類があります。 またkintoneは2種類のインフラ(国内顧客向けオンプレ基盤、グローバル顧客向けAWS基盤)で運用しているので、 E2Eテストもそれぞれの環境で実施しています。</p> <p>このE2Eテストに問題がありました。</p> <p>まず第一に時間がかかることです。特にSeleniumテストは40分ほどかかります。これは件数が多いことが一因です。現状、Seleniumテストが4000件、APIテストが3300件ほどとなっています。</p> <p>第二に、不安定という問題があります。 不安定なテストは少しずつ修正しているのですが、テストの追加や製品コードの変更によるメンテナンスは継続的に必要なため、完全に解消することは困難です。 また、2つのインフラ基盤で同じテストを同時に実行しており、 どちらか一方でも失敗するとリリース候補アーカイブは出来上がらないようようなCI構成にしており、不安定さが増しています。</p> <p>第三に、E2Eテストのリソースに限りがあり、複数のCIが同時に多数実行されると律速されるという問題もあります。</p> <h2 id="解決方法の検討">解決方法の検討</h2> <p>この問題の解決策の検討では、既存の仕組みを大きく変えないことを一つの条件にしていました。</p> <p>まず、E2Eテストへの依存度の高さが問題であることはわかっているのですが、 この改善は試験戦略の見直しなどが必要で時間がかかるため、保留しました。 <a href="https://blog.cybozu.io/entry/2023/07/03/162424">フロントエンドの刷新活動が並行して行われており、 そちらで新しい試験戦略も実践されている</a>のですが、 刷新前の画面に対しては利用できないなどの理由もあります。</p> <p>また、ビルドの仕組みも大きくは変えないことにしました。 現在<a href="https://blog.cybozu.io/entry/2022/12/21/170000#%E3%83%81%E3%83%BC%E3%83%A0%E7%99%BA%E8%B6%B3%E3%81%AE%E8%83%8C%E6%99%AF%E3%81%A8%E3%83%81%E3%83%BC%E3%83%A0%E6%A7%8B%E6%88%90">逆コンウェイ戦略に基づいてチーム分割</a>を行っており、 各チームごとにビルドの仕組みも選択できることも期待されると思われるため、統一的なツールの新規導入は行いませんでした。 <a href="https://bazel.build/">bazel</a>や<a href="https://please.build/">please</a>の調査を簡単に行ったのですが、同様の理由により採用していません。</p> <p>最終的には、ソースコードハッシュ値計算ツールの<a href="https://github.com/mitoma/sver">sver</a>を使用することにしました。 既存の仕組みをそのまま維持しながら、可能な場合にはテストをスキップするCIが実現可能なためです。なお、sverはYakumo(グローバル顧客向けAWS基盤)で使われているソースコードのハッシュ値計算ツールのOSS版です。</p> <h2 id="sverを使ったテストのスキップによるCI高速化">sverを使ったテストのスキップによるCI高速化</h2> <p>sverを使ってどのようにCIの高速化を行ったのかをAPIテストを例に説明します。</p> <p>kintoneでは前述のようにバックエンドとフロントエンドを分けてビルドし、その後APIテストを実行しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231012/20231012091742.jpg" alt="kintone&#x306E;&#x30EF;&#x30FC;&#x30AF;&#x30D5;&#x30ED;&#x30FC;&#x3002;&#x30D3;&#x30EB;&#x30C9;&#x30B8;&#x30E7;&#x30D6;&#x306F;&#x30D5;&#x30ED;&#x30F3;&#x30C8;&#x30A8;&#x30F3;&#x30C9;&#x3068;&#x30D0;&#x30C3;&#x30AF;&#x30A8;&#x30F3;&#x30C9;&#x7528;&#x306E;2&#x3064;&#x304C;&#x3042;&#x308B;&#x304C;&#x3001;API&#x30C6;&#x30B9;&#x30C8;&#x306F;&#x30D0;&#x30C3;&#x30AF;&#x30A8;&#x30F3;&#x30C9;&#x306E;&#x30D3;&#x30EB;&#x30C9;&#x30B8;&#x30E7;&#x30D6;&#x306B;&#x306E;&#x307F;&#x4F9D;&#x5B58;&#x3057;&#x3066;&#x3044;&#x308B;" width="1200" height="406" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この依存関係を見てもわかるように、フロントエンドのジョブはAPIテストには影響がありません。 つまり、フロントエンドのコード変更を行っても、APIテストの結果は変わらないということです。 言い換えると、以前APIテストが成功しており、その時点のAPIテストに依存するソースコードと変わっていなければ、そのAPIテストはスキップできるはずです。</p> <p>この「依存するソースコード」を宣言的に列挙して、その内容のハッシュを計算してくれるツールとしてsverを使っています。</p> <p><figure class="figure-image figure-image-fotolife" title="APIテストがスキップできる例"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231012/20231012091738.jpg" alt="API&#x30C6;&#x30B9;&#x30C8;&#x304C;&#x6210;&#x529F;&#x3057;&#x305F;&#x3068;&#x3059;&#x308B;&#x3002;&#x3053;&#x306E;&#x5F8C;&#x3001;JS&#x306E;&#x5909;&#x66F4;&#x306E;&#x307F;&#x306E;&#x30B3;&#x30DF;&#x30C3;&#x30C8;&#x304C;&#x884C;&#x308F;&#x308C;&#x3066;CI&#x304C;&#x8D77;&#x52D5;&#x3057;&#x305F;&#x3068;&#x304D;&#x3001;API&#x30C6;&#x30B9;&#x30C8;&#x306E;&#x30BD;&#x30FC;&#x30B9;&#x30B3;&#x30FC;&#x30C9;&#x306E;&#x30CF;&#x30C3;&#x30B7;&#x30E5;&#x5024;&#x306F;&#x5909;&#x308F;&#x3089;&#x306A;&#x3044;&#x3002;&#x305D;&#x306E;&#x305F;&#x3081;&#x3001;API&#x30C6;&#x30B9;&#x30C8;&#x306F;&#x30B9;&#x30AD;&#x30C3;&#x30D7;&#x3067;&#x304D;&#x308B;" width="1200" height="872" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>APIテストがスキップできる例</figcaption></figure></p> <p>ここではAPIテストを例として説明しましたが、Seleniumテストや単体テストなどのジョブでも同様のスキップが行なえます。</p> <p>また、<a href="https://mitomasan.hatenablog.com/entry/2022/07/15/080000">sver作者のブログ記事</a>にはより詳細な説明が書かれているので、そちらもご参照ください。</p> <h2 id="kintoneでのsverの利用方法">kintoneでのsverの利用方法</h2> <p>それでは実際にsverをどのように利用したかを説明していきます。</p> <h3 id="sver設定ファイルの書き方">sver設定ファイルの書き方</h3> <p>sverではソースコードの依存関係の記述にsver.tomlという設定ファイルを使用します(<a href="https://github.com/mitoma/sver#config">参考</a>)。 この中ではプロファイルという単位で複数の依存関係を記述することができます。 また、この設定ファイルはモノレポ構成を考慮して複数配置できるようになっています。</p> <p>kintoneのレポジトリも複数のコンポーネントからなるモノレポ構成のため、 設定ファイルも各コンポーネントごとに配置しました。</p> <pre class="code" data-lang="" data-unlink># 設定ファイル配置のイメージ . ├── README.md ├── frontend │ └── sver.toml ├── frontend-sub │ └── sver.toml ├── backend │ └── sver.toml └── etc └── sver └── sver.toml // コンポーネントに分けられないジョブの設定ファイル。後述します</pre> <p>例えばfrontendコンポーネントの設定ファイルは以下のようになります。</p> <pre class="code toml" data-lang="toml" data-unlink># frontend/sver.toml [frontend-build] # *1 dependencies = [ &#34;.github&#34;, # *2 # &#34;frontend/&#34;, # *3 明示的に指定しなくてもsver.tomlのあるディレクトリ以下のものは依存に含まれる ] [frontend-test] dependencies = [ ... ]</pre> <p>工夫した点の一つは、CIジョブとプロファイルを1対1対応させたことです(*1)。 この例ではfrontend-buildジョブに対応するプロファイルがfrontend-buildとなっています。 これにより、ジョブ単位で依存ファイルを管理できるようになり、 ジョブ単位でのスキップが行いやすくなりました。 プロファイルがジョブと対応するので、CIスクリプトも追加で依存に含める形になっています(*2)。 なお、sver.tomlのあるディレクトリ以下のファイルはすべてデフォルトで依存に含まれます(*3)。</p> <p>コンポーネントに分けられないようなジョブに対応するプロファイルは、sver専用のフォルダを作ってそちらで記述しました(*4)。 専用フォルダを作った理由は、ルートディレクトリにsver.tomlを配置してすると、すべてのファイルが依存関係に含まれてしまい、不要なものを一つずつ除外設定するのが困難だったためです。</p> <pre class="code toml" data-lang="toml" data-unlink># etc/sver/sver.toml # *4 [selenium-test] dependencies = [ &#34;frontend:frontend-build&#34;, # *5 &#34;frontend-sub:frontend-sub-build&#34;, # *5 &#34;backend:backend-build&#34;, # *5 &#34;.github&#34;, ... ]</pre> <p>そして、ジョブ間の依存関係はプロファイルの依存関係として記述するようにしました(*5)。 こちらについてはもう少し説明します。 例えば、E2Eテストがスキップ可能であると判定するには、テストスクリプトとテスト対象が前回の成功したCIから差分がないことを調べる必要があります。 テスト対象は、通常上流ジョブで作られた成果物になるため、それを生成している上流ジョブ(に対応するプロファイル)も依存関係に含めています。</p> <p>なお、E2EテストではGitHubの環境変数や利用するインフラの設定によっても結果が変わる場合があります。 kintoneの場合は環境の設定が変わるケースが少なく、そういった情報もテストスクリプト内で表現されることが多いので特別な配慮は行っていません。</p> <h3 id="キャシュの保存先GitHub-Actions-CacheAmazon-S3">キャシュの保存先(GitHub Actions Cache、Amazon S3)</h3> <p>sverを使ったスキップ処理を実装する場合、 sverのハッシュ値をどこかに保存する必要があります。</p> <p>kintoneのCIはGitHub Actionsとなっており、 当初はすぐに使える <a href="https://github.com/actions/cache">actions/cache</a> を使用しました。 artifactを使う方法もありますが、 kintoneではジョブ数が多く、CIのSummary画面でartifactが大量に表示されると見にくいという懸念もあり採用しませんでした。</p> <p>なお、GitHub Actionsのキャッシュはactions/cacheで利用するとブランチごとのスコープになってしまうので、 ブランチを跨いでテストをスキップできるように、キャッシュの存在確認にはAPIを利用しました。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># APIの利用例</span> <span class="synIdentifier">hit</span>=<span class="synPreProc">$(</span><span class="synSpecial">gh api -X GET -F </span><span class="synStatement">&quot;</span><span class="synConstant">key=</span><span class="synPreProc">$key</span><span class="synStatement">&quot;</span><span class="synSpecial"> </span><span class="synStatement">&quot;</span><span class="synConstant">/repos/{owner}/{repo}/actions/caches</span><span class="synStatement">&quot;</span><span class="synSpecial"> --jq </span><span class="synStatement">'</span><span class="synConstant">.total_count &gt; 0</span><span class="synStatement">'</span><span class="synPreProc">)</span> </pre> <p>当初はこの形で運用していたのですが、キャッシュの容量が上限を超える状態が続いており、 sverのキャッシュも1日程度で切れてしまい不便だったので、現在はAmazon S3に移行しています。 APIでのアクセスではキャッシュの有効期限が伸びないというのも原因の一つだったと思います。</p> <h3 id="sverを使ったジョブの書き方">sverを使ったジョブの書き方</h3> <p>次に、sverを使ったジョブの書き方を説明します。 sverを導入したジョブは以下のような構成となります。</p> <p><figure class="figure-image figure-image-fotolife" title="sverを使った場合のジョブの構成"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yellow-sabotech/20231012/20231012091745.jpg" alt="&#x30C6;&#x30B9;&#x30C8;&#x30B8;&#x30E7;&#x30D6;&#x306F;sver&#x60C5;&#x5831;&#x751F;&#x6210;&#x30B8;&#x30E7;&#x30D6;&#x306B;&#x4F9D;&#x5B58;&#x3059;&#x308B;&#x3002;&#x3055;&#x3089;&#x306B;&#x5225;&#x306E;&#x30B8;&#x30E7;&#x30D6;&#x306B;&#x4F9D;&#x5B58;&#x3059;&#x308B;&#x30C6;&#x30B9;&#x30C8;&#x30B8;&#x30E7;&#x30D6;&#x306F;&#x305D;&#x3061;&#x3089;&#x3078;&#x306E;&#x4F9D;&#x5B58;&#x3082;&#x6301;&#x3063;&#x3066;&#x3044;&#x308B;&#x3002;" width="891" height="504" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span><figcaption>sverを使った場合のジョブの構成</figcaption></figure></p> <p>順に説明していきます。</p> <h4 id="sver情報生成ジョブ-ハッシュ生成とキャッシュの存在確認">sver情報生成ジョブ: ハッシュ生成とキャッシュの存在確認</h4> <p>このジョブは、sverを使ったハッシュ生成とそれに対応するキャッシュが存在するかの確認を行います。 今回は一つのジョブで複数のsverプロファイルを計算するようにしたので、outputsはjsonになります。 例えば、selenium-testとfrontend-testというプロファイルを同時に計算する場合で、前者にキャッシュがあり、後者にキャッシュがない場合、以下のようなjsonをジョブのouputsの一つとして出力します。</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">selenium-test</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">key</span>&quot;: &quot;<span class="synConstant">sver-selenium-test-e0ff4e2dc244</span>&quot;, &quot;<span class="synStatement">hit</span>&quot;: <span class="synConstant">true</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">frontend-test</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">key</span>&quot;: &quot;<span class="synConstant">sver-frontend-test-79117f3ac781</span>&quot;, &quot;<span class="synStatement">hit</span>&quot;: <span class="synConstant">false</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p>keyがキャッシュのキーで、後ろの文字列がsverの生成したハッシュ値です。 hitはキャッシュが存在するかどうかを表します。</p> <h4 id="ビルドジョブ-依存ファイル以外に依存しないことの確認">ビルドジョブ: 依存ファイル以外に依存しないことの確認</h4> <p>sverでプロファイルを指定したジョブでは、 チェックアウト後に依存ファイル以外を削除します(*1)。 これにより、暗黙的な依存がないことを保証できるようになります。 これはpleaseなどで行われているsandbox機能を参考にしたものです。</p> <p>先日、sverの<a href="https://github.com/mitoma/sver/releases/tag/v0.1.18">v0.1.18</a>で実装された<code>sver export</code>でも同様の処理が行えると思います。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">backend-build</span><span class="synSpecial">:</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@v3 <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> mitoma/sver-actions/setup@v1 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Delete unnecessary files <span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash <span class="synIdentifier">run</span><span class="synSpecial">:</span> comm -23 &lt;(git ls-files | sort) &lt;(sver list <span class="synConstant">&quot;${{ env.target }}&quot;</span> | sort) | xargs -r -d <span class="synConstant">'\n'</span> rm -rf <span class="synComment"> # *1</span> <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">target</span><span class="synSpecial">:</span> backend:backend-build <span class="synStatement">- </span>.... </pre> <h4 id="テストジョブ-ジョブ成功後にキャッシュ保存">テストジョブ: ジョブ成功後にキャッシュ保存</h4> <p>sverでスキップを行う対象のテストジョブでは、 ビルドのジョブのような依存ファイル以外の削除に加えて、 追加の処理が必要になります。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synIdentifier">selenium-test</span><span class="synSpecial">:</span> <span class="synIdentifier">needs</span><span class="synSpecial">:</span> <span class="synSpecial">[</span> sver-check-caches, <span class="synComment"> # *1</span> frontend-build, frontend-sub-build, backend-build <span class="synSpecial">]</span> <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ <span class="synType">!</span> fromJSON(needs.sver-check-caches.outputs.caches).selenium-test.hit }} <span class="synComment"> # *2</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@v3 <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> mitoma/sver-actions/setup@v1 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Delete unnecessary files <span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash <span class="synIdentifier">run</span><span class="synSpecial">:</span> comm -23 &lt;(git ls-files | sort) &lt;(sver list <span class="synConstant">&quot;${{ env.target }}&quot;</span> | sort) | xargs -r -d <span class="synConstant">'\n'</span> rm -rf <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">target</span><span class="synSpecial">:</span> etc/sver:selenium-test <span class="synStatement">- </span>.... <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Save sver cache <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/cache/save@v3 <span class="synComment"> # *3</span> <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">key</span><span class="synSpecial">:</span> ${{ fromJSON(needs.sver-check-caches.outputs.caches).selenium-test.key }} <span class="synIdentifier">path</span><span class="synSpecial">:</span> /path/to/cache-file </pre> <p>まずはsver情報生成ジョブをneedsに加えます(*1)。 そして、キャッシュが存在する場合はジョブ自体をスキップします(*2)。 それ以降は依存ファイル以外を削除する点などはビルドジョブと同じですが、 ジョブ成功時にキャッシュを保存する点(*3)が異なります。</p> <h4 id="下流ジョブのifの書き方">下流ジョブのifの書き方</h4> <p>上記の方式でテストのスキップが可能になります。 ただ、GitHub Actionsだと途中のジョブがスキップされると、それより下流のジョブが実行されません。 そのため、下流のジョブでは下記(*1)のような記述を行い、上流ジョブがスキップされていても処理を進められるようにしました。 kintoneのCIの場合「リリース候補アーカイブのアップロード」ジョブでこの対策を行っています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synIdentifier">archive-upload</span><span class="synSpecial">:</span> <span class="synIdentifier">needs</span><span class="synSpecial">:</span> selenium-test <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ <span class="synType">!</span> cancelled() <span class="synType">&amp;&amp;</span> <span class="synType">!</span> failure() }} <span class="synComment"> # *1</span> </pre> <h2 id="結果">結果</h2> <p>この仕組みの導入の結果をいくつか紹介します。</p> <p>まず、sverを使ったスキップ処理は、E2Eテストも含めて27件のジョブに導入しました。 依存関係にあるジョブ(ビルドを行うジョブ)も含めると43件のジョブに対応しました。</p> <p>また、メインのブランチでのCIの実行状態も調べました。 このブランチにはトピックブランチの変更がマージされていきます。</p> <p>まず、単体テストや静的解析チェックのジョブはスキップ率が高い事がわかりました。 あるジョブを調べたところ、スキップ率は6割程でした。 スキップ率が高いのは、これらのジョブはプルリクエスト上でも常に実行されており、マージ後もそのハッシュ値が変わらないことが多いためです。</p> <p>E2Eテストについては、APIとSeleniumでスキップ率は大きく異なりました。 APIテストでは6割、Seleniumテストでは1割ほどでした。 E2Eテストは単体テストや静的解析チェックのジョブと異なり、プルリクエスト上での実行は手動起動となっており、キャッシュはそれほどされずスキップ率は低めになります。ただ APIテストはJavascriptやCSSの変更では依存関係のファイルが変更されることがないため、スキップ率が高くなったようです。</p> <h2 id="課題と今後の展開">課題と今後の展開</h2> <p>sverの導入により改善は見られましたが、まだまだ課題があります。</p> <p>まず、Seleniumテストのスキップ率が低い点です。 ただ、Selenimテストはほぼすべての依存を含むので、キャッシュ率を上げることは難しいのではと感じています。 キャッシュのキーの計算方法として、sverによるソースコードのハッシュだけではなく、成果物のハッシュ値を使い分けることも検討しても良いかもしれません。 成果物のハッシュ値を使うと、成果物に影響のないソースコードの差分(package.jsonのdevDependenciesの更新など)があってもキャッシュが利用できるようになります。</p> <p>また、CIでテストが実行中にメインのブランチにプルリクエストがマージがされてCIがもう一つ起動する場合、 先行するCIジョブと同じソースコードハッシュであっても、 まだキャッシュが保存されていないためスキップを行えないという問題もあります。 特にSeleniumテストは時間がかかるのでこの状況が発生しやすいです。</p> <p>また、今回導入したsverの設定ファイルで依存関係を正しく記述するのが難しいという問題があります。 特にジョブ間の依存関係が変わったときに、それに正しく追従するのが難しいです。 例えば、アーカイブ作成ジョブAに上流ジョブBができたとします。ジョブBの成果物をジョブAで使用する場合、その依存関係をsverの設定ファイルにも反映する必要があるのですが、それを忘れてもジョブは失敗せずに進んでしまいます。 対策としては、プロファイルに対応するジョブの依存関係が変わったときに、それに気がつけるCIを作る方法がありそうです。 ただ、今更ではありますが、kintoneのように最終的な成果物が複数のコンポーネントを組み合わせたものとなるモジュラモノリスにはsverは向いていないのかもしれません。</p> <p>また、ビルド結果もsverの仕組みを使ってキャッシュできるとよりCIが高速化するので、取り組んでみたいです。 sverはYakumo(グローバル顧客向けAWS基盤)で使われているツールのOSS版なのですが、Yakumoではハッシュ値はイメージのタグとして使われています。 <a href="https://blog.cybozu.io/entry/2020/06/04/140000">マージボタン1つで本番適用するための仕組み</a>で「ソースコードのハッシュ値」として書かれているものがそれに該当します。同様の仕組みを作ることも検討したいです。</p> <h2 id="まとめ">まとめ</h2> <p>CIにソースコードハッシュ値計算ツールの<a href="https://github.com/mitoma/sver">sver</a>を導入しました。 これにより、CIジョブに関係あるファイルが変更されていないときにジョブをスキップ可能にしました。 スキップ率は、単体テストやAPIテストでは6割、Seleniumテストは1割です。</p> <p>今後もCIの安定化、高速化を進めて行きます。</p> yellow-sabotech 社内で使う npm パッケージの作成に Deno を採用した話 hatenablog://entry/820878482972996510 2023-10-11T11:00:00+09:00 2023-10-11T11:00:01+09:00 こんにちわ。フロントエンドエキスパートチームの@nus3_です。 最近、社内用の npm パッケージを作る必要があり、そのパッケージは依存が少なく、実装もシンプルだったので、npm パッケージの作成には Deno と dnt を採用しました。 dnt とは dnt は Deno で実装したモジュールを CJS、ESM に対応した npm パッケージに変換してくれるビルドツールです。 使い方も簡単で、次のように dnt が提供するbuild関数にエントリーポイントや出力先などの必要な情報を渡すだけです。 import { build } from "https://deno.land/x/dnt… <p>こんにちわ。フロントエンドエキスパートチームの<a href="https://twitter.com/nus3_">@nus3_</a>です。</p> <p>最近、社内用の npm パッケージを作る必要があり、そのパッケージは依存が少なく、実装もシンプルだったので、npm パッケージの作成には Deno と <a href="https://github.com/denoland/dnt">dnt</a> を採用しました。</p> <h2 id="dnt-とは">dnt とは</h2> <p>dnt は Deno で実装したモジュールを CJS、ESM に対応した npm パッケージに変換してくれるビルドツールです。</p> <p>使い方も簡単で、次のように dnt が提供する<code>build</code>関数にエントリーポイントや出力先などの必要な情報を渡すだけです。</p> <pre class="code ts" data-lang="ts" data-unlink>import { build } from &#34;https://deno.land/x/dnt@0.38.1/mod.ts&#34;; await build({ entryPoints: [&#34;./mod/index.ts&#34;], // Denoで実装したモジュールのエントリーポイント outDir: &#34;./npm&#34;, // 出力先のディレクトリ shims: {}, // Deno名前空間などDeno独自の実装をNode.jsやブラウザで実行できるshimに置き換える設定 package: { // package.jsonの内容 name: &#34;package-name&#34;, version: Deno.args[0], // ... }, });</pre> <p>あとは定義した<code>build</code>関数を Deno 側で実行すれば npm パッケージが出力されます。実際に dnt の<code>build</code>関数を実行すると以下のようなログがターミナル上に表示されます。</p> <pre class="code shell" data-lang="shell" data-unlink>❯ deno task build Task build deno run -A build_npm.ts [dnt] Transforming... [dnt] Running npm install... added 4 packages, and audited 5 packages in 2s found 0 vulnerabilities [dnt] Building project... [dnt] Type checking ESM... [dnt] Emitting ESM package... [dnt] Emitting script package... [dnt] Running post build action... [dnt] Running tests... &gt; test &gt; node test_runner.js Running tests in ./script/nus3_test.js... test テストケース ... ok Running tests in ./esm/nus3_test.js... test テストケース ... ok [dnt] Complete!</pre> <p>このログにも表示されていますが、dnt には大きく 2 つの特徴があります。</p> <ol> <li>Deno で実装したモジュールが<a href="https://docs.deno.com/runtime/manual/node/npm_specifiers"><code>npm:</code> specifiers</a>や、<a href="https://esm.sh/">esm.sh</a>などの CDN、<a href="https://docs.deno.com/runtime/manual/basics/import_maps">Import Maps</a>から npm パッケージを使用している場合、それらのパッケージを依存関係として npm パッケージの package.json に追加する</li> <li>ビルド後の ESM、CJS 形式のファイルそれぞれに対して Deno で実装したテストを Node.js で実行する</li> </ol> <h3 id="dnt-はどのように-Deno-のモジュールを-npm-パッケージとしてビルドするのか">dnt はどのように Deno のモジュールを npm パッケージとしてビルドするのか</h3> <p>では、実際に dnt がどのように Deno のモジュールを npm パッケージに変換しているのか、ざっくりとした流れを見てみましょう。</p> <p>dnt(v0.38.1 時点) のディレクトリ構成は次のようになっています。</p> <pre class="code" data-lang="" data-unlink>. ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── deno.jsonc ├── lib // wasmの出力先、shimsやts_morphのimport先 ├── mod.ts // dntのbuildメソッドが定義されてる場所 ├── rs-lib // deno_node_transform ├── rust-toolchain.toml ├── tests ├── transform.ts └── wasm // dnt-wasm</pre> <p>ディレクトリ構成からもわかる通り、dnt は Deno のモジュールと Rust のクレートが同一リポジトリで管理されています。</p> <p>dnt の<code>build</code>関数はリポジトリのルート直下の mod.ts で定義されており、<a href="https://github.com/denoland/dnt/blob/4ee98f48c59508158eb82aef1464f639c25cf8d9/mod.ts#L533-L551">引数で指定されたエントリーポイントなどの情報を<code>transform</code>関数に渡します</a>。</p> <p>この<a href="https://github.com/denoland/dnt/blob/4ee98f48c59508158eb82aef1464f639c25cf8d9/lib/pkg/dnt_wasm.generated.js#L243"><code>transform</code>関数は dnt_wasm_bg.wasm という wasm ファイルから提供</a>されています。dnt_wasm_bg.wasm は <a href="https://github.com/denoland/wasmbuild">wasmbuild</a> を使って Rust のコード(dnt-wasm)がコンパイルされたものです。</p> <p>dnt-wasm では、<a href="https://github.com/denoland/dnt/blob/4ee98f48c59508158eb82aef1464f639c25cf8d9/wasm/src/lib.rs#L74-L85">deno_node_transform が提供する transform 関数が実行</a>されています。</p> <p>筆者が Rust の知識が浅いこともあり deno_node_transform の<code>transform</code>関数が行なっている処理の詳細を把握はできていないんですが、<a href="https://github.com/denoland/dnt/blob/4ee98f48c59508158eb82aef1464f639c25cf8d9/transform.ts#L101-L103"><code>transform</code>関数のコメント</a>を読む限り、エントリーポイントを解析して、依存するモジュールを取得し、TypeScript のコードをメモリ上に出力してるようです。</p> <p>この<code>transform</code>の返り値(TypeScript のコード)を <a href="https://github.com/dsherret/ts-morph">ts_morph</a>という TypeScript Compiler API のラッパーに渡します。ts_morph を使って、<a href="https://ts-morph.com/emitting">TypeScript のコードから型定義ファイルと JS を emit</a>しています。</p> <p>また、emit する際に、<a href="https://github.com/denoland/dnt/blob/4ee98f48c59508158eb82aef1464f639c25cf8d9/mod.ts#L353-L370">ESM</a>、<a href="https://github.com/denoland/dnt/blob/4ee98f48c59508158eb82aef1464f639c25cf8d9/mod.ts#L373-L393">CJS</a>用の変換をしています。</p> <p><figure class="figure-image figure-image-fotolife" title="ざっくりとしたdntの処理の流れ"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/cybozuinsideout/20231004/20231004175106.png" alt="dnt&#x306E;&#x51E6;&#x7406;&#x306E;&#x6D41;&#x308C;" width="876" height="611" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ざっくりとしたdntの処理の流れ</figcaption></figure></p> <h2 id="Deno-と-dnt-を採用した理由">Deno と dnt を採用した理由</h2> <p>実際に<a href="https://zenn.dev/cybozu_frontend/articles/deno-npm-pacakge-dnt">フロントエンドエキスパートチームの探求時間に dnt を試した</a>際に、次の点が良いと感じていました。</p> <ul> <li>Lint、Format、Test、TypeCheck が設定なしで使える</li> <li>CJS、ESM 両方に対応した npm パッケージが作成できる</li> </ul> <p>また、今回作成する社内 npm パッケージは依存が少なく、実装もシンプルだったので、今後、何か問題があった際に移行するコストも低いと考え、採用を決めました。</p> <h2 id="実際に採用してみて">実際に採用してみて</h2> <p>採用した理由でも述べましたが、やはり 1 番良かったのは、Linter や Formatter、tsconfig の設定をせずにすぐに開発を始められる点でした。おかげで実装したい機能に集中できたと感じています。</p> <p>テストに関しても、ドキュメントを読むとビルトインで入っているテスト用の API の使い方などがすぐに理解でき、スムーズに書くことができました。(dnt でテストを実行すると CJS、ESM 形式のファイルそれぞれに対してテストを実行するので、モックをちゃんと restore しないと、ESM 形式だけパスして、CJS だけ落ちるようなケースはありましたが)</p> <p>今回は作成するパッケージの依存関係が少ないこともあり、パッケージのアップデートは手動で行なっていますが、運用を続けていくことを考えるとどのように自動化するかは今後、対応する必要がありそうです。</p> <p>これから運用を続けていく上で問題が発生することもあるかもしれませんが、今回実際に採用してみて、小規模な npm パッケージを作る際に Deno と dnt を使うと考慮すべき点を減らせ、実装したい機能にすぐに着手でき、とても良かったです。</p> cybozuinsideout