トレタ開発者ブログ 飲食店向け予約/顧客台帳サービス「トレタ」、モバイルオーダー「トレタO/X」などを運営するトレタの開発メンバーによるブログです。 2023-12-25T01:06:29+09:00 toreta-dev Hatena::Blog hatenablog://blog/6653458415126345620 トレタAdventCalendar2023:エンジニアkentaroが語るインフラ移行の舞台裏 hatenablog://entry/6801883189069504659 2023-12-25T01:06:29+09:00 2024-01-06T15:00:42+09:00 こちらはトレタAdventCalendar2023 24日目の記事です。 タイトルはAIタイトルアシストにつけてもらいました。 qiita.com はじめに こんにちは、トレタでソフトウェアエンジニアをしているkentaroです。 トレタO/Xという飲食店向けモバイルオーダーの開発を担当しています。 担当しているゲートウェイサーバのインフラを GCP に移行したのでその話を紹介したいと思います。 O/Xのシステム構成図。赤枠がゲートウェイサーバー。ちなみに筆者はユーザー向けアプリ・スタッフ向けアプリの開発も担当している。 なぜインフラを移行するのか 移行前は AWS の EKS 環境でした。当… <p>こちらはトレタAdventCalendar2023 24日目の記事です。<br/> タイトルはAIタイトルアシストにつけてもらいました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2023%2Ftoreta" title="トレタのカレンダー | Advent Calendar 2023 - Qiita" 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://qiita.com/advent-calendar/2023/toreta">qiita.com</a></cite></p> <h1 id="はじめに">はじめに</h1> <p>こんにちは、トレタでソフトウェアエンジニアをしている<a href="https://twitter.com/kenkenken_3">kentaro</a>です。<br/> <a href="https://toreta.in/toreta-ox/">トレタO/Xという飲食店向けモバイルオーダー</a>の開発を担当しています。<br/> 担当しているゲートウェイサーバのインフラを GCP に移行したのでその話を紹介したいと思います。</p> <p><figure class="figure-image figure-image-fotolife" title="赤枠がゲートウェイサーバー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kenkenken_3/20240106/20240106145127.png" width="852" height="448" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>O/Xのシステム構成図。赤枠がゲートウェイサーバー。ちなみに筆者はユーザー向けアプリ・スタッフ向けアプリの開発も担当している。</figcaption></figure></p> <h1 id="なぜインフラを移行するのか">なぜインフラを移行するのか</h1> <p>移行前は AWS の EKS 環境でした。当時は環境を追加したい等インフラに手を加える際にはインフラエンジニアへ作業を依頼して実施してもらう形をとっていました。<br/> このスタイルのメリットはアプリケーション担当のエンジニア(自分)の工数があまりかからないことと、インフラを得意とするエンジニアによる作業なので安心感があったことです。<br/> デメリットとしてはインフラエンジニアが複数のプロジェクトを横断してサポートしていたので日程調整のコストが発生していたことと、担当エンジニアのインフラの解像度が低くなってしまいがちなことでした。</p> <p>ちょうどO/Xでは各サービスの担当者がオーナーシップを持って開発を推し進めていくことを重要視していく機運が高まっていた(し、自分もその方針のほうが性に合っていると考えていた)ので、当時のスタイルのデメリットのほうが大きくなってきていました。<br/> そこで、多少不慣れであってもサービスの開発者自身がインフラも含めて一気通貫で開発ができるようにしていく意思決定を行った次第です。</p> <p>採用したのはGoogle Cloud の Cloud Run で、オペレーションの負担をなるべく小さくするという意図があります。</p> <h1 id="アーキテクチャ">アーキテクチャ</h1> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kenkenken_3/20231225/20231225005417.png" alt="&#x30B2;&#x30FC;&#x30C8;&#x30A6;&#x30A7;&#x30A4;&#x30B5;&#x30FC;&#x30D0;&#x30FC;&#x306E;&#x30A4;&#x30F3;&#x30D5;&#x30E9;&#x30A2;&#x30FC;&#x30AD;&#x30C6;&#x30AF;&#x30C1;&#x30E3;&#x56F3;" width="822" height="585" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>アキーテクチャは上図のとおりです。<br/> Cloud Run の前段に Cloud Load Balancing を配置しています。今後 Cloud Armor などを入れやすくするのが目的です。</p> <h3 id="デプロイ周り">デプロイ周り</h3> <p>Cloud Build で Docker build して Artifact Registry に push し、 Cloud Run にデプロイしています。</p> <h3 id="Infrastructure-as-Code-IaC">Infrastructure as Code (IaC)</h3> <p>Terraform を利用し、 GitHub でバージョン管理しています。<br/> PRでのレビューを実施できるのが嬉しいですし、手動でのうっかりミスが発生しないという点も嬉しいです。<br/> Apply は Terraform Cloud を利用しています。特定のブランチへ向けたPRがマージされたら自動的に Plan が、設定によっては Apply まで行われるので便利です。</p> <h3 id="担当者のスキルセット">担当者のスキルセット</h3> <p>普段は TypeScript でバックエンドもフロントエンドも開発しています。 <br/> また、 Flutter を触ることもあります。<br/> O/X にジョインするまでは予約台帳アプリのiOS開発を担当していました。</p> <p>今は得意な言語も好きな言語も TypeScript です。</p> <p>というわけでインフラ周りを業務でガッツリ触るのは今回が初でした。</p> <h1 id="移行を終えた感想はどうなのよ">移行を終えた感想はどうなのよ?</h1> <h3 id="よかったこと">よかったこと</h3> <p>インフラに変更を加えたいときに、チームレベルでさっと対応できるのがスピード感があっていいなと感じています。 オーナーシップの醸成にも寄与しているかなと。</p> <h3 id="大変だったこと">大変だったこと</h3> <p>インフラ初心者なので、何かするたびにつまづくのが大変でした。<br/> 具体的には Terraform Cloud で Plan や Apply コケまくる問題が発生したことです。<br/> トライ &amp; エラーを繰り返したり、インフラの知見を持つメンバーに助けてもらったりしながらなんとか移行を終えました。</p> <h1 id="おわりに">おわりに</h1> <p>経験のない分野のことでもどんどん挑戦していきたい方、あるいは自分の得意分野でバリューを発揮したい方。飲食店の未来をアップデートする事業に興味があれば気軽に話を聞きにきていただければと思います。何卒!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%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://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> kenkenken_3 GPTsでGoのテストアドバイザーを作ってみた hatenablog://entry/6801883189068708065 2023-12-22T09:00:00+09:00 2023-12-22T09:00:02+09:00 はじめに こんにちは、サーバーサイドエンジニアの@shiroemonsです。 こちらはトレタAdventCalendar2023 22日目の記事です。 11月上旬、OpenAIからChatGPTの新機能「GPTs」が発表されました。 この新機能は、ChatGPTを特定の目的や用途に合わせてカスタマイズできる点が特徴です。 この記事では、GPTs作成と作成したGPTsを紹介しようと思います。 GPTsとは GPTsは、自然言語処理を行うAI技術であるChatGPTを、特定の目的や用途に合わせてカスタマイズできる機能です。 この機能は、OpenAIが開発したGPTモデルに基づいており、膨大なテキス… <h2 id="はじめに">はじめに</h2> <p>こんにちは、サーバーサイドエンジニアの<a href="https://twitter.com/shiroemons">@shiroemons</a>です。</p> <p>こちらはトレタAdventCalendar2023 22日目の記事です。</p> <p>11月上旬、OpenAIからChatGPTの新機能「GPTs」が発表されました。</p> <p>この新機能は、ChatGPTを特定の目的や用途に合わせてカスタマイズできる点が特徴です。</p> <p>この記事では、GPTs作成と作成したGPTsを紹介しようと思います。</p> <h2 id="GPTsとは">GPTsとは</h2> <p>GPTsは、自然言語処理を行うAI技術であるChatGPTを、特定の目的や用途に合わせてカスタマイズできる機能です。</p> <p>この機能は、OpenAIが開発したGPTモデルに基づいており、膨大なテキストデータから学習した自然言語理解と生成の能力を持っています。</p> <p>GPTsの特徴はその柔軟性にあります。ユーザーは自分のニーズに合わせてモデルをカスタマイズし、特定の業界やタスクに適したAIアドバイザーを作成できます。</p> <p>これにより、ビジネス、教育、研究など、様々な分野での応用が可能になります。</p> <p>詳細は<a href="https://openai.com/blog/introducing-gpts">OpenAIのブログ</a>をご覧ください。</p> <blockquote><p><strong>注記:</strong> GPTsを使用、作成するには、ChatGPT Plus($20/月)の加入が必要です。</p></blockquote> <h2 id="GPTs作成方法">GPTs作成方法</h2> <p>GPTsを作成する手順は非常にシンプルです。以下のステップの通りです。</p> <ol> <li><strong>ChatGPTホーム画面のアクセス</strong><br/> ChatGPTホーム画面から左側のメニューにある「Explore」または「探索する」を選択します。これにより、GPTsの作成に必要なさまざまなリソースにアクセスできます。</li> </ol> <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/s/shiroemons/20231221/20231221231204.png" width="782" height="618" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>左メニューの「探索する」をクリックする</figcaption></figure></p> <ol> <li><strong>新規GPTの作成</strong><br/> 「My GPTs」セクションから「Create a GPT」を選択します。ここでは、新たに作成するGPTの基本設定を行います。</li> </ol> <p><figure class="figure-image figure-image-fotolife" title="「Create a GPT」をクリックする"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiroemons/20231221/20231221231853.png" width="1200" height="353" loading="lazy" title="" class="hatena-fotolife" style="width:600px" itemprop="image"></span><figcaption>「Create a GPT」をクリックする</figcaption></figure></p> <ol> <li><strong>詳細設定の入力</strong><br/> 対話形式で、GPTの目的やアイコン画像、参照情報などを設定します。ここでは、GPTがどのような機能を持つか、どのような応答を提供するかを定義します。 アイコン画像は、DALL·E 3で作成した画像を使用することができます。</li> </ol> <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/s/shiroemons/20231221/20231221231802.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" style="width:600px" itemprop="image"></span><figcaption>対話形式で設定していきます</figcaption></figure></p> <p>このプロセスは非常に直感的で、特別な技術知識は必要ありません。ただし、特定の用途や業界に特化したGPTを作成する場合は、その分野に関する知識があるとより効果的なカスタマイズが可能です。</p> <h2 id="活用事例の紹介Go-Testerの作成">活用事例の紹介:「Go Tester」の作成</h2> <p>GPTsが使えるようになってすぐ、私はGo言語のテストに関する課題を解決するための特化したアドバイザーを作成しました。</p> <p><figure class="figure-image figure-image-fotolife" title="Go Tester"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiroemons/20231221/20231221232655.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" style="width:600px" itemprop="image"></span><figcaption>Go Tester</figcaption></figure></p> <h3 id="背景">背景</h3> <p>Go言語のテストに関する知見が不足していると感じ、Go言語のテスト方法に関するアドバイスを提供する「Go Tester」というアドバイザーをGPTsを使って作成しました。</p> <h3 id="機能">機能</h3> <p>「Go Tester」はGo言語に関するテストに特化したアドバイザーです。</p> <p>Goのテストに関する質問をすると、具体的なアドバイスやテストの書き方の提案を行います。</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/s/shiroemons/20231221/20231221235612.png" width="540" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></span><figcaption>サンプル</figcaption></figure></p> <h3 id="利用のメリット">利用のメリット</h3> <ul> <li><strong>初心者から中級者まで</strong>: Goのテストに関する基本的な知識から、より高度なテクニックに関する質問にも答えられます。</li> <li><strong>効率的な学習支援</strong>: 実際のテストケースに即したアドバイスを受けることで、Goのテストスキルを効率的に向上させることができます。</li> <li><strong>実用性の高い提案</strong>: 完璧ではありませんが、実際のプロジェクトで役立つテストの書き方を提案してくれます。</li> </ul> <h3 id="まとめ">まとめ</h3> <p>Go Testerは、Go言語のテストに関する知識を深め、より良いテストコードを書くための有用なツールです。</p> <p>GPTsを使って、特定の技術分野に特化したアドバイザーを作成することで、エンジニアの学習と実践の支援が可能になります。</p> <h3 id="公開について">公開について</h3> <p>「Go Tester」はGPTsの機能を試すために作成したツールです。現在、このツールの公開は行っておらず、社内限定での使用となっています。(社内データやコードを使用しているわけではありません。)</p> <p>特に、弊社内でGo言語を使用するプロジェクトの開発メンバーには、Goのテスト方法に関する知識を深めるためのリソースとして共有しています。</p> <h2 id="所感と展望">所感と展望</h2> <p>GPTsは、対話形式で簡単にカスタマイズ可能なAIアドバイザーを作成できることに、本当に驚きました。</p> <p>現在の「Go Tester」はまだ初期段階にあり、設定が粗いため、回答の精度には改善の余地があります。</p> <p>今後は、このアドバイザーの精度を向上させるためにさらなる努力を続けるつもりです。</p> <h2 id="終わり">終わり</h2> <p>トレタでは一緒にチームで働いてくれるエンジニアのメンバーやプロダクトマネージャーとして活躍してくれる人を求めています。飲食店の未来をアップデートする事業に興味のある方はお気軽に話を聞きにきてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%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://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> shiroemons 21世紀のターミナル「Warp」のショートカットキーの紹介 hatenablog://entry/6801883189068222010 2023-12-20T09:00:00+09:00 2023-12-20T11:15:21+09:00 はじめに こんにちは、サーバーサイドエンジニアの@shiroemonsです。 こちらはトレタAdventCalendar2023 20日目の記事です。 前日の記事でシリコンMacに移行した記事を書きました。 tech.toreta.in 最近、Warpというターミナルを使い始めました。今回の記事はもともとWarpの機能について詳しく紹介するつもりでしたが、他のブログと内容が重複してしまうことに気づきました。 実際に、Warpを使い始めたばかりで、まだ完全には慣れていないため、学ぶべきことも多いです。そこで、この記事ではWarpで使える便利なショートカットキーに焦点を当ててみました。この記事を通… <h2 id="はじめに">はじめに</h2> <p>こんにちは、サーバーサイドエンジニアの<a href="https://twitter.com/shiroemons">@shiroemons</a>です。</p> <p>こちらはトレタAdventCalendar2023 20日目の記事です。</p> <p>前日の記事でシリコンMacに移行した記事を書きました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2023%2F12%2F19%2F090000" title="シリコンMacへの開発環境移行体験談 - トレタ開発者ブログ" 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://tech.toreta.in/entry/2023/12/19/090000">tech.toreta.in</a></cite></p> <p>最近、<a href="https://www.warp.dev/">Warp</a>というターミナルを使い始めました。今回の記事はもともとWarpの機能について詳しく紹介するつもりでしたが、他のブログと内容が重複してしまうことに気づきました。</p> <p>実際に、Warpを使い始めたばかりで、まだ完全には慣れていないため、学ぶべきことも多いです。そこで、この記事ではWarpで使える便利なショートカットキーに焦点を当ててみました。この記事を通じて、Warpのショートカットキーを効率よく覚えるお手伝いができればと思います。</p> <h2 id="Warpのショートカットキー一覧">Warpのショートカットキー一覧</h2> <p>公式サイトに掲載されている情報を基に、より理解しやすいように順序を変えてWarpのショートカットキーをまとめてみました。公式の情報を参考にしつつ、読者にとって分かりやすい形で情報を整理しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.warp.dev%2Ffeatures%2Fkeyboard-shortcuts" title="Keyboard Shortcuts - Warp Documentation" 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.warp.dev/features/keyboard-shortcuts">docs.warp.dev</a></cite></p> <h3 id="Warp-Essentials">Warp Essentials</h3> <table> <thead> <tr> <th> ショートカットキー </th> <th> テキスト表記 </th> <th> 機能</th> </tr> </thead> <tbody> <tr> <td> ⌘ + L </td> <td> <code>cmd</code> + <code>L</code> </td> <td> ターミナル入力へのフォーカス </td> </tr> <tr> <td> ⌃ + R </td> <td> <code>ctrl</code> + <code>R</code></td> <td> コマンド検索 </td> </tr> <tr> <td> ⌃ + ⇧ + R </td> <td> <code>ctrl</code> + <code>shift</code> + <code>R</code> </td> <td> ワークフロー</td> </tr> <tr> <td> ⌃ + ` </td> <td> <code>ctrl</code> + <code>`</code></td> <td> AIコマンドの提案 </td> </tr> <tr> <td> ⌃ + I </td> <td> <code>ctrl</code> + <code>I</code></td> <td> Warpifyサブシェル </td> </tr> <tr> <td> ⌃ + ⌘ + L </td> <td> <code>ctrl</code> + <code>cmd</code> + <code>L</code> </td> <td> 設定パレットの起動 </td> </tr> <tr> <td> ⌃ + ⌘ + T </td> <td> <code>ctrl</code> + <code>cmd</code> + <code>T</code> </td> <td> テーマピッカーの開始 </td> </tr> </tbody> </table> <h3 id="Blocks">Blocks</h3> <table> <thead> <tr> <th> ショートカットキー </th> <th> テキスト表記 </th> <th> 機能 </th> </tr> </thead> <tbody> <tr> <td> ⌘ + A </td> <td> <code>cmd</code> + <code>A</code> </td> <td> すべてのブロックを選択 </td> </tr> <tr> <td> ⌘ + B </td> <td> <code>cmd</code> + <code>B</code> </td> <td> 選択したブロックをブックマーク </td> </tr> <tr> <td> ⌘ + K </td> <td> <code>cmd</code> + <code>K</code> </td> <td> ブロックのクリア </td> </tr> <tr> <td> ⌘ + I </td> <td> <code>cmd</code> + <code>I</code> </td> <td> 選択したコマンドを再入力 </td> </tr> <tr> <td> ⌘ + ↑ </td> <td> <code>cmd</code> + <code>↑</code> </td> <td> 前のブロックを選択 </td> </tr> <tr> <td> ⌘ + ↓ </td> <td> <code>cmd</code> + <code>↓</code> </td> <td> 次のブロックを選択 </td> </tr> <tr> <td> ⇧ + ↑ </td> <td> <code>shift</code> + <code>↑</code> </td> <td> 上の選択したブロックを拡張 </td> </tr> <tr> <td> ⇧ + ↓ </td> <td> <code>shift</code> + <code>↓</code> </td> <td> 下の選択したブロックを拡張 </td> </tr> <tr> <td> ⌥ + ↑ </td> <td> <code>option</code> + <code>↑</code> </td> <td> 最も近い上のブックマークを選択 </td> </tr> <tr> <td> ⌥ + ↓ </td> <td> <code>option</code> + <code>↓</code> </td> <td> 最も近い下のブックマークを選択 </td> </tr> <tr> <td> ⇧ + ⌘ + I </td> <td> <code>shift</code> + <code>cmd</code> + <code>I</code> </td> <td> 選択したコマンドをルートとして再入力 </td> </tr> <tr> <td> ⌃ + M </td> <td> <code>ctrl</code> + <code>M</code></td> <td> ブロックのコンテキストメニューを開く </td> </tr> <tr> <td> ⇧ + ⌘ + S </td> <td> <code>shift</code> + <code>cmd</code> + <code>S</code> </td> <td> 選択したブロックの共有 </td> </tr> <tr> <td> ⇧ + ⌘ + C </td> <td> <code>shift</code> + <code>cmd</code> + <code>C</code> </td> <td> コマンドのコピー </td> </tr> <tr> <td> ⌥ + ⇧ + ⌘ + C </td> <td> <code>option</code> + <code>shift</code> + <code>cmd</code> + <code>C</code> </td> <td> コマンド出力のコピー </td> </tr> </tbody> </table> <h3 id="Input-Editor">Input Editor</h3> <table> <thead> <tr> <th> ショートカットキー </th> <th> テキスト表記 </th> <th> 機能 </th> </tr> </thead> <tbody> <tr> <td> ⌘ + A </td> <td> <code>cmd</code> + <code>A</code> </td> <td> 全て選択 </td> </tr> <tr> <td> ⌘ + Z </td> <td> <code>cmd</code> + <code>Z</code> </td> <td> 元に戻す </td> </tr> <tr> <td> ⌘ + ⇧ + Z </td> <td> <code>cmd</code> + <code>shift</code> + <code>Z</code> </td> <td> やり直し </td> </tr> <tr> <td> ⌘ + ← </td> <td> <code>cmd</code> + <code>←</code> </td> <td> ホーム </td> </tr> <tr> <td> ⌘ + → </td> <td> <code>cmd</code> + <code>→</code> </td> <td> エンド </td> </tr> <tr> <td> ⌘ + ↓ </td> <td> <code>cmd</code> + <code>↓</code> </td> <td> カーソルを最下部へ移動 </td> </tr> <tr> <td> ⌘ + I </td> <td> <code>cmd</code> + <code>I</code> </td> <td> コマンドの検証 </td> </tr> <tr> <td> ⌘ + BackSpace </td> <td> <code>cmd</code> + <code>backspace</code> </td> <td> 左側を全て削除 </td> </tr> <tr> <td> ⌘ + Delete </td> <td> <code>cmd</code> + <code>delete</code></td> <td> 右側を全て削除 </td> </tr> <tr> <td> ⌃ + A </td> <td> <code>ctrl</code> + <code>A</code></td> <td> 行の開始へ移動 </td> </tr> <tr> <td> ⌃ + E </td> <td> <code>ctrl</code> + <code>E</code></td> <td> 行の終わりへ移動 </td> </tr> <tr> <td> ⌃ + P </td> <td> <code>ctrl</code> + <code>P</code></td> <td> カーソルを上に移動 </td> </tr> <tr> <td> ⌃ + B </td> <td> <code>ctrl</code> + <code>B</code></td> <td> カーソルを左に移動</td> </tr> <tr> <td> ⌃ + F </td> <td> <code>ctrl</code> + <code>F</code></td> <td> カーソルを右に移動/自動提案を受け入れる </td> </tr> <tr> <td> ⌃ + N </td> <td> <code>ctrl</code> + <code>N</code></td> <td> カーソルを下に移動 </td> </tr> <tr> <td> ⌃ + G </td> <td> <code>ctrl</code> + <code>G</code></td> <td> 次の出現箇所を選択 </td> </tr> <tr> <td> ⌃ + J </td> <td> <code>ctrl</code> + <code>J</code></td> <td> 改行を挿入 </td> </tr> <tr> <td> ⌃ + D </td> <td> <code>ctrl</code> + <code>D</code></td> <td> 削除 </td> </tr> <tr> <td> ⌃ + H </td> <td> <code>ctrl</code> + <code>H</code></td> <td> 前の文字を削除 </td> </tr> <tr> <td> ⌃ + W </td> <td> <code>ctrl</code> + <code>W</code></td> <td> 左の単語をカット </td> </tr> <tr> <td> ⌃ + K </td> <td> <code>ctrl</code> + <code>K</code></td> <td> 右側をカット </td> </tr> <tr> <td> ⌃ + L </td> <td> <code>ctrl</code> + <code>L</code></td> <td> 画面のクリア </td> </tr> <tr> <td> ⌃ + C </td> <td> <code>ctrl</code> + <code>C</code></td> <td> コマンドエディタのクリア </td> </tr> <tr> <td> ⌃ + U </td> <td> <code>ctrl</code> + <code>U</code></td> <td> 選択した行をコピーしてクリア </td> </tr> <tr> <td> ⌃ + ⇧ + ↑ </td> <td> <code>ctrl</code> + <code>shift</code> + <code>↑</code> </td> <td> 上にカーソルを追加 </td> </tr> <tr> <td> ⌃ + ⇧ + ↓ </td> <td> <code>ctrl</code> + <code>shift</code> + <code>↓</code> </td> <td> 下にカーソルを追加 </td> </tr> <tr> <td> ⌃ + ⇧ + A </td> <td> <code>ctrl</code> + <code>shift</code> + <code>A</code> </td> <td> 行の開始まで選択 </td> </tr> <tr> <td> ⌃ + ⇧ + E </td> <td> <code>ctrl</code> + <code>shift</code> + <code>E</code> </td> <td> 行の終わりまで選択 </td> </tr> <tr> <td> ⌃ + ⇧ + B </td> <td> <code>ctrl</code> + <code>shift</code> + <code>B</code> </td> <td> 左に1文字選択 </td> </tr> <tr> <td> ⌃ + ⇧ + F </td> <td> <code>ctrl</code> + <code>shift</code> + <code>F</code> </td> <td> 右に1文字選択 </td> </tr> <tr> <td> ⌃ + ⇧ + P </td> <td> <code>ctrl</code> + <code>shift</code> + <code>P</code> </td> <td> 上を選択 </td> </tr> <tr> <td> ⌃ + ⇧ + N </td> <td> <code>ctrl</code> + <code>shift</code> + <code>N</code> </td> <td> 下を選択 </td> </tr> <tr> <td> ⌃ + ⌥ + ← </td> <td> <code>ctrl</code> + <code>option</code> + <code>←</code> </td> <td> 1サブワード後ろへ移動 </td> </tr> <tr> <td> ⌃ + ⌥ + → </td> <td> <code>ctrl</code> + <code>option</code> + <code>→</code> </td> <td> 1サブワード前へ移動 </td> </tr> <tr> <td> Meta + . </td> <td> <code>meta</code> + <code>.</code></td> <td> 前のコマンドの最後の単語を挿入 </td> </tr> <tr> <td> Meta + A </td> <td> <code>meta</code> + <code>A</code></td> <td> 段落の開始へ移動 </td> </tr> <tr> <td> Meta + E </td> <td> <code>meta</code> + <code>E</code></td> <td> 段落の終わりへ移動 </td> </tr> <tr> <td> Meta + F </td> <td> <code>meta</code> + <code>F</code></td> <td> 1単語前へ移動</td> </tr> <tr> <td> Meta + B </td> <td> <code>meta</code> + <code>B</code></td> <td> 1単語後ろへ移動</td> </tr> <tr> <td> Meta + D </td> <td> <code>meta</code> + <code>D</code></td> <td> 右の単語をカット </td> </tr> <tr> <td> ⌥ + BackSpace </td> <td> <code>option</code> + <code>backspace</code> </td> <td> 左の単語を削除 </td> </tr> <tr> <td> ⌥ + Delete </td> <td> <code>option</code> + <code>delete</code></td> <td> 右の単語を削除 </td> </tr> <tr> <td> ⌥ + ⌘ + F </td> <td> <code>option</code> + <code>cmd</code> + <code>F</code> </td> <td> 選択範囲の折りたたみ </td> </tr> <tr> <td> ⌥ + ⌘ + [ </td> <td> <code>option</code> + <code>cmd</code> + <code>[</code> </td> <td> 折りたたみ </td> </tr> <tr> <td> ⌥ + ⌘ + ] </td> <td> <code>option</code> + <code>cmd</code> + <code>]</code> </td> <td> 展開 </td> </tr> <tr> <td> ⇧ + ⌘ + K </td> <td> <code>shift</code> + <code>cmd</code> + <code>K</code> </td> <td> 選択した行をクリア </td> </tr> <tr> <td> ⇧ + Meta + &lt; </td> <td> <code>shift</code> + <code>meta</code> + <code>&lt;</code> </td> <td> バッファの開始へ移動 </td> </tr> <tr> <td> ⇧ + Meta + > </td> <td> <code>shift</code> + <code>meta</code> + <code>&gt;</code> </td> <td> バッファの終わりへ移動 </td> </tr> <tr> <td> ⇧ + Meta + B </td> <td> <code>shift</code> + <code>meta</code> + <code>B</code> </td> <td> 左の1単語を選択 </td> </tr> <tr> <td> ⇧ + Meta + F </td> <td> <code>shift</code> + <code>meta</code> + <code>F</code> </td> <td> 右の1単語を選択 </td> </tr> </tbody> </table> <p>Metaキーは、馴染みがないですね...</p> <h3 id="Terminal">Terminal</h3> <table> <thead> <tr> <th> ショートカットキー </th> <th> テキスト表記 </th> <th> 機能 </th> </tr> </thead> <tbody> <tr> <td> ⌘ + D </td> <td> <code>cmd</code> + <code>D</code> </td> <td> 右側にパネルを分割 </td> </tr> <tr> <td> ⇧ + ⌘ + D </td> <td> <code>shift</code> + <code>cmd</code> + <code>D</code> </td> <td> 下にパネルを分割 </td> </tr> <tr> <td> ⌘ + W </td> <td> <code>cmd</code> + <code>W</code> </td> <td> パネルを閉じる(現在のセッションを閉じる) </td> </tr> <tr> <td> ⌥ + ⌘ + ↑ </td> <td> <code>option</code> + <code>cmd</code> + <code>↑</code> </td> <td> パネルを上に切り替え </td> </tr> <tr> <td> ⌥ + ⌘ + ← </td> <td> <code>option</code> + <code>cmd</code> + <code>←</code> </td> <td> パネルを左に切り替え </td> </tr> <tr> <td> ⌥ + ⌘ + → </td> <td> <code>option</code> + <code>cmd</code> + <code>→</code> </td> <td> パネルを右に切り替え </td> </tr> <tr> <td> ⌥ + ⌘ + ↓ </td> <td> <code>option</code> + <code>cmd</code> + <code>↓</code> </td> <td> パネルを下に切り替え </td> </tr> <tr> <td> ⌥ + ⌘ + V </td> <td> <code>option</code> + <code>cmd</code> + <code>V</code> </td> <td> 詳細なアクセシビリティ通知の設定 </td> </tr> <tr> <td> ⌘ + , </td> <td> <code>cmd</code> + <code>,</code> </td> <td> 設定を開く </td> </tr> <tr> <td> ⌘ + G </td> <td> <code>cmd</code> + <code>G</code> </td> <td> 検索クエリの次の出現箇所を検索 </td> </tr> <tr> <td> ⌘ + P </td> <td> <code>cmd</code> + <code>P</code> </td> <td> コマンドパレットの切り替え </td> </tr> <tr> <td> ⌘ + R </td> <td> <code>cmd</code> + <code>R</code> </td> <td> マウスレポーティングの切り替え </td> </tr> <tr> <td> ⌘ + [ </td> <td> <code>cmd</code> + <code>[</code> </td> <td> 前のパネルをアクティベート </td> </tr> <tr> <td> ⌘ + ] </td> <td> <code>cmd</code> + <code>]</code> </td> <td> 次のパネルをアクティベート </td> </tr> <tr> <td> ⌃ + ⌘ + K </td> <td> <code>ctrl</code> + <code>cmd</code> + <code>K</code> </td> <td> キーバインディングエディタを開く </td> </tr> <tr> <td> ⌃ + ⌘ + ↑ </td> <td> <code>ctrl</code> + <code>cmd</code> + <code>↑</code> </td> <td> パネルのサイズ調整: 分割線を上に移動 </td> </tr> <tr> <td> ⌃ + ⌘ + ← </td> <td> <code>ctrl</code> + <code>cmd</code> + <code>←</code> </td> <td> パネルのサイズ調整: 分割線を左に移動 </td> </tr> <tr> <td> ⌃ + ⌘ + → </td> <td> <code>ctrl</code> + <code>cmd</code> + <code>→</code> </td> <td> パネルのサイズ調整: 分割線を右に移動 </td> </tr> <tr> <td> ⌃ + ⌘ + ↓ </td> <td> <code>ctrl</code> + <code>cmd</code> + <code>↓</code> </td> <td> パネルのサイズ調整: 分割線を下に移動 </td> </tr> <tr> <td> ⌃ + ⇧ + ? </td> <td> <code>ctrl</code> + <code>shift</code> + <code>?</code> </td> <td> リソースセンターを開く </td> </tr> <tr> <td> ⇧ + ⌘ + G </td> <td> <code>shift</code> + <code>cmd</code> + <code>G</code> </td> <td> 検索クエリの前の出現箇所を検索 </td> </tr> <tr> <td> ⇧ + ⌘ + P </td> <td> <code>shift</code> + <code>cmd</code> + <code>P</code> </td> <td> ナビゲーションパレットの切り替え </td> </tr> <tr> <td> ⇧ + ⌘ + Enter </td> <td> <code>shift</code> + <code>cmd</code> + <code>enter</code> </td> <td> アクティブなパネルの最大化の切り替え </td> </tr> </tbody> </table> <h3 id="Fundamentals">Fundamentals</h3> <table> <thead> <tr> <th> ショートカットキー </th> <th> テキスト表記 </th> <th> 機能 </th> </tr> </thead> <tbody> <tr> <td> ⌘ + - </td> <td> <code>cmd</code> + <code>-</code> </td> <td> フォントサイズの縮小 </td> </tr> <tr> <td> ⌘ + = </td> <td> <code>cmd</code> + <code>=</code> </td> <td> フォントサイズの拡大 </td> </tr> <tr> <td> ⌘ + 0 </td> <td> <code>cmd</code> + <code>0</code> </td> <td> フォントサイズをデフォルトにリセット </td> </tr> <tr> <td> ⌘ + F </td> <td> <code>cmd</code> + <code>F</code> </td> <td> 検索 </td> </tr> <tr> <td> ⌘ + C </td> <td> <code>cmd</code> + <code>C</code> </td> <td> コピー </td> </tr> <tr> <td> ⌘ + V </td> <td> <code>cmd</code> + <code>V</code> </td> <td> 貼り付け </td> </tr> <tr> <td> ⌘ + T </td> <td> <code>cmd</code> + <code>T</code> </td> <td> 新規タブを開く </td> </tr> <tr> <td> ⇧ + ⌘ + T </td> <td> <code>shift</code> + <code>cmd</code> + <code>T</code> </td> <td> 閉じたタブを再開 </td> </tr> <tr> <td> ⌘ + 1 </td> <td> <code>cmd</code> + <code>1</code> </td> <td> 1番目のタブに切り替え </td> </tr> <tr> <td> ⌘ + 2 </td> <td> <code>cmd</code> + <code>2</code> </td> <td> 2番目のタブに切り替え </td> </tr> <tr> <td> ⌘ + 3 </td> <td> <code>cmd</code> + <code>3</code> </td> <td> 3番目のタブに切り替え </td> </tr> <tr> <td> ⌘ + 4 </td> <td> <code>cmd</code> + <code>4</code> </td> <td> 4番目のタブに切り替え </td> </tr> <tr> <td> ⌘ + 5 </td> <td> <code>cmd</code> + <code>5</code> </td> <td> 5番目のタブに切り替え </td> </tr> <tr> <td> ⌘ + 6 </td> <td> <code>cmd</code> + <code>6</code> </td> <td> 6番目のタブに切り替え </td> </tr> <tr> <td> ⌘ + 7 </td> <td> <code>cmd</code> + <code>7</code> </td> <td> 7番目のタブに切り替え </td> </tr> <tr> <td> ⌘ + 8 </td> <td> <code>cmd</code> + <code>8</code> </td> <td> 8番目のタブに切り替え </td> </tr> <tr> <td> ⌘ + 9 </td> <td> <code>cmd</code> + <code>9</code> </td> <td> 9番目のタブに切り替え </td> </tr> <tr> <td> ⌃ + ⇧ + ← </td> <td> <code>ctrl</code> + <code>shift</code> + <code>←</code> </td> <td> タブを左に移動(タブごと移動、切り替えではない) </td> </tr> <tr> <td> ⌃ + ⇧ + → </td> <td> <code>ctrl</code> + <code>shift</code> + <code>→</code> </td> <td> タブを右に移動(タブごと移動、切り替えではない) </td> </tr> <tr> <td> ⇧ + ⌘ + { </td> <td> <code>shift</code> + <code>cmd</code> + <code>{</code> </td> <td> 前のタブをアクティブ化(タブの切り替え) </td> </tr> <tr> <td> ⇧ + ⌘ + } </td> <td> <code>shift</code> + <code>cmd</code> + <code>}</code> </td> <td> 次のタブをアクティブ化(タブの切り替え) </td> </tr> <tr> <td> ⌘ + Q </td> <td> <code>cmd</code> + <code>Q</code> </td> <td> Warpを終了する </td> </tr> </tbody> </table> <h2 id="一部カスタマイズ">一部カスタマイズ</h2> <p>日本語のMagic Keyboardを使用しているんですが、フォントサイズの拡大のショートカットが効かなかったので、以下にカスタマイズしています。</p> <table> <thead> <tr> <th> ショートカットキー </th> <th> テキスト表記 </th> <th> 機能 </th> </tr> </thead> <tbody> <tr> <td> ⌘ + ^ </td> <td> <code>cmd</code> + <code>^</code> </td> <td> フォントサイズの拡大 </td> </tr> </tbody> </table> <p>他のアプリと競合しているショートカットキーや上手く効かないショートカットキーもあるかと思います、そこは適宜使う使わないを判断してショートカットキーをカスタマイズして行くと良いと思います。</p> <h2 id="まとめ">まとめ</h2> <p>この記事では、Warpのショートカットキーに焦点を当て、まとめました。これらのショートカットキーは、ターミナル操作を効率的にするのに大いに役立ちます。</p> <p>Warpを使い始めたばかりの方には、これらのショートカットキーを覚えて使ってみることをお勧めします。日々の作業がよりスムーズになるでしょう。</p> <p>自分もこれから覚えていきます💪</p> <h2 id="終わり">終わり</h2> <p>トレタでは一緒にチームで働いてくれるエンジニアのメンバーやプロダクトマネージャーとして活躍してくれる人を求めています。飲食店の未来をアップデートする事業に興味のある方はお気軽に話を聞きにきてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%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://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> shiroemons シリコンMacへの開発環境移行体験談 hatenablog://entry/6801883189067791089 2023-12-19T09:00:00+09:00 2024-01-12T16:21:27+09:00 はじめに こんにちは、サーバーサイドエンジニアの@shiroemonsです。 こちらはトレタAdventCalendar2023 19日目の記事です。 この記事では、開発用PCをインテルMacからシリコンMacに移行した際の経験を共有します。 背景 私の開発PCは、頻繁ではないですがフリーズやパフォーマンスの低下を経験していました。 特に、Google Meetでの会議中のフリーズは、作業の妨げになっていました。 そんな中、M3チップを搭載したMacBook Proの発表があり、これを機に新しいMacBook Proへの移行を決意しました。 12月9日に新しいMacBook Proが届いたこと… <h2 id="はじめに">はじめに</h2> <p>こんにちは、サーバーサイドエンジニアの<a href="https://twitter.com/shiroemons">@shiroemons</a>です。</p> <p>こちらはトレタAdventCalendar2023 19日目の記事です。</p> <p>この記事では、開発用PCをインテルMacからシリコンMacに移行した際の経験を共有します。</p> <h2 id="背景">背景</h2> <p>私の開発PCは、頻繁ではないですがフリーズやパフォーマンスの低下を経験していました。</p> <p>特に、Google Meetでの会議中のフリーズは、作業の妨げになっていました。</p> <p>そんな中、M3チップを搭載したMacBook Proの発表があり、これを機に新しいMacBook Proへの移行を決意しました。</p> <p>12月9日に新しいMacBook Proが届いたことで、移行のプロセスが始まりました。</p> <h2 id="スペックの変化">スペックの変化</h2> <p>移行前後のPCのスペックを比較すると、大きな変化がありました。</p> <p>メモリが増えたことにより、複数の開発ツールやアプリケーションを同時に動かしてもスムーズに作業できるようになりました。</p> <table> <thead> <tr> <th> </th> <th> 旧PC </th> <th> 新PC </th> </tr> </thead> <tbody> <tr> <td> 種類 </td> <td> 13インチ, 2019, Thunderbolt 3ポート x 4 </td> <td> 14インチMacBook Pro </td> </tr> <tr> <td> 仕上げ </td> <td> スペースグレイ </td> <td> スペースブラック </td> </tr> <tr> <td> メモリ </td> <td> 16GB </td> <td> 36GB </td> </tr> <tr> <td> ストレージ </td> <td> 512GB SSD </td> <td> 512GB SSD </td> </tr> <tr> <td> プロセッサ </td> <td> 2.8 GHz クアッドコアIntel Core i7 </td> <td> 1コアCPU、14コアGPU、16コアNeural Engine搭載Apple M3 Proチップ </td> </tr> </tbody> </table> <h2 id="移行アシスタントを使用しない理由">移行アシスタントを使用しない理由</h2> <p>新しいMacBook Proへの移行にあたり、私は意図的に移行アシスタントの使用を避ける選択をしました。この決断に至った理由は以下の通りです。</p> <h3 id="同僚の経験からの学び">同僚の経験からの学び</h3> <p>私の前にインテルMacからシリコンMacへ移行した同僚から、移行アシスタントを使用した際の経験を聞きました。彼らの体験によると、移行アシスタントを使用しても、チップの違いにより多くのアプリの再インストールが必要でした。</p> <h3 id="再インストール-vs-新規インストール">再インストール vs 新規インストール</h3> <p>移行アシスタントを使用する場合と同様にアプリを再インストールする必要があることから、新規インストールの方が状況に応じてより柔軟で、手軽な方法と考えました。</p> <h3 id="クリーンな環境の維持">クリーンな環境の維持</h3> <p>移行アシスタントを使うことにより、必要のない古いファイルや使用していないアプリが新しいPCに転送される可能性がありました。新しいPCをできるだけ「クリーン」な状態に保ちたいという思いから、手動での移行を選択しました。</p> <p>このように、移行アシスタントを使用しないことで、必要なアプリやファイルのみを選択し、新PCを最適化された状態で使い始めることができました。</p> <h2 id="移行方法">移行方法</h2> <p>新しいMacBook Proへの移行は、移行アシスタントを使用せず、手動で行いました。以下は、移行プロセスの主なステップです。</p> <h3 id="必要なアプリのインストール">必要なアプリのインストール</h3> <ul> <li><strong>プロセス</strong>: 私はまず、旧PCから新PCで使用するアプリのリストを作成しました。次に、このリストに基づいて新PCに必要なアプリを一つずつインストールしました。</li> <li><strong>目的</strong>: 新しい環境にのみ必要なアプリを選び、不要なものは除外することで、新PCを最適化しました。</li> </ul> <h3 id="ファイルの共有">ファイルの共有</h3> <ul> <li><strong>方法</strong>: 重要なファイルやローカル環境の設定ファイルなどは、AirDropを使用して新PCに転送しました。</li> <li><strong>利点</strong>: AirDropの使用により、ファイルの移行を迅速かつ簡単に行うことができました。</li> </ul> <h3 id="進捗管理">進捗管理</h3> <ul> <li><strong>ツール</strong>: 移行作業の進捗は、Notionに記録していきました。</li> <li><strong>効果</strong>: これにより、作業の進行状況を把握し、計画的に進めることができました。</li> </ul> <h3 id="認証情報の管理">認証情報の管理</h3> <ul> <li><strong>ツールの利用</strong>: 移行の際、同僚からのアドバイスに従い、認証情報やログインデータは1Passwordで一元管理しました。</li> <li><strong>成果</strong>: この方法により、移行中のログインや認証プロセスがスムーズに行われ、特に大きな問題は発生しませんでした。</li> </ul> <p>このような手動での移行プロセスは、新しい開発環境を清潔かつ整理された状態で維持することに役立ちました。 また、必要なアプリやファイルのみを移行することで、効率的かつ最適化された作業環境を実現しました。</p> <h2 id="開発環境の変更点">開発環境の変更点</h2> <h3 id="Shellの変更fish-shellからzshへ">Shellの変更:fish shellからzshへ</h3> <p>今回のPC移行に伴い、Shellをfish shellからzshに変更しました。</p> <h4 id="fish-shellの使用経験">fish shellの使用経験</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ffishshell.com%2F" title="fish shell" 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://fishshell.com/">fishshell.com</a></cite></p> <ul> <li><strong>使用感</strong>: 旧PCでfish shellを使用していた期間は、概ね満足していました。特に、ユーザーフレンドリーなインターフェースや豊富な機能は高く評価しています。</li> </ul> <h4 id="POSIX非互換の問題">POSIX非互換の問題</h4> <p>しかし、fish shellの最大の欠点は<strong>POSIX非互換</strong>であることです。 これにより、一部のスクリプトやコマンドが期待通りに動作しない場合があり、開発効率に影響を与えることがありました。 実際、私と同様にこの問題に直面し、zshに戻る開発者も多く見かけました。</p> <h4 id="zshへの回帰">zshへの回帰</h4> <ul> <li><strong>理由</strong>: POSIX互換性の重要性を再認識し、より一般的なシェルスクリプトやツールとの互換性を高めるために、zshへ戻ることを決めました。</li> <li><strong>結果</strong>: zshに戻ってからは、互換性の問題に悩まされることなく、スムーズな開発が可能になりました。</li> </ul> <p>この変更は、開発環境の安定性と互換性を考慮したものです。 POSIX互換性は、多様な開発環境やツールとの互動において重要な要素であり、これによりより幅広い開発作業を効率的に行えるようになりました。</p> <h3 id="ターミナルの変更iTerm2からWarpへ">ターミナルの変更:iTerm2からWarpへ</h3> <p>開発環境の改善の一環として、私はターミナルアプリケーションをiTerm2からWarpに変更しました。</p> <h4 id="Warpとは">Warpとは</h4> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.warp.dev%2F" title="Warp: Your terminal, reimagined" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.warp.dev/">www.warp.dev</a></cite></p> <ul> <li><strong>Rust言語ベース</strong>: WarpはRustで開発されており、高速で安定したパフォーマンスを提供します。</li> <li><strong>スタイリッシュなUI</strong>: モダンで直感的なユーザーインターフェースが特徴です。</li> <li><strong>動作環境</strong>: 現在WarpはmacOSでのみ利用可能です。</li> <li><strong>登録が必要</strong>: Warpを使用するには、事前にサインアップが必要です。</li> </ul> <h4 id="Warpの機能">Warpの機能</h4> <p>Warpには機能が多く搭載されていますが、詳細な紹介は省略します。</p> <p>公式のDemoがありますので、興味がある方はご覧ください。</p> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/XWQY8LgkiXM?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Warp Official Demo | Everything You Need To Know! (Updated for 2023)"></iframe><cite class="hatena-citation"><a href="https://youtu.be/XWQY8LgkiXM">youtu.be</a></cite></p> <h4 id="bindkeyの非対応">bindkeyの非対応</h4> <p>Warpの主な注意点として、<code>bindkey</code> コマンドの未対応が挙げられます。 これはGitHubの<a href="https://github.com/warpdotdev/warp/issues/537">Issue</a>でも取り上げられています。 私は、<code>ghq</code> と <code>fzf</code> を使ったリポジトリ検索に <code>bindkey</code> を使用していましたが、以下の関数を用意することでこの問題を回避しました。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># fzf + ghq を使用したリポジトリ検索</span> <span class="synStatement">fg</span><span class="synPreProc">()</span> <span class="synSpecial">{</span> <span class="synStatement">local</span><span class="synIdentifier"> repo_name</span><span class="synStatement">=&quot;</span><span class="synPreProc">$(</span><span class="synSpecial">ghq list </span><span class="synStatement">|</span><span class="synSpecial"> fzf-tmux --reverse +m</span><span class="synPreProc">)</span><span class="synStatement">&quot;</span> <span class="synStatement">if </span><span class="synSpecial">[[</span> <span class="synStatement">-n</span> <span class="synStatement">&quot;</span><span class="synPreProc">$repo_name</span><span class="synStatement">&quot;</span> <span class="synSpecial">]]</span><span class="synStatement">;</span> <span class="synStatement">then</span> <span class="synStatement">cd</span> <span class="synStatement">&quot;</span><span class="synPreProc">$(</span><span class="synSpecial">ghq root</span><span class="synPreProc">)</span><span class="synConstant">/</span><span class="synPreProc">$repo_name</span><span class="synStatement">&quot;</span> <span class="synStatement">fi</span> <span class="synSpecial">}</span> </pre> <p>この関数により、<code>ghq</code> で管理しているリポジトリを <code>fzf</code> で効率的に検索し、選択したリポジトリへ素早く移動できるようになりました。</p> <h2 id="移行完了と感想">移行完了と感想</h2> <p>新しいMacBook Proの開発環境でのビルドやテストを実行して確認し、無事移行作業を完了しました。</p> <p>移行アシスタントを使わない選択は、大正解だったと感じています。</p> <p>開発パフォーマンスも向上したと感じます。特にDockerの起動速度の向上は感動しました。</p> <p>この記事が皆さんのPC移行の参考になれば幸いです。</p> <h2 id="終わり">終わり</h2> <p>トレタでは一緒にチームで働いてくれるエンジニアのメンバーやプロダクトマネージャーとして活躍してくれる人を求めています。飲食店の未来をアップデートする事業に興味のある方はお気軽に話を聞きにきてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%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://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> shiroemons 仕様書を書くしごと hatenablog://entry/6801883189067828138 2023-12-18T14:25:09+09:00 2023-12-18T14:25:09+09:00 こちらはトレタAdventCalendar2023 18日目の記事です。 qiita.com こんにちは、トレタでトレタO/Xという飲食店向けモバイルオーダーのプロダクトマネージャーを行なっている北川です。 前回の記事で私がプロダクトマネージャーとして日頃行っている仕事内容について書きましたが、今回はプロダクトマネジメントの仕事の中で半分くらいの割合を占めている仕様書を書くというドキュメンテーションについてまとめたいと思います。 とはいえ、今のやり方がベストプラクティスとはまだ考えておらず日々アップデートをしつづけている最中です。ドキュメンテーションのやり方は組織や人によって千差万別だと思うの… <p>こちらはトレタAdventCalendar2023 18日目の記事です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fadvent-calendar%2F2023%2Ftoreta" title="トレタのカレンダー | Advent Calendar 2023 - Qiita" 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://qiita.com/advent-calendar/2023/toreta">qiita.com</a></cite></p> <p>こんにちは、トレタでトレタO/Xという飲食店向けモバイルオーダーのプロダクトマネージャーを行なっている北川です。</p> <p>前回の記事で私がプロダクトマネージャーとして日頃行っている仕事内容について書きましたが、今回はプロダクトマネジメントの仕事の中で半分くらいの割合を占めている仕様書を書くというドキュメンテーションについてまとめたいと思います。</p> <p>とはいえ、今のやり方がベストプラクティスとはまだ考えておらず日々アップデートをしつづけている最中です。ドキュメンテーションのやり方は組織や人によって千差万別だと思うので、あくまでも一つの例としてドキュメントライティングをしている人の参考になればと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftoreta.in%2Ftoreta-ox%2F" title="トレタO/X|飲食店のモバイルオーダーシステムで注文にワクワク感を" 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://toreta.in/toreta-ox/">toreta.in</a></cite></p> <h1 id="TLDR">TL;DR</h1> <p>この記事では以下の内容について記載しています。</p> <ul> <li>合意形成のために書く仕様書の内容</li> <li>開発チームに伝える仕様書の内容</li> </ul> <h1 id="仕様書の粒度">仕様書の粒度</h1> <p>おおまかな開発プロセスは前回の記事で書いた通りで、主に要件定義のフェーズと開発フェーズの2つのフェーズに分けられますが、仕様書を作成するのは基本的には要件定義のフェーズで行います。</p> <p>要件定義フェーズで作成する仕様書はプロダクトのステークホルダー(プロダクトオーナー、セールスメンバー、リードエンジニアなど)との合意形成を行うための「機能仕様書」と、決まった仕様から各開発チーム向けに詳細を記載した「ユースケース仕様書」の2種類に分けています。</p> <p>これはそのドキュメントの想定する読者と記述の粒度が異なるために分けています。 それぞれについて説明していきます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2023%2F12%2F10%2F150624" title="トレタでの開発プロセスとプロダクトマネージャーのしごと - トレタ開発者ブログ" 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://tech.toreta.in/entry/2023/12/10/150624">tech.toreta.in</a></cite></p> <h2 id="機能仕様書">機能仕様書</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20231218/20231218135514.png" width="764" height="1200" loading="lazy" title="" class="hatena-fotolife" style="width:300px" itemprop="image"></span></p> <p>機能仕様書では、ロードマップやユーザーFBなどのバックログにあるイシューから具体的にどう実現するかを仕様として記述したものです。</p> <p>多くの場合はこの仕様書をもとにステークホルダー(プロダクトオーナー、セールスメンバー、リードエンジニアなど)を集めたミーティングを開催し、仕様として問題がないか、スケジュールとしていつ頃着手するのか、などの内容を決定し合意形成を行います。</p> <p>そのため、作成段階ではタイトルに[Draft] と付けてまだ仕様としてFixしていないことを表します。</p> <p>定形のフォーマットは定めていませんが、主に以下の内容を記載します。</p> <pre class="code" data-lang="" data-unlink>概要 要件 機能要件 非機能要件 対応案   影響範囲 比較表    議事録</pre> <h3 id="目的と結論を冒頭にまとめる">目的と結論を冒頭にまとめる</h3> <p>今回の記事でもTL;DRを書きましたが、<strong>先頭にその仕様書の目的をまずは書きます</strong>。これは弊社で技術顧問を行っていただいているRyosuke Iwanagaさんのやり方を参考にさせていただいています。</p> <p>仕様書として細かく書いても全員が隅から隅まで読んでくれるとは限りません(大抵の人は流し読みします)。なので冒頭にこの仕様書は何を解決するための内容で、結論としてどういった仕様にしようとしている、というのをなるべく簡潔に書くように心がけています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fblog.riywo.com%2F2021%2F01%2Fhow-to-write-high-quality-technical-doc%2F" title="質の高い技術文書を書く方法 - As a Futurist..." 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://blog.riywo.com/2021/01/how-to-write-high-quality-technical-doc/">blog.riywo.com</a></cite></p> <h3 id="非機能要件も忘れずに">非機能要件も忘れずに</h3> <p>何を解決したいのか、達成したいかを要件としてまとめて箇条書きなどで書くことで、議論が発展して脱線仕出した時に、そもそも解決したいことって何でしたっけ?と振り返るときに有用です。</p> <p>この時に考慮が漏れがちなのが非機能要件です。機能要件はユーザーからのフィードバックなどの基のイシューから導出しやすいですが、非機能要件は顕在化されていない場合があります。</p> <p>例えば、将来的に似たような問題が発生したときも考慮すべきかといった拡張性やメンテナンス容易性であったり、個人情報などを含まないかなどのセキュリティの面などを洗い出しておく必要があります。</p> <h3 id="松竹梅の比較表を設ける">松竹梅の比較表を設ける</h3> <p>実施工数や拡張性をどこまで重視するかで対応案が複数になるケースがあります。その場合はPros/Consをまとめた比較表を作り最終決定者が選択しやすいようにいわゆる松竹梅の3プランにまとめます。ここでのポイントは、<strong>中間の竹プランが選ばれやすいので誘導したいプランを軸にして松と梅を添えます</strong>。これはアンカリングと呼ばれる認知バイアスを利用した手法です。</p> <p>割れ窓を取り急ぎ塞ぐ程度の場当たり的な対応は梅プラン、あまり負債も作らずに最小限で達成できる対応を竹プラン、たっぷり工数をかけて将来性含め完璧に対応するのを松プラン、といった形です。</p> <h3 id="意思決定の経緯を残す">意思決定の経緯を残す</h3> <p>仮に仕様書がなかったとしても、仕様を基にアウトプットされたアプリケーションコードなどから仕様を逆引きすることはできますが、なぜその仕様になったのかを探ることは難しいです。</p> <p>どのような議論が行われ、どのような意思決定が行われたのかを議事録として記述または議事録のリンクを貼ります。</p> <h2 id="ユースケース仕様書">ユースケース仕様書</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20231218/20231218140850.png" width="612" height="987" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ユースケース仕様書は、前述の機能仕様書から各チーム向けにブレイクダウンした内容の仕様書です。名前の通りユースケースの粒度で作成しているので、「ユーザーが商品を新規登録する」などのタイトルの仕様書になります。<strong>画面仕様書ではなくユースケース単位</strong>にすることで、ユーザーはどういった目的をもっていて、どういったUIを操作し、システムはどうふるまうのか、といった一連のUXをまとめることができます。</p> <p>ユースケース仕様書は、<strong>開発チーム内のメンバーの認識を一定に揃えるため</strong>に記述します。読者としてはエンジニア、QAエンジニア、カスタマーサクセスのメンバーを想定しています。それぞれのメンバーは仕様書を基にアプリケーションコードやテスト計画書、ユーザーマニュアルをアウトプットとして作ります。それぞれのアウトプットに齟齬が生まれないように、誰が読んでも同じ理解ができることを意識して仕様書を記述します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20231218/20231218141158.png" width="1012" height="852" loading="lazy" title="" class="hatena-fotolife" style="width:500px" itemprop="image"></span></p> <p>ユースケース仕様書はユースケースの分だけ書くことになり量が多いため、書式はテンプレート化しています。テンプレート化はかなり試行錯誤して改良を重ねていますが、一つ参考にしたのは「UI Spec」という書式です。こちらも同様にユースケースをベースとした書き方となっています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgoodpatch.com%2Fblog%2Fmvp-ui-spec" title="ユーザー体験を軸とした開発仕様書「UI Spec」とは|Goodpatch 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://goodpatch.com/blog/mvp-ui-spec">goodpatch.com</a></cite></p> <pre class="code" data-lang="" data-unlink>ユースケース 要件 画面仕様 Figmaへのリンク 画面デザイン 遷移元 表示形式/入力形式 インタラクティブ項目 バリデーション </pre> <h3 id="ユースケース">ユースケース</h3> <p>「誰」が「何」を「どうする」を記述します</p> <p>例. ユーザーが商品を新しく登録する</p> <h3 id="要件">要件</h3> <p>どういったアウトプットが期待されるかを記述します。E2Eテストの各シナリオのような記述をします。(テストコードでそのまま”describe”と”it”になると理想的)</p> <p>例.</p> <ul> <li>商品を登録できること</li> <li>不正な入力をすると商品が登録されないこと</li> </ul> <h3 id="画面仕様">画面仕様</h3> <p>Figmaのデザインのリンクとスナップショットを貼りつけます。</p> <p>リンクだけではなくなぜスナップショットなのは、常にFigmaと完全に同期が取れない可能性があるからです。 この仕様書は仕様変更があった際に随時アップデートしていく仕様書です。理想的にはFigmaと同時に更新されるべきですが、どうしても片方が更新漏れになってしまうこともあります。</p> <p>そのため仕様書内の記載で齟齬が起きないようにスナップショットを添付しています。</p> <h3 id="表示形式入力形式">表示形式・入力形式</h3> <p>Figma内で記載しきれない細かな内容を記述します。</p> <ul> <li>参照データ:データモデルとのマッピング情報</li> <li>表示条件:非表示にする場合があれば、データモデルの値と条件式</li> </ul> <p>一覧表示であれば、</p> <ul> <li>表示順、ページネーション有無、表示件数</li> </ul> <p>入力形式であれば、バリデーションの内容を記述します。</p> <ul> <li>必須項目であるか</li> <li>最小値/最大値があるか</li> <li>正規表現などのフォーマット制限があるか</li> </ul> <h3 id="インタラクティブ項目">インタラクティブ項目</h3> <p>ボタンクリックなどのユーザーインタラクションによってどのような振る舞いをするのかを記述します</p> <p>例.</p> <ul> <li>保存ボタン: 登録を行い、一覧画面へ遷移する</li> <li>キャンセルボタン:一覧画面へ遷移する</li> </ul> <h3 id="バリデーション">バリデーション</h3> <p>上記の入力形式で記載した以外のバリデーション内容があれば記述します。主にサーバーバリデーションに近い内容となります。</p> <p>例.</p> <ul> <li>名前などが重複して登録できるか</li> <li>登録件数の上限があるか</li> </ul> <h1 id="さいごに">さいごに</h1> <p>このように私が業務で書いているドキュメントの書き方的な部分を紹介しましたが、冒頭でも書いたようにまだ自分でもこのやり方がベストプラクティスではないと思ってますし、まだまだ書式や運用方法を磨かなければいけないと思っています。</p> <p>例えば今はNotionを使って書いていますが、Notionでは履歴管理の機能が足りないと感じていて追記した部分のみを伝える手段がなかったり、修正依頼を承認して反映するような運用が難しいのでGithubに移そうかと検討したりしています。</p> <p>より良い開発手法の探究やプロダクト開発に興味がある方をお待ちしています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%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://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> mkitagawa-312 トレタでの開発プロセスとプロダクトマネージャーのしごと hatenablog://entry/6801883189065604274 2023-12-10T15:06:24+09:00 2023-12-10T15:06:24+09:00 こちらはトレタAdventCalendar2023 10日目の記事です。 こんにちは、株式会社トレタでモバイルオーダーアプリ・トレタO/Xのプロダクトマネージャーをしている北川です。 toreta.in 年月はあっという間に過ぎ去るもので、プロダクトマネージャーという肩書きをいただいてからもうすぐ2年が経ちます。 手探りで始めたプロダクトマネジメント業務も少しは板についてきたと思うので、この記事では自分がトレタで行なっているプロダクトマネジメントの仕事についてまとめようと思います。 他のプロダクトマネージャーやトレタでの開発プロセスに興味のある方の参考になれば幸いです。 プロダクトマネジメント… <p>こちらはトレタAdventCalendar2023 10日目の記事です。</p> <p>こんにちは、株式会社トレタでモバイルオーダーアプリ・トレタO/Xのプロダクトマネージャーをしている北川です。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftoreta.in%2Ftoreta-ox%2F" title="トレタO/X|飲食店のモバイルオーダーシステムで注文にワクワク感を" 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://toreta.in/toreta-ox/">toreta.in</a></cite></p> <p>年月はあっという間に過ぎ去るもので、プロダクトマネージャーという肩書きをいただいてからもうすぐ2年が経ちます。 手探りで始めたプロダクトマネジメント業務も少しは板についてきたと思うので、この記事では自分がトレタで行なっているプロダクトマネジメントの仕事についてまとめようと思います。</p> <p>他のプロダクトマネージャーやトレタでの開発プロセスに興味のある方の参考になれば幸いです。</p> <h1 id="プロダクトマネジメントとは">プロダクトマネジメントとは</h1> <p>まず「プロダクトマネジメント」という定義は一般的にも曖昧です。プロダクトマネジメントの定義はプロダクトマネージャーの数だけある、ということが「プロダクトマネージャーのしごと」という本にも示されています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.oreilly.co.jp%2Fbooks%2F9784814400430%2F" title="プロダクトマネージャーのしごと 第2版" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.oreilly.co.jp/books/9784814400430/">www.oreilly.co.jp</a></cite></p> <p>私がプロダクトマネージャーになってまず手にとった「プロダクトマネジメントのすべて」という本にはプロダクトマネージャーがやるべきことや必要なスキル、知識などが幅広く紹介されています。 これをすべてやるなんて超人すぎる…と途方に暮れたものです。</p> <ul> <li>プロダクトマネージャーの仕事・・・プロダクトを育てる、チームビルディング、ステークイホルダーとのコミュニケーション等</li> <li>プロダクトマネジメントの必要とされるスキル・・・発想力、計画力、実行力、仮説検証力、リスク管理力、チーム構成力等</li> <li>プロダクトマネジメントに必要とされる知識・・・ビジネス、UX、エンジニアリング等</li> </ul> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.shoeisha.co.jp%2Fbook%2Fdetail%2F9784798166520" title="プロダクトマネジメントのすべて 事業戦略・IT開発・UXデザイン・マーケティングからチーム・組織運営まで | 翔泳社" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.shoeisha.co.jp/book/detail/9784798166520">www.shoeisha.co.jp</a></cite></p> <p>トレタでのプロダクトマネージャーはその本にあるようなミニCEOほどの権限はなく、そこはプロダクトオーナー(PO)が担っています。また、トレタでPM という職種にもプロダクトマネージャー(PdM)とプロジェクトマネージャー(PjM) の2つがあります。</p> <p> <table> <tr> <th> プロダクトオーナー(PO)</th> <td>プロダクトのアウトカムに責任を持つ人</td> </tr> <tr> <th> プロダクトマネージュー(PdM)</th> <td>プロダクトをどう作る考える人</td> </tr> <tr> <th> プロジェクトマネージャー(PjM)</th> <td>プロジェクトの進行管理をする人 </td> </tr> </table></p> <p>なのでプロダクトマネージャーとしては、エンジニアリングの視点としてやりたいことに対してシステムとしてどう作るか、そのためのチームをどう構成にするか、という範囲が責務になります。</p> <p>もう一つ、自分がプロダクトマネージャーの定義として大事にしているのは「<strong>Executionに責任を持つ人</strong>」ということです。この言葉はVPoE handbookという記事から引用したものです。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fshimizu%2Fn%2Fn21b91e059f33" title="VPoE handbook | エンジニア組織のマネジメントに悩んでいた三年前に戻れるなら渡したい。VPoE handbookを書き終えました (目次&amp;サマリ付)|Takayuki Shimizu" 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/shimizu/n/n21b91e059f33">note.com</a></cite></p> <p>とにかくプロダクトを成長させるため、事業を前に進めるためなら何でもする、ということです。</p> <p>チームがなければ作る、人が足りなければ採用を強化する、チームやメンバーが進む道を阻む障害があれば取り除くということを小さいことでも何でもやります。ボールが落ちていたらとりあえず拾う、というのをプロダクトマネージャーの仕事として大切にしています。</p> <h1 id="普段の開発プロセス">普段の開発プロセス</h1> <p>日々の業務としては基本的に開発プロジェクトに沿って動きます。</p> <p>通常のイシューまたは機能単位での開発の流れは以下の様に行なっています。プロダクトマネージャーとして一番関わるのは要件定義と仕様作成の部分です。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20231210/20231210142101.png" width="1200" height="639" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>要件定義フェーズは「Why」を深掘り「What」と「How」を決める工程です。</p> <p>開発項目としてやることが並べられたプロダクトバックログは、期初に定めたプロダクトロードマップのイシューと、ユーザーから届くフィードバックのイシューで構成されています。 そこからユーザーが本当に欲しいものは何か、本当に必要とされている機能は何かを深掘りし、どういった形でいつのタイミングで提供するのかを以下のメンバーで決めていきます。</p> <p> <table> <tr> <th> UX/UIデザイナー</th> <td>要求に対してユーザーにどういった体験を提供するかのUXおよびUIの提案を行う。アウトプットとして画面デザインを作る。 </td> </tr> <tr> <th> プロダクトマネージュー(PdM)</th> <td>エンジニアリング視点でシステム上どう実現するかを提案する。アウトプットとしてシステムの全体設計と、システム内のふるまいを仕様として定義する。</td> </tr> <tr> <th> プロジェクトマネージャー(PjM)</th> <td>各チーム内のタスクリストや稼働状況を照らし合わせてどのタイミングで行い、いつリリースするかのスケジュールを提案する。</td> </tr> <tr> <th>プロダクトオーナー(PO)</th> <td>プロダクトバックログの優先順位を決め、各要件の最終決定を行う。 </td> </tr> </table></p> <h2 id="質とスピード">質とスピード</h2> <p>我々が顧客としている飲食店のユーザー属性やユースケースは多種多様です。チェーン店や個店などの規模感の違いや、居酒屋やカフェなどの業態の違いなど店内オペレーションは店舗ごとにまるで異なります。それらすべてを満たすための仕様や設計にまとめるのは難しさがあり、すべてはトレードオフになります。トレードオフのレバーのどれを押すか、どう設計するかに迷ったときの判断軸はしばしば会社のバリューに頼ります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20231210/20231210143346.png" width="1200" height="669" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr%2Fn%2Fnd4571676124e" title="バリューを新しく定義しました。|トレタのnote" 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/toreta_hr/n/nd4571676124e">note.com</a></cite></p> <p>トレタでは行動指針として6つのバリューが示されています。その中でも特に意識するのは「<strong>速攻</strong>」と「<strong>圧倒的品質</strong>」です。相反する2つに思われますが、デリバリーの速さと品質の両方を両立できるが最善です。</p> <p>有名なt_wadaさんの「<strong>質とスピード</strong>」という話は弊社の開発現場でも浸透していて、これはまさに品質とスピードの両方を両立しましょうという話です。</p> <p><a href="https://speakerdeck.com/twada/quality-and-speed-aws-dev-day-2023-tokyo-edition">&#x8CEA;&#x3068;&#x30B9;&#x30D4;&#x30FC;&#x30C9;&#xFF08;AWS Dev Day 2023 Tokyo &#x7279;&#x5225;&#x7DE8;&#x3001;&#x8CEA;&#x7591;&#x5FDC;&#x7B54;&#x7528;&#x8CC7;&#x6599;&#x4ED8;&#x304D;&#xFF09; / Quality and Speed AWS Dev Day 2023 Tokyo Edition - Speaker Deck</a></p> <p>これはコーディングだけではなく、設計や仕様を作る場面においてもこのエッセンスはあてはまると自分は考えています。 ここでは「質」とは「<strong>保守性</strong>」と語られています。つまり、スピードを重視することで中長期的な保守性を下げるような仕様や設計にすべきではありません。</p> <p>たとえば、特定の店舗だけ機能を分岐させたいというようなカスタマイズの要望があったとします。それを実現するために店舗情報に専用のフラグを新しく足してアプリケーション上で分岐処理をさせよう、というやり方が1つ考えられます。</p> <p>ただしその場合、他の条件が今後出てきて絡みあったときに複雑さは増し、QAコストも跳ね上がるため保守性は下がっていきます。フラグを持たせずに他の状態で分岐条件を判断させることはできないか、フラグを持たせるにしても将来的に2値でいいのか、などよりシンプル(疎結合)で将来的にも負債になりづらい方法がないかを常に模索する必要があります。そしてそのやり方の方が多少のデリバリーの遅れがあるとしても、質が下がりにくいのであればそちらを採用します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20231210/20231210143419.png" width="1200" height="845" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p> <p>スピードに関しては「スコープを削る」ことをよく行います。ただ、スコープを削るだけではなくその機能で果たしたい最終的なあるべきゴールを描いてからフェーズを分割します。その上で1stリリースとして要件を最低限満たすミニマムな部分はどこかを探り短い開発期間でリリースできるようにします。これのいいところは、将来を見据えることで場当たり的ではない仕様にしやすくなるという点です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20231210/20231210143430.png" width="832" height="640" loading="lazy" title="" class="hatena-fotolife" style="width:400px" itemprop="image"></span></p> <h1 id="終わりに">終わりに</h1> <p>前述したとおりプロダクトマネージャーの仕事は幅広いので、今回書ききれなかった内容は山ほどあります。そちらは追って別の記事で書きたいと思います。</p> <p>また、トレタでは一緒にチームで働いてくれるエンジニアのメンバーや自分と同じくプロダクトマネージャーとして活躍してくれる人を求めています。飲食店の未来をアップデートする事業に興味のある方はお気軽に話を聞きにきてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%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://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> mkitagawa-312 GitHub ActionsとtblsでDBスキーマ変更に対応するER図の自動生成する hatenablog://entry/4207112889981193186 2023-05-29T17:00:29+09:00 2023-05-30T15:50:57+09:00 はじめに こんにちは、サーバーサイドエンジニアの @shiroemons です。 プルリクエストにDBスキーマの変更が含まれた場合、ER図を自動生成するために、tblsとGitHub Actionsを組み合わせた設定を行いました。 DBスキーマの変更は開発プロセスにおいて頻繁に発生しますが、手動でER図やドキュメントを更新することは煩雑で効率が悪い作業です。 そこで、GitHub Actionsとtblsを使用することで、ER図の自動生成と更新を容易に実現できました。 今回は、設定したGitHub Actionsの設定ファイルを紹介します。 必要なツールと環境 今回紹介するツールと環境はこちら… <h2 id="はじめに">はじめに</h2> <p>こんにちは、サーバーサイドエンジニアの <a href="https://twitter.com/shiroemons">@shiroemons</a> です。</p> <p>プルリクエストにDBスキーマの変更が含まれた場合、ER図を自動生成するために、tblsとGitHub Actionsを組み合わせた設定を行いました。</p> <p>DBスキーマの変更は開発プロセスにおいて頻繁に発生しますが、手動でER図やドキュメントを更新することは煩雑で効率が悪い作業です。 そこで、GitHub Actionsとtblsを使用することで、ER図の自動生成と更新を容易に実現できました。</p> <p>今回は、設定したGitHub Actionsの設定ファイルを紹介します。</p> <h2 id="必要なツールと環境">必要なツールと環境</h2> <p>今回紹介するツールと環境はこちらです。</p> <ul> <li>CI: GitHub Actions</li> <li>DB: PostgreSQL</li> <li>マイグレーションツール: <strong><a href="https://github.com/k0kubun/sqldef">psqldef</a></strong></li> <li>テーブル定義書作成: <strong><a href="https://github.com/k1LoW/tbls">tbls</a></strong></li> </ul> <h3 id="tbls-について">tbls について</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fk1LoW%2Ftbls" title="GitHub - k1LoW/tbls: tbls is a CI-Friendly tool for document a database, written in Go." 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/k1LoW/tbls">github.com</a></cite></p> <p>tblsは、データベースのスキーマ情報をドキュメント化するためのコマンドラインツールです。データベースからスキーマ情報を取得し、MarkdownやPlantUMLなどの形式で出力することができます。</p> <p>このツールを使用することで、データベースのスキーマ情報をわかりやすく、自動化された形でドキュメント化することができます。tblsは、PostgreSQL, MySQL, SQLite, Microsoft SQL Server など、複数のデータベース管理システムに対応しています。</p> <h2 id="ディレクトリ構成と設定ファイルの説明">ディレクトリ構成と設定ファイルの説明</h2> <h3 id="ディレクトリ構成">ディレクトリ構成</h3> <ul> <li>.github/workflows/tbls.yml <ul> <li>今回解説するGitHubActionsのファイル</li> </ul> </li> <li>db/schema/*.sql <ul> <li>スキーマファイル群</li> </ul> </li> <li>db/dbdoc/ <ul> <li>tblsのER図自動生成先</li> </ul> </li> <li>db/tbls.yml <ul> <li>tblsの設定ファイル</li> <li>設定ファイルの中身は後述</li> </ul> </li> </ul> <h3 id="設定内容">設定内容</h3> <h4 id="githubworkflowstblsyml">.github/workflows/tbls.yml</h4> <p>こちらが今回作成したGitHub Actionsの内容です。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> tbls <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">pull_request</span><span class="synSpecial">:</span> <span class="synIdentifier">types</span><span class="synSpecial">:</span> <span class="synStatement">- </span>opened <span class="synStatement">- </span>synchronize <span class="synStatement">- </span>reopened <span class="synIdentifier">paths</span><span class="synSpecial">:</span> <span class="synStatement">- </span>db/schema/*.sql <span class="synStatement">- </span>db/tbls.yml <span class="synIdentifier">branches-ignore</span><span class="synSpecial">:</span> <span class="synStatement">- </span>production <span class="synStatement">- </span>staging <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">tbls</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> generate-and-push <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">permissions</span><span class="synSpecial">:</span> <span class="synIdentifier">contents</span><span class="synSpecial">:</span> write <span class="synIdentifier">pull-requests</span><span class="synSpecial">:</span> write <span class="synIdentifier">services</span><span class="synSpecial">:</span> <span class="synIdentifier">postgres</span><span class="synSpecial">:</span> <span class="synIdentifier">image</span><span class="synSpecial">:</span> postgres:15-alpine <span class="synIdentifier">ports</span><span class="synSpecial">:</span> <span class="synStatement">- </span>5432:5432 <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">POSTGRES_HOST_AUTH_METHOD</span><span class="synSpecial">:</span> trust <span class="synIdentifier">POSTGRES_DB</span><span class="synSpecial">:</span> sampledbname <span class="synIdentifier">options</span><span class="synSpecial">:</span> &gt;- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries <span class="synConstant">5</span> <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">TBLS_DSN</span><span class="synSpecial">:</span> <span class="synConstant">&quot;postgres://postgres:@127.0.0.1:5432/sampledbname?sslmode=disable&quot;</span> <span class="synIdentifier">TBLS_DOC_PATH</span><span class="synSpecial">:</span> <span class="synConstant">&quot;db/dbdoc&quot;</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="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">repository</span><span class="synSpecial">:</span> ${{ github.event.pull_request.head.repo.full_name }} <span class="synIdentifier">ref</span><span class="synSpecial">:</span> ${{ github.event.pull_request.head.ref }} <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-go@v3 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">go-version</span><span class="synSpecial">:</span> ^1.20 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Execute migration <span class="synIdentifier">run</span><span class="synSpecial">:</span> | go install github.com/k0kubun/sqldef/cmd/psqldef@latest psqldef --file schema.sql sampledbname <span class="synIdentifier">working-directory</span><span class="synSpecial">:</span> ./db/schema <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Remove dbdocs <span class="synIdentifier">run</span><span class="synSpecial">:</span> | rm -rf ./*.md ./*.svg <span class="synIdentifier">working-directory</span><span class="synSpecial">:</span> ./db/dbdoc <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> k1low/setup-tbls@v1 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Run tbls for generate database document <span class="synIdentifier">run</span><span class="synSpecial">:</span> | tbls doc -c ./db/tbls.yml -f <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Count uncommit files <span class="synIdentifier">id</span><span class="synSpecial">:</span> check_diff <span class="synIdentifier">run</span><span class="synSpecial">:</span> | git status --porcelain | wc -l file_count=$(git status --porcelain | wc -l) echo <span class="synConstant">&quot;file_count=$file_count&quot;</span> &gt;&gt; $GITHUB_OUTPUT <span class="synIdentifier">working-directory</span><span class="synSpecial">:</span> ./db <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Commit ER graph <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ steps.check_diff.outputs.file_count <span class="synType">!=</span> <span class="synConstant">'0'</span> }} <span class="synIdentifier">uses</span><span class="synSpecial">:</span> EndBug/add-and-commit@v9 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">default_author</span><span class="synSpecial">:</span> github_actions <span class="synIdentifier">message</span><span class="synSpecial">:</span> <span class="synConstant">&quot;CI: ER diagram and markdown update triggered by actions&quot;</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Report commit on pull request <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ steps.check_diff.outputs.file_count <span class="synType">!=</span> <span class="synConstant">'0'</span> }} <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/github-script@v6 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">script</span><span class="synSpecial">:</span> | github.rest.issues.createComment({ <span class="synIdentifier">issue_number</span><span class="synSpecial">:</span> context.issue.number, <span class="synIdentifier">owner</span><span class="synSpecial">:</span> context.repo.owner, <span class="synIdentifier">repo</span><span class="synSpecial">:</span> context.repo.repo, <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synConstant">'CI: ER diagrams and markdown auto-committed by Actions 🤖'</span> }) </pre> <h4 id="docstblsyml">docs/tbls.yml</h4> <p>tblsのER図の自動生成ではデフォルトでコメントを出力しない設定となっています。</p> <p>コメントも出力されるように設定ファイルに記載しています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">er</span><span class="synSpecial">:</span> <span class="synComment"> # Add table/column comment to ER diagram</span> <span class="synComment"> # Default is false</span> <span class="synIdentifier">comment</span><span class="synSpecial">:</span> <span class="synConstant">true</span> </pre> <h2 id="GitHub-Actionsワークフローの解説">GitHub Actionsワークフローの解説</h2> <h3 id="ワークフローのトリガーを設定する">ワークフローのトリガーを設定する</h3> <p>プルリクエストが開かれた、同期された、再開された場合に、このGitHub Actionsがトリガーされます。また、db/schema/*.sql および db/tbls.yml の内容が変更されたときにのみ実行されます。</p> <p>マージ先がstaging、productionブランチの場合、発火しないようにしています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">pull_request</span><span class="synSpecial">:</span> <span class="synIdentifier">types</span><span class="synSpecial">:</span> <span class="synStatement">- </span>opened <span class="synStatement">- </span>synchronize <span class="synStatement">- </span>reopened <span class="synIdentifier">paths</span><span class="synSpecial">:</span> <span class="synStatement">- </span>db/schema/*.sql <span class="synStatement">- </span>db/tbls.yml <span class="synIdentifier">branches-ignore</span><span class="synSpecial">:</span> <span class="synStatement">- </span>production <span class="synStatement">- </span>staging </pre> <h3 id="パーミッションを設定する">パーミッションを設定する</h3> <ul> <li><code>contents: write</code>: リポジトリのコンテンツに対して書き込み権限を持ちます。これにより、github-actions[bot] がコミットおよびプッシュすることができます。</li> <li><code>pull-requests: write</code>: プルリクエストに対して書き込み権限を持ちます。これにより、プルリクエストにコメントを追加することができます。</li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synIdentifier">permissions</span><span class="synSpecial">:</span> <span class="synIdentifier">contents</span><span class="synSpecial">:</span> write <span class="synIdentifier">pull-requests</span><span class="synSpecial">:</span> write </pre> <h3 id="データベースを事前に起動する">データベースを事前に起動する</h3> <p>今回は、以下のように宣言し、PostgreSQLを起動しておきます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synIdentifier">services</span><span class="synSpecial">:</span> <span class="synIdentifier">postgres</span><span class="synSpecial">:</span> <span class="synIdentifier">image</span><span class="synSpecial">:</span> postgres:15-alpine <span class="synIdentifier">ports</span><span class="synSpecial">:</span> <span class="synStatement">- </span>5432:5432 <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">POSTGRES_HOST_AUTH_METHOD</span><span class="synSpecial">:</span> trust <span class="synIdentifier">POSTGRES_DB</span><span class="synSpecial">:</span> sampledbname <span class="synIdentifier">options</span><span class="synSpecial">:</span> &gt;- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries <span class="synConstant">5</span> </pre> <h3 id="環境変数を設定する">環境変数を設定する</h3> <p>プロセスに必要な環境変数が設定されています。</p> <p>データベース接続用のDSNやドキュメントのパスを指定しています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">TBLS_DSN</span><span class="synSpecial">:</span> <span class="synConstant">&quot;postgres://postgres:@127.0.0.1:5432/sampledbname?sslmode=disable&quot;</span> <span class="synIdentifier">TBLS_DOC_PATH</span><span class="synSpecial">:</span> <span class="synConstant">&quot;db/dbdoc&quot;</span> </pre> <h3 id="コードをクローンしてGitHubの設定をする">コードをクローンしてGitHubの設定をする</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">repository</span><span class="synSpecial">:</span> ${{ github.event.pull_request.head.repo.full_name }} <span class="synIdentifier">ref</span><span class="synSpecial">:</span> ${{ github.event.pull_request.head.ref }} </pre> <h3 id="マイグレーションを実行する">マイグレーションを実行する</h3> <ul> <li>Go言語をセットアップします。 <ul> <li>マイグレーションツールとして、psqldefを使用しています。</li> <li>psqldefはGo言語で作成されているため、Go言語をセットアップしておきます。</li> </ul> </li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-go@v3 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">go-version</span><span class="synSpecial">:</span> ^1.20 </pre> <ul> <li>マイグレーションを実行します。 <ul> <li>psqldef を go install でインストールします。</li> <li>その後にマイグレーションを実行します。</li> </ul> </li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Execute migration <span class="synIdentifier">run</span><span class="synSpecial">:</span> | go install github.com/k0kubun/sqldef/cmd/psqldef@latest psqldef --file schema.sql sampledbname <span class="synIdentifier">working-directory</span><span class="synSpecial">:</span> ./db/schema </pre> <h3 id="ER図関連のファイルを削除">ER図関連のファイルを削除</h3> <p>スキーマの変更でテーブルの削除が含まれる場合は、ファイルを削除していないとそのまま残り続けることとなります。そのため、ER図を自動生成する前にファイルを削除しておきます。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Remove dbdocs <span class="synIdentifier">run</span><span class="synSpecial">:</span> | rm -rf ./*.md ./*.svg <span class="synIdentifier">working-directory</span><span class="synSpecial">:</span> ./db/dbdoc </pre> <p>tblsには、<code>--rm-dist</code>というオプションが用意してあります。 今回、tblsには生成の責務に集中してもらうためオプションは使用せずに明示的に削除するようにしました。</p> <h3 id="tblsのセットアップと実行">tblsのセットアップと実行</h3> <ul> <li>tblsをセットアップする <ul> <li>tblsには、GitHub Actionsで簡単に使用できるためのActionが用意されています。</li> <li>この一文を記載することで tbls を使用できるようになります。</li> </ul> </li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> k1low/setup-tbls@v1 </pre> <p>参照: <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fk1low.hatenablog.com%2Fentry%2F2023%2F02%2F16%2F093315" title="tblsをセットアップするGitHub Actionとしてsetup-tbls(を作るツールとしてgh-setup)を作った - Copy/Cut/Paste/Hatena" 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://k1low.hatenablog.com/entry/2023/02/16/093315">k1low.hatenablog.com</a></cite></p> <ul> <li>tblsの実行し、ER図を生成する <ul> <li>tblsを実行する際、ER図にコメントを含めるためにtblsの設定ファイルを指定します。</li> </ul> </li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Run tbls for generate database document <span class="synIdentifier">run</span><span class="synSpecial">:</span> | tbls doc -c ./db/tbls.yml -f </pre> <h3 id="ER図に差分がある場合プルリクエストに対してその差分をコミットする">ER図に差分がある場合、プルリクエストに対してその差分をコミットする</h3> <ul> <li>差分ファイル数を取得する</li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Count uncommit files <span class="synIdentifier">id</span><span class="synSpecial">:</span> check_diff <span class="synIdentifier">run</span><span class="synSpecial">:</span> | git status --porcelain | wc -l file_count=$(git status --porcelain | wc -l) echo <span class="synConstant">&quot;file_count=$file_count&quot;</span> &gt;&gt; $GITHUB_OUTPUT <span class="synIdentifier">working-directory</span><span class="synSpecial">:</span> ./db </pre> <ul> <li>差分ファイルをコミットする <ul> <li>差分ファイルがある場合のみコミットされるように条件を設定しています。</li> <li>GitHub Actionで簡単にAdd と Commit ができる <code>EndBug/add-and-commit</code>を使用しました。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fmarketplace%2Factions%2Fadd-commit" title="Add &amp; Commit - GitHub Marketplace" 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/marketplace/actions/add-commit">github.com</a></cite></li> </ul> </li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Commit ER graph <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ steps.check_diff.outputs.file_count <span class="synType">!=</span> <span class="synConstant">'0'</span> }} <span class="synIdentifier">uses</span><span class="synSpecial">:</span> EndBug/add-and-commit@v9 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">default_author</span><span class="synSpecial">:</span> github_actions <span class="synIdentifier">message</span><span class="synSpecial">:</span> <span class="synConstant">&quot;CI: ER diagram and markdown update triggered by actions&quot;</span> </pre> <h3 id="プルリクエストにER図を更新した旨をコメントする">プルリクエストにER図を更新した旨をコメントする</h3> <p>こちらも同様に、差分ファイルがある場合のみプルリクエストにコメントするように条件を設定しています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Report commit on pull request <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ steps.check_diff.outputs.file_count <span class="synType">!=</span> <span class="synConstant">'0'</span> }} <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/github-script@v6 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">script</span><span class="synSpecial">:</span> | github.rest.issues.createComment({ <span class="synIdentifier">issue_number</span><span class="synSpecial">:</span> context.issue.number, <span class="synIdentifier">owner</span><span class="synSpecial">:</span> context.repo.owner, <span class="synIdentifier">repo</span><span class="synSpecial">:</span> context.repo.repo, <span class="synIdentifier">body</span><span class="synSpecial">:</span> <span class="synConstant">'CI: ER diagrams and markdown auto-committed by Actions 🤖'</span> }) </pre> <h2 id="まとめ">まとめ</h2> <p>ER図を自動生成してくれるGitHub Actionsの設定ファイルを紹介しました。</p> <p>今回は、紹介した設定はGoプロジェクトですが、マイグレーションの設定部分を変更することで Ruby on Railsのプロジェクトでも応用が効きます。</p> <p>GitHub Actionsとtblsの組み合わせによる自動化は、開発効率向上、品質保持、そして情報共有の容易化に貢献します。</p> <p>また、柔軟なカスタマイズ性を持つため、チームのニーズに応じて機能拡張や改善が容易に実現可能です。</p> <h3 id="参考記事">参考記事</h3> <p>大変参考になりました。ありがとうございます。</p> <p><a href="https://devblog.thebase.in/entry/auto_generated_er_graph_by_tbls_and_github_actions">tblsとGitHub Actionsを使ってDBマイグレーションを含むPRには自動更新したER図を追加する - BASEプロダクトチームブログ</a></p> shiroemons Cloud Buildの結果をSlackに通知する hatenablog://entry/4207112889942774120 2022-12-22T09:00:00+09:00 2023-05-02T16:26:17+09:00 この記事はトレタ Advent Calendar 2022の22日目の記事です。 はじめに こんにちは、3年連続で22日に記事を書くサーバーサイドエンジニアの @shiroemons です。 前回は、Cloud Buildでpsqldefを使用してCloud SQLにマイグレーションする方法を紹介しました。 tech.toreta.in Cloud Buildは便利ですが、結果を確認しに行かないといけないのが少々手間です。 そこで今回は、Cloud Buildの結果をSlackに通知する方法を紹介したいと思います。 今回のゴール 成功の場合の通知画面 成功ステータス(SUCCESS)と ✅ が… <p>この記事は<a href="https://qiita.com/advent-calendar/2022/toreta">トレタ Advent Calendar 2022</a>の22日目の記事です。</p> <h2 id="はじめに">はじめに</h2> <p>こんにちは、3年連続で22日に記事を書くサーバーサイドエンジニアの <a href="https://twitter.com/shiroemons">@shiroemons</a> です。</p> <p>前回は、Cloud Buildでpsqldefを使用してCloud SQLにマイグレーションする方法を紹介しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2022%2F12%2F15%2Fcloud-build-psqldef-cloud-sql" title="Cloud Buildからpsqldefを使用してCloud SQL for PostgreSQLにマイグレーションする - トレタ開発者ブログ" 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://tech.toreta.in/entry/2022/12/15/cloud-build-psqldef-cloud-sql">tech.toreta.in</a></cite></p> <p>Cloud Buildは便利ですが、結果を確認しに行かないといけないのが少々手間です。</p> <p>そこで今回は、Cloud Buildの結果をSlackに通知する方法を紹介したいと思います。</p> <h2 id="今回のゴール">今回のゴール</h2> <ul> <li><p>成功の場合の通知画面</p> <ul> <li>成功ステータス(SUCCESS)と ✅ が表示されている <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiroemons/20221206/20221206155504.png" width="1200" height="464" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> </ul> </li> <li><p>失敗の場合の通知画面</p> <ul> <li>成功以外のステータス(FAILUREなど)と ❌ が表示されている <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiroemons/20221206/20221206155537.png" width="1200" height="475" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></li> </ul> </li> </ul> <h2 id="Slackアプリ">Slackアプリ</h2> <h3 id="アプリを新規作成">アプリを新規作成</h3> <p>専用のSlackがあるわけではないので、以下より新規作成してください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fapi.slack.com%2Fapps" title="Slack API: Applications | Slack" 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://api.slack.com/apps">api.slack.com</a></cite></p> <p>アプリ名とアイコンは各自で設定してください。</p> <p>ここでは、アプリ名を「Cloud Build 結果通知」としています。</p> <p>アイコンは、<a href="https://cloud.google.com/icons?hl=ja">&#x30A2;&#x30FC;&#x30AD;&#x30C6;&#x30AF;&#x30C1;&#x30E3;&#x56F3;&#x7528;&#x306E;&#x30A2;&#x30A4;&#x30B3;&#x30F3;&#x306E;&#x30E9;&#x30A4;&#x30D6;&#x30E9;&#x30EA; - Google Cloud Platform</a>のSVGとPNGのアイコンのものを設定しています。</p> <h3 id="Webhook-URLを生成">Webhook URLを生成</h3> <ol> <li>作成したSlackアプリのIncoming Webhooksにアクセスする。</li> <li>[Add New Webhook to Workspace]ボタンをクリックする。</li> <li>通知するチャンネルを選択する。</li> <li>[許可する]ボタンをクリックする。</li> <li>Webhook URLが生成される。</li> </ol> <h2 id="Google-Cloud-側の作業">Google Cloud 側の作業</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloud.google.com%2Fbuild%2Fdocs%2Fconfiguring-notifications%2Fconfigure-slack%3Fhl%3Dja" title="Slack 通知を構成する  |  Cloud Build のドキュメント  |  Google Cloud" 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://cloud.google.com/build/docs/configuring-notifications/configure-slack?hl=ja">cloud.google.com</a></cite></p> <p>上記の資料を元に作業を行います。</p> <h3 id="必要なAPIを有効化">必要なAPIを有効化</h3> <p><a href="https://cloud.google.com/build/docs/configuring-notifications/configure-slack?hl=ja">始める前に</a> の[API を有効にする]ボタンから有効にできます。</p> <h3 id="Secret-Manager">Secret Manager</h3> <ul> <li>Webhook URLを保存します。 <ul> <li>ここでは、シークレット名を <code>cloudbuild_slack_notifier_webhook_url</code> で保存しました。</li> </ul> </li> </ul> <h3 id="IAM">IAM</h3> <ul> <li><code>&lt;プロジェクト番号&gt;-compute@developer.gserviceaccount.com</code> に対して、 <ul> <li>ロール <code>Secret Manager のシークレット アクセサー</code> を付与します。</li> <li>ロール <code>Storage オブジェクト閲覧者</code> を付与します。</li> </ul> </li> </ul> <p>Google Cloud側の作業は以上です。</p> <h2 id="ローカル環境での作業">ローカル環境での作業</h2> <p>macOSを対象として記載しております。</p> <h3 id="事前準備">事前準備</h3> <ol> <li><p>Google Cloud SDKをインストールする。</p> <pre><code class="`sh"> brew install --cask google-cloud-sdk # Homebrew のバージョンが 2.6 以前の場合 # brew cask install google-cloud-sdk </code></pre></li> <li><p>アカウント連携する。</p> <pre><code class="`sh"> gcloud auth login </code></pre></li> <li>gcloud configの設定する。 <ul> <li>対象プロジェクトに設定されているか確認します。</li> </ul> <pre><code class="`sh"> gcloud config list </code></pre></li> <li>プロジェクトを設定する。 <ul> <li>対象プロジェクトを設定します。</li> </ul> <pre><code class="`sh"> gcloud config set project &lt;プロジェクトID&gt; </code></pre></li> <li>デフォルトのリージョンを設定する。 <ul> <li>リージョンを<code>asia-northeast1</code>(東京)に設定します。</li> </ul> <pre><code class="`sh"> gcloud config set run/region asia-northeast1 </code></pre></li> </ol> <h3 id="Slack-Notifier-の-clone">Slack Notifier の clone</h3> <p>公式がGitHubで公開している <a href="https://github.com/GoogleCloudPlatform/cloud-build-notifiers">GoogleCloudPlatform/cloud-build-notifiers</a> をcloneします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>git clone git@github.com:GoogleCloudPlatform/cloud-build-notifiers.git </pre> <p>clone したディレクトリへ移動します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">cd</span> cloud-build-notifiers </pre> <h3 id="config-ファイルslackyamlの作成">config ファイル(slack.yaml)の作成</h3> <ul> <li><p>exampleファイルからコピーして作成します。</p> <pre><code class="``sh"> cp ./slack/slack.yaml.example ./slack/slack.yaml </code></pre></li> <li><p>configファイル(slack.yaml)をカスタマイズします。</p> <ul> <li>カスタマイズの差分は、こちらです。</li> </ul> </li> </ul> <pre class="code lang-diff" data-lang="diff" data-unlink><span class="synComment"># Copyright 2020 Google LLC</span> <span class="synComment">#</span> <span class="synComment"># Licensed under the Apache License, Version 2.0 (the &quot;License&quot;);</span> <span class="synComment"># you may not use this file except in compliance with the License.</span> <span class="synComment"># You may obtain a copy of the License at</span> <span class="synComment">#</span> <span class="synComment"># http://www.apache.org/licenses/LICENSE-2.0</span> <span class="synComment">#</span> <span class="synComment"># Unless required by applicable law or agreed to in writing, software</span> <span class="synComment"># distributed under the License is distributed on an &quot;AS IS&quot; BASIS,</span> <span class="synComment"># WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.</span> <span class="synComment"># See the License for the specific language governing permissions and</span> <span class="synComment"># limitations under the License.</span> apiVersion: cloud-build-notifiers/v1 kind: SlackNotifier metadata: <span class="synSpecial">- name: example-slack-notifier</span> <span class="synIdentifier">+ name: cloudbuild-slack-notifier</span> spec: notification: <span class="synSpecial">- filter: build.status == Build.Status.SUCCESS</span> <span class="synIdentifier">+ filter: build.status in [Build.Status.SUCCESS, Build.Status.FAILURE, Build.Status.INTERNAL_ERROR, Build.Status.TIMEOUT]</span> params: buildStatus: $(build.status) <span class="synIdentifier">+ buildTriggerName: $(build.substitutions['TRIGGER_NAME'])</span> <span class="synIdentifier">+ buildRepository: $(build.substitutions['REPO_NAME'])</span> <span class="synIdentifier">+ buildBranch: $(build.substitutions['BRANCH_NAME'])</span> <span class="synIdentifier">+ buildCommit: $(build.substitutions['SHORT_SHA'])</span> delivery: webhookUrl: secretRef: webhook-url template: type: golang <span class="synSpecial">- uri: gs://example-gcs-bucket/slack.json</span> <span class="synIdentifier">+ uri: gs://&lt;プロジェクトID&gt;-notifiers-config/slack.json</span> secrets: - name: webhook-url <span class="synSpecial">- value: projects/example-project/secrets/example-slack-notifier-webhook-url/versions/latest</span> <span class="synIdentifier">+ value: projects/&lt;プロジェクトID&gt;/secrets/&lt;シークレット名&gt;/versions/&lt;バージョン&gt;</span> </pre> <ul> <li>カスタマイズの内容は以下の通りです。 <ul> <li><code>metadata.name</code> <ul> <li><code>example</code> から <code>cloudbuild</code> に変更します。</li> </ul> </li> <li><code>spec.notification.filter</code> <ul> <li>デフォルトのままだと成功しか通知しないため、失敗やタイムアウトも通知するように変更します。</li> </ul> </li> <li><code>spec.notification.params</code> <ul> <li>Slack通知時に情報が足りないのため以下の情報を追加します。 <ul> <li>トリガー名、リポジトリ名、ブランチ名、短いコミットハッシュ</li> </ul> </li> </ul> </li> <li><code>spec.notification.template.uri</code> <ul> <li><strong>プロジェクトごとに変更が必要</strong></li> <li>Slack Notifierのデプロイ時に自動で配置されるパスに変更します。</li> <li><strong>※※※注意事項: ここの設定はSlack Notifier の デプロイ後に使用する値です。デプロイ時には使用されないことに注意※※※</strong></li> </ul> </li> <li><code>secrets.value</code> <ul> <li><strong>プロジェクトごとに変更が必要</strong></li> <li>WebhookURLを設定したSecret Managerのパスを設定します。</li> <li>バージョンは、基本的に <code>latest</code> のままで良いと思います。</li> </ul> </li> </ul> </li> </ul> <h3 id="Slackテンプレートファイルslackjson-をカスタマイズ">Slackテンプレートファイル(slack.json) をカスタマイズ</h3> <ul> <li>デフォルトのSlackテンプレートのままだと味気なく情報量も少ないのでカスタマイズします。</li> <li>以下の<strong>slack.json</strong>の内容を <code>./slack/slack.json</code> に上書きする。</li> </ul> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">section</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">mrkdwn</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: &quot;<span class="synConstant">Cloud Build build state *{{.Params.buildStatus}}*. {{ if eq .Params.buildStatus `SUCCESS` }}✅{{ else }}❌{{ end }}</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">section</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">mrkdwn</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: &quot;<span class="synConstant">*Trigger:* {{.Params.buildTriggerName}}</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">section</span>&quot;, &quot;<span class="synStatement">fields</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">mrkdwn</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: &quot;<span class="synConstant">*ProjectId:*</span><span class="synSpecial">\n</span><span class="synConstant">{{.Build.ProjectId}}</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">mrkdwn</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: &quot;<span class="synConstant">*Repository:*</span><span class="synSpecial">\n</span><span class="synConstant">{{.Params.buildRepository}}</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">mrkdwn</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: &quot;<span class="synConstant">*Branch:*</span><span class="synSpecial">\n</span><span class="synConstant">{{.Params.buildBranch}}</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">mrkdwn</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: &quot;<span class="synConstant">*Commit:*</span><span class="synSpecial">\n</span><span class="synConstant">{{.Params.buildCommit}}</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">divider</span>&quot; <span class="synSpecial">}</span>, <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">actions</span>&quot;, &quot;<span class="synStatement">elements</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">button</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">plain_text</span>&quot;, &quot;<span class="synStatement">text</span>&quot;: &quot;<span class="synConstant">View Build Logs</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">value</span>&quot;: &quot;<span class="synConstant">click_me_123</span>&quot;, &quot;<span class="synStatement">url</span>&quot;: &quot;<span class="synConstant">{{.Build.LogUrl}}</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span> </pre> <h3 id="Setup-シェルsetupshの修正">Setup シェル(setup.sh)の修正</h3> <ul> <li>Cloud Storageのロケーションを設定する。 <ul> <li>デフォルトのままだと、マルチリージョンの<code>us</code>が設定されます。</li> <li><code>upload_config</code> 関数を修正し、デュアルリージョンの<code>asia1</code>に設定しています。</li> </ul> </li> </ul> <pre class="code lang-diff" data-lang="diff" data-unlink>upload_config() { # We allow this `mb` command to error since we rely on the `cp` command hard- # erroring if there's an actual problem (since `mb` fails if the bucket # already exists). <span class="synSpecial">- gsutil mb &quot;${DESTINATION_BUCKET_URI}&quot;</span> <span class="synIdentifier">+ gsutil mb -l asia1 &quot;${DESTINATION_BUCKET_URI}&quot;</span> gsutil cp &quot;${SOURCE_CONFIG_PATH}&quot; &quot;${DESTINATION_CONFIG_PATH}&quot; || fail &quot;failed to copy config to GCS&quot; if [ ! -z &quot;${SOURCE_TEMPLATE_PATH}&quot; ]; then gsutil cp &quot;${SOURCE_TEMPLATE_PATH}&quot; &quot;${DESTINATION_TEMPLATE_PATH}&quot; || fail &quot;failed to copy template to GCS&quot; fi } </pre> <ul> <li>スケールできる最大数を設定する。 <ul> <li><code>deploy_notifier</code> 関数を修正し、最大1台までに設定しています。</li> </ul> </li> </ul> <pre class="code lang-diff" data-lang="diff" data-unlink>deploy_notifier() { gcloud run deploy &quot;${SERVICE_NAME}&quot; \ --image=&quot;${IMAGE_PATH}&quot; \ --no-allow-unauthenticated \ <span class="synIdentifier">+ --max-instances=1 \</span> --update-env-vars=&quot;CONFIG_PATH=${DESTINATION_CONFIG_PATH},PROJECT_ID=${PROJECT_ID}&quot; || fail &quot;failed to deploy notifier service -- check service logs for configuration error&quot; } </pre> <ul> <li>サブスクリプションの有効期限を無期限に設定する。</li> </ul> <pre class="code lang-diff" data-lang="diff" data-unlink>create_pubsub_subscription() { gcloud pubsub subscriptions create &quot;${SUBSCRIPTION_NAME}&quot; \ --topic=cloud-builds \ --push-endpoint=&quot;${SERVICE_URL}&quot; \ <span class="synSpecial">- --push-auth-service-account=&quot;${INVOKER_SA}&quot;</span> <span class="synIdentifier">+ --push-auth-service-account=&quot;${INVOKER_SA}&quot; \</span> <span class="synIdentifier">+ --expiration-period=&quot;never&quot;</span> } </pre> <h3 id="Slack-Notifier-の-デプロイ">Slack Notifier の デプロイ</h3> <p>すべての設定が完了したのでsetupシェルを実行します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>./setup.sh slack ./slack/slack.yaml <span class="synSpecial">-t</span> ./slack/slack.json </pre> <p>以下のように最後に出力されれば成功です。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>+ <span class="synStatement">echo</span><span class="synConstant"> </span><span class="synStatement">'</span><span class="synConstant">** NOTIFIER SETUP COMPLETE **</span><span class="synStatement">'</span> ** NOTIFIER SETUP COMPLETE ** </pre> <ul> <li>デプロイで実行される内容 <ol> <li>Cloud Storageに「<code>&lt;project id&gt;-notifiers-config</code>」を作成</li> <li>slack.yamlとslack.jsonを作成したCloud Storageにアップロード</li> <li>Cloud Runに「<code>slack-notifier</code>」をデプロイ</li> <li>必要なIAMの設定</li> <li>サービスアカウント「<code>Cloud Run Pub/Sub Invoker</code>」を作成</li> <li>必要なIAMの設定</li> <li>Cloud Pub/Subトピックに「<code>cloud-builds</code>」を作成</li> <li>Cloud Pub/Subサブスクリプションに「<code>slack-subscription</code>」を作成</li> </ol> </li> </ul> <p>失敗した場合は、ログやCloud Runのログを確認してください。</p> <p>2回目以降の実行の場合は、作成済みのものは <code>ERROR</code> と表示されますがとくに問題ありません。</p> <h2 id="動作確認">動作確認</h2> <p>Slack Notifierのデプロイ成功後は、実際にCloud Buildを動かし、動作確認を行いましょう。</p> <p>以下のような通知がSlackに届いていれば成功/完成です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiroemons/20221206/20221206155504.png" width="1200" height="464" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiroemons/20221206/20221206155537.png" width="1200" height="475" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="苦労したところ">苦労したところ</h2> <p>Google Cloudに公式ガイドがあるのは嬉しいですね。Slack通知ようのアプリもあるのもありがたいです。</p> <p>ただ既存のままだと情報量が少なくそっけなかったので、slack.jsonをカスタマイズして必要そうな情報も表示することができました。</p> <p>カスタマイズを行うにあたり参照に記載したリンクからCloud BuildやSlackについて調べながら進めました。</p> <p>苦労したところは、成功や失敗のステータスを絵文字(✅や❌)で表現するところです。</p> <pre class="code" data-lang="" data-unlink>&#34;Cloud Build build state *{{.Params.buildStatus}}*. {{ if eq .Params.buildStatus `SUCCESS` }}✅{{ else }}❌{{ end }}&#34;</pre> <p>Go Templateの記載で解決することができました。</p> <h2 id="さいごに">さいごに</h2> <p>今回は、Cloud Buildの結果をSlackに通知するための設定方法を紹介しました。</p> <p>トレタでは、Cloud Buildを使用しているプロジェクトに今回の設定方法を共有し、 Cloud Buildの結果がSlack通知で届くように設定しています。</p> <p>Cloud Buildの結果をSlack通知したいと悩んでいる方の助けになれば幸いです。</p> <p>なお、トレタではエンジニアの募集を全方位で行なっております。</p> <p>コロナ禍を乗り越えた飲食店の新しい姿を探求する仲間をお待ちしております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fengineer%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://corp.toreta.in/recruit/engineer/">corp.toreta.in</a></cite></p> <h2 id="参考">参考</h2> <ul> <li>Cloud Build <ul> <li><a href="https://cloud.google.com/build/docs/configuring-notifications/notifiers?hl=ja">https://cloud.google.com/build/docs/configuring-notifications/notifiers?hl=ja</a></li> <li><a href="https://cloud.google.com/build/docs/configuring-notifications/configure-slack?hl=ja">https://cloud.google.com/build/docs/configuring-notifications/configure-slack?hl=ja</a></li> <li><a href="https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds?hl=ja">https://cloud.google.com/build/docs/api/reference/rest/v1/projects.builds?hl=ja</a></li> <li><a href="https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values?hl=ja">https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values?hl=ja</a></li> <li><a href="https://github.com/GoogleCloudPlatform/cloud-build-notifiers">https://github.com/GoogleCloudPlatform/cloud-build-notifiers</a></li> </ul> </li> <li>Slack <ul> <li><a href="https://api.slack.com/apps">https://api.slack.com/apps</a></li> <li><a href="https://app.slack.com/block-kit-builder/T5LLAJ415">https://app.slack.com/block-kit-builder/T5LLAJ415</a></li> <li><a href="https://api.slack.com/reference/surfaces/formatting#basics">https://api.slack.com/reference/surfaces/formatting#basics</a></li> </ul> </li> </ul> <h2 id="過去の記事">過去の記事</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2022%2F12%2F08%2Fgig4-pcd" title="G.I.G プログラムの参加とProfessional Cloud Developerの受験記録 - トレタ開発者ブログ" 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://tech.toreta.in/entry/2022/12/08/gig4-pcd">tech.toreta.in</a></cite></p> shiroemons トレタ予約番を支える技術 hatenablog://entry/4207112889946426039 2022-12-19T16:22:14+09:00 2022-12-19T16:22:14+09:00 こんにちは、トレタ予約番事業で開発リーダーをしている北川です。 トレタ2022年アドベントカレンダー19日目の記事として、トレタ予約番サービスについて今まであまり公表していなかった裏側の仕組みについて紹介したいと思います。 トレタ予約番とは まずトレタ予約番というサービスの紹介です。 トレタ予約番は、飲食店の電話に自動応答ロボットが24時間応答し、予約の受付/変更/キャンセルを対応してくれるサービスです。 飲食店にとっては今まで営業時間外にかかってきて取りこぼしていた予約電話を受けられるようになったり、多忙な営業時間の電話応答はトレタ予約番に任せつつ、スタッフ対応が必要な電話だけトレタ予約番か… <p>こんにちは、トレタ予約番事業で開発リーダーをしている北川です。</p> <p><a href="https://qiita.com/advent-calendar/2022/toreta">トレタ2022年アドベントカレンダー19日目</a>の記事として、トレタ予約番サービスについて今まであまり公表していなかった裏側の仕組みについて紹介したいと思います。</p> <h2 id="トレタ予約番とは">トレタ予約番とは</h2> <p>まずトレタ予約番というサービスの紹介です。</p> <p>トレタ予約番は、飲食店の電話に自動応答ロボットが24時間応答し、予約の受付/変更/キャンセルを対応してくれるサービスです。</p> <p>飲食店にとっては今まで営業時間外にかかってきて取りこぼしていた予約電話を受けられるようになったり、多忙な営業時間の電話応答はトレタ予約番に任せつつ、スタッフ対応が必要な電話だけトレタ予約番からスタッフに繋ぐことで、業務負荷の軽減に役立てていただいています。</p> <p>自動応答の電話というと宅配便の受取時間変更の電話のようなIVRをイメージされるかと思いますが、大きな違いとしてはユーザーはプッシュボタンでの操作ではなく、音声で操作を行う点です。</p> <p>それを実現するために、音声認識や音声合成などの技術要素を色々と利用しているので、その仕組みについて紹介していきます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftoreta.in%2Fjp%2Ftoreta-yoyakuban%2F" title="トレタ予約番|飲食店向けAIによる予約電話サービスなら" 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://toreta.in/jp/toreta-yoyakuban/">toreta.in</a></cite></p> <p>具体的なトレタ予約番に電話したときの様子はこちらのサービスサイトに動画があるので、是非見て聞いてみてください。</p> <h2 id="会話の仕組み">会話の仕組み</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221219/20221219161834.png" width="1200" height="389" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="電話サービス">電話サービス</h3> <p>まず、電話のサービスにはAWSのAmazon Connectを利用しています。</p> <p>Amazon Connectはコールセンター向けの電話サービスです。</p> <p>電話を受電した際にどのコールスタッフに繋ぐかなど応答時のフローを柔軟に設定することができ、AWS Lambdaを使うことでプログラマブルな制御を入れることができるのも特徴です。</p> <p>Amazon ConnectにはCCPというコントロールパネルのSDKや、CCPと連携するための<a href="https://github.com/amazon-connect/amazon-connect-streams">Amazon Connect Streams</a>というライブラリが提供されています。これらを使い、電話の応答・切断・転送などをプログラムで制御します。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Faws.amazon.com%2Fjp%2Fconnect%2F" title="Amazon Connect(クラウドベースのコンタクトセンター)| AWS" 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://aws.amazon.com/jp/connect/">aws.amazon.com</a></cite></p> <h3 id="音声認識">音声認識</h3> <p>通話中の音声データから、音声認識を行いテキストデータに変換します。</p> <p>音声認識にはGCPのSpeech-to-Textを利用しています。Speech-to-TextのAPIに音声データを入力してほぼリアルタイムに音声認識結果のテキストデータを取得することができます。</p> <p>もちろん日本語に対応していますし、GCP以外の音声認識エンジンをいくつか精度検証を行いましたが現時点ではGCPが頭ひとつ抜けて精度がよい印象です。</p> <p>後述していますが、専用の学習モデルに切り替えられたりチューニングなどで音声認識の向上を行うことも可能です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloud.google.com%2Fspeech-to-text" title="Speech-to-Text: Automatic Speech Recognition  |  Google Cloud" 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://cloud.google.com/speech-to-text">cloud.google.com</a></cite></p> <h3 id="自然言語処理">自然言語処理</h3> <p>音声認識で取得したテキストデータは文字起こしされた文章なので、そこから意味を解釈して必要なデータを抽出するのが自然言語処理です。</p> <p>例えば、予約の人数について「2名です」という発話であれば「2」という数値データを抽出します。</p> <p>人によって「2名」や「2人」など語彙の揺れがあるので、自然言語処理によってそれらの揺れを吸収します。</p> <p>自然言語処理についてはトレタ予約番を共同開発しているユニロボット株式会社のunirobot cloudを使用しています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.unirobot.com%2Funirobot-cloud%2F" title="unirobot-cloud - ユニロボット 「いつかあたり前の日常」を提案し続ける" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.unirobot.com/unirobot-cloud/">www.unirobot.com</a></cite></p> <h3 id="会話シナリオ">会話シナリオ</h3> <p>予約電話の会話は基本的にシナリオがあり、大まかに以下のステップを踏みながら会話を進めます。</p> <ol> <li>日時の質問</li> <li>人数の質問</li> <li>連絡先の確認</li> <li>予約内容の確認</li> </ol> <p>日時の質問であればユーザーが日時を答えることが期待値であり、自然言語処理を行なった結果が日時として認識できれば次のステップに進みます。日時以外の内容であれば再度日時を言い直すように応答します。</p> <p>また、ユーザーの発話内容をサーバー側の処理を通すことでシステムの応答内容を分岐させる場合もあります。</p> <p>例えばユーザーが「23時」と答えた場合、日時としては正しいですが店舗の営業時間外であれば正しく日時として、正しくないことを伝え再度日時を質問します。</p> <p>最後に予約確認まで進めば、トレタの予約台帳のAPIに対して予約登録を行い、登録完了を伝えて通話終了となります。</p> <h3 id="音声合成">音声合成</h3> <p>シナリオに沿ってシステムの回答を音声合成サービスを通して音声データとして作成します。大体は決まった文言ではありますが、ユーザーの発話内容を復唱したりするため毎会話ごとに音声合成を行なっています。</p> <p>現在は株式会社エーアイのAITalkを利用しています。日本語がとても自然なのと、イントネーションや感情値など細かいチューニングが行えるので、明るく活発な印象にすることができています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.ai-j.jp%2F" title=" | | 音声合成ソフト、読み上げ、人工・電子音声の「株式会社 エーアイ(AI)」" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.ai-j.jp/">www.ai-j.jp</a></cite></p> <h2 id="サービス向上のとりくみ">サービス向上のとりくみ</h2> <p>基本的な動きとしては前述の通りですが、より自然に、よりユーザーにストレスがないように細かいチューニングを日々行なっています。</p> <p>サービスの質に直結する音声認識精度の向上に向けた取り組みについていくつか紹介します。</p> <h3 id="学習モデルのチューニング">学習モデルのチューニング</h3> <p>現在使用しているGCPのSpeechToTextには認識精度を向上させるためのチューニング方法がいくつか提供されています。</p> <p>例えば、音声文字変換モデルで電話の通話に最適化された <code>phone_call</code> モデルがあります。通話音声は音の帯域が狭く音質が悪いためこのモデルを利用するのは有効的です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcloud.google.com%2Fspeech-to-text%2Fdocs%2Ftranscription-model" title="Select a transcription model  |  Cloud Speech-to-Text Documentation  |  Google Cloud" 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://cloud.google.com/speech-to-text/docs/transcription-model">cloud.google.com</a></cite></p> <p>その他にも <code>PhraseSet</code> を設定することで任意の単語の精度を上げることができます。例えば「はい」「いいえ」の回答を要求する会話の部分において、「いいえ」を「家(いえ)」と誤認識してしまうことがありましたが、「はい」「いいえ」をフレーズ設定することで正しく認識する確率が上がりました。</p> <h3 id="意味を汲み取る">意味を汲み取る</h3> <p>正しく音声認識をしても文脈で意味が異なるケースがあります。</p> <p>例えば、予約の日時を聞いた時の回答が「7時」であった場合、予約の文脈であれば「午前7時」ではなく「午後7時(19時)」が正しいと考えるのが一般的です。</p> <p>厳密にはその店舗の営業時間を参照し、7時なのか19時なのかを判別し正しいと思われる方で音声認識の結果を補正します。</p> <p>他にも、例えば予約の日時を「7月(しちがつ)」と認識した場合、その時の日付が1月であれば「1月(いちがつ)」を「7月(しちがつ)」と誤認識してしまった可能性が高いです。</p> <p>「2月(にがつ)」「4月(しがつ)」などの様に、日時としては正しいが聞き間違い(誤認識)をしてしまうことは多々あります。 その場合はその時の日付や時間帯なども考慮して、音声認識した結果を改めるようにしています。</p> <p>もっと難しい判定としては「大丈夫」などのどちらにも捉えられるワードがあります。</p> <p>「この内容でよろしいですか?」「大丈夫です」の場合、おそらくYesと思われます。</p> <p>「店舗にお繋ぎしますか?」「大丈夫です」の場合、おそらくNoと思われます。</p> <p>このように曖昧な表現は各会話ごとにどう解釈させるかを定義として入れて対応しています。</p> <h1 id="さいごに">さいごに</h1> <p>トレタ予約番は正式ローンチして1年以上経つサービスですが、AIの業界は日進月歩の世界なのでその時その時でよりよい音声認識エンジンであったり合成音声エンジンを切り替えたり、多くの試行錯誤やチューニングを行なっています。</p> <p>いろいろと知見も溜まり新しい機能やシステムのリプレイスを考えたり、やりたいことはまだまだあるので興味を持たれたエンジニアの方は是非話を聞きにいらしてみてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fengineer%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://corp.toreta.in/recruit/engineer/">corp.toreta.in</a></cite></p> mkitagawa-312 Cloud Buildからpsqldefを使用してCloud SQL for PostgreSQLにマイグレーションする hatenablog://entry/4207112889943637665 2022-12-15T09:00:00+09:00 2022-12-23T10:46:55+09:00 この記事はトレタ Advent Calendar 2022の15日目の記事です。 はじめに こんにちは、サーバーサイドエンジニアの @shiroemons です。 前回の記事に書いた通り、認定資格を取得してからGoogle Cloudを頻繁に活用するようになりました。 tech.toreta.in 現在のプロジェクトでは、Cloud Buildを用いてdocker buildやCloud Runへのデプロイを行っています。 また、データベース(Cloud SQL)へのマイグレーションもCloud Buildから行っています。 今回は、Cloud Buildからpsqldefというマイグレーショ… <p>この記事は<a href="https://qiita.com/advent-calendar/2022/toreta">トレタ Advent Calendar 2022</a>の15日目の記事です。</p> <h2 id="はじめに">はじめに</h2> <p>こんにちは、サーバーサイドエンジニアの <a href="https://twitter.com/shiroemons">@shiroemons</a> です。</p> <p>前回の記事に書いた通り、認定資格を取得してからGoogle Cloudを頻繁に活用するようになりました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2022%2F12%2F08%2Fgig4-pcd" title="G.I.G プログラムの参加とProfessional Cloud Developerの受験記録 - トレタ開発者ブログ" 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://tech.toreta.in/entry/2022/12/08/gig4-pcd">tech.toreta.in</a></cite></p> <p>現在のプロジェクトでは、Cloud Buildを用いてdocker buildやCloud Runへのデプロイを行っています。</p> <p>また、データベース(Cloud SQL)へのマイグレーションもCloud Buildから行っています。</p> <p>今回は、Cloud Buildから<code>psqldef</code>というマイグレーションツールを使用して、Cloud SQL for PostgreSQLにマイグレーションする方法を紹介します。</p> <p>ただし、Cloud SQL for PostgreSQLなどの設定は完了している前提のため、省略しています。</p> <h2 id="psqldefとは">psqldefとは</h2> <p>SQLで羃等にDBスキーマ管理ができるツール「sqldef」のPostgreSQL用のツールです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fk0kubun%2Fsqldef" title="GitHub - k0kubun/sqldef: Idempotent schema management for MySQL, PostgreSQL, and more" 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/k0kubun/sqldef">github.com</a></cite></p> <p>PostgreSQL用の他に、MySQL用のmysqldefやSQLite3用のsqlite3defなども存在します。</p> <p>sqldefについての細かい説明は、ここでは省略します。</p> <h2 id="Cloud-Buildとは">Cloud Buildとは</h2> <p>Cloud Buildは、Google Cloud 上でビルドを実行するサービスです。</p> <p>Cloud Buildでは、ビルド構成ファイルと呼ばれる設定ファイルにビルドやデプロイの方法(指示)を記述します。記述した指示に基づいてタスクを実行し、ビルドやデプロイを行います。</p> <p>Cloud SQLへのマイグレーションで使用した構成ファイルの紹介と説明をします。</p> <h2 id="Cloud-Buildの構成ファイル">Cloud Buildの構成ファイル</h2> <p>Cloud SQLへのマイグレーションで使用したCloud Buildの構成ファイルはこちらになります。</p> <ul> <li>cloudbuild-migration.yaml</li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/wget <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64'</span> <span class="synStatement">- </span><span class="synConstant">'-O'</span> <span class="synStatement">- </span>./cloud_sql_proxy <span class="synIdentifier">id</span><span class="synSpecial">:</span> cloud_sql_proxy download <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/gcloud <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'-c'</span> <span class="synStatement">- </span>| chmod +x ./cloud_sql_proxy <span class="synType">&amp;&amp;</span> ./cloud_sql_proxy --version <span class="synIdentifier">id</span><span class="synSpecial">:</span> cloud_sql_proxy version <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> bash <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/wget <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span>&gt;- https://github.com/k0kubun/sqldef/releases/latest/download/psqldef_linux_amd64.tar.gz <span class="synIdentifier">id</span><span class="synSpecial">:</span> psqldef download <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/gcloud <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'-c'</span> <span class="synStatement">- </span>| tar -zxvf psqldef_linux_amd64.tar.gz <span class="synType">&amp;&amp;</span> ./psqldef --version <span class="synIdentifier">id</span><span class="synSpecial">:</span> psqldef version <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> bash <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/gcloud <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'-c'</span> <span class="synStatement">- </span>&gt; ./cloud_sql_proxy -instances=$_INSTANCE_CONNECTION_NAME=tcp:$_DATABASE_PORT &amp; sleep $_SLEEP_SEC; ./psqldef --dry-run --file $_SCHEMA_FILE --host $_DATABASE_HOST --port $_DATABASE_PORT --user $_DATABASE_USER --password $$DATABASE_PASS $_DATABASE_NAME <span class="synIdentifier">id</span><span class="synSpecial">:</span> psqldef dry-run <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> bash <span class="synIdentifier">secretEnv</span><span class="synSpecial">:</span> <span class="synStatement">- </span>DATABASE_PASS <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/gcloud <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'-c'</span> <span class="synStatement">- </span>&gt; ./cloud_sql_proxy -instances=$_INSTANCE_CONNECTION_NAME=tcp:$_DATABASE_PORT &amp; sleep $_SLEEP_SEC; ./psqldef --file $_SCHEMA_FILE --host $_DATABASE_HOST --port $_DATABASE_PORT --user $_DATABASE_USER --password $$DATABASE_PASS $_DATABASE_NAME <span class="synIdentifier">id</span><span class="synSpecial">:</span> psqldef execute <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> bash <span class="synIdentifier">secretEnv</span><span class="synSpecial">:</span> <span class="synStatement">- </span>DATABASE_PASS <span class="synIdentifier">substitutions</span><span class="synSpecial">:</span> <span class="synIdentifier">_SCHEMA_FILE</span><span class="synSpecial">:</span> ./schema.sql <span class="synIdentifier">_DATABASE_HOST</span><span class="synSpecial">:</span> 127.0.0.1 <span class="synIdentifier">_DATABASE_PORT</span><span class="synSpecial">:</span> <span class="synConstant">'5432'</span> <span class="synIdentifier">_DATABASE_NAME</span><span class="synSpecial">:</span> db_name <span class="synIdentifier">_DATABASE_USER</span><span class="synSpecial">:</span> db_user <span class="synIdentifier">_DATABASE_PASSWORD_KEY</span><span class="synSpecial">:</span> database_password <span class="synIdentifier">_INSTANCE_REGION</span><span class="synSpecial">:</span> asia-northeast1 <span class="synIdentifier">_INSTANCE_ID</span><span class="synSpecial">:</span> database <span class="synIdentifier">_INSTANCE_CONNECTION_NAME</span><span class="synSpecial">:</span> <span class="synConstant">'${PROJECT_ID}:${_INSTANCE_REGION}:${_INSTANCE_ID}'</span> <span class="synIdentifier">_SLEEP_SEC</span><span class="synSpecial">:</span> <span class="synConstant">'5'</span> <span class="synIdentifier">availableSecrets</span><span class="synSpecial">:</span> <span class="synIdentifier">secretManager</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">versionName</span><span class="synSpecial">:</span> &gt;- projects/$PROJECT_ID/secrets/${_DATABASE_PASSWORD_KEY}/versions/latest <span class="synIdentifier">env</span><span class="synSpecial">:</span> DATABASE_PASS </pre> <p>(代入変数部分を変更して動作することを確認しています。)</p> <h2 id="各ビルドステップの説明">各ビルドステップの説明</h2> <ul> <li>よく使用するフィールドについて簡単に説明します。 <ul> <li>name: クラウドビルダーの指定(Docker..etc)</li> <li>args: ビルダーに渡す引数のリスト</li> <li>id: ビルドステップに対して一意の識別子</li> <li>entrypoint: エントリポイントを指定 (bash etc)</li> <li>secretEnv: Cloud KMS暗号鍵を使用して暗号化された環境変数のリスト</li> </ul> </li> <li>詳しい説明は、 <a href="https://cloud.google.com/build/docs/build-config-file-schema?hl=ja#structure_of_a_build_config_file">ビルド構成ファイルの構造</a> を参照ください。</li> </ul> <h3 id="ステップ1-Cloud-SQL-Proxyのダウンロード">ステップ1: Cloud SQL Proxyのダウンロード</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/wget <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'https://dl.google.com/cloudsql/cloud_sql_proxy.linux.amd64'</span> <span class="synStatement">- </span><span class="synConstant">'-O'</span> <span class="synStatement">- </span>./cloud_sql_proxy <span class="synIdentifier">id</span><span class="synSpecial">:</span> cloud_sql_proxy download </pre> <ul> <li>Cloud SQLに接続する際、Cloud SQL Proxyを使用して接続します。 <ul> <li>Cloud SQL Proxy を使用するには、Cloud SQL Admin API を有効にする必要があります。</li> <li><a href="https://console.cloud.google.com/apis/api/sqladmin.googleapis.com/overview">こちら</a>から有効にできます。</li> </ul> </li> <li>Cloud SQL Proxyをwgetでダウンロードします。</li> </ul> <h3 id="ステップ2-Cloud-SQL-Proxyの実行権限付与とCloud-SQL-Proxyのバージョン確認">ステップ2: Cloud SQL Proxyの実行権限付与とCloud SQL Proxyのバージョン確認</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/gcloud <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'-c'</span> <span class="synStatement">- </span>| chmod +x ./cloud_sql_proxy <span class="synType">&amp;&amp;</span> ./cloud_sql_proxy --version <span class="synIdentifier">id</span><span class="synSpecial">:</span> cloud_sql_proxy version <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> bash </pre> <ul> <li>ダウンロードしたCloud SQL Proxyに実行権限を付与します。</li> <li>どのバージョンを使用したかわかるようにバージョンを確認します。</li> <li>ファイルの存在確認の意図もあります。</li> </ul> <h3 id="ステップ3-psqldef-の最新版をダウンロード">ステップ3: psqldef の最新版をダウンロード</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/wget <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span>&gt;- https://github.com/k0kubun/sqldef/releases/latest/download/psqldef_linux_amd64.tar.gz <span class="synIdentifier">id</span><span class="synSpecial">:</span> psqldef download </pre> <ul> <li>マイグレーションに必要なpsqldef(最新版)をwgetでダウンロードします。</li> </ul> <h3 id="ステップ4-psqldef-の解凍とpsqldef-のバージョン確認">ステップ4: psqldef の解凍とpsqldef のバージョン確認</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/gcloud <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'-c'</span> <span class="synStatement">- </span>| tar -zxvf psqldef_linux_amd64.tar.gz <span class="synType">&amp;&amp;</span> ./psqldef --version <span class="synIdentifier">id</span><span class="synSpecial">:</span> psqldef version <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> bash </pre> <ul> <li>ダウンロードしたpsqldefは圧縮されているため解凍します。</li> <li>どのバージョンを使用したかわかるようにバージョンを確認します。</li> <li>ファイルの存在確認の意図もあります。</li> </ul> <h3 id="ステップ5-psqldef-の-dry-run-実行">ステップ5: psqldef の dry-run 実行</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/gcloud <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'-c'</span> <span class="synStatement">- </span>&gt; ./cloud_sql_proxy -instances=$_INSTANCE_CONNECTION_NAME=tcp:$_DATABASE_PORT &amp; sleep $_SLEEP_SEC; ./psqldef --dry-run --file $_SCHEMA_FILE --host $_DATABASE_HOST --port $_DATABASE_PORT --user $_DATABASE_USER --password $$DATABASE_PASS $_DATABASE_NAME <span class="synIdentifier">id</span><span class="synSpecial">:</span> psqldef dry-run <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> bash <span class="synIdentifier">secretEnv</span><span class="synSpecial">:</span> <span class="synStatement">- </span>DATABASE_PASS </pre> <ul> <li>Cloud SQLに接続する場合、Cloud SQL Proxyを先に起動しておく必要があります。</li> <li>Cloud SQL Proxyの起動に一定時間かかるため <code>sleep</code> を入れています。</li> <li>設定に必要な値は、すべて代入変数で定義しておきます。 <ul> <li>代入変数を用いることでビルド構成ファイル自体の変更をしなくてもよくなります。</li> </ul> </li> <li>psqldefを本実行する前に、dry-runで実行してエラーがないことを確認しておきます。</li> <li>DBのパスワードは、Secret Managerに保存して使用しています。</li> </ul> <h3 id="ステップ6-psqldef-の-実行">ステップ6: psqldef の 実行</h3> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> gcr.io/cloud-builders/gcloud <span class="synIdentifier">args</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'-c'</span> <span class="synStatement">- </span>&gt; ./cloud_sql_proxy -instances=$_INSTANCE_CONNECTION_NAME=tcp:$_DATABASE_PORT &amp; sleep $_SLEEP_SEC; ./psqldef --file $_SCHEMA_FILE --host $_DATABASE_HOST --port $_DATABASE_PORT --user $_DATABASE_USER --password $$DATABASE_PASS $_DATABASE_NAME <span class="synIdentifier">id</span><span class="synSpecial">:</span> psqldef execute <span class="synIdentifier">entrypoint</span><span class="synSpecial">:</span> bash <span class="synIdentifier">secretEnv</span><span class="synSpecial">:</span> <span class="synStatement">- </span>DATABASE_PASS </pre> <p>内容は、dry-runとほぼ同じです。psqldefのdry-runオプションを外した内容です。</p> <h2 id="代入変数substitutionsの説明">代入変数(substitutions)の説明</h2> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">substitutions</span><span class="synSpecial">:</span> <span class="synIdentifier">_SCHEMA_FILE</span><span class="synSpecial">:</span> ./schema.sql <span class="synIdentifier">_DATABASE_HOST</span><span class="synSpecial">:</span> 127.0.0.1 <span class="synIdentifier">_DATABASE_PORT</span><span class="synSpecial">:</span> <span class="synConstant">'5432'</span> <span class="synIdentifier">_DATABASE_NAME</span><span class="synSpecial">:</span> db_name <span class="synIdentifier">_DATABASE_USER</span><span class="synSpecial">:</span> db_user <span class="synIdentifier">_DATABASE_PASSWORD_KEY</span><span class="synSpecial">:</span> database_password <span class="synIdentifier">_INSTANCE_REGION</span><span class="synSpecial">:</span> asia-northeast1 <span class="synIdentifier">_INSTANCE_ID</span><span class="synSpecial">:</span> database <span class="synIdentifier">_INSTANCE_CONNECTION_NAME</span><span class="synSpecial">:</span> <span class="synConstant">'${PROJECT_ID}:${_INSTANCE_REGION}:${_INSTANCE_ID}'</span> <span class="synIdentifier">_SLEEP_SEC</span><span class="synSpecial">:</span> <span class="synConstant">'5'</span> </pre> <table> <thead> <tr> <th>代入変数名 </th> <th> 説明 </th> </tr> </thead> <tbody> <tr> <td> <code>_SCHEMA_FILE</code> </td> <td> psqldef でマイグレーションするスキーマファイル </td> </tr> <tr> <td> <code>_DATABASE_HOST</code> </td> <td> DBのホスト </td> </tr> <tr> <td> <code>_DATABASE_PORT</code> </td> <td> DBのポート </td> </tr> <tr> <td> <code>_DATABASE_NAME</code> </td> <td> DB名 </td> </tr> <tr> <td> <code>_DATABASE_USER</code> </td> <td> DBのユーザー名 </td> </tr> <tr> <td> <code>_DATABASE_PASSWORD_KEY</code> </td> <td> DBのパスワードを Secret Manager に保存した際のシークレット名 </td> </tr> <tr> <td> <code>_INSTANCE_REGION</code> </td> <td> Cloud SQLのインスタンスのリージョン </td> </tr> <tr> <td> <code>_INSTANCE_ID</code> </td> <td> Cloud SQLのインスタンスID </td> </tr> <tr> <td> <code>_INSTANCE_CONNECTION_NAME</code> </td> <td> Cloud SQLの接続名(環境変数や代入変数で値を作成する) </td> </tr> <tr> <td> <code>_SLEEP_SEC</code> </td> <td> Cloud SQL Proxyの起動を待つスリープ(秒数を指定する) </td> </tr> </tbody> </table> <p>数値の指定の場合、クォートが必要です。そのため、DBのポートやスリープの秒数にシングルクォートを付けています。</p> <h2 id="availableSecretsの説明">availableSecretsの説明</h2> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">availableSecrets</span><span class="synSpecial">:</span> <span class="synIdentifier">secretManager</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">versionName</span><span class="synSpecial">:</span> &gt;- projects/$PROJECT_ID/secrets/${_DATABASE_PASSWORD_KEY}/versions/latest <span class="synIdentifier">env</span><span class="synSpecial">:</span> DATABASE_PASS </pre> <ul> <li>availableSecretsは、Cloud BuildでSecret Managerのシークレットを使用する時のフィールドです。 <ul> <li>Secret Managerの詳しい使い方は、<a href="https://cloud.google.com/build/docs/securing-builds/use-secrets?hl=ja">Secret Manager のシークレットの使用</a> を参照ください。</li> </ul> </li> </ul> <p>DBのパスワードを、Secret Managerに保存しており、そこから最新の設定内容を取得するようにしています。</p> <h2 id="Cloud-Build-実行に必要なロール">Cloud Build 実行に必要なロール</h2> <ul> <li>今回のCloud Build実行には以下の2つのロールが必要です。 <ul> <li><code>Cloud SQL クライアント</code></li> <li><code>Secret Manager のシークレット アクセサー</code></li> </ul> </li> </ul> <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/s/shiroemons/20221214/20221214111914.png" alt="" width="1200" height="841" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実行ログ</figcaption></figure></p> <h2 id="振り返り">振り返り</h2> <p>Cloud Buildでsqldefを用いてマイグレーションする方法をネットで探しても出てこず苦労しました。</p> <p>Cloud SQL Proxyとpsqldefを<code>waitFor</code>を使用して分けて実行させてみたりと試行錯誤の連続でした。</p> <p>Cloud BuildでCloud SQL Proxyを用いてCloud SQLへの接続がどうしてもうまく行かず、Google Cloudのサポートも利用しました。</p> <p>Cloud SQL Proxyの起動には、少し時間を要するためスリープを入れることで接続できない問題は解決しました。</p> <h2 id="さいごに">さいごに</h2> <p>この記事が、Cloud Buildからpsqldefもしくはmysqldefを使用してマイグレーションを検討している方の助けになれば幸いです。</p> <p>次回の <a href="https://qiita.com/advent-calendar/2022/toreta">トレタ Advent Calendar 2022</a> の22日目は、Cloud Buildの結果をSlackに通知する方法を紹介したいと思います。</p> <p>トレタではエンジニアの募集を全方位で行なっております。</p> <p>コロナ禍を乗り越えた飲食店の新しい姿を探求する仲間をお待ちしております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fengineer%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://corp.toreta.in/recruit/engineer/">corp.toreta.in</a></cite></p> shiroemons G.I.G プログラムの参加とProfessional Cloud Developerの受験記録 hatenablog://entry/4207112889942546355 2022-12-08T09:00:00+09:00 2022-12-23T10:47:21+09:00 この記事はトレタ Advent Calendar 2022の8日目の記事です。 はじめに こんにちは、サーバーサイドエンジニアの @shiroemons です。 今回は Google Cloud主催の G.I.G プログラム という招待制の特別トレーニングプログラムに参加し、Professional Cloud Developer認定試験を受験しました。 G.I.G プログラムの内容から受験の体験について記そうと思います。 G.I.G プログラムについて G.I.G プログラムとは? G.I.G. は Google Cloud Innovators Gym の略称です。 Google Clou… <p>この記事は<a href="https://qiita.com/advent-calendar/2022/toreta">トレタ Advent Calendar 2022</a>の8日目の記事です。</p> <h2 id="はじめに">はじめに</h2> <p>こんにちは、サーバーサイドエンジニアの <a href="https://twitter.com/shiroemons">@shiroemons</a> です。</p> <p>今回は Google Cloud主催の G.I.G プログラム という招待制の特別トレーニングプログラムに参加し、Professional Cloud Developer認定試験を受験しました。</p> <p>G.I.G プログラムの内容から受験の体験について記そうと思います。</p> <h2 id="GIG-プログラムについて">G.I.G プログラムについて</h2> <h3 id="GIG-プログラムとは">G.I.G プログラムとは?</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiroemons/20221205/20221205175538.png" width="1200" height="480" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>G.I.G. は <strong>G</strong>oogle Cloud <strong>I</strong>nnovators <strong>G</strong>ym の略称です。 Google Cloud様が主催する、各業界をリードするエンジニアに向けた、Google Cloud Platform の特別招待制トレーニングプログラムです。</p> <h3 id="対象認定資格">対象認定資格</h3> <p>G.I.G プログラムで対象となるのは、以下の3つの認定資格になります。</p> <ul> <li><a href="https://cloud.google.com/certification/cloud-architect?hl=ja">Professional Cloud Architect</a></li> <li><a href="https://cloud.google.com/certification/cloud-developer?hl=ja">Professional Cloud Developer</a></li> <li><a href="https://cloud.google.com/certification/guides/data-engineer?hl=ja">Professional Data Engineer</a></li> </ul> <p>私は、 Professional Cloud Developer を選択しました。</p> <p>Professional Cloud Developerは、Google Cloud (旧称 GCP) におけるアプリケーション開発者向けの認定資格です。</p> <h3 id="プログラム内容">プログラム内容</h3> <ul> <li>全3回のGoogle Cloud のエンジニアによるセッション</li> <li>Coursera を使用した学習コースの無料提供</li> <li>Google Spaces を利用したGoogle Cloud エンジニアによる学習サポート</li> <li>Google Cloud 認定資格取得のサポート</li> </ul> <h3 id="プログラムの修了条件">プログラムの修了条件</h3> <ul> <li>特別セッションの受講</li> <li>Coursera 規定コース5つ以上の受講完了</li> <li>Google Cloud 認定資格の合格報告</li> </ul> <h3 id="セッション">セッション</h3> <p>セッションは、全てGoogle Meetでのオンライン開催でした。</p> <ul> <li>特別セッション</li> <li>補習セッション</li> </ul> <h3 id="Coursera">Coursera</h3> <p><a href="https://jp.coursera.org/">Coursera</a>とは、オンライン学習プラットフォームです。</p> <p>Google Cloud が提供する Coursera の全コースを無料で受講できました。</p> <p>取得する資格に応じて5つ以上のコースを受講する必要がありました。</p> <p>各コースは、解説動画を視聴し、Qwiklabs<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup> と連携したハンズオンを行い、理解度チェックテストを受けるのサイクルでした。 各コースは、ほぼ日本語対応がされていましたが、解説動画の音声は英語で日本語は字幕でした。字幕は別途、テキスト化されて助かりました。</p> <h2 id="試験対策">試験対策</h2> <p>G.I.G プログラムのカリキュラムをこなすだけで合格できるものでもないため、自己学習は必須です。</p> <ul> <li>Coursera <ul> <li>Google Cloudの基礎については、動画や字幕テキストで確認</li> <li>Qwiklabsでの実践の理解</li> </ul> </li> <li><a href="https://cloud.google.com/certification/guides/cloud-developer?hl=ja">Professional Cloud Developer 認定試験ガイド</a> <ul> <li>大まかな出題範囲の確認</li> </ul> </li> <li><a href="https://docs.google.com/forms/d/e/1FAIpQLSc_67KaPnNwQrLZ7kuhw-aubz7gMAwY6DQwRJYcW0qlG-iajA/viewform?hl=ja">Professional Cloud Developer 模擬試験</a> <ul> <li>模擬試験で試験の雰囲気を掴む</li> <li>理解するまで行う</li> </ul> </li> <li><a href="https://blog.g-gen.co.jp/entry/professional-cloud-developer">Professional Cloud Developer試験対策マニュアル</a> <ul> <li>出題範囲と傾向を熟読</li> </ul> </li> <li>書籍 <ul> <li><a href="https://gihyo.jp/book/2021/978-4-297-12301-7">図解即戦力 Google Cloudのしくみと技術がこれ1冊でしっかりわかる教科書</a> <ul> <li>Courseraや試験対策マニュアルで理解できなかった部分を補いました</li> <li>主に、IAMやKubernetesなど</li> </ul> </li> </ul> </li> </ul> <h2 id="認定試験について">認定試験について</h2> <h3 id="試験の実施方法">試験の実施方法</h3> <p>試験は、以下のどちらかを選択することができます。</p> <ul> <li>遠隔地から<strong>オンライン監視試験</strong></li> <li>テストセンターで行う<strong>オンサイト監視試験</strong></li> </ul> <p>私は、試験会場に行くのが面倒と思い、オンライン監視試験を選択し、試験の予約をしました。</p> <h3 id="試験当日2022年7月6日">試験当日(2022年7月6日)</h3> <p>試験日が近づくにつれ、試験環境が自宅では適さないことがわかり、試験当日はオフィス出社し会議室を借りて受験しました。 (懇親会で聞いた話ですが、自宅のお風呂場で受験した方もいたそうです。)</p> <p>オンライン監視試験は受験専用のアプリケーション(ブラウザー)を手元のマシンにインストールして行います。 不正防止のため、モニター出力無効化、専用アプリ以外は表示できないなど制限される仕様となっていました。</p> <p>試験予定時間の10分前くらいに開始ボタンが表示されました。</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/s/shiroemons/20221205/20221205175133.png" width="578" height="238" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>試験開始前</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="試験開始10分前に開始ボタンが表示"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/shiroemons/20221205/20221205175210.png" width="646" height="262" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>試験開始10分前に開始ボタンが表示</figcaption></figure></p> <p>開始ボタンをクリックすると専用アプリが立ち上がりました。 しばらくすると、画面が更新されチャットベースでの英語でのコミュニケーションが始まりました。</p> <p>普段ならDeepL翻訳など使って理解できるけど、不正防止のため専用アプリが立ち上がっているため翻訳サイトが使えなかったためあたふたしました...</p> <p>途中からGoogle翻訳されたような日本語になりました。</p> <p>以下の確認がありました。</p> <ul> <li>身分証明書の提示</li> <li>試験環境の確認、Webカメラで部屋を映しながら確認</li> <li>質問事項の質疑応答 <ul> <li>部屋のドアはどこにつながっているのか</li> <li>携帯はどこにあるのか</li> </ul> </li> </ul> <p>など指示があるのでそれに従います。</p> <p>無事に確認が取れたようで試験開始となりました。</p> <p>試験時間は 120 分、問題数は 60 問</p> <p>1問1問丁寧に問題を確認し進めていきました。すぐにわからない問題は1度スキップして最後まで進みました。</p> <p>最後まで解いた後に、スキップした問題を解きつつ、すでに回答した問題の見直しを行いました。</p> <h3 id="試験結果">試験結果</h3> <p>時間になり試験が終わると...</p> <p>「合格」!!!!</p> <p>7月14日に無事デジタル認定証が届きました🎉</p> <h2 id="認定試験後">認定試験後</h2> <p>G.I.G. プログラム事務局に認定資格の合格報告が必要だったため、デジタル認定証が届くまでドキドキでしたが無事に合格報告できました。</p> <p>その後、G.I.G. プログラム 第4期オンライン修了式と懇親会に参加しました。</p> <p>無事に G.I.G プログラムを修了することができました。</p> <h2 id="振り返りまとめ">振り返り・まとめ</h2> <ul> <li>特別セッション <ul> <li>Google Cloudの社員の方が行ってくれたため、基本的なことから最新情報まで教えていただき、非常に貴重な体験でした。</li> <li>ハンズオンでも実際のGoogle Cloudをさわって動くところが学べたので良かったです。</li> </ul> </li> <li>Coursera <ul> <li>Qwiklabsで実際のGoogle Cloudの環境にふれられたのが良かったです。</li> <li>もともと英語のコンテンツを日本語化したものでしたので、解説動画は英語音声だったので、なかなか頭に入らず苦労しました...</li> </ul> </li> <li>試験対策マニュアル <ul> <li>的確すぎた。ありがとう。</li> </ul> </li> <li>書籍 <ul> <li>あの1冊でGoogle Cloudの基本的な部分は網羅されていたので学びが大きかった。</li> </ul> </li> <li>試験 <ul> <li>オンライン試験は大変だったので、次受けるときはオフサイト試験にします。</li> </ul> </li> </ul> <p>無事に Professional Cloud Developer に合格できてよかった。</p> <p>無事に G.I.G. プログラム 修了できてよかった。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.credential.net%2F75860e3a-b166-4be2-86a1-1c85c64203f9%3Fkey%3D829c55169385ec9d4d6a739d8dcdf8b568c321fa6a05d5470253a32fac8aea67%26record_view%3Dtrue" title="Professional Cloud Developer • 悟史 森田 • Google Cloud" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.credential.net/75860e3a-b166-4be2-86a1-1c85c64203f9?key=829c55169385ec9d4d6a739d8dcdf8b568c321fa6a05d5470253a32fac8aea67&record_view=true">www.credential.net</a></cite></p> <h2 id="さいごに">さいごに</h2> <p>Professional Cloud Developer認定資格を取得したあとは、Google Cloud をさわることが増え、Cloud Build・Cloud Run・Cloud SQLを活用して開発を進めています。</p> <p>スムーズに開発が進めることができているのも、G.I.G プログラムに参加したことが大きく影響しています。招待していただき、本当にありがとうございました!</p> <p><a href="https://qiita.com/advent-calendar/2022/toreta">トレタ Advent Calendar 2022</a>の15日目は、Cloud Buildについてなにか書ければなと思っています。</p> <p>なお、トレタではエンジニアの募集を全方位で行なっております。</p> <p>コロナ禍を乗り越えた飲食店の新しい姿を探求する仲間をお待ちしております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fengineer%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://corp.toreta.in/recruit/engineer/">corp.toreta.in</a></cite></p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> Qwiklabsとは、Google Cloud のラボ環境を一時的に払い出してくれて、ハンズオンができるサービスです。<a href="#fnref:1" rev="footnote">&#8617;</a></li> </ol> </div> shiroemons 飲食店モバイルオーダー・トレタO/Xの開発反省会 hatenablog://entry/4207112889942435316 2022-12-05T10:21:53+09:00 2022-12-05T10:21:53+09:00 こんにちは、トレタのモバイルオーダー事業でプロダクトマネージャーを行なっている北川です。 こちらはトレタアドベントカレンダー2日目の記事です。この記事では2022年の振り返りとして、今年の前半に取り組んだモバイルオーダー・トレタO/Xでの決済トラブルについてどう対応していったのかを記そうと思います。 toreta.in 決済でのトラブルというのは基本的に起こしてはならないものであり、それを記事にすることは憚られましたが、自分が苦闘しているなかで同様に決済への試みをしている記事を大いに参考にさせていただいたので、自分も誰かの助けになればと思いこの記事を公開します。 あらためて、導入店の飲食店様と… <p>こんにちは、トレタのモバイルオーダー事業でプロダクトマネージャーを行なっている北川です。</p> <p>こちらはトレタアドベントカレンダー2日目の記事です。この記事では2022年の振り返りとして、今年の前半に取り組んだモバイルオーダー・トレタO/Xでの決済トラブルについてどう対応していったのかを記そうと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftoreta.in%2Ftoreta-ox%2F" title="トレタO/X|飲食店向けのモバイルオーダーサービスなら" 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://toreta.in/toreta-ox/">toreta.in</a></cite></p> <p>決済でのトラブルというのは基本的に起こしてはならないものであり、それを記事にすることは憚られましたが、自分が苦闘しているなかで同様に<a href="https://engineering.mercari.com/blog/entry/20220220-db66c76db7">決済への試みをしている記事</a>を大いに参考にさせていただいたので、自分も誰かの助けになればと思いこの記事を公開します。</p> <p>あらためて、導入店の飲食店様と来店してご利用いただいていたお客様には、多大なご迷惑をおかけしたことをこの場を借りてお詫び申し上げます。</p> <h2 id="トレタOXの決済の概要">トレタO/Xの決済の概要</h2> <p>まずはトレタO/Xでの決済について説明します。 トレタO/Xでは店員を介さずにユーザーが自分のスマホから飲食代金を決済することが可能です。 ユーザーは飲食後にトレタO/Xでお会計画面を開き、支払い方法として、<strong>オンライン決済</strong>か<strong>オフライン決済</strong>かを選択することができます。</p> <p>オフライン決済は従来の決済のように、店員を呼び出しレジでの会計となります。 オンライン決済であれば、クレジットカード情報を入力して決済を完了すればそのまま退店することができます。</p> <p>会計待ちなどの煩わしさが減り、店員と来店者の両者の手間が省かれる体験となっています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205095242.png" width="1200" height="539" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="多重決済の問題">多重決済の問題</h2> <p>運用が少しずつ軌道にのりサーバーでの処理リクエスト数が日に日に増えていく中で、お会計が重複して行われてしまう多重決済の問題が発生しました。</p> <p>調査したところ、多重決済が発生するケースには2パターンがありました。</p> <ol> <li>決済が途中で失敗したケース</li> <li>ひとつのお会計に対して複数の決済が同時に行われたケース</li> </ol> <p>まず1つ目の<strong>決済が途中で失敗したケース</strong>についてですが、これはシステムの構成が関わっています。</p> <p>トレタO/Xのバックエンドはマイクロサービスの構成となっており、決済においては「注文サービス」と「決済サービス」を跨いだ処理となります。また、それらの処理をオーケストレーションするAPIのGatewayサーバーがあります。</p> <p>決済の処理としては、</p> <ol> <li>オーダーアプリからGatewayサーバーへ決済のリクエストを送る</li> <li>Gatewayサーバーは、注文サービスへ会計情報を問い合わせる</li> <li>Gatewayサーバーは、取得した会計情報の金額で決済サービスへ決済処理をリクエストする</li> <li>決済サービスは、決済プラットフォームのStripeを利用しており、Stripeへ決済をリクエストする</li> <li>決済が完了したらGatewayサーバーは、注文サービスに該当の注文に対しての支払いレコードを登録し、会計を支払い済みのステータスに更新する</li> </ol> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205095448.png" width="984" height="192" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>さて、今回発生したトラブルでは決済サービスからレスポンスが返ったあとに注文サービスへの書き込みでエラーが発生していました。</p> <p>注文や決済などが重なりリクエストが集中する時間帯においてサーバーへの負荷が上がり、一部のリクエストでタイムアウトが発生したためです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205095532.png" width="987" height="203" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>決済サービスへの書き込みに失敗するとユーザー側にはエラーの表示がされますが、決済処理をバックオフができないため、決済サービスでの決済は完了されてたままに注文サービスでの会計は未会計のステータスのまま、というデータの不整合が起きた状態となります。</p> <p>そして会計が未会計のステータスなので、ユーザーから支払いを再試行することが可能であり二重の支払いが行われてしまいます。</p> <h2 id="リトライと冪等性">リトライと冪等性</h2> <p>これを解決するには、<strong>エラーとなった箇所でのリトライ</strong>をすればよいと考えられます。</p> <p>ただし、サーバーからタイムアウトのレスポンスが返ってきた場合にはその処理の結果が成功/失敗かがわからないため、単純にリトライしてしまうと1つの決済に対してリトライ回数分の支払いレコードが登録されてしまうことになります。</p> <p>そのため注文サービスの支払いレコード登録APIには<strong>冪等性</strong>の担保が必要となります。</p> <p>冪等性対応の仕様としては、<a href="https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header">The Idempotency-Key HTTP Header Fielddraft-ietf-httpapi-idempotency-key-header-02</a>を基に一部簡略化して実装しました。</p> <ul> <li>HTTPリクエストのリクエストヘッダーにIdempotency-Keyを設定する</li> <li>過去に同一のIdempotency-Keyの値でのリクエストがあれば、処理は行わずにステータスコードを409(Conflicted)でレスポンスを返す</li> </ul> <p>また、StripeのAPIにも同様に<a href="https://stripe.com/docs/api/idempotent_requests">冪等キーを付与する仕組み</a>があるので決済サーバー側もこちらを用いて冪等対応をします。</p> <p>では<strong>冪等キー</strong>には何を使用するのが妥当でしょうか。</p> <p>注文をとりまとめた会計レコードがもつ会計IDがあるので、一見すると会計IDを使うと会計に対して一回以上支払いができないように制限でれるように考えられます。</p> <p>しかし、支払いは会計に対して本当に一度だけでしょうか。</p> <p>オンライン決済やオフライン決済が複合して行われることを想定すると、会計に対して複数回の支払いを行うケースはいくつか考えられます。</p> <ul> <li>クーポン(金券)で一部を払い、残りをクレジットカードで払う</li> <li>クレジットカードで一部を払い、残りを現金で払う</li> <li>グループ内で割り勘し、それぞれがクレジットカードで払う※</li> </ul> <p>※ 現在は機能として未実装</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205095750.png" width="931" height="240" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>会計に対して支払いは一回とは限らず、複数回行えるとした方がよさそうです。 そのため冪等キーは支払いに対して毎回ユニークなキー(UUIDなど)を発行することになりました。</p> <h2 id="同時支払いの問題">同時支払いの問題</h2> <p>次に、2つめの問題である<strong>同時支払い</strong>について考えていきます。</p> <p>同時支払いはいわゆるECサイトなどでの決済では起きづらいですが、複数人の会計がひとつにまとめられている飲食店での決済においては発生する頻度が高まります。</p> <p>同時支払いが起こるシチュエーションは主に2パターンあります。</p> <h3 id="複数のユーザーが同時に払うケース">複数のユーザーが同時に払うケース</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205095859.png" width="359" height="254" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>飲食店における会計はグループ内で共有しているため、グループ内の誰でも会計を行うことが可能です。 そのため一人が支払いを開始して完了する前に別の人が支払いを開始してしまうと、両者が決済してしまうことにます。</p> <p>なお、前述の通り1会計につき1支払いとは限らないので支払いを1回だけに絞ることはできません。</p> <h3 id="オンライン会計とオフライン会計が同時に行われてしまうケース">オンライン会計とオフライン会計が同時に行われてしまうケース</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205095936.png" width="322" height="252" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>複数人の同時支払いと同様に、オンライン会計とレジでの現金払いが同時に行われてしまうケースも可能性としてあります。</p> <p>オフライン決済だと現金以外にクーポン利用もあり、クーポンであれば過払いも許容されます。(例. 1,000円のお会計に3,000円のクーポンで支払う)</p> <h2 id="注文の排他制御">注文の排他制御</h2> <p>この問題を解決するには、支払いに対しての<strong>排他制御</strong>が考えられます。</p> <p>誰かが支払い行為を開始した時点で支払い枠にロックをかけ、他の支払いは通さないようにさせます。他の人がロックを取得している状態で新しくロックの取得をしようとすると失敗するため、支払いは常に1つずつ処理されることになります。</p> <p>排他制御を加えた決済の処理としては、</p> <ol> <li>オーダーアプリからGatewayサーバーへ決済のリクエストを送る</li> <li>Gatewayサーバーは、注文サービスへ会計情報を問い合わせ、<strong>ロックを取得する</strong></li> <li>Gatewayサーバーは、取得した会計情報の金額で決済サービスへ決済処理をリクエストする</li> <li>決済サービスは、Stripeへ決済をリクエストする</li> <li>決済が完了したらGatewayサーバーは、注文サービスに該当の注文に対しての支払いレコードを登録し、会計を支払い済みのステータスに更新する</li> </ol> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205100046.png" width="1012" height="229" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="対応方針のまとめ">対応方針のまとめ</h2> <p>2つの多重決済の問題に対して、以下の対応方針で解決できそうなことがわかりました。</p> <ul> <li>支払いの排他制御を行う</li> <li>各処理で失敗したら冪等性を担保しつつリトライを行う</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205100119.png" width="978" height="233" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="補償トランザクション">補償トランザクション</h3> <p>では<strong>リトライを何度も行っても解決しない</strong>場合はどうすればよいでしょうか。</p> <p>決済サービスへの決済処理が完了するまであればバックオフが可能です。 その場合、取得したロックを解除してユーザーからの再試行を行える状態にします。</p> <p>では決済処理完了後に、注文サービスへの支払い完了登録が行えない場合はどうでしょうか。</p> <p>バックオフはできませんし、エラー終了すると支払いのロックは残ったままで<strong>デッドロック</strong>となってしまいます。 同様に、バックオフ中にロックの解除に失敗し続けた場合もデッドロックのままとなってしまいます。</p> <p>現状ではこの状況に陥った場合には、お客様からは支払いを頂かずに後日トレタ側で飲食店へ補填する対応としています。 そもそも注文サービスが何度リトライをしても失敗してしまうケースとしては、サーバーがダウンしている可能性が高いので、その他の処理も続行が難しいと考えられるためです。</p> <p>最善の策ではないので、発生頻度などを注視しながらあるべき対応を引き続き検討している状況です。</p> <h3 id="実装手段">実装手段</h3> <p>これらの実装にあたって、今回は<a href="https://cloud.google.com/workflows?hl=ja">GCPのWorkflows</a>の利用を取り入れました。</p> <p>Workflowsはタスクを登録することで外部API呼び出しなどの定義されたステップを順に実行してくれるというサービスです。 AWSであれば似たようなサービスとしてStepFunctionsがあります。</p> <p>リトライの仕組みを持ち合わせているため、リトライ回数やインターバルなど柔軟なリトライ処理を容易に組むことができます。</p> <p>今回はこのWorkflowsを使い、外部API呼び出しとそのレスポンスに応じたリトライ、バックオフ処理を実装しました。また、タスク登録時にタスクのIDを得られるので、冪等キーにはこれを利用しました。</p> <p>ちなみにWorkflowsでの使いづらい点を挙げておくと、基本的に定義はすべてyaml形式であるため、プログラマブルな細かい制御は難しく、テストもしづらい点です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20221205/20221205100212.png" width="1200" height="320" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="さいごに">さいごに</h2> <p>以上が今回行ったトレタO/Xにおける多重決済に対しての対応のアプローチでした。</p> <p>分散トランザクションの難しさに改めてて対峙し、自分の未熟さを痛感しましたがここで得た教訓とノウハウを今後の開発に活かし、より安全なシステムの開発に向けて精進していこうと思います。</p> <p>なお、トレタではエンジニアの募集を全方位で行なっております。</p> <p>コロナ禍を乗り越えた飲食店の新しい姿を探求する仲間をお待ちしております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fengineer%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://corp.toreta.in/recruit/engineer/">corp.toreta.in</a></cite></p> mkitagawa-312 入社前不安だったことは杞憂だった話 hatenablog://entry/4207112889915962854 2022-09-13T15:39:50+09:00 2022-09-13T16:02:07+09:00 はじめに はじめまして。2022年の5月から株式会社トレタでフロントエンドエンジニアとして働いている武市です。 実務としては、モバイルオーダー「トレタO/X」のフロントエンドを担当しています。 詳しくは代表のnoteをご覧ください。 note.com 私は愛媛県からフルリモートで勤務しております。今回はトレタに入社する前に不安だったことをベースに記載していこうと思います。 転職時の不安を少しでも拭えたら幸いです。 簡単な経歴 元々は飲食店を経営していたのですが廃業してしまい人生どん底状態で腐っていました。このままではダメだと奮起し社会的に需要の高いプログラミングを学習。IT業界へ転職しました。… <h1 id="はじめに">はじめに</h1> <p>はじめまして。2022年の5月から株式会社トレタでフロントエンドエンジニアとして働いている武市です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toreta_takechi/20220913/20220913160153.jpg" width="1200" height="833" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>実務としては、モバイルオーダー「トレタO/X」のフロントエンドを担当しています。</p> <p>詳しくは代表のnoteをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fhitoshi%2Fn%2Fn3a8c27662f3a" title="ついにリリース。「コスト削減をゴールにしない」逆張りの店内モバイルオーダー、トレタO/Xとはどんなサービスなのか|ひとし|note" 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/hitoshi/n/n3a8c27662f3a">note.com</a></cite></p> <p>私は愛媛県からフルリモートで勤務しております。今回はトレタに入社する前に不安だったことをベースに記載していこうと思います。</p> <p>転職時の不安を少しでも拭えたら幸いです。</p> <h2 id="簡単な経歴">簡単な経歴</h2> <p>元々は飲食店を経営していたのですが廃業してしまい人生どん底状態で腐っていました。このままではダメだと奮起し社会的に需要の高いプログラミングを学習。IT業界へ転職しました。エンジニア歴としては2年と長いわけではありませんが、以前の会社では良くも悪くもリソースが足りておらず既存サービスの保守・運用だけでなく新規サービスであるフィットネスクラブ向けの月額顧客管理システムのPM兼開発を、立ち上げからさせていただき貴重な経験を積むことが出来ました。直近の技術スタックはVue.js,React,PHPです。</p> <h2 id="転職へのモチベーション">転職へのモチベーション</h2> <p>飲食店を経営していたこともあり僕の友人は飲食関係の友人が多いのですが、コロナ禍で休業、廃業する店は少なくありません。今でもお店が回らないと聞けば手伝いに行ったりもしていますが、人手不足は深刻です。プログラミングを学び飲食業界に貢献できることはないのか、そう考えていた矢先見つけたのがトレタでした。</p> <h2 id="転職時に不安だったこと">転職時に不安だったこと</h2> <h5 id="地方から働いて不便はないか">地方から働いて不便はないか</h5> <p>前職はオフラインでの業務でしたが、トレタではフルリモートになりました。どのようなオンボーディングになるのか不安だったのですが、業務の中でわからない部分や、設計に悩んだ際もメンター制度があるおかげで気兼ねなく相談できる環境が整っています。私は入社初日からフルリモートでは働いているのですが不便を感じたことは一度もないです。 ※メンター制度とは・・フルリモート環境下における新入社員のオンボーディングサポートの制度。新入社員には入社時に1人メンターがつき、定期的な1on1の実施や相談役としてフォローを行い、新入社員をサポートする制度。</p> <h5 id="人間関係はうまくいくのか">人間関係はうまくいくのか</h5> <p>トレタで人間関係で不満に思っている人はいないんじゃないかと思うぐらい穏やかな人が多いです。一度、『怖い人っていないんですか?』と聞いたことがありますが『そんな人いない、みんな楽しく仕事しているから心に余裕があるんだよ』と答えが返ってきて安心しました。</p> <h5 id="思ったような仕事を担当できるか">思ったような仕事を担当できるか</h5> <p>前職ではPMとして勤務していたこともあり、開発業務に集中できる環境ではありませんでした。トレタではBizサイド、PM、バックエンド、フロントエンド、QAと業務分担がしっかりされておりフロントエンドエンジニアとして集中できる環境が整っています。</p> <h5 id="これまでの経験スキルが通用するか">これまでの経験・スキルが通用するか</h5> <p>トレタではマイクロサービスを採択していることにより高い設計力が求められます。品質の高いソフトウェアを開発をする為に、求められる技術レベルが高くなったため日々勉強する毎日です。技術力の高い方からレビューしていただける環境は非常にありがたい。トレタにいればもっともっとプログラミングのレベルを高めることができるとワクワクしています!</p> <h2 id="まとめ">まとめ</h2> <p>飲食業界をDX化することで社会貢献ができ、従業員一人一人の生活を大切にしているワークライフバランスの取れた会社です。トレタに入社して幸福度の高さは人生で一番だと思います。</p> <p>興味がある方は是非カジュアル面談へお越しください! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集・採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> toreta_takechi トレタO/XのtoCアプリをリニューアルしたら諸々整理できてよかったという話 hatenablog://entry/13574176438084008470 2022-04-25T12:37:26+09:00 2022-04-25T13:43:11+09:00 こんにちは、パパエンジニアのkentaroです. 2022年2月、トレタO/Xをご利用いただいているワンダーテーブルさまの「よなよなビアワークス」向けのモバイルオーダーアプリ(以下 toCアプリ と呼びます)をFlutter WebからReactにリニューアルしました。 なお、「トレタO/X」については代表のnoteをご覧ください。 note.com 背景 当時のtoCアプリはFlutter1系で作られており、いくつか問題点を抱えていました。代表的なことの1つはNullSafety対応されていないことです。もう1つは状態管理も現在デファクトスタンダードになっているライブラリ(Riverpod)… <p>こんにちは、パパエンジニアの<a href="https://twitter.com/kenkenken_3">kentaro</a>です.</p> <p>2022年2月、トレタO/Xをご利用いただいている<a href="https://yonayonabeerworks.com/">ワンダーテーブルさまの「よなよなビアワークス」</a>向けのモバイルオーダーアプリ(以下 <code>toCアプリ</code> と呼びます)をFlutter WebからReactにリニューアルしました。</p> <p>なお、「トレタO/X」については代表のnoteをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fhitoshi%2Fn%2Fn3a8c27662f3a" title="ついにリリース。「コスト削減をゴールにしない」逆張りの店内モバイルオーダー、トレタO/Xとはどんなサービスなのか|ひとし|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/hitoshi/n/n3a8c27662f3a">note.com</a></cite></p> <h2>背景</h2> <p>当時のtoCアプリはFlutter1系で作られており、いくつか問題点を抱えていました。代表的なことの1つはNullSafety対応されていないことです。もう1つは状態管理も現在デファクトスタンダードになっているライブラリ(Riverpod)への乗り換えが必要ということです。 <br /> (ちなみにスタッフ向けアプリはFlutter2系・NullSafety対応済み・Riverpod採用しています)<br /> ほぼ作り直しとなることが見込まれたので機会を見計らっていましたが、iOS15でFlutter Webが動作しなくなったことがきっかけで一気に検討が進み、最終的にはReactで作り直すという意思決定をしました。</p> <p>参考までにチームメンバーが作成してくれたIssueをご紹介します。当時もワークアラウンドで回避可能でしたが、これによりリニューアルの議論が進むきっかけになりました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Fflutter%2Fflutter%2Fissues%2F89655" title="[web] Rendering issues and crash with iOS15&#39;s Safari and new macOS Safari · Issue #89655 · flutter/flutter" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://github.com/flutter/flutter/issues/89655">github.com</a></cite></p> <h3>なぜReactなのか</h3> <p>当初はモバイルアプリ化する可能性があったため、Flutter Webを採用していましたが、その後の事業方針でtoCアプリはモバイルアプリ化しないことになりました。</p> <p>他法人向けtoCアプリはAngularで作っていましたが正社員採用や業務委託契約による体制拡大を考えた際、より人を集めやすいReactで作り直すことになりました。</p> <h2>リニューアルプロジェクト</h2> <p>2021年9月からアプリのリニューアルプロジェクトが始動しました。</p> <p>最初の1ヶ月程は開発環境の構築と並行し、Flutter版toCアプリの仕様策定や、複雑なロジックの図解をしました。<br /> プロジェクト序盤に現状を整理して可視化したことにより、過剰に複雑になっていた仕様をできるだけシンプルにすることができました。振り返ってみた際にとても有効な取り組みだったと感じています。<br /> 例えばアプリ起動時の状態判定が複雑だったのですが、ここをある程度シンプルな形にできたため、メンテナンス性が向上しました。</p> <p>そこから11月末までは実装フェーズでした。<br /> フロントエンド開発が得意なメンバーはcomponentの量産に集中し、デザイナーと連携してUIの実装を推し進めてくれました。<br /> 自分は今回が初のWebフロントエンドの開発業務でしたが、Flutter版toCアプリの開発に携わっていたのでドメイン知識はチーム内でも豊富な部類でした。<br /> そこでなるべくロジック寄りの実装を担当し、不慣れなWebフロントエンドの開発でも実装のスピードを落とさないように工夫しました。<br /> メンバーそれぞれの強みを活かし、チームとしての成果を最大化できたと感じています。</p> <p>12月・1月はQAとテストで出た不具合の対応をしつつ、残タスクの消化やリリース準備を行い、2月のリリースに漕ぎ着けました。</p> <p>プロジェクトメンバー全員が何かしらのO/X別プロジェクトと兼任している状態ではありましたが、リリース遅延することなくやりきることができました。</p> <h3>React版で追加した機能</h3> <p>基本的にはFlutter版をReact化する、というプロジェクトだったのですが1つだけ新しい機能を追加しています。<br /> これまでのクレジットカードによる支払いに加え、Apple Pay / Google Pay に対応しました!是非ご利用ください!</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kenkenken_3/20220418/20220418125459.jpg" alt="&#x3088;&#x306A;&#x3088;&#x306A;&#x30D3;&#x30A2;&#x30EF;&#x30FC;&#x30AF;&#x30B9;&#x5411;&#x3051;&#x30C8;&#x30EC;&#x30BF;O/X&#x306E;toC&#x30A2;&#x30D7;&#x30EA;&#x3067;Apple Pay&#x30DC;&#x30BF;&#x30F3;&#x304C;&#x8868;&#x793A;&#x3055;&#x308C;&#x3066;&#x3044;&#x308B;&#x753B;&#x50CF;" width="567" height="735" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>振り返ってみて</h2> <p>チームメンバーに「このプロジェクトを振り返ってみてどうでしたか?」というインタビューをしました。</p> <h3>プロジェクトマネージャー</h3> <ul> <li>PoCで手探りで作ってきた部分をきれいに作り直すいい機会だった</li> <li>転職市場やフリーランス市場ではFlutterエンジニアよりReactエンジニアのほうが多いので、長い目で見て保守しやすくなったと捉えている</li> </ul> <h3>デザイナー</h3> <ul> <li>画面・コンポーネントの名前が人によって呼び方が人によって異なっていたが、それを統一できたためコミュニケーションがしやすくなった <ul> <li>例: 「注文リスト」を「カート」と呼ぶ人もいたが、「注文リスト」に統一</li> </ul> </li> <li>画面遷移フローの図をわかりやすくすることができた</li> <li>コンポーネントの整理ができた</li> </ul> <h3>QA</h3> <ul> <li>FlutterのIntegration Testについて、当時のバージョンではWebに対応していなくて書けなかった</li> <li>Webの多様なE2E Testing Framework使えるので、今後テストの自動化を進めていくことができる</li> <li>仕様整理しながらプロジェクトを進めることができた</li> </ul> <h3>エンジニア</h3> <ul> <li>(プロジェクトマネージャーと同じく)PoCで手探りで作ってきた部分をきれいに作り直すいい機会だった</li> <li>O/Xを開発するにあたって理解しておくといいことをドキュメント化することができたので、今後新しいエンジニアが来てもスムーズにオンボーディングができそう</li> </ul> <h3>個人的な感想</h3> <ul> <li>業務でのWebフロントエンドの開発に挑戦できた</li> </ul> <p>この辺りの話は <code>トレタO/Xの開発にジョインしてやってきたこと</code> という記事の後半にも書いています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2021%2F12%2F24%2F100512" title="トレタO/Xの開発にジョインしてやってきたこと - トレタ開発者ブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tech.toreta.in/entry/2021/12/24/100512">tech.toreta.in</a></cite></p> <p>それぞれの目線でコメントもらいましたが「諸々整理できたのでよかった」というのは職種関係なく感じていたようです。</p> <h2>終わりに</h2> <p>プロダクトを作りエンハンスを続けていると、細かいところに綻びが出ていることに気がつきはするのですが、立ち止って整理するという意思決定に至らないことも多いのではないかと思います。<br /> トレタでは新しいプロダクト開発に取り組んでいますが、今回のリニューアルプロジェクトで得た知見・ドキュメント等が非常に役に立っています。<br /> 中長期的に考えると事業をさらに発展させていくために、短期的に立ち止まり、整理したり仕切り直したりすることは必要なのだと感じています。</p> <p>今回は良いきっかけに恵まれましたが、一方で施策を前に進めることも必要です。プロジェクトや企業の状況に応じて判断する必要があることなので、ステークホルダーとロードマップを握りながらリファクタリングを組み込んでいくのが良いように感じました。</p> <h3>お約束の</h3> <p>最後に、一緒に走ったり立ち止まったりしてくれる仲間を大募集中です!<br /> とりあえず話を聞いてみたいという方、カジュアル面談もウェルカムですのでお気軽にお問い合わせください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集・採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> kenkenken_3 トレタに入社してみて hatenablog://entry/13574176438070643440 2022-03-23T13:00:00+09:00 2022-03-23T13:00:00+09:00 はじめに はじめまして、2022年1月にトレタに入社したエンジニアの葉坂です。 トレタO/Xの決済基盤周りを担当しています。 www.toreta-ox.com 入社して2ヶ月ほど経ちましたので、トレタに入社してみての感想を述べていこうと思います。 特に、現在転職を考えている方の参考になれば幸いです。 簡単な経歴 エンジニア歴としては約5年ほどで、サーバーサイドエンジニアをやっています。 新規サービスの立ち上げから成熟したサービスの保守・運用まで幅広く携わってきました。 直近の技術スタックとしてはRuby, PHP, Flutterです。 ちなみに現在はGoを習得中です。 入社してみての感想 … <h2>はじめに</h2> <p>はじめまして、2022年1月にトレタに入社したエンジニアの葉坂です。</p> <p><a href="https://www.toreta-ox.com/">トレタO/X</a>の決済基盤周りを担当しています。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.toreta-ox.com%2F" title="トレタO/X | お客さまに楽しい注文体験を。" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.toreta-ox.com/">www.toreta-ox.com</a></cite></p> <p>入社して2ヶ月ほど経ちましたので、トレタに入社してみての感想を述べていこうと思います。</p> <p>特に、現在転職を考えている方の参考になれば幸いです。</p> <h2>簡単な経歴</h2> <p>エンジニア歴としては約5年ほどで、サーバーサイドエンジニアをやっています。</p> <p>新規サービスの立ち上げから成熟したサービスの保守・運用まで幅広く携わってきました。</p> <p>直近の技術スタックとしてはRuby, PHP, Flutterです。</p> <p>ちなみに現在はGoを習得中です。</p> <h2>入社してみての感想</h2> <h3>オンボーディングが手厚い</h3> <p>メンター制度があるという話は聞いていたので、そこまで入社への不安はなかったのですが、想像していたよりオンボーディングが手厚く、嬉しい驚きでした。</p> <h4>入社1ヶ月前</h4> <p>まずは、入社前にメンターとの顔合わせMTGがありました。</p> <p>実際の業務に入ってからの顔合わせだと、つい業務中心の話になり堅苦しくなってしまいますが、入社前MTGということで、意識しすぎることなく話ができました。</p> <p>おかげで、実際に入社してからも大きな感覚の相違なく業務ができています。</p> <p>また、上記の顔合わせMTG以外にも、チームの雰囲気をつかめるようにと、サーバーサイド定例MTGにも参加させてもらいました。</p> <p>メンターや、一緒にお仕事していくメンバーと入社前に、がっつり顔合わせをするのは初めてでしたが、良い経験でした。</p> <h4>入社初日</h4> <p>基本フルリモートではありますが、入社初日は出社での勤務でした(希望に応じて入社初日からリモートすることも可能)。</p> <p>すぐさま業務にあたるのではなく、まずは丁寧な会社案内がありました。</p> <p>各部署の担当者がそれぞれ、社内ルールや社内で利用しているツール、会社のミッション、事業戦略や組織の役割等、時間をかけて案内してくれました。</p> <p>どの担当者の方もその都度、僕の疑問に対してすぐに回答をくれたりと、終始真摯に向き合ってくれました。</p> <p>特に、ツールへの理解を深めるためにと担当者がしてくれた対応がとても丁寧で助かりました。</p> <p>複雑な操作に関して、実際に操作デモを行い時間をかけて解説してくれました。</p> <h4>入社2日目以降</h4> <p>入社2日目以降はリモートでの勤務でした。</p> <p>実際にチームに配属され、サービスやアーキテクチャ全体の説明等、十分な時間を設けてくれました。</p> <p>フルリモートでのコミュニケーションが疎かにならないよう、また、新人が不明点で行き詰まらないよう、オンボーディング用のslackチャンネルを作成してくれました。</p> <p>もちろんメンターの方も終始サポートに入ってくれ、大きな不安もなく業務に入っていくことができました。</p> <p>また、業務外でも、ウェルカムランチを実施してくれ、新しく入る人にも打ち解けやすい雰囲気作りをしてくれました。</p> <p>他にも、下記のようなサポートもあります。</p> <ul> <li>メンターとの1on1(初月は毎週、以降は月1回)</li> <li>入社後人事面談(入社後1ヶ月、3ヶ月、6ヶ月)</li> </ul> <p>などなど、トレタには新入社員が早期にパフォーマンスを発揮できるようなサポートがたくさんありました。</p> <h3>社内勉強会が豊富</h3> <p>面接時に聞いていた通り、トレタは社内勉強会が充実しており、エンジニアのスキル成長の環境作りに力を入れていると感じました。</p> <p>「<a href="https://note.com/toreta_hr/n/nf79b13aba3a2">Go勉強会</a>」、「<a href="https://www.wantedly.com/companies/toreta/post_articles/307642">RDB勉強会</a>」、 エンジニアによる技術共有会の「<a href="https://note.com/toreta_hr/n/n3ed141d93824">テックトーク</a>」、 「システム設計のワークショップ」など実に多くの勉強会が開催されています。</p> <p>※こちらはすべて業務時間内での開催です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr%2Fn%2Fnf79b13aba3a2%2F" title="様々な領域のエンジニアが集い学ぶ!トレタの技術顧問 tenntennさんによるGo勉強会|トレタのnote|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/toreta_hr/n/nf79b13aba3a2/">note.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fpost_articles%2F307642%2F" title="既存サービスの改善の気づきにも!RDB勉強会を始めました! | 株式会社トレタ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/post_articles/307642/">www.wantedly.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr%2Fn%2Fn3ed141d93824%2F" title="自由なテーマが学びにつながる!トレタ技術共有会「テックトーク」|トレタのnote|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/toreta_hr/n/n3ed141d93824/">note.com</a></cite></p> <h3>柔軟性の高い働き方ができる</h3> <p>フレックス(11時〜16時がコアタイム)とフルリモートを導入しているので、各々のライフスタイルに合わせて働けます。</p> <p>時間的にも場所的にも自由度が高いので、非常に仕事がしやすいです。</p> <p>この情勢を鑑みてすぐにフルリモートを取り入れたり、<a href="https://note.com/toreta_hr/n/nee74bb8cae7e/">コロナワクチン副反応による特別休暇制度</a>、 業務以外の目的(観光、帰省、気分転換)で自宅以外の場所に宿泊しながら働くことができる「<a href="https://note.com/toreta_hr/n/nd6db156df1b6/">どこでもトレタ</a>」 制度を導入したりと、情勢や時代に合わせて柔軟かつ迅速に社内制度を整えていく組織力があります。</p> <p>何より、僕が一番感じているトレタの魅力は、トレタ社員が皆さん温厚な方ばかりなことです。</p> <p>心理的安全性が高いため、本心から働きやすい環境に感じています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr%2Fn%2Fnee74bb8cae7e%2F" title="コロナワクチン副反応による特別休暇制度を導入します|トレタのnote|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/toreta_hr/n/nee74bb8cae7e/">note.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr%2Fn%2Fnd6db156df1b6%2F" title="【トレタ流働き方改革】「どこでもトレタ」運用開始!おすすめのエリアは沖縄?軽井沢?|トレタのnote|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/toreta_hr/n/nd6db156df1b6/">note.com</a></cite></p> <h3>入社前は知らなかった変わった取り組み</h3> <p>トレタでは他社にはない面白い制度がありました。</p> <p>なんと、<a href="https://note.com/toreta_hr/n/nd310501d450c">社内ラジオ</a>があり、毎週、社員がパーソナリティを務めて放送していたのです。</p> <p>この社内ラジオは社員積極参加型のため、新入社員の自己紹介企画もありました。</p> <p>僕ももちろんゲストとして参加しました。</p> <p>こういった場で話すのは初めてで、とても緊張しましたが、面白い体験ができたと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr%2Fn%2Fnd310501d450c%2F" title="リモートワークの中、1年間の社内ラジオを通じて見えてきたもの|トレタのnote|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/toreta_hr/n/nd310501d450c/">note.com</a></cite></p> <h2>これから</h2> <p>入社して2ヶ月経ちましたが、忖度なしに入社前との悪いギャップはなく、トレタに入社してよかったなと感じる毎日です。</p> <p>とはいえ、飲食という業界はコロナで一番打撃を受けている業界ですし、もちろん業務上は課題もたくさんありますので、 これからも自分が今まで培ってきた技術力でしっかりコミットしていきたいと思います。</p> <h2>さいごに</h2> <p>最後まで読んでいただきありがとうございます。</p> <p>いかがでしたでしょうか。</p> <p>トレタの雰囲気、伝わりましたでしょうか。</p> <p>また、少しでもトレタに興味を持っていただけたでしょうか。</p> <p>トレタではメンバーを採用中です!</p> <p>先日資金調達を実施し、絶賛採用中です!</p> <p>外食産業をもっとやりがいを持って働ける環境にするというミッションのために、トレタと一緒に成長してくれるメンバーを募集中です!</p> <p>ご応募、ご紹介共にお待ちしております!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2F" title="採用ページTOP | 株式会社トレタ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://corp.toreta.in/recruit/">corp.toreta.in</a></cite></p> you8_h トレタO/X toCアプリで行ったパフォーマンス観点の対応 hatenablog://entry/13574176438062444755 2022-03-11T18:13:58+09:00 2022-03-14T12:22:18+09:00 こんにちは!トレタでフロントエンドエンジニアをしている shira です。 この記事ではトレタO/Xというモバイルオーダー事業のうち、自分が関わっているtoCアプリで行ったパフォーマンス観点の対応について紹介します。 トレタO/Xに関しては、次の記事をご覧ください。 note.com 前提 トレタO/XではtoB向けとtoC向けのアプリケーションがあり、toBは共通、toCは各法人別という構成になっています。 この記事ではトレタO/XのtoCアプリのうち、塚田農場(エー・ピー・ホールディングス)様向けtoCアプリ (以下「toCアプリ」という。)の紹介をさせていただきます。 この記事で紹介する… <p>こんにちは!トレタでフロントエンドエンジニアをしている <a href="https://twitter.com/9v9Shira">shira</a> です。</p> <p>この記事ではトレタO/Xというモバイルオーダー事業のうち、自分が関わっているtoCアプリで行ったパフォーマンス観点の対応について紹介します。</p> <p>トレタO/Xに関しては、次の記事をご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fhitoshi%2Fn%2Fn3a8c27662f3a" title="ついにリリース。「コスト削減をゴールにしない」逆張りの店内モバイルオーダー、トレタO/Xとはどんなサービスなのか|ひとし|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/hitoshi/n/n3a8c27662f3a">note.com</a></cite></p> <h2>前提</h2> <p>トレタO/XではtoB向けとtoC向けのアプリケーションがあり、toBは共通、toCは各法人別という構成になっています。</p> <p>この記事ではトレタO/XのtoCアプリのうち、塚田農場(エー・ピー・ホールディングス)様向けtoCアプリ (以下「toCアプリ」という。)の紹介をさせていただきます。</p> <p>この記事で紹介するtoCアプリは2020年末から開発しているWebアプリケーションで、小さくリリースを繰り返し、改善してきました。</p> <p>この記事では、パフォーマンス観点で行った対応の一部をご紹介します。</p> <h2>パフォーマンス観点の課題と対策</h2> <h3>1. 画像が多く、読み込みに時間がかかる</h3> <h4>課題</h4> <p>toCアプリでは全てのメニューに対して画像を表示しています。そのため、パフォーマンス観点で一番気にしていた点は画像の多さでした。パフォーマンスに影響することが明らかなため、初期リリース前に最低限の対応を入れたいと考えました。</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/punipunityan/20220214/20220214015009.png" alt="f:id:punipunityan:20220214015009p:plain" width="1200" height="765" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>塚田農場メニュー一覧画面</figcaption></figure></p> <h4>対応方針</h4> <p>まず、画像サイズを小さくする方法を検討し、<strong>WebP</strong> を使うことにしました。</p> <p>Googleによると、WebP に変換することで PNG は 26% 小さくなり、JPEG は 25〜34% 小さくなるとされています。期待大です。(参考:<a href="https://developers.google.com/speed/webp">An image format for the Web</a>)</p> <p>WebPは基本的にモダンブラウザで利用可能ですが、対応していないブラウザのために WebP の他に PNG もしくは JPEG 画像を用意する必要がありました。</p> <p>前提として、運用上変更が必要になる画像は全てContentfulに登録しています。メニューの画像も同様です。</p> <p>最初は登録時にそれぞれ2種類の画像を登録する必要があると大変だな、と思っていたのですが、Contentfulだと簡単にできる方法がありましたので、その方法を紹介します。</p> <h4>対応方法</h4> <p>Contentfulでは画像のファイル形式を簡単に変更することが可能です。</p> <p>参考までに、具体的なコードを記載します。</p> <p>Content Delivery API でデータを取得した際、画像の情報は以下の形式で取得することができます。</p> <p>(以下、<a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/assets/asset/get-a-single-asset/console/js">ドキュメント</a>より引用)</p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">fields</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">title</span>&quot;: &quot;<span class="synConstant">Nyan Cat</span>&quot;, &quot;<span class="synStatement">file</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">contentType</span>&quot;: &quot;<span class="synConstant">image/png</span>&quot;, &quot;<span class="synStatement">fileName</span>&quot;: &quot;<span class="synConstant">Nyan_cat_250px_frame.png</span>&quot;, &quot;<span class="synStatement">url</span>&quot;: &quot;<span class="synConstant">//images.ctfassets.net/yadj1kx9rmg0/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png</span>&quot;, &quot;<span class="synStatement">details</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">image</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">width</span>&quot;: <span class="synConstant">250</span>, &quot;<span class="synStatement">height</span>&quot;: <span class="synConstant">250</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">size</span>&quot;: <span class="synConstant">12273</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">metadata</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">tags</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">sys</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Link</span>&quot;, &quot;<span class="synStatement">linkType</span>&quot;: &quot;<span class="synConstant">Tag</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">nyCampaign</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">sys</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">nyancat</span>&quot;, &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Asset</span>&quot;, &quot;<span class="synStatement">space</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">sys</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">Link</span>&quot;, &quot;<span class="synStatement">linkType</span>&quot;: &quot;<span class="synConstant">Space</span>&quot;, &quot;<span class="synStatement">id</span>&quot;: &quot;<span class="synConstant">yadj1kx9rmg0</span>&quot; <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">createdAt</span>&quot;: &quot;<span class="synConstant">2016-12-20T10:43:35.772Z</span>&quot;, &quot;<span class="synStatement">updatedAt</span>&quot;: &quot;<span class="synConstant">2016-12-20T10:43:35.772Z</span>&quot;, &quot;<span class="synStatement">revision</span>&quot;: <span class="synConstant">1</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p><code>fields.file.url</code> が画像のURLになります。 ファイル形式を変更したい場合、<code>fm</code> パラメータを使います。WebPにするには、画像のURLに<code>?fm=webp</code>をつけ <code>[fields.file.url]?fm=webp</code> のようにするだけでOKです。</p> <p>アプリケーションで画像を指定する際は以下のように指定しました。</p> <pre class="code lang-html" data-lang="html" data-unlink><span class="synIdentifier">&lt;</span><span class="synStatement">picture</span><span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">source</span><span class="synIdentifier"> srcset=</span><span class="synConstant">&quot;//images.ctfassets.net/yadj1kx9rmg0/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png?fm=webp'&quot;</span><span class="synIdentifier"> </span><span class="synType">type</span><span class="synIdentifier">=</span><span class="synConstant">&quot;image/webp&quot;</span><span class="synIdentifier"> /&gt;</span> <span class="synIdentifier">&lt;</span><span class="synStatement">img</span><span class="synIdentifier"> </span><span class="synType">src</span><span class="synIdentifier">=</span><span class="synConstant">&quot;//images.ctfassets.net/yadj1kx9rmg0/4gp6taAwW4CmSgumq2ekUm/9da0cd1936871b8d72343e895a00d611/Nyan_cat_250px_frame.png&quot;</span><span class="synIdentifier"> </span><span class="synType">alt</span><span class="synIdentifier">=</span><span class="synConstant">&quot;&quot;</span><span class="synIdentifier"> /&gt;</span> <span class="synIdentifier">&lt;/</span><span class="synStatement">picture</span><span class="synIdentifier">&gt;</span> </pre> <p><code>&lt;picture&gt;</code> 要素内の<code>&lt;source&gt;</code> 要素に <code>type="image/webp"</code> を指定し、<code>srcset</code> には <code>[fields.file.url]?fm=web</code> 形式のURLを指定しています。img タグには WebP非対応ブラウザ向けに元々のURL <code>fields.file.url</code>(PNG / JPEG 画像)を指定します。 これにより、WebPに対応しているブラウザではWebP形式の画像を表示し、そうでない場合は元々の画像を表示するようになりました。</p> <p>さくっと対応できる上、効果が大きいので、とても便利でした!✨ ✨ ✨</p> <h4>今後の課題</h4> <p>WebPに対応することである程度改善できましたが、Contentful側に登録してある元々の画像が大きすぎるケースが発生しており、その点が課題です。画像を登録する方に気を付けていただく方法もありますが、同じ画像を異なるレイアウトで表示している部分もあるため、toCアプリ側での対策も検討したいと考えています。Contentful の <a href="https://www.contentful.com/developers/docs/references/images-api/">Images API</a> を使って、画像をリサイズできるため、デバイスやレイアウトに応じたサイズの画像を返すような改修を将来的に入れたいと考えています。 また、一部WebP未対応箇所があるため、その点も今後改修予定です。</p> <h3>2. 動画がすぐに再生されない</h3> <h4>課題</h4> <p>toCアプリでは、注文完了時ランダムに画像や動画を表示する仕様となっています。</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/p/punipunityan/20220309/20220309194207.png" alt="f:id:punipunityan:20220309194207p:plain" width="378" height="816" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure></p> <p>この機能の開発中、動画の再生が遅れるという問題が発生しました。 インターネットの速度が速い場合は問題なかったのですが、あまり良くない場合に発生するようでした。</p> <h4>対応方針</h4> <p>まず、動画素材の調整を行いました。 具体的には、動画の長さ調整や画質的に問題ない範囲での圧縮などを行いました。(この対応はデザイナーが対応してくださいました。)</p> <p>アプリ側では事前読み込みが効果的ではないかという仮説のもと、試してみることにしました。 具体的には、注文完了時に表示する画像・動画をアプリ起動時にpreloadで全て取得するよう対応しました。(最初prefetchを使おうとしましたが、safariで使えないようなのでpreloadを採用しました。)</p> <p>アプリ起動時にpreloadを行うことにしたのは、ユーザーが必ず開く画面だからです。</p> <p>結果、注文完了画面遷移時、動画がスムーズに再生するようになりました 🎉 🎉 🎉</p> <h4>今後の課題</h4> <p>この対応によって、必要ではないファイル(画像・動画)も取得してしまうという課題は残ります。</p> <p>また、前項に記載したWebP対応が preload 対応部分の画像に関しては対応できていません。</p> <p>いくつか改善の余地は残っている状況ですが、その点は今後検討していきたいと考えています。</p> <h3>3. BFFを導入するとレスポンスが遅くなった</h3> <h4>課題</h4> <p>アプリの要件が複雑になってきたこともあり、BFFを導入する方針となりました。 BFFへの切り替え対応を行い、Vercelのプレビュー環境で動作確認をしたところ、レスポンスが明らかに遅くなっていることがわかりました。</p> <p>前提として、toCアプリは以下と通信を行なっていました。</p> <ul> <li>APIサーバー</li> <li>Contentful</li> <li>Firebase</li> </ul> <p>BFF対応の際、APIサーバーへのリクエストとContentfulへのリクエストをBFF経由で行うようにしたのですが、そのうちAPIサーバーへのリクエストのレスポンスが明らかに遅くなってしまいました。 APIによっては、6秒弱かかっているケースもありました。</p> <p>トレタO/Xは</p> <p>toCアプリ &lt;=> APIサーバー &lt;=> 各バックエンドサービスという構成になっています。</p> <p><figure class="figure-image figure-image-fotolife" title="トレタO/Xのシステム構成"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/p/punipunityan/20220214/20220214021125.png" alt="f:id:punipunityan:20220214021125p:plain" width="852" height="448" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>トレタO/Xのシステム構成</figcaption></figure></p> <p>BFFは、上記の図のtoCアプリとAPIサーバーの間に入るものとなっています。 具体的には Vercel の Serverless Functions を利用しています。 また、BFFでは一部情報をFirestoreにキャッシュして利用しているため、Firebaseとの通信も発生します。</p> <h4>対策</h4> <p>対策はいくつか考え、簡単に試せるものから試しました。 検討した案は以下になります。</p> <ul> <li>VercelのServerless FunctionsのRegionを日本にする</li> <li>まとめられるリクエストがないか見直す</li> <li>処理中にスピナーを出す</li> <li>Firebaseをやめてupstash使う</li> </ul> <p>結局上から3つを行いました。順番に、検討・対応した内容を紹介します。</p> <p><strong>VercelのServerless FunctionsのRegionを日本にする</strong><br /> Vercel の Serverless Functions の Region が <code>sfo1</code> になっていたのを <code>hnd1</code> に変更しました。<code>sfo1</code> になっていたのはデフォルトのままにしていたためです。( 2021年1月14日より前に作成されたプロジェクトは、デフォルトで米国のサンフランシスコ(sfo1)になり、それ以降は新しいデフォルトの米国ワシントンDC(iad1)になるようです。 参考:<a href="https://vercel.com/docs/concepts/functions/regions#default-region">Default Region</a>)</p> <p>APIサーバーは日本にあるため、この変更によりBFFとAPIサーバーの物理的距離が近くなりました。</p> <p>結論、この対応は一番効果的でした。この変更により、元々の速度と大差なく動くようになりました 🎉 🎉 🎉</p> <p>Regionの影響の大きさを感じました🌏</p> <p>参考:Regions for Serverless Functions</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fvercel.com%2Fdocs%2Fconcepts%2Ffunctions%2Fregions" title="Regions for Serverless Functions" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://vercel.com/docs/concepts/functions/regions">vercel.com</a></cite></p> <p><strong>まとめられるリクエストがないか見直す</strong><br /> リクエストを直列で飛ばすとユーザーを待たせる時間が長くなってしまうため、並列でリクエストを飛ばせる箇所がないか見直しました。すでにある程度対応済みでしたが、見直したところ2箇所程度まとめられるところがあったのでまとめました。具体的には Promise.all() を使って並列でリクエストを飛ばすようにしています。</p> <p><strong>処理中にスピナーを出す</strong><br /> これは根本的な改善ではありませんが以下理由で対応しました。</p> <ul> <li>BFFを挟んだことにより多少レスポンスが遅くなることはある程度仕方ないと考えられる</li> <li>そもそも、あらゆる要因によってレスポンスが遅くなる場合があるが、その際、ユーザーに対してフィードバックがないという課題があった</li> </ul> <p>ちなみに、これまでの通信中の表示制御は以下のようになっていました。(フィードバックを得て改善する前提で対応していました。)</p> <ul> <li>APIとの通信時 <ul> <li>体感でサクサクだったので、GETは読み込み中の表示はなし、POSTはボタンの連打防止対応を入れていた</li> </ul> </li> <li>Contentfulとの通信時 <ul> <li>体感でやや待たされると感じられるため、「読み込み中...」文言を画面中央に表示</li> </ul> </li> </ul> <p>この改善により、体験のばらつきやレスポンス遅延時の懸念がある程度解消されました。</p> <p><strong>Firebase をやめて upstash を使う</strong><br /> Firebase にキャッシュするのをやめて、upstash を使うと早くなるのでは?という案がありました。ただ、他プロジェクトで試した方によると、100ms くらい早くなるけど体感はあまり変わらないとのことでした。それに加え、VercelのServerless FunctionのRegionの変更によって大幅に改善していたため、この対応は見送ることにしました。</p> <h2>まとめ</h2> <p>写真を見てお腹が空いた方、O/Xが気になった方、ぜひ塚田農場様に足を運んでみてください😊</p> <p>この記事では、開発中に見つかったパフォーマンス観点での懸念と対策についていくつか紹介させていただきました。</p> <p>toCアプリを開発し始めて1年以上経過しました。まだまだ改善したいことはたくさんあります。最近では日々の機能追加に加え、週1日改善Dayを設ける取り組みをはじめました。長くメンテナンスしていくためにコードの見直しを進めているところです。</p> <p>課題を見つけて改善するのが好きな方、一緒に改善しましせんか!</p> punipunityan トレタに入社してから3ヶ月間で感じたこと hatenablog://entry/13574176438052301793 2022-01-24T12:52:32+09:00 2022-01-24T12:52:32+09:00 はじめに はじめまして。2021年の10月から株式会社トレタでサーバーサイドエンジニアとして働いている神山です。 実務としては、トレタO/Xの認証/権限管理サービス周りを担当しています。 なおトレタO/Xについては代表のnoteをご覧ください。 note.com 今回はトレタに入社してから感じたことや、どのような業務をしているかなど記載していこうと思います。 トレタのエンジニアはこんな感じなんだなぁ、というのが伝われば幸いです。 簡単な経歴と転職の経緯 大学を卒業後、新卒で入社したのは産業電気機器のメーカーでハードウェアエンジニアとして4年半ほど勤務していました。 作る仕事がしたいという思いか… <h1>はじめに</h1> <p>はじめまして。2021年の10月から株式会社トレタでサーバーサイドエンジニアとして働いている神山です。</p> <p>実務としては、トレタO/Xの認証/権限管理サービス周りを担当しています。</p> <p>なおトレタO/Xについては代表のnoteをご覧ください。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fhitoshi%2Fn%2Fn3a8c27662f3a" title="ついにリリース。「コスト削減をゴールにしない」逆張りの店内モバイルオーダー、トレタO/Xとはどんなサービスなのか|ひとし|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/hitoshi/n/n3a8c27662f3a">note.com</a></cite></p> <p>今回はトレタに入社してから感じたことや、どのような業務をしているかなど記載していこうと思います。</p> <p>トレタのエンジニアはこんな感じなんだなぁ、というのが伝われば幸いです。</p> <h2>簡単な経歴と転職の経緯</h2> <p>大学を卒業後、新卒で入社したのは産業電気機器のメーカーでハードウェアエンジニアとして4年半ほど勤務していました。</p> <p>作る仕事がしたいという思いから入社したのですが、新規プロジェクトが中々立ち上がらないことから悩み、ソフトウェアエンジニアへの方針転換を決めました。</p> <p>2020年1月からは受託開発企業に入社し、金融機関関連サービスの開発や、交通関係の予約システムの開発に従事していました。</p> <p>サービス視点を持って開発したい、もっとスキルを伸ばしていきたいという思いから転職活動を開始、2021年10月にトレタに入社しました。</p> <h2>入社の決め手</h2> <h5>1. トレタO/Xのエンハンスのために設計機会が豊富</h5> <p>トレタO/Xはローンチされてからまだ日の浅いサービスであることを知りました。</p> <p>エンハンスのための設計余地がまだまだあるのではないかと考えていました。</p> <p>加えて、マイクロサービスはモノリスを分散していく用途で採用している会社が多いかとおもいます。</p> <p>新規サービスとしてマイクロサービスに携わることができるのは非常に貴重な経験なのではと考えました。</p> <h5>2. より難易度の高い設計能力を求められる</h5> <p>トレタO/XはSaaSであり、それを実現するためにマイクロサービスの構成となっています。</p> <p>SaaSであるということは当然ながら、ユーザーの様々なユースケースをクリアするように機能設計をしていかなければなりません。</p> <p>加えてマイクロサービスによる機能実現を図るため、各サービスにおけるデータ整合性などを考慮し設計をする必要があります。</p> <p>以上を踏まえると、難易度の高い設計が必要となるのではと考えていました。</p> <p>マイクロサービスに関する詳しい構成はこちらで解説していますのでご覧ください。</p> <p><a href="https://blog.hatena.ne.jp/toreta-dev/toreta-dev.hatenablog.com/edit?entry=13574176438044016049">https://blog.hatena.ne.jp/toreta-dev/toreta-dev.hatenablog.com/edit?entry=13574176438044016049</a></p> <h5>3. 労働環境が良い</h5> <p>コロナの影響もあり、かなり早い段階からフルリモートを取り入れている会社であることを知りました。</p> <p>フルリモートであることを必須の条件とは考えていなかったのですが、移動時間があまり好きではない僕としては出勤時間を0にできるというのは大きな魅力でした・・。</p> <h5>4. 心理的安全性が高い</h5> <p>面接の段階から、穏やかな方々が多いのではないかという印象がありました。</p> <p>加えて様々な媒体でトレタのことを調べていたのですが、自社の利益だけを追求する、という方針を取らない印象が強くありました。</p> <p>サービスを利用してくださる飲食店の方々のことを考え、どうすれば業界全体がよくなるのかという視点を常に持ち続けていることが凄く印象に残ったのを覚えています。</p> <h2>実際に入社して感じたこと</h2> <h5>1. トレタO/Xのエンハンスのために設計機会が豊富</h5> <p>こちらは入社前に予想していた通りでした。</p> <p>僕が担当しているのは認証/権限管理サービスというトレタO/Xの中のごく一部ではあるのですが、入社してすぐに機能設計を行うこととなりました。</p> <p>トレタの設計はかなり綿密で、機能のユースケース洗い出しから一つ一つのデータの概念、またその概念の定義から考えていくこととなります。</p> <p>この時CTOとともにミーティングを重ねつつ設計を進めたのですが、</p> <p>自身よりも圧倒的に技術力のある方からレビューを受けられるというのは、かなり嬉しいカルチャーショックでした。</p> <p>現段階では設計フェーズは終わり、2022年の早い段階で機能リリースできるように実装を進めています。</p> <h5>2. より難易度の高い設計能力を求められる</h5> <p>こちらに関してもサービスの難易度の高さを痛感することとなりました。</p> <p>マイクロサービスを採択していることにより、当然ながら各サービスで保持している機能、保持しているデータが存在します。</p> <p>これらのデータ整合を保ちつつ今後のエンハンスを見据えた機能設計をするためには、様々な前提条件を把握していかなければなりません。</p> <p>ただ前提条件を抑えるだけでもかなり四苦八苦しており、そこからどうすればより良いユーザー体験を作り出すことができるのかというのは非常に難しく、その一方で技術者としては良い環境だと感じています。</p> <h5>3. 労働環境が良い</h5> <p>こちらも入社前伺っていた通り、エンジニアは全員フルリモートに対応して業務に当たっています。</p> <p>また通勤時間がない分、副業をしている社員もいるようです。</p> <p>僕自身も副業や勉強の時間をしたり、自身の趣味に時間を割くことができるようになりました。</p> <h5>4. 心理的安全性が高い</h5> <p>こちらも入社前に予想していた通りでした。</p> <p>業務の中でわからない部分や、設計のために意見や意図を収集したりするのですが、質問した全員が快く相談に乗ってくださります。</p> <p>やはり穏やかな社員が多く、プライベートな話題でもちょっとした笑いを混ぜつつ会話をする方が多いです。</p> <h2>まとめ</h2> <p>入社して何より感じるのは、今トレタは転換期を迎えているということです。</p> <p>コロナによる飲食業界の意識変革、今までのモノリシックな技術からの脱却、O/Xのリリースなど新しい挑戦の場が多く用意されています。</p> <p>だからこそ、事業の成長とともにエンジニアとしてもスキルアップを図ることができるのではないかと考えています。</p> <h1>さいごに</h1> <p>トレタでは一緒に開発する仲間を募集しています。<br> 興味がある方は是非カジュアル面談へお越しください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集 採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> prkrsign890 トレタO/Xの開発にジョインしてやってきたこと hatenablog://entry/13574176438045497371 2021-12-24T10:05:12+09:00 2021-12-26T11:50:02+09:00 こんにちは、パパエンジニアのkentaroです. 本記事はトレタ Advent Calendar 2020の 24 日目の記事です。 去年の終わりにO/Xの開発に携わるようになってから様々な経験を積むことができたので、今回はそれを振り返ってみたいと思います。 なお、「トレタO/X」については代表のnoteをご覧ください。 note.com O/Xジョイン前 トレタの主力商品である予約/顧客台帳サービスのiOSアプリ開発を担当していました。 toreta.in トレタ入社以前も2014年8月から Swift / Objective-C によるiOS開発をずっとやってきていた、というキャリアです。… <p>こんにちは、パパエンジニアの<a href="https://twitter.com/kenkenken_3">kentaro</a>です.</p> <p>本記事は<a href="https://qiita.com/advent-calendar/2021/toreta">トレタ Advent Calendar 2020</a>の 24 日目の記事です。<br /> 去年の終わりにO/Xの開発に携わるようになってから様々な経験を積むことができたので、今回はそれを振り返ってみたいと思います。</p> <p>なお、「トレタO/X」については代表のnoteをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Fhitoshi%2Fn%2Fn3a8c27662f3a" title="ついにリリース。「コスト削減をゴールにしない」逆張りの店内モバイルオーダー、トレタO/Xとはどんなサービスなのか|ひとし|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/hitoshi/n/n3a8c27662f3a">note.com</a></cite></p> <h2>O/Xジョイン前</h2> <p>トレタの主力商品である予約/顧客台帳サービスのiOSアプリ開発を担当していました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftoreta.in%2Fjp%2F" title="トレタ | 【シェアNo.1】お店の予約を、まるごとタブレット1台で。" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://toreta.in/jp/">toreta.in</a></cite></p> <p>トレタ入社以前も2014年8月から Swift / Objective-C によるiOS開発をずっとやってきていた、というキャリアです。<br /> (なお、その前は営業等をやっておりエンジニアではありませんでした。これはこれで語れる話なのですが、別の機会に譲ります。)</p> <h2>O/Xジョイン</h2> <p>2020年11月中旬、当時開催されていたネイティブ定例に<a href="https://tech.toreta.in/entry/2021/08/10/144917">kitagawaさん</a>が参加し、「O/Xでエンジニアが足りていないんだけど、やってみたい人いませんか?」というのに手を挙げたのがきっかけです。 まずはワンダーテーブルさまのよなよなビアワークス向けのモバイルオーダーアプリ開発を担当することになりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kenkenken_3/20211223/20211223201847.png" alt="yonayona" width="800" height="444" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>技術スタックとしてはフロントエンドがFlutter Web、バックエンドがFirebaseという構成でした。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kenkenken_3/20211223/20211223201930.png" alt="firebase" width="1084" height="590" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>ジョインしたはいいがしんどい</h3> <p>11月中に諸々のインプットをしてもらい、12月入ったところで環境構築を行い実際に開発を開始しました。<br /> 今振り返ってみてもこの頃はめちゃくちゃしんどかったです。</p> <p>しんどさの原因は「なんもわからん」状態だったこと。</p> <p>技術面ではそれまで慣れ親しんできたSwiftからDartに変わり、Flutterもしくは採用しているpackageが提供する機能などとにかくキャッチアップすることだらけでした。<br /> 公式ドキュメントが充実していること、SwiftUIの経験があったので宣言的UIへの抵抗感がなかったことは救いでしたが、開発効率はiOSのときとは比べ物にならないほど落ちていた実感がありました。</p> <p>また、モバイルオーダーという文脈だと予約事業とは異なったドメイン知識が要求されるのですが、それが圧倒的に不足していたことも辛かったです。</p> <h3>しんどさをなくす</h3> <p>これはもう特効薬はなく、愚直に「わかるようになる」しかありませんでした。</p> <p>技術面では同僚エンジニアとビデオ会議をしまくり、実装で詰まっているところの相談をしたり、PRの意図の確認をさせてもらったりしていました。<br /> その後は試行錯誤しながらコードを書いては動作確認をしたり、PR作ったり、うまくいかなかったらまた相談したり…と、ひたすらプロダクトと向き合うしかありませんでした。</p> <p>ドメイン知識に関しては社内のドキュメントを読み漁り、わからんことがあれば有識者に質問をしたり…のような当たり前のことを継続し続けていました。</p> <p>2021年2月に自分が担当した比較的大きめな機能追加がリリースされた頃にはしんどさはほとんど無くなっていたように記憶しています。</p> <h2>共通基盤対応</h2> <p>この頃のバックエンドはPoC期間のプロトタイピング用のものだったため、製品版として共通基盤を利用する対応が必要となってきていました。<br /> 全体像としては<a href="https://tech.toreta.in/entry/2021/12/21/182542">マイクロサービスへの挑戦とその結果</a>を参照ください。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kenkenken_3/20211223/20211223202036.png" alt="micro-service" width="953" height="455" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>自分はフロントエンド側の以下対応を担当しました。</p> <ul> <li>ユーザー向けアプリの共通基盤対応 <ul> <li>共通基盤に向けてのAPIリクエスト周りの実装</li> </ul> </li> <li>スタッフ向けアプリのリプレイス <ul> <li>共通基盤に向けてのAPIリクエスト周りの実装</li> <li>Flutter2系を採用</li> <li>Riverpodによる状態管理</li> </ul> </li> </ul> <h3>主な挑戦</h3> <p>APIクライアントの実装に挑戦してみました。<br /> 詳細は割愛しますが、リクエスト用抽象classをextendsしたリクエストclassを渡してあげると、リクエストclass側で期待している型のレスポンスが受け取れるような仕組みになっています。<br /> SwiftではProtocolとGenericsで実装するイメージですが、同じ要領で抽象classとGenericsを使い実装しました。<br /> Dartでも抽象的な処理が書けるんだなーと感動したのを覚えています。</p> <h3>6月リリース</h3> <p>こちらの共通基盤対応は無事6月にリリースされました!</p> <h2>議事録を書くようになった</h2> <p>この頃から日々のMTGやスタンドアップの議事録を書くようになりました。<br /> もともと自分とは別の法人のプロジェクトを担当しているQAが取り組みをはじめていて「ええやん!」と思って真似したのがきっかけです。<br /> 話したのはいいんだけど、結局何が決まって何がNext Actionなんだっけ?というのがテキストとして残るとその場にいなかった人にも共有しやすいし、後から検索もできて便利だなと感じています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2021%2F12%2F14%2F120000" title="しっかりと整備されたドキュメントはなんぼあってもいいですからね、という話 - トレタ開発者ブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tech.toreta.in/entry/2021/12/14/120000">tech.toreta.in</a></cite></p> <p>ちなみにNotionに各種議事録が作成されているのですが、会議体ごとにまとめて表示できるようにしてみたりもしました。<br /> これを作ってたらNotion力が向上したような気がします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kenkenken_3/20211223/20211223202131.png" alt="notion" width="837" height="664" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>React化対応</h2> <p>PoCではFlutterで作っていたユーザー向けモバイルオーダーアプリをReactにリプレイスするプロジェクトです。<br /> 自分は2021年9月から本格的に参加しています。<br /> こちらはまだリリースされておらず、絶賛開発中です。</p> <h3>Webフロントエンドへの挑戦</h3> <p>Flutter版もWebアプリではあったのですが、基本的にプラットフォームのことをあまり気にせず開発できるので、このプロジェクトでWebのフロントエンド開発に業務で初めて携わる感覚がありました。<br /> 技術スタックとしては、TypeScript・React・Next.jsを採用しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/kenkenken_3/20211223/20211223203106.png" alt="" width="697" height="212" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>個人ブログを上記技術スタックで開発した経験はあったものの、業務での開発はやはり一味違いました。<br /> TypeScriptの重鎮にPRをレビューしてもらうので色々と指摘されることはあるのですが、意図・根拠が明確でめちゃくちゃ勉強になりました。<br /> まだまだ勉強することがたくさんありますが、楽しんでやれているのはいいことかなと思っています。</p> <h2>振り返ってみて</h2> <p>本当に色々なことやったんだなーと感じました。1年じゃなくて、2, 3年くらい経ってるんじゃねーの?という感覚です。<br /> 新しい環境に飛び込むとこんなにも密度が高いんだなーという気持ち…<br /> まだ書きたいことはあったけど、キリがないので特に印象深いことに絞って書きました。</p> <p>あと、SwiftもDartもTypeScriptも好きだなーということに気がつけました。みんないい言語!!</p> <h2>最後に</h2> <p>新しい技術に挑戦したい方も、得意な技術でバリューを発揮したいという方も、もし興味がありましたらお気軽にカジュアル面談等お申し込みください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集 採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> kenkenken_3 RailsでBFFを構築した際、複雑にならないように気をつけたこと hatenablog://entry/13574176438044891168 2021-12-22T11:00:00+09:00 2021-12-22T11:02:02+09:00 はじめに この記事はトレタ Advent Calendar 2021 の22日目の記事です。 こんにちは、昨年と同じ日の投稿になりました、サーバーサイドエンジニアの @shiroemons です。 GoとRuby on Railsを書いてます。 最近、RailsでBFF(Backends For Frontends)を構築しました。 BFFについての細かい説明は省略しますが、簡単に言いますとフロントエンドからのリクエストに応じて複数のバックエンドに対して、APIコールしデータを取得し内容を加工してフロントエンドに返却するAPIサーバーです。 構築した際に、複雑にならないように気をつけたポイント… <h2>はじめに</h2> <p>この記事は<a href="https://qiita.com/advent-calendar/2021/toreta">トレタ Advent Calendar 2021</a> の22日目の記事です。</p> <p>こんにちは、昨年と同じ日の投稿になりました、サーバーサイドエンジニアの <a href="https://twitter.com/shiroemons">@shiroemons</a> です。</p> <p>GoとRuby on Railsを書いてます。</p> <p>最近、RailsでBFF(Backends For Frontends)を構築しました。 BFFについての細かい説明は省略しますが、簡単に言いますとフロントエンドからのリクエストに応じて複数のバックエンドに対して、APIコールしデータを取得し内容を加工してフロントエンドに返却するAPIサーバーです。</p> <p>構築した際に、複雑にならないように気をつけたポイントを3点紹介します。</p> <h2>バックエンド毎のClientをPOROで作成</h2> <p>データを取得するバックエンドは、GraphQLとRESTと異なるAPIが存在していました。</p> <p>GraphQLと通信する際、<a href="https://github.com/github/graphql-client">graphql-client</a>を検討しましたが、AuthorizationヘッダーやIdempotency-Keyヘッダーをリクエスト毎に変える必要があったため、使い方がアンマッチでしたので不採用としました。 採用した方法は、HTTPクライアントライブラリの <a href="https://github.com/lostisland/faraday">Faraday</a> を使用し、各バックエンド用(GraphQL, REST)のClientをPORO(Plain Old Ruby Object) で作成して対応しました。</p> <p>サンプルを以下に記載しています。</p> <h3>GraphQL用のClient</h3> <ul> <li>親クラス</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/models/graphql_example_client/client.rb</span> <span class="synPreProc">module</span> <span class="synType">GraphqlExampleClient</span> <span class="synPreProc">class</span> <span class="synType">Client</span> <span class="synPreProc">def</span> <span class="synIdentifier">initialize</span>(auth_token, **options) <span class="synIdentifier">@auth_token</span> = auth_token <span class="synIdentifier">@options</span> = options <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">call</span> response = run <span class="synType">JSON</span>.parse(response.body) <span class="synPreProc">rescue</span> <span class="synType">JSON</span>::<span class="synType">ParserError</span> { <span class="synConstant">errors</span>: [ { <span class="synConstant">code</span>: response.status, <span class="synConstant">message</span>: response.body } ] } <span class="synPreProc">end</span> <span class="synStatement">private</span> <span class="synPreProc">def</span> <span class="synIdentifier">run</span> <span class="synType">Faraday</span>.post(example_graphql_url, request_body, headers) <span class="synPreProc">end</span> <span class="synComment"># https://example.com/graphql</span> <span class="synPreProc">def</span> <span class="synIdentifier">example_graphql_url</span> <span class="synIdentifier">ENV</span>[<span class="synSpecial">'</span><span class="synConstant">EXAMPLE_GRAPHQL_URL</span><span class="synSpecial">'</span>] <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">request_body</span> { <span class="synConstant">query</span>: query, <span class="synConstant">variables</span>: variables }.to_json <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">query</span> <span class="synStatement">raise</span> <span class="synSpecial">'</span><span class="synConstant">query called on parent.</span><span class="synSpecial">'</span> <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">variables</span> <span class="synStatement">raise</span> <span class="synSpecial">'</span><span class="synConstant">variables called on parent.</span><span class="synSpecial">'</span> <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">headers</span> { <span class="synSpecial">'</span><span class="synConstant">Content-Type</span><span class="synSpecial">'</span> =&gt; <span class="synSpecial">'</span><span class="synConstant">application/json</span><span class="synSpecial">'</span>, <span class="synSpecial">'</span><span class="synConstant">Authorization</span><span class="synSpecial">'</span> =&gt; <span class="synIdentifier">@auth_token</span>, <span class="synSpecial">'</span><span class="synConstant">Idempotency-Key</span><span class="synSpecial">'</span> =&gt; idempotency_key } <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">idempotency_key</span> ... <span class="synPreProc">end</span> <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>親クラスを継承し、1クエリ単位でファイルを作成 <ul> <li>プレフィックスに<code>Query</code>や<code>Mutation</code>を付けておく</li> </ul> </li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/models/graphql_example_client/query_locations.rb</span> <span class="synPreProc">module</span> <span class="synType">GraphQLExampleClient</span> <span class="synPreProc">class</span> <span class="synType">QueryLocations</span> &lt; <span class="synType">Client</span> <span class="synStatement">private</span> <span class="synPreProc">def</span> <span class="synIdentifier">query</span> &lt;&lt;~<span class="synSpecial">GRAPHQL</span>.freeze <span class="synConstant"> query locations($where: LocationWhereClause!, $limit: Int, $offset: Int) {</span> <span class="synConstant"> locations(where: $where, limit: $limit, offset: $offset) {</span> <span class="synConstant"> locations {</span> <span class="synConstant"> id</span> <span class="synConstant"> name</span> <span class="synConstant"> createdAt</span> <span class="synConstant"> updatedAt</span> <span class="synConstant"> }</span> <span class="synConstant"> }</span> <span class="synConstant"> }</span> <span class="synConstant"> </span><span class="synSpecial">GRAPHQL</span> <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">variables</span> { <span class="synConstant">where</span>: { <span class="synConstant">createdAt</span>: { <span class="synConstant">gt</span>: <span class="synConstant">0</span> } }, <span class="synConstant">limit</span>: limit, <span class="synConstant">offset</span>: offset } <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">limit</span> ... <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">offset</span> ... <span class="synPreProc">end</span> <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>使い方</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink>response_body = <span class="synType">GraphQLExampleClient</span>::<span class="synType">QueryLocations</span>.new(<span class="synIdentifier">@auth_token</span>, <span class="synConstant">limit</span>: params[<span class="synConstant">:limit</span>], <span class="synConstant">offset</span>: params[<span class="synConstant">:offset</span>]).call </pre> <h3>REST用のClient</h3> <ul> <li>親クラス</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/models/rest_example_client/client.rb</span> <span class="synPreProc">module</span> <span class="synType">RestExampleClient</span> <span class="synPreProc">class</span> <span class="synType">Client</span> <span class="synPreProc">def</span> <span class="synIdentifier">initialize</span>(auth_token, **options) <span class="synIdentifier">@auth_token</span> = auth_token <span class="synIdentifier">@options</span> = options <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">call</span> response = run <span class="synType">JSON</span>.parse(response.body) <span class="synPreProc">rescue</span> <span class="synType">JSON</span>::<span class="synType">ParserError</span> { <span class="synConstant">error</span>: { <span class="synConstant">code</span>: response.status, <span class="synConstant">message</span>: response.body } } <span class="synPreProc">end</span> <span class="synStatement">private</span> <span class="synPreProc">def</span> <span class="synIdentifier">rest_example_base_url</span> <span class="synIdentifier">ENV</span>[<span class="synSpecial">'</span><span class="synConstant">REST_EXAMPLE_BASE_URL</span><span class="synSpecial">'</span>] <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">run</span> <span class="synStatement">raise</span> <span class="synSpecial">'</span><span class="synConstant">run called on parent.</span><span class="synSpecial">'</span> <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">request_body</span> <span class="synStatement">raise</span> <span class="synSpecial">'</span><span class="synConstant">request_body called on parent.</span><span class="synSpecial">'</span> <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">headers</span> { <span class="synSpecial">'</span><span class="synConstant">Content-Type</span><span class="synSpecial">'</span> =&gt; <span class="synSpecial">'</span><span class="synConstant">application/json</span><span class="synSpecial">'</span>, <span class="synSpecial">'</span><span class="synConstant">Authorization</span><span class="synSpecial">'</span> =&gt; <span class="synIdentifier">@auth_token</span> } <span class="synPreProc">end</span> <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>親クラスを継承し1API単位でファイルを作成 <ul> <li>プレフィックスに<code>Get</code>や<code>Post</code>を付けておく</li> </ul> </li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/models/rest_example_client/post_example.rb</span> <span class="synPreProc">module</span> <span class="synType">RestExampleClient</span> <span class="synPreProc">class</span> <span class="synType">PostExample</span> &lt; <span class="synType">Client</span> <span class="synStatement">private</span> <span class="synPreProc">def</span> <span class="synIdentifier">run</span> <span class="synType">Faraday</span>.post(url, request_body, headers) <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">url</span> <span class="synSpecial">&quot;#{</span>rest_example_base_url<span class="synSpecial">}</span><span class="synConstant">/v1/example</span><span class="synSpecial">&quot;</span> <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">request_body</span> { <span class="synConstant">hoge</span>: hoge, ... }.to_json <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">hoge</span> <span class="synIdentifier">@options</span>[<span class="synConstant">:hoge</span>] <span class="synPreProc">end</span> <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>使い方</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink>response_body = <span class="synType">RestExampleClient</span>::<span class="synType">PostExample</span>.new(<span class="synIdentifier">@auth_token</span>, <span class="synConstant">hoge</span>: params[<span class="synConstant">:hoge</span>]).call </pre> <p>使い方は、<code>new</code>して<code>call</code>するだけなので簡単です✨</p> <p>1リクエスト毎にファイルが別れているのでコードをスッキリ書くことができます✨</p> <h2>Interactor層の導入</h2> <p>BFFの特性上、クライアントからの1リクエストで同じバックエンドに対し別々のAPIを呼び出したり、他のバックエンドに対してもAPIを呼び出すことがあります。 愚直に書くとすぐに複雑になりテストも書きづらくなります。 そこで、複雑なビジネスロジックを解消するために、Interactor層を導入しました。 Interactor層を導入することで、ビジネスロジックをカプセル化することができます。</p> <p>使用したgemは、 <a href="https://github.com/collectiveidea/interactor-rails">collectiveidea/interactor-rails</a> です。 導入方法は、READMEを参照ください。</p> <p>サンプルを以下に記載しています。 良いサンプルを用意できなかったため、具体例を使って紹介します。</p> <ul> <li>処理 <ul> <li>請求書から領収書IDを取得する処理(GraphQL)</li> <li>既存の領収書を無効化する処理(GraphQL)</li> <li>領収書を再発行する処理(REST)</li> </ul> </li> </ul> <h3>Interactorのサンプル</h3> <ul> <li>取りまとめ役のファイル</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/interactors/invoice_receipt.rb</span> <span class="synPreProc">class</span> <span class="synType">InvoiceReceipt</span> <span class="synPreProc">include</span> <span class="synType">Interactor</span>::<span class="synType">Organizer</span> <span class="synComment"># 上から順番に実行されます。</span> organize <span class="synType">InvoiceReceipt</span>::<span class="synType">GraphqlExampleFetchInvoiceWithReceipt</span>, <span class="synType">InvoiceReceipt</span>::<span class="synType">GraphqlExampleVoidReceipts</span>, <span class="synType">InvoiceReceipt</span>::<span class="synType">RestExamplePostReissueReceipt</span> <span class="synPreProc">end</span> </pre> <ul> <li>請求書から領収書IDを取得する処理</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># /app/interactors/invoice_receipt/graphql_example_fetch_invoice_with_receipt.rb</span> <span class="synPreProc">class</span> <span class="synType">InvoiceReceipt</span>::<span class="synType">GraphqlExampleFetchInvoiceWithReceipt</span> <span class="synPreProc">include</span> <span class="synType">Interactor</span> <span class="synPreProc">def</span> <span class="synIdentifier">call</span> result = fetch_invoice_with_receipt <span class="synStatement">if</span> result[<span class="synConstant">:errors</span>].present? context.fail!(<span class="synConstant">error</span>: result[<span class="synConstant">:errors</span>]) <span class="synStatement">else</span> context.receipt_id = result[<span class="synConstant">:data</span>][<span class="synConstant">:receipt_id</span>] <span class="synStatement">end</span> <span class="synPreProc">end</span> <span class="synStatement">private</span> <span class="synComment"># GraphQLから請求書を取得する処理(GraphQLのQueryを実行する処理)</span> <span class="synPreProc">def</span> <span class="synIdentifier">fetch_invoice_with_receipt</span> ... <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>既存の領収書を無効化する処理</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/interactors/invoice_receipt/graphql_example_void_receipts.rb</span> <span class="synPreProc">class</span> <span class="synType">InvoiceReceipt</span>::<span class="synType">GraphqlExampleVoidReceipts</span> <span class="synPreProc">include</span> <span class="synType">Interactor</span> <span class="synPreProc">def</span> <span class="synIdentifier">call</span> <span class="synStatement">return</span> <span class="synStatement">if</span> context.receipt_id.blank? result = void_receipts <span class="synStatement">if</span> result[<span class="synConstant">:errors</span>].present? context.fail!(<span class="synConstant">error</span>: result[<span class="synConstant">:errors</span>]) <span class="synStatement">else</span> context.void_receipt = result[<span class="synConstant">:data</span>] <span class="synStatement">end</span> <span class="synPreProc">end</span> <span class="synComment"># 途中で失敗した場合、rollbackメソッドが呼び出される</span> <span class="synPreProc">def</span> <span class="synIdentifier">rollback</span> ... <span class="synPreProc">end</span> <span class="synStatement">private</span> <span class="synComment"># 既存の領収書を無効化する処理(GraphQLのMutationを実行する処理)</span> <span class="synPreProc">def</span> <span class="synIdentifier">void_receipts</span> ... <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>領収書を再発行する処理</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/interactors/invoice_receipt/rest_example_post_reissue_receipt.rb</span> <span class="synPreProc">class</span> <span class="synType">InvoiceReceipt</span>::<span class="synType">RestExamplePostReissueReceipt</span> <span class="synPreProc">include</span> <span class="synType">Interactor</span> <span class="synPreProc">def</span> <span class="synIdentifier">call</span> result = post_reissue_receipt <span class="synStatement">if</span> result[<span class="synConstant">:error</span>].present? context.fail!(<span class="synConstant">error</span>: result[<span class="synConstant">:error</span>]) <span class="synStatement">else</span> context.receipt = result <span class="synStatement">end</span> <span class="synPreProc">end</span> <span class="synStatement">private</span> <span class="synComment"># 領収書を再発行する処理(RESTのPostを実行する処理)</span> <span class="synPreProc">def</span> <span class="synIdentifier">post_reissue_receipt</span> ... <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>実際の使われ方</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synPreProc">class</span> <span class="synType">Invoices</span>::<span class="synType">ReceiptsController</span> &lt; <span class="synType">ApplicationController</span> <span class="synPreProc">def</span> <span class="synIdentifier">create</span> <span class="synComment"># 領収書再発行処理</span> result = <span class="synType">InvoiceReceipt</span>.call( <span class="synConstant">auth_token</span>: <span class="synIdentifier">@auth_token</span>, <span class="synConstant">location_id</span>: params[<span class="synConstant">:location_id</span>], <span class="synConstant">invoice_id</span>: params[<span class="synConstant">:invoice_id</span>], <span class="synConstant">mail_address</span>: params[<span class="synConstant">:email</span>], <span class="synConstant">name</span>: params[<span class="synConstant">:name</span>] ) <span class="synStatement">if</span> result.success? render <span class="synConstant">json</span>: <span class="synType">ReceiptPresenter</span>.new.as_json, <span class="synConstant">status</span>: <span class="synConstant">:ok</span> <span class="synStatement">else</span> render <span class="synConstant">json</span>: <span class="synType">ErrorsPresenter</span>.new(result.error), <span class="synConstant">status</span>: <span class="synConstant">:bad_request</span> <span class="synStatement">end</span> <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <p>使い方は、取りまとめ役のファイルを<code>call</code>するだけなので簡単です✨</p> <p>個別の責務を1ファイルで書くことで、可読性も向上し複雑にならずコントローラーもスッキリ書くことができます✨</p> <h2>Presenter層の導入</h2> <p>BFFということで、クライアントが必要とする形でレスポンスを返さないといけません。 APIモードで動かしているためViewファイルでJSONの組み立ては行わず、Presenter層を導入して対応しました。 こちらもPORO(Plain Old Ruby Object)で実装しました。</p> <p>サンプルを以下に記載しています。</p> <h3>Presenterのサンプル</h3> <ul> <li>親クラス</li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/presenters/presenter.rb</span> <span class="synPreProc">class</span> <span class="synType">Presenter</span> <span class="synPreProc">def</span> <span class="synIdentifier">initialize</span>(object = <span class="synConstant">nil</span>) <span class="synIdentifier">@object</span> = object <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">as_json</span> <span class="synStatement">raise</span> <span class="synSpecial">'</span><span class="synConstant">as_json called on parent.</span><span class="synSpecial">'</span> <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>親クラスを継承してファイルを作成 <ul> <li>サフィックスに<code>Presenter</code>を付ける</li> </ul> </li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/presenters/locations_presenter.rb</span> <span class="synPreProc">class</span> <span class="synType">LocationsPresenter</span> &lt; <span class="synType">Presenter</span> <span class="synPreProc">def</span> <span class="synIdentifier">as_json</span>(*) { <span class="synConstant">locations</span>: locations } <span class="synPreProc">end</span> <span class="synStatement">private</span> <span class="synPreProc">def</span> <span class="synIdentifier">locations</span> <span class="synIdentifier">@object</span>&amp;.map { |<span class="synIdentifier">o</span>| location(o) } || [] <span class="synPreProc">end</span> <span class="synPreProc">def</span> <span class="synIdentifier">location</span>(object) { <span class="synConstant">id</span>: object[<span class="synConstant">:id</span>], <span class="synConstant">name</span>: object[<span class="synConstant">:name</span>], <span class="synConstant">created_at</span>: object[<span class="synConstant">:created_at</span>], <span class="synConstant">updated_at</span>: object[<span class="synConstant">:updated_at</span>] } <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <ul> <li>実際の使われ方 <ul> <li>mergeメソッドを使用したりして整形しています</li> </ul> </li> </ul> <pre class="code lang-ruby" data-lang="ruby" data-unlink><span class="synComment"># app/controllers/locations_controller.rb</span> <span class="synPreProc">class</span> <span class="synType">LocationsController</span> &lt; <span class="synType">ApplicationController</span> <span class="synPreProc">def</span> <span class="synIdentifier">index</span> response_body = <span class="synType">GraphQLExampleClient</span>::<span class="synType">QueryLocations</span>.new(<span class="synIdentifier">@auth_token</span>, <span class="synConstant">limit</span>: params[<span class="synConstant">:limit</span>], <span class="synConstant">offset</span>: params[<span class="synConstant">:offset</span>]).call status = <span class="synConstant">:ok</span> <span class="synStatement">if</span> response_body[<span class="synConstant">:errors</span>] response_json = <span class="synType">ErrorsPresenter</span>.new(response_body[<span class="synConstant">:errors</span>]) status = <span class="synConstant">:bad_request</span> <span class="synStatement">else</span> locations_data = response_body[<span class="synConstant">:data</span>][<span class="synConstant">:locations</span>] total = locations_data[<span class="synConstant">:total</span>] locations = <span class="synType">LocationsPresenter</span>.new(locations_data[<span class="synConstant">:locations</span>]) pagination = <span class="synType">PaginationPresenter</span>.new(<span class="synType">Pagination</span>.new(total, params[<span class="synConstant">:per_page</span>], params[<span class="synConstant">:page</span>])) response_json = locations.as_json.merge(pagination.as_json) <span class="synStatement">end</span> render <span class="synConstant">json</span>: response_json, <span class="synConstant">status</span>: status <span class="synPreProc">end</span> <span class="synPreProc">end</span> </pre> <p>使い方は、<code>new</code>して<code>as_json</code>するだけなので簡単です✨</p> <h2>おわりに</h2> <p>コードの紹介がメインとなりましたが、できるだけ複雑にならないように気をつけてコードを書いています。雰囲気だけでも読み取れてもらえたら幸いです。</p> <p>トレタに少しでも興味を持っていただいた方がいれば、カジュアル面談などお気軽にご応募ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集 採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> shiroemons マイクロサービスへの挑戦とその結果 hatenablog://entry/13574176438044016049 2021-12-21T18:25:42+09:00 2021-12-21T18:28:03+09:00 こんにちは。エンジニアのkitagawaです。 こちらはトレタアドベントカレンダー2021 21日目の記事です。 今年は新規サービスのトレタO/Xに心血注いだ一年でした。 振り返りを込めて、トレタO/Xのバックエンドとしてマイクロサービスを導入したことについて紹介します。 新規サービス「トレタO/X」 www.toreta-ox.com トレタO/Xは飲食店向けのモバイルオーダーアプリです。飲食店の来店者はテーブルごとに渡されるQRコードが印字された紙を自身のスマートフォンで読み込んで、トレタO/Xアプリを開きます。 トレタO/Xから料理の注文が行えて、オンライン決済でスムーズに退店することが… <p>こんにちは。エンジニアのkitagawaです。</p> <p>こちらは<a href="https://qiita.com/advent-calendar/2021/toreta">トレタアドベントカレンダー2021</a> 21日目の記事です。</p> <p>今年は新規サービスの<a href="https://www.toreta-ox.com/">トレタO/X</a>に心血注いだ一年でした。 振り返りを込めて、トレタO/Xのバックエンドとしてマイクロサービスを導入したことについて紹介します。</p> <h2>新規サービス「トレタO/X」</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.toreta-ox.com%2F" title="トレタO/X | お客さまに楽しい注文体験を。" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.toreta-ox.com/">www.toreta-ox.com</a></cite> トレタO/Xは飲食店向けのモバイルオーダーアプリです。飲食店の来店者はテーブルごとに渡されるQRコードが印字された紙を自身のスマートフォンで読み込んで、トレタO/Xアプリを開きます。 トレタO/Xから料理の注文が行えて、オンライン決済でスムーズに退店することができます。</p> <p>トレタO/Xの特徴としては、そのお店ごとの雰囲気を表現したリッチなUIです。 そのためシステム構成は、メニューブックを柔軟に表現できるように各社ごとにフロントエンドのアプリをそれぞれ作っています。 一方でバックエンドは共通の機能を提供するSaaSモデルを提供しています。 そうすることで、各社ごとに異なったUIを提供しつつ、共通のバックエンドで機能エンハンスを行えるようにしています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20210803/20210803101055.png" alt="f:id:mkitagawa-312:20210803101055p:plain" width="800" height="444" 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/m/mkitagawa-312/20210803/20210803101115.png" alt="f:id:mkitagawa-312:20210803101115p:plain" width="800" height="444" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>マイクロサービスを導入した背景</h2> <p>トレタO/Xの開発が本格的に始動したのは約2年前です。 コロナ渦で飲食店が厳しい状況を強いられている中、トレタも大きな打撃を受けました。 そこで今まで予約台帳サービスをはじめとする飲食店の「予約」事業の会社から、「飲食店支援」の会社へ変わろうと新しい事業の模索が行われました。 いくつかの新規サービスが立ち上がり、その中で注力事業として置いたのがトレタO/Xです。</p> <p>トレタO/XはプロトタイプでのPoC期間を経て、次はシステムをスケールさせるために製品版としてバックエンドのリニューアルを行いました。 その際にシステム構成を検討する上で主軸とした考えが、<strong>システムの再利用性</strong>です。</p> <h3>「強くてニューゲーム」ができる開発組織</h3> <p>トレタO/Xを予約事業の次の事業の柱にする意気込みでバックエンドの設計検討を行いましたが、 その先を見据えるとトレタが次は単に「オーダーアプリ」の会社になるのではなく、「飲食店支援」の会社として今後も次々と新規サービスを出していく会社になる必要があると考えました。</p> <p>そこでトレタO/Xのシステムはその足がかりとなるように、トレタO/Xで作ったシステムの一部が他の新規サービスでも再利用できるような構成を理想の姿としました。 そうすることで、今後の新規サービスを立ち上げる際には毎回0からのスクラッチではなく、<strong>資産を流用することで開発期間が短縮された「強くてニューゲーム」</strong>が行える組織になると考えました。</p> <p>そのため、「O/Xのバックエンド」ではなく「トレタの共通基盤システム」として、特定のコンテキストで区切られたシステムを組み合わせて構成するマイクロサービスを自然と選ぶことになりました。</p> <h2>トレタO/Xのシステム構成</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20211220/20211220230607.png" alt="f:id:mkitagawa-312:20211220230607p:plain" width="852" height="448" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> トレタO/Xのシステムは大まかに以下のマイクロサービスで構成されています。</p> <ul> <li>認証サービス <ul> <li>Auth0をベースにし、各種サービスごとの権限やロールなどを管理するサービス</li> </ul> </li> <li>注文サービス <ul> <li>トレタO/Xのメインとなる注文データやユーザーのセッション情報を扱うサービス</li> <li>変更履歴が全て保存されるDatomicをDBとして、Clojureで書かれたGraphQLサーバー</li> </ul> </li> <li>決済サービス <ul> <li>決済や返金なでの決済処理や決済データを管理するサービス</li> <li>Stripeなどの決済プラットフォームをラップし抽象化する</li> </ul> </li> <li>印刷サービス <ul> <li>注文データやQRコードをキッチンプリンタへ印刷するサービス</li> <li>印刷ジョブを管理し、ネットワークプリンタから定期的に送られてくる印刷ジョブチェックのリクエストを捌く</li> </ul> </li> <li>APIサーバー <ul> <li>クライアントアプリと各種マイクロサービスとの間をかけもつファサード層</li> <li>外部公開用のAPIを提供する</li> </ul> </li> </ul> <p>ご覧の通り使用している言語がバラバラでも成立するのがマイクロサービスの一つの利点かと思います。 技術選定は在籍エンジニアの技術スタックであったり、システム要件としてデータベースにあわせた言語や連携先サービスのSDKの提供状況などを加味しています。</p> <h2>マイクロサービスにしてみて</h2> <p><strong>「マイクロサービスは銀の弾丸ではない」</strong>とはよく言われるもので覚悟はしていましたが、やはりいろいろと地雷は踏みました。</p> <h3>分散トランザクションによる問題</h3> <ul> <li>タイムアウトなどにで一部にだけデータが入りロールバックできず不整合</li> <li>リトライや冪等性、到達保証など考慮する点が多い</li> <li>トラッキングIDを入れないと調査に一苦労</li> <li>結局は分散トランザクションが生じている時点でドメイン境界が正しく切れていない</li> </ul> <h3>インフラコストの問題</h3> <ul> <li>それぞれのコンテナ実行環境構築と管理するためのインフラエンジニアが不足</li> <li>デプロイさせるシステムの順番を間違えると事故が起きる可能性もある</li> </ul> <h3>徐々にマイクロサービス化するという幻想</h3> <p>苦労する部分は多いですがやってみて良かったと思うことの一つに、最初からマイクロサービスを前提にしていたからこそ正しく正規化や抽象化されたデータ構造に近づけたという点です。</p> <p>「初期の段階からマイクロサービスにするな」というのもよく言われる話です。 ただ、モノリスをつくってから徐々に切り出していく、というのもそう簡単ではありまえせん。 トレタでも予約台帳のサーバーは約8年越しの巨大なモノリスとなっており、部分的に切り崩そうとしてもデータが依存しあったりコードが絡みあっていたりで、話があがるたびには消えていました。 そもそもリプレイスや大規模リファクタリングは事業フェーズのタイミングとリソース(人、金)が揃わないとなかなか行うことができません。</p> <p>今回のようにPoCでコンテキストの境界を検証したり仕様を概ね出し切っておき、プロトタイプを脱する時点でマイクロサービスを検討するのは良いタイミングだったかと思います。</p> <h1>さいごに</h1> <p>マイクロサービスにするとやはりエンジニアの頭数がどうしても必要になってきます。 トレタではエンジニアの採用を現在全方面オープンしています。 飲食店のDX化が急速に進んでいる昨今で、一緒に未来の飲食業界を作っていく仲間をお待ちしてます!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集 採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> mkitagawa-312 アクセシビリティ本の輪読会をした話 hatenablog://entry/13574176438040270580 2021-12-19T01:01:26+09:00 2021-12-28T02:09:21+09:00 こんにちは。フロントエンドエンジニアの白濱です。 この記事は トレタ Advent Calendar 2021 の 19日目の記事です。 昨年のアドベントカレンダーでは 「アクセシビリティを気にし出したきっかけと、2020年の振り返り」 という記事を書きました。 zenn.dev その中で、「来年は輪読会をやるぞ!」と言っていました。 今年実際にアクセシビリティ本の輪読会を行なったので、この記事ではそのことについて振り返りたいと思います。 プロダクト開発部の有志での輪読会を提案 去年のアドベントカレンダーを書いている最中、気持ちが高まって勢いで提案しました。 当時は仕事で関わりがあったデザイナ… <p>こんにちは。フロントエンドエンジニアの白濱です。</p> <p>この記事は <a href="https://qiita.com/advent-calendar/2021/toreta">トレタ Advent Calendar 2021</a> の 19日目の記事です。</p> <p>昨年のアドベントカレンダーでは <a href="https://zenn.dev/shira/articles/222c09429e6583">「アクセシビリティを気にし出したきっかけと、2020年の振り返り」</a> という記事を書きました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fzenn.dev%2Fshira%2Farticles%2F222c09429e6583" title="アクセシビリティを気にし出したきっかけと、2020年の振り返り" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://zenn.dev/shira/articles/222c09429e6583">zenn.dev</a></cite></p> <p>その中で、「来年は輪読会をやるぞ!」と言っていました。</p> <p>今年実際にアクセシビリティ本の輪読会を行なったので、この記事ではそのことについて振り返りたいと思います。</p> <h2>プロダクト開発部の有志での輪読会を提案</h2> <p>去年のアドベントカレンダーを書いている最中、気持ちが高まって勢いで提案しました。</p> <p>当時は仕事で関わりがあったデザイナーさんがお一人だけだったということもあり、提案した時ちょっとドキドキしていた気がします。笑 <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/p/punipunityan/20211216/20211216032459.png" alt="f:id:punipunityan:20211216032459p:plain" width="1200" height="979" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure> <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/p/punipunityan/20211219/20211219001919.png" alt="f:id:punipunityan:20211219001919p:plain" width="1200" height="822" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure></p> <h2>輪読会開催の目的</h2> <p>輪読会を提案した目的は以下のようなものでした。</p> <ul> <li>アクセシビリティに関して知り、考える<strong>きっかけを作る</strong>こと</li> <li>アクセシビリティ向上に取り組む<strong>土台を作る</strong>こと</li> <li>プロジェクトを超えてデザイナーとエンドエンジニアの<strong>情報共有を気軽に行えるようにする</strong>こと</li> </ul> <h2>2021年上期の輪読会</h2> <h3>読んだ本</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.amazon.co.jp%2Fdp%2F4862462650%2F" title="デザイニングWebアクセシビリティ - アクセシブルな設計やコンテンツ制作のアプローチ | 太田良典, 伊原力也 |本 | 通販 | Amazon" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.amazon.co.jp/dp/4862462650/">www.amazon.co.jp</a></cite></p> <p>まずは、<a href="https://www.amazon.co.jp/dp/4862462650/">「デザイニングWebアクセシビリティ - アクセシブルな設計やコンテンツ制作のアプローチ」</a> を読みました。</p> <p>わかりやすい具体例が多く掲載されており、初心者にとってとても読みやすい本です。</p> <p>この本を選んだ理由は、有識者の方からおすすめされた本の中で、<strong>一番デザイナーと一緒に読みたいと思っていた本</strong>だからです。</p> <p>去年、実装面での改善を行おうとした際、実装だけでは変えようがないことが思っていたより多いと感じました。</p> <p>そのため、デザイン時点でアクセシビリティの考慮が必要なことを一緒に学びたいと思っていました。</p> <h3>輪読会の進め方</h3> <p>参加メンバーはデザイナー全員に加え、興味を持っていたエンジニア・PMで合わせて5〜10名ほどで行なっていました。</p> <p>進め方はメンバーで相談し、以下のように進めていました。</p> <ul> <li>毎週月曜日17時から18時開催</li> <li>事前準備なし</li> <li>前半30分、各自黙読(毎回約20ページほど)</li> <li>後半30分、感想・疑問など自由に共有</li> <li>議事録をとって共有</li> </ul> <p>各々属しているプロジェクトで多忙だったため、<strong>なるべく負担にならないやり方を採用</strong>しました。</p> <p>1冊読み終わったあとは、読んで終わりだと勿体無いので、それをどう業務に活かしていくかを考えました。</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/p/punipunityan/20211216/20211216034603.png" alt="f:id:punipunityan:20211216034603p:plain" width="1200" height="680" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></figure></p> <p>チェックリストを独自で作った理由は</p> <ul> <li>自分たちで本を見返してピックアップすることにより、気をつけようという意識が高まること</li> <li>まずは小さく始めるため、いくつかピックアップする方が良い</li> </ul> <p>と考えたからです。ただ、まとめみると結果的に80項目ほどになり、いきなり始めるにはちょっと大変な量になりました。</p> <p>チェックリストを作って以降、運用は個人任せにになっているので、今後のやり方は考えていきたいです。</p> <h2>2021年下期の輪読会</h2> <p>1冊読み終わった時点で、当初の目的としていた</p> <ul> <li>アクセシビリティに関して知り、考えるきっかけを作ること</li> <li>プロジェクトを超えてデザイナーとエンドエンジニアの情報共有を気軽に行えるようにすること</li> </ul> <p>は達成できたと感じていました。</p> <p>その上で、より学んでいきたいと言う声が多かったので下期も輪読会を行いました。</p> <h3>読んだ本</h3> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.amazon.co.jp%2Fdp%2F4862464513" title="Form Design Patterns ―シンプルでインクルーシブなフォーム制作実践ガイド | Adam Silver, 土屋 一彦, 株式会社Bスプラウト |本 | 通販 | Amazon" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.amazon.co.jp/dp/4862464513">www.amazon.co.jp</a></cite></p> <p>下期は<a href="https://www.amazon.co.jp/dp/4862464513">「Form Design Patterns ―シンプルでインクルーシブなフォーム制作実践ガイド」</a>という本を読みました。</p> <p>こちらの本も具体例が多く掲載されており、今後も読み返したい一冊です。</p> <p>トレタの既存プロダクトを時折振り返りながら読み進め、有意義な輪読会となりました。</p> <h3>輪読会の進め方</h3> <p>下期も上期とほぼ同じような進め方でしたが、議事録のとり方は変えました。<br /> 上期は私が議事録を書いていたのですが、下期は各自読みながら感想や疑問点をまとめる形に変更しました。<br /> このやり方に変更することで、それまでより効率的に進めることができるようになりました。</p> <h2>輪読会を終えて</h2> <p>最後に、輪読会参加メンバーから輪読会の振り返りコメントをいただいたので紹介したいと思います。 (わかりやすくするため、デザイナーとエンジニア分けて掲載しています。)</p> <h3>良かったこと</h3> <h4>デザイナー</h4> <ul> <li>輪読会という形式でエンジニアと一緒に読み進めたので、ディスカッションすることで理解度が増した。</li> <li>フォームのUI・作法の理解度が深まったことで、デザインとして採用する際にもなぜこれが適切なのか説明しやすくなった。アクセシビリティを理解せずに推奨されたフォームをアレンジすると機能しなくなるケースもあるなど学びも多かった。</li> <li>トレタのプロダクトにもあるようなフォームが実例をもとにまとめられており、既存のUIにもアクセシビリティ観点で問題のある箇所を発見することができた。例えば、これまではスクリーンリーダーユーザーに対して読み上げられる要素や順番について考慮ができていなかったことなど。</li> </ul> <h4>エンジニア</h4> <ul> <li>これまで意識できていなかったことも多々あり、認識を改めることができた</li> <li>本で読んだことに関して共通認識が持てているので、実際の業務における相談がやりやすくなった</li> <li>普段業務で関わっていないデザイナーともコミュニケーションを取りやすくなった</li> </ul> <h3>課題に感じたこと</h3> <h4>デザイナー</h4> <ul> <li>これまでトレタは飲食店向けのtoBプロダクトが主要でユーザーの状況もある程度限られており、「誰にとっても使いやすいこと」より「特定のユーザーにとって使いやすいこと」を重視したいた。でも<a href="https://www.toreta-ox.com/">O/X</a>のようなプロダクトではこれまでのスタンスを見直す必要がある。知識の習得で終わらせずに、既存プロダクトの改修など実践に移すためにどう取り組むかが今後の課題。</li> </ul> <h4>エンジニア</h4> <ul> <li>チェックリストを作ったが、運用が個人任せになっており取り組みにくい状態になっている</li> <li>自動チェックの仕組みを増やすなどやりたいことはたくさんあるが、なかなか時間を作れていない</li> <li>アクセシビリティ輪読会から実践に移していく場合、輪読会として動き続けるのは難しい。サブプロジェクトもしくはプロジェクトとして立ち上げたりと、やり方を考える必要がありそう。プロジェクト毎の改善活動の一貫としてやるのが現実的かも。</li> </ul> <h3>今後取り組んでいきたいこと</h3> <h4>デザイナー</h4> <ul> <li>フォーム周りは文字入力の時など実際触ってみないと気がつかないことが特に多い。デザイン段階でもユーザーの状況・環境を具体的にイメージできるようプロトタイピングなどで積極的に検証していきたい。</li> <li>既存プロダクトのアクセシビリティチェックをして現状の問題点を把握したい。そのうえでトレタのサービスに触れるユーザーの特性を整理して、まずは最低限遵守するべきアクセシビリティのルールを作りたい。</li> <li>策定したルールをもとに既存コンポーネントの再整備を行いたい。</li> </ul> <h4>エンジニア</h4> <ul> <li>まずは、自身が関わっているプロジェクトで「知覚可能」のチェックを行い、issueを作成を行いたい</li> <li>自動チェックの仕組みを増やしたい</li> <li>改善のための時間を確保したい</li> </ul> <h2>おまけ:フロントエンドチームへの知見共有</h2> <p>この記事では輪読会にフォーカスして振り返りましたが、そのほかの取り組みも少し紹介させてください。</p> <p>フロントエンドチームでは週一で定例があり、そこで技術共有会を行なっています。 自分のターンの時は、主にアクセシビリティ関連の情報共有を行いました。</p> <p>以下、話題にした内容です。</p> <ul> <li>個人的に、「東京都新型コロナウイルス感染症対策サイト」のアクセシビリティプレ試験に参加したので、その際のTips共有(どんなツールを使ってどういったチェックをした、など)</li> <li>アクセシビリティ輪読会で作成したチェックリストに関する共有</li> <li>自動チェックツールをいくつか検討している話(stylelint-a11y、acot)</li> <li>カルーセルUIが嫌われている理由を改めて考えた話(とても盛り上がった)</li> <li>「知覚可能」の観点で自身が関わっているプロジェクトの改善点を検討した話</li> <li>押せないボタンがデフォルト非活性なのは推奨じゃないらしいという話(これも盛り上がったと思ってる)</li> </ul> <p>(ここでは詳しいことはここでは書きませんが、雰囲気だけ伝わればと思います。)</p> <h2>終わりに</h2> <p>2021年アクセシビリティ改善に費やした時間は、自身の稼働でいうとの2%に満たないと思います。<br /> 関わっているプロジェクトの 0->1フェーズであったこともあり、アクセシビリティ改善活動に割く余力が正直あまりありませんでした。</p> <p>それでも、<strong>確実に成果があった1年</strong>だと思っています。</p> <p>昨年、アクセシビリティ啓蒙活動をされている方から、</p> <p><strong>「勝手にPRを出すのはアンチパターン」</strong></p> <p>というお話を聞いてから、<strong>ひとりで進めてしまわないよう輪読会の実施や、フロントエンドチームへの知見共有を心がけてやってきました。</strong>(どちらにせよ、ひとりでできることは限られますが。)</p> <p>今年、それはできたと思っています。</p> <p>来年は、実際の改善をより進めていけるよう、取り組んでいきたいと考えています。</p> <h2>トレタでは一緒に働くメンバーを募集しています!</h2> <p>トレタは、<strong>エンジニアとして色々とチャレンジできるチャンスが多い環境</strong>だと思っています。</p> <p>少しでも興味があれば、カジュアル面談などお気軽にご応募ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集 採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%2F" title="中途採用 | 株式会社トレタ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> punipunityan Flutterアプリにテストコードを書きました。 hatenablog://entry/13574176438045353061 2021-12-15T00:00:00+09:00 2021-12-23T11:40:17+09:00 はじめに この記事はトレタ Advent Calendar 2021の15日目の記事です。 こんにちは、iOSエンジニアのarisaです。 本日は、Flutterのテストを書いたはなしです。 弊社のトレタO/Xでは、大きく分けて2つのプロダクトがあります。 お店のスタッフさんが使う画面と、お店に来たお客さんが使う画面です。 そのうち、お店のスタッフさんが使う方のアプリでは、Flutterを採用しています。 今回私がテストを実装したのは、そんなお店のスタッフさん用のtoBアプリです。 やったこと とても簡単なユニットテストから実装し始めました。 Intから「〇〇円」というString表示に変換す… <h1>はじめに</h1> <p>この記事は<a href="https://qiita.com/advent-calendar/2021/toreta">トレタ Advent Calendar 2021</a>の15日目の記事です。</p> <p>こんにちは、iOSエンジニアのarisaです。<br> 本日は、Flutterのテストを書いたはなしです。</p> <p>弊社のトレタO/Xでは、大きく分けて2つのプロダクトがあります。<br> お店のスタッフさんが使う画面と、お店に来たお客さんが使う画面です。<br> そのうち、お店のスタッフさんが使う方のアプリでは、Flutterを採用しています。<br> 今回私がテストを実装したのは、そんなお店のスタッフさん用のtoBアプリです。<br></p> <h1>やったこと</h1> <p>とても簡単なユニットテストから実装し始めました。 Intから「〇〇円」というString表示に変換するextensionを例とします。</p> <pre class="code lang-dart" data-lang="dart" data-unlink><span class="synType">extension</span> <span class="synType">OxInt</span> <span class="synStatement">on</span> <span class="synType">int</span> { <span class="synType">String</span> <span class="synIdentifier">formatYen</span>() { <span class="synType">final</span> numberFormat = <span class="synType">NumberFormat</span>(<span class="synConstant">'#,###'</span>); <span class="synStatement">return</span> <span class="synConstant">'</span><span class="synPreProc">${numberFormat.format(this)}</span><span class="synConstant">円'</span>; } </pre> <pre class="code lang-dart" data-lang="dart" data-unlink> <span class="synType">void</span> <span class="synIdentifier">main</span>() { <span class="synIdentifier">group</span>(<span class="synConstant">'formatYen'</span>, () { <span class="synIdentifier">test</span>(<span class="synConstant">'basic'</span>, () { <span class="synType">const</span> price = <span class="synConstant">1000</span>; <span class="synType">final</span> result = price.<span class="synIdentifier">formatYen</span>(); <span class="synIdentifier">expect</span>(result, <span class="synConstant">'1,000円'</span>); }); <span class="synIdentifier">test</span>(<span class="synConstant">'zero'</span>, () { <span class="synType">const</span> price = <span class="synConstant">0</span>; <span class="synType">final</span> result = price.<span class="synIdentifier">formatYen</span>(); <span class="synIdentifier">expect</span>(result, <span class="synConstant">'0円'</span>); }); <span class="synIdentifier">test</span>(<span class="synConstant">'cheap'</span>, () { <span class="synType">const</span> price = <span class="synConstant">100</span>; <span class="synType">final</span> result = price.<span class="synIdentifier">formatYen</span>(); <span class="synIdentifier">expect</span>(result, <span class="synConstant">'100円'</span>); }); <span class="synIdentifier">test</span>(<span class="synConstant">'Expensive'</span>, () { <span class="synType">const</span> price = <span class="synConstant">1000000</span>; <span class="synType">final</span> result = price.<span class="synIdentifier">formatYen</span>(); <span class="synIdentifier">expect</span>(result, <span class="synConstant">'1,000,000円'</span>); }); <span class="synIdentifier">test</span>(<span class="synConstant">'minus-cheap'</span>, () { <span class="synType">const</span> price = <span class="synStatement">-</span><span class="synConstant">100</span>; <span class="synType">final</span> result = price.<span class="synIdentifier">formatYen</span>(); <span class="synIdentifier">expect</span>(result, <span class="synConstant">'-100円'</span>); }); <span class="synIdentifier">test</span>(<span class="synConstant">'minus'</span>, () { <span class="synType">const</span> price = <span class="synStatement">-</span><span class="synConstant">1000</span>; <span class="synType">final</span> result = price.<span class="synIdentifier">formatYen</span>(); <span class="synIdentifier">expect</span>(result, <span class="synConstant">'-1,000円'</span>); }); <span class="synIdentifier">test</span>(<span class="synConstant">'minus-expensive'</span>, () { <span class="synType">const</span> price = <span class="synStatement">-</span><span class="synConstant">1000000</span>; <span class="synType">final</span> result = price.<span class="synIdentifier">formatYen</span>(); <span class="synIdentifier">expect</span>(result, <span class="synConstant">'-1,000,000円'</span>); }); }); } </pre> <p>こんな感じで少しずつ実装しています。<br> 実は今まで、私はテストコードに触れたことがなかったため、たった1つの関数でもこれだけ複数の状況について考慮しなければいけないとは……と少し驚いてしまいました。<br> 私はO/Xの開発チームには遅れて加わったのですが、テストコードを書くためには元のコードをしっかり理解しないといけないので、<br> 今回の実装により、だいぶん理解が深まって来たのではないかと思っています。<br></p> <p>私がテストコードを担当するまで、テストのカバー率は約7%でした。<br> でも、この1ヶ月で約2倍の13%まで上昇させることができました。<br> 引き続き来年もテストのカバー率を上げていき、安定した開発の一助となれればと思っています。<br></p> <h1>最後に</h1> <p>トレタでは一緒に開発する仲間を募集しています。<br> 興味がある方は是非カジュアル面談へお越しください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集 採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> yukimura03 しっかりと整備されたドキュメントはなんぼあってもいいですからね、という話 hatenablog://entry/13574176438042317024 2021-12-14T12:00:00+09:00 2021-12-21T14:31:04+09:00 この記事は、トレタのアドベントカレンダー2021の14日目の記事です。 。。。いや、なんぼはあると管理的な意味で困っちゃいますね。 みなさん、ドキュメント作ってますか?こんにちは。トレタでQAをしている福富です。 普段は(こないだプレスリリースも出た)デジタル塚田農場プロジェクトのO/Xの方でQAをしています。 (プレスリリースはこちら、O/Xについてはこちらをご参照ください!) こちらのプロジェクト、自分はキックオフから関わってきまして、たくさんのドキュメントをまとめてきました。 スピードが重要視されるプロジェクトにおいてドキュメントをいっぱい作るのは大変ですし、管理も辛いです。 でも、作っ… <p>この記事は、<a href="https://qiita.com/advent-calendar/2021/toreta">トレタのアドベントカレンダー2021</a>の14日目の記事です。<br><br></p> <p>。。。いや、なんぼはあると管理的な意味で困っちゃいますね。<br><br></p> <center><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/F/FukuromiQA/20211220/20211220223604.png" alt="f:id:FukuromiQA:20211220223604p:plain" width="400" height="352" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></center> <p>みなさん、ドキュメント作ってますか?こんにちは。トレタでQAをしている福富です。<br> 普段は(こないだプレスリリースも出た)デジタル塚田農場プロジェクトのO/Xの方でQAをしています。<br> (プレスリリースは<a href="https://corp.toreta.in/news/press/2021-11-30-2584/">こちら</a>、O/Xについては<a href="https://www.toreta-ox.com/">こちら</a>をご参照ください!)<br><br></p> <p>こちらのプロジェクト、自分はキックオフから関わってきまして、たくさんのドキュメントをまとめてきました。<br> スピードが重要視されるプロジェクトにおいてドキュメントをいっぱい作るのは大変ですし、管理も辛いです。<br> でも、作ってきたドキュメントに助けられることも多々ありました。<br> 今回は、自分がドキュメントをまとめる目的ならびに気をつけていること、実際にどんなドキュメントを作っているのかを紹介してみたいと思います。<br></p> <h3>なぜドキュメントをまとめるのか</h3> <p>ドキュメントをまとめる理由って、そのドキュメントの内容にもよると思うんですけど。。。<br> 個人的に一番大事なのは<b>未来のチームの生産性を担保する</b>ことだと思っています。<br></p> <p>時間が経つと、チームメンバーは移り変わっていくものです。<br> 最初のころは阿吽の呼吸で物事を進められても、メンバーが変わればそううまくはいきません。<br> ドキュメント化されていない部分の疑問が出てきて、調査、回答にかかる時間が増えていきます。<br> そうなると必然的に「自分がやりたかったこと」を対応する時間が減っていくので、生産性が下がっちゃいますよね。<br> そんなとき、しっかりとドキュメントがまとまっていれば「基本的にはそこ読めば解決!」となり、調査にかかる時間も削減されます。<br> 質問される回数もきっと減るので、差し込み対応の数も少なくなって集中できますね!<br> もちろん、ドキュメントが最新化されているということが前提になりますが、<br> 弊チームでは、カンバンボードに「DOC UPDATING(仕様更新)」を追加し、ドキュメントの更新漏れを防ぐようにしています。 <figure class="figure-image figure-image-fotolife" title="QA後、DOC UPDATINGを経由してDONEにする感じ。"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/F/FukuromiQA/20211221/20211221123711.jpg" alt="f:id:FukuromiQA:20211221123711j:plain" width="952" height="207" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>QA後、DOC UPDATINGを経由してDONEにする感じ。</figcaption></figure></p> <h3>まとめるにあたり意識していること</h3> <h4>そのドキュメントは誰のため? を意識する</h4> <p>今から作成するドキュメントは誰のためのものなのか、ターゲットをしっかりと定めましょう。<br> 例えばの話ですけど、仕様ドキュメントを作るとして、それが開発者向けなのか、QA向けなのか、それとも営業さん向けなのかによって書き方って変わると思うんですよね。<br> だから、できる限り作成する前にターゲットは定めておきましょう。<br></p> <h4>書き始める前にフォーマットを考える</h4> <p>ちゃんとしたドキュメントを作るとなると、フォーマットの統一は必要不可欠です。<br> フォーマットなしのメモ書きでは、後から見直したときや他の人が見たときに解読に時間がかかってしまいます。<br> そうなると、時間短縮のために開発者に質問をすることも増えるので、あんまりドキュメントとしての意味をなさなくなっちゃいますね。<br> 上記のターゲットとあわせて(というか、ターゲット次第で書く内容も変わるのでフォーマットも変わると思います)、しっかりと決めておきましょう。<br> もちろん、書いていく中で必要な項目が増えてフォーマットが変わることもあります。要は読みやすく整理されていればいいのです。<br></p> <h4>溜め込まない すぐに更新する(強い気持ちで)</h4> <p>大事なのことなので2度言います。<br> <b>溜め込まない すぐに更新する。</b><br> 更新を怠るとドキュメントの鮮度が落ちるっていうのは当然として、<br> 溜め込んでしまうと更新量が多くなって、更新するために必要な時間が増えて、でもそんな時間はなくて。。。みたいな状況になって更新が難しくなります。<br> そうならないよう、ドキュメントはこまめに更新しましょうね。</p> <h3>過去に作成したドキュメント</h3> <p>ここからは実際に自分が作成してきたドキュメントについて簡単に紹介します。<br> まとめておいてよかった点や、これからの課題についてもまとめるので、なんかの参考になればいいなと思います。<br></p> <h4>テストログ、テスト仕様書</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/F/FukuromiQA/20211220/20211220223021.jpg" alt="f:id:FukuromiQA:20211220223021j:plain" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h5>これはなに?</h5> <p>QAエンジニアが改修に対してどんなテストをするか(したか)を残しておくドキュメント。<br></p> <h5>誰のため?</h5> <ul> <li>QAエンジニア、開発者</li> </ul> <h5>まとめておいてよかったなと思うシーン</h5> <ul> <li>開発者とのテスト設計レビューが非常にやりやすい(画面共有で一緒に見ながらできるため)</li> <li>ドキュメントとして残しておくと、その後の改修プロジェクトにケースを流用できる</li> <li>リグレッションテストは既存のテスト仕様書をコピーして書き足していくだけでいいので準備がラク</li> </ul> <h5>これからの課題</h5> <ul> <li>今までずっと設計書ドキュメントをコピペしながらやってきたので、ケースをマスタ化したい</li> <li>リグレッションテストの準備がラクなのでなかなか自動化に踏み出せないでいる(言い訳!)</li> </ul> <h4>簡易な画面仕様、機能仕様</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/F/FukuromiQA/20211220/20211220221528.jpg" alt="f:id:FukuromiQA:20211220221528j:plain" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h5>これはなに?</h5> <p>画面仕様はこの画面にどんな情報が表示されているか、画面遷移や入力時のバリデーションなどを記載するもの。<br> 機能仕様は画面を横断する機能について、事前準備や利用の流れ、注意事項等をまとめておくもの。<br></p> <h5>誰のため?</h5> <ul> <li>プロジェクトに関わる開発者、QA、デザイナー、ビジネスサイドのメンバーなど全員</li> <li>途中から参画するメンバー</li> </ul> <h5>まとめておいてよかったなと思うシーン</h5> <ul> <li><b>みんな欲しいと思っていたのか、めっちゃお礼を言われる</b></li> <li>改修前に開発者さんも仕様をまとめてくれるようになり、開発前にみんなでレビューできるようになった</li> </ul> <h5>これからの課題</h5> <ul> <li>現在は<b>強い気持ちで</b>継続的に更新しているが、その気持ちが途絶えた途端に負債化しかねないところ(どんな仕様書にも言えることだが。。。)</li> </ul> <h4>セットアップマニュアル類</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/F/FukuromiQA/20211220/20211220223316.jpg" alt="f:id:FukuromiQA:20211220223316j:plain" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h5>これはなに?</h5> <p>O/Xは導入時に店舗にてプリンタ機器を設置する必要があり、そのセットアップ手順をまとめたもの。<br></p> <h5>誰のため?</h5> <ul> <li>導入サポートで店舗にてセットアップを実施するメンバー</li> </ul> <h5>まとめておいてよかったなと思うシーン</h5> <ul> <li>初期段階では導入サポート的なメンバーがおらず、色んな人が店舗にてセットアップを行ったが、このドキュメントがあることでみんなすんなりと作業を進められたと思う</li> <li>店舗側でセットアップを行う場合の手順書をこのドキュメントをもとに作成したのでスムーズにできた</li> </ul> <h5>これからの課題</h5> <ul> <li>特になさそう</li> </ul> <h4>毎日のスタンドアップの議事メモ</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/F/FukuromiQA/20211220/20211220222837.jpg" alt="f:id:FukuromiQA:20211220222837j:plain" width="960" height="540" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h5>これはなに?</h5> <p>チームで毎日行っているスタンドアップにて議論した内容をメモし、Slackにて共有しているもの。</p> <h5>誰のため?</h5> <ul> <li>スタンドアップ参加者、参加していないがプロジェクトに関わっているメンバー</li> </ul> <h5>まとめておいてよかったなと思うシーン</h5> <ul> <li>スタンドアップに参加していないメンバーが、議事メモを見て回答などのアクションを起こしてくれることが多い</li> <li>過去のスタンドアップで決めたことを簡単に思い出せる</li> </ul> <h3>おわりに</h3> <p>と、まあこんな感じです。<br> スタンドアップの議事メモは毎日更新ということもありますが、この1年でだいたい400くらいのドキュメントを作成してきました。<br> 1日1ドキュメント以上と考えると、なんだか達成感が湧きますね。<br> 冒頭でも言ったとおり、ドキュメントの管理はすごく大変です。でもやっぱりドキュメントに助けられる場面も多いんですね。<br> とはいえ負担も大きいので、ドキュメントの整備をするなら自分ができる範囲でやるのが一番よいと思います。<br> 自分はこれからもドキュメント残し魔として、大量のドキュメントとともに生きていこうと思います。<br> それではまた!<br></p> <h3>トレタでは一緒に働くメンバーを募集しています!</h3> <p>トレタはいろんな職種で一緒に働くメンバーを募集しています!<br> QAとしては上流からすべての工程に関わることになります。<br> 非常にスピードがはやい環境の中でQAとしてのスキルを磨くことができます!<br><br> 少しでも興味があれば、カジュアル面談などお気軽にご応募ください!<br></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集 採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%2F" title="中途採用 | 株式会社トレタ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> FukuromiQA 優秀なデザイナーが上流工程に加わると開発が捗る話 hatenablog://entry/13574176438039334743 2021-12-06T10:58:36+09:00 2021-12-08T19:25:03+09:00 こんにちは、何がなんでもアドベントカレンダーを埋めようとしている鄧(でん)です。 Data is a precious thing and will last longer than the systems themselves. -- Tim Berners-Lee 今日はトレタのエンジニアとデザイナーの連携についてお話させてください。 ちょっと前までのトレタでは主にデザイナーが顧客にヒアリングした上でユースケースを整理し、ワイヤーフレームを作ります。そこからよくも悪くもワイヤフレームが仕様書となり、そこからフロントエンドエンジニアに渡され、フロントエンジニアはワイヤーフレームと API の… <p>こんにちは、何がなんでもアドベントカレンダーを埋めようとしている鄧(でん)です。</p> <blockquote><p>Data is a precious thing and will last longer than the systems themselves.</p> <p>-- Tim Berners-Lee</p></blockquote> <p>今日はトレタのエンジニアとデザイナーの連携についてお話させてください。</p> <p>ちょっと前までのトレタでは主にデザイナーが顧客にヒアリングした上でユースケースを整理し、ワイヤーフレームを作ります。そこからよくも悪くもワイヤフレームが仕様書となり、そこからフロントエンドエンジニアに渡され、フロントエンジニアはワイヤーフレームと API の間に板挟みになって、どう API につなげるか、API が足りない場合は DB のどの項目をどう API に反映させるかのすり合わせが始まりますが、お世辞でも効率がいいとは言えません。</p> <p>そこで新しいサービスとして O/X を設計した際には流れを逆転させ、まずはデザイナーとエンジニア(~= サーバーサイドエンジニア)が長期的な運用を意識しつつ、お客さんの課題を理解し、ソリューションと共通の言語(~= ユビキタス言語を意識している何か)を定義してすり合わせてからフロントエンジニアに渡すやり方をチャレンジしております。</p> <p>ここ際にユースケースを達成するために保存する、あるいは表示する情報を整理しますが、デザイナーは情報アーキテクチャ(Information Architecture )、エンジニアは ER モデル使います。細かい表現や制約は若干違ったりしますが、本質的には <strong>情報を整理する</strong> と言う同じルーツを持っているので、場合によってはペアリングして設計を進めることも可能です。</p> <p>デザイナーは最終的にワイヤーフレーム、エンジニアはデータモデル(例:ER 図)を作りますが、フロントエンジニアに渡す前にワイヤーフレームとデータモデルをすり合わせて抜け漏れがないかを確認します。(O/X の API は基本データモデルの CRUD が多いのでほぼそのまま適応することが可能です)</p> <p><figure class="figure-image figure-image-fotolife" title="デザインに対してのアノテーション例"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toreta-dev/20211208/20211208192455.png" alt="f:id:toreta-dev:20211208192455p:plain" width="1200" height="720" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デザインに対してのアノテーション例</figcaption></figure></p> <p>具体的にはワイヤーフレームに対してデータモデルを被せてレイヤーを分かりやすくしているのですが、トレタでは figma を使うことが多いのでこのように色を使ってデータモデルの境界線を表示した上で、文字でデータモデルの項目とのマッピングを表現しております。</p> <p>この際にデザイナーと同じく QA もディスカッションに入るので客さんの課題やスースケース、ワイヤーフレーム、設計意図、データモデル、API 仕様など全てを俯瞰的に把握することが可能です。</p> <p>ここで優秀なデザイナーと優秀なエンジニアの共通点としてはお客さんのペインややりたいことを理解することができ、情報を整理する能力が優れており、それを言語化する能力も優れていると認識しており、そのようなメンバーが集まっているプロジェクトは居心地がいいと感じております。</p> <p>さて、トレタのデザインプロセスについてエンジニア視点でお話しさせていただきましたがいかがでしょうか?もしご意見、ご指摘などがございましたらぜひお聞かせいただければと思います。</p> <h3>最後に宣伝</h3> <p>トレタでは一緒に飲食店の未来を作るデザイナーを積極募集中です!まずはカジュアルに話を聞きたい方もこちらからご応募いただけます!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%2Fmidcareer-261%2F" title="プロダクトデザイナー | 株式会社トレタ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://corp.toreta.in/recruit/midcareer/midcareer-261/">corp.toreta.in</a></cite></p> toreta-dev 優秀な QA メンバーが上流工程に加わると開発が捗る話 hatenablog://entry/13574176438039320590 2021-12-04T10:40:59+09:00 2021-12-04T10:40:59+09:00 こんにちは、なんとか今年のアドベントカレンダーを埋めようとしている鄧(でん)です。 今日は社内での QA メンバーの活動についてお話させてください。 今までのトレタでは 要件定義(ふんわり)→ 設計(ふんわり)→ 実装 → QA の流れでどちらかというとウォーターフォール型の開発が多かったと記憶しております。リリースはできていたのである意味ワークしていたと言えますが、QA の視点からすると この画面や API はどんな課題を解決するためのものか なぜその設計に至ったのか 今の挙動やレスポンスは果たして当初意図した正しものなのか など、QA メンバーのフィードバックから、コンテキストが正しくバト… <p>こんにちは、なんとか今年のアドベントカレンダーを埋めようとしている鄧(でん)です。</p> <p>今日は社内での QA メンバーの活動についてお話させてください。</p> <p>今までのトレタでは <code>要件定義(ふんわり)→ 設計(ふんわり)→ 実装 → QA</code> の流れでどちらかというとウォーターフォール型の開発が多かったと記憶しております。リリースはできていたのである意味ワークしていたと言えますが、QA の視点からすると</p> <ul> <li>この画面や API はどんな課題を解決するためのものか</li> <li>なぜその設計に至ったのか</li> <li>今の挙動やレスポンスは果たして当初意図した正しものなのか</li> </ul> <p>など、QA メンバーのフィードバックから、コンテキストが正しくバトンタッチされていないことが多いと感じました。</p> <h3>改善内容</h3> <p>サービスの品質や保守容易性(maintainability)を担保するために QA のメンバーにお願いして以下の施策に協力していただきました。</p> <h4>ドキュメント整備</h4> <p>まずは、QA メンバーの協力を得て API シーケンスからエラーコードの整備など、暗黙知だった部分を仕様とドキュメントとして言語化しました。まだまだ、改善の余地は多い(というか伸び代しかない)のですが、API 利用者目線でも全貌が把握しやすくなりましたし、設計意図、動作確認など、テストケースを設計する際にも便利になったと認識しております。</p> <p><strong>API シーケンス図</strong></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/t/toreta-dev/20211204/20211204101829.png" alt="f:id:toreta-dev:20211204101829p:plain" width="1200" height="656" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>API シーケンス図</figcaption></figure></p> <p><strong>エラーコード一覧</strong></p> <p><figure class="figure-image figure-image-fotolife" title="エラーコード一覧"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toreta-dev/20211204/20211204101933.png" alt="f:id:toreta-dev:20211204101933p:plain" width="1200" height="702" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>エラーコード一覧</figcaption></figure></p> <h4>テスト駆動</h4> <p>一部のサービスではコードを書く前に QA メンバーの協力を得て、テスト内容をコードより先に書く所謂テスト駆動の手法を検証してます。トレタは TypeScript、Ruby on Rails、Golang、Clojure などさまざまな言語やフレームワークを扱っております、それを QA が全て把握するのは現実的ではないと思いますが、仕様を把握した上で日本語として言語化することなら実行可能ですので、それを実際にコードを書く前に仕様書ベースですり合わせ、実装と共に担当エンジニアがテストコードとして翻訳して CI を通してことによって、毎回 QA の手を借りずとも継続的品質を担保しやすくなりました。</p> <h4>情報を俯瞰できる環境づくり</h4> <p>最後に、QA メンバーには要件定義段階から会話に参加頂き、開題と設計意図などの情報をキャッチしやすくしました。これによって 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/t/toreta-dev/20211204/20211204102657.png" alt="f:id:toreta-dev:20211204102657p:plain" width="1200" height="950" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>品質の重要性</figcaption></figure></p> <h3>最後に宣伝</h3> <p>トレタでは一緒に飲食店の未来を作る QA エンジニアを積極募集中です!まずはカジュアルに話を聞きたい方もこちらからご応募いただけます!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%2Fmidcareer-256%2F" title="QAエンジニア | 株式会社トレタ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://corp.toreta.in/recruit/midcareer/midcareer-256/">corp.toreta.in</a></cite></p> <h3>参考資料</h3> <ul> <li><a href="https://speakerdeck.com/twada/quality-and-speed-2020-autumn-edition?slide=20">&#x8CEA;&#x3068;&#x30B9;&#x30D4;&#x30FC;&#x30C9;&#xFF08;2020&#x79CB;100&#x5206;&#x62E1;&#x5927;&#x7248;&#xFF09; / Quality and Speed 2020 Autumn Edition - Speaker Deck</a></li> <li><a href="https://logmi.jp/tech/articles/324154">&#x4E0A;&#x6D41;&#x5DE5;&#x7A0B;&#x304B;&#x3089;&ldquo;&#x4FEF;&#x77B0;&#x3057;&#x3066;&#x8003;&#x3048;&#x308B;&rdquo;&#x3053;&#x3068;&#x3092;&#x610F;&#x8B58;&#x3059;&#x308B; LINE&#x306E;&#x30D7;&#x30ED;&#x30C0;&#x30AF;&#x30C8;&#x3092;&#x652F;&#x3048;&#x308B;QA&#x30A8;&#x30F3;&#x30B8;&#x30CB;&#x30A2;&#x306E;&#x304B;&#x305F;&#x3061; - &#x30ED;&#x30B0;&#x30DF;&#x30FC;Tech</a></li> </ul> toreta-dev リモート環境下でのコミュニケーション改善へ取り組んだこと hatenablog://entry/13574176438038577947 2021-12-02T12:07:26+09:00 2021-12-16T09:49:32+09:00 こんにちは。エンジニアのkitagawaです。 こちらはトレタアドベントカレンダー2021 2日めの記事です。 今年はまだまだ立ち上げ時期のトレタO/Xプロジェクトでマネージャーとして、エンジニアが開発に専念できる環境づくりを多くやっていました。 今となっては全員フルリモートで働くの当たり前の状況となりましたが、 まだまだコミュニケーションが不足していたり、まだまだリモート環境とのつきあいかたを模索しているような状況です。 そんな中でコミュニケーション改善に向けて取り組んでみたことを紹介しようと思います。 決定事項を流す「決めた」チャンネル リモート環境でのコミュニケーションは基本的にSlac… <p>こんにちは。エンジニアのkitagawaです。 こちらは<a href="https://qiita.com/advent-calendar/2021/toreta">トレタアドベントカレンダー2021</a> 2日めの記事です。</p> <p>今年はまだまだ立ち上げ時期の<a href="https://www.toreta-ox.com/">トレタO/Xプロジェクト</a>でマネージャーとして、エンジニアが開発に専念できる環境づくりを多くやっていました。</p> <p>今となっては全員フルリモートで働くの当たり前の状況となりましたが、 まだまだコミュニケーションが不足していたり、まだまだリモート環境とのつきあいかたを模索しているような状況です。 そんな中でコミュニケーション改善に向けて取り組んでみたことを紹介しようと思います。</p> <h2>決定事項を流す「決めた」チャンネル</h2> <p>リモート環境でのコミュニケーションは基本的にSlackとGoogle Meetで行っています。 その環境の中で課題の一つに、<strong>「共有事項がどこまでの人に伝わってるかがわからない」</strong> という状況がありました。</p> <p>よくあるケースは、</p> <ul> <li>スレッドで議論された結論が要約されることなくふんわり終わる</li> <li>見逃さないようにスレッドをウォッチしているとスレッドの長さが100件を超え出してツラくなる</li> <li>別チャンネルで似たような議論がされている</li> <li>Meetで話した結果が議事録として残っていない</li> </ul> <p>そこでSlack新しくに<strong>「決めた」チャンネル</strong>を作りました。</p> <p>このチャンネルは議論の結論をサマリーした内容を投稿するためのチャンネルです。 すべてのメンバーはとりあえずここのチャンネルを見ておけば、様々な場所で議論された結果を把握することができます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20211202/20211202104959.png" alt="f:id:mkitagawa-312:20211202104959p:plain" width="693" height="488" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>投稿フォームをワークフローで作っていますが、特定のチャンネルで「決めた」のリアクションをすると決めたチャンネルに転記する仕組みも入れているので、よりここに書き込みやすくなっています。</p> <p>特に「あの時話したあれって結果どうなったんだっけ」がとても探しやすくなりました。</p> <h2>全体共有会を設ける</h2> <p>O/Xプロジェクトはそれなりの人数が関わっていて、いくつものWeekly定例であったりスタンドアップが行われています。 ただ、プロダクトオーナーから各開発メンバーまで関わる全メンバーが集まる会というのがなかったので新しく設けました。</p> <p>実際に会の中で行うことは、</p> <ul> <li>ビジネス側からの1ヶ月の振り返りと今後の営業スケジュール</li> <li>開発側からのリリース報告や障害報告と、今後の開発スケジュール</li> <li>プロダクトオーナーから一言</li> </ul> <p>という構成です。</p> <p>この会を設けた狙いとしては、「全メンバーの目線合わせ」と「メンバーのモチベーション維持」の2つです。</p> <ul> <li>自分が行っているタスクはどこに寄与し、どんな効果を産むのかを理解する</li> <li>達成したことをメンバーのみんなで祝い労う</li> <li>顧客からの声やフィードバックを共有し、良かった/悪かったこと含めてメンバーに伝える</li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20211202/20211202111706.png" alt="f:id:mkitagawa-312:20211202111706p:plain" width="1200" height="609" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>開発メンバーは日々のタスクをこなしていると、新機能リリースを行ってもすぐに次のタスクに追われてしまい、 目前のタスクだけを見ていて視野が狭まったり、徐々にモチベーションが下がったりしてしまうものです。 メンバーの上から下までそれぞれがやってきたことをちゃんと皆で振り返る、というのはとても効果があったと感じています。</p> <h2>オンラインランチ会を開催</h2> <p>リモート環境でよく聞く問題として、<strong>雑談が減った</strong>というのがあると思います。 トレタでもリモートになる前までは会社にカウンターがあり、そこで行われた雑談からいろいろなアイデアがでたり部署をまたいだコミュニケーションが行われていました。 雑談が減ったことで、特に問題だと思ったのが<strong>新入社員へのケア</strong>です。</p> <p>新入社員には大体の場合メンターがつくのですが、メンターとのコミュニケーションはあっても、同じチーム以外の人や定例で会う人以外は接点があまりないです。 雑談の会を今までいろいろと試してみましたが、</p> <ul> <li>雑談する時間を業務中にとっても、業務の方を優先してしまう</li> <li>いざ雑談するとなってもかしこまってしまう</li> </ul> <p>という理由でなかなか定着しませんでした。 そこで今回はランチ休憩の時間を使って、オンラインでみんなでランチを行う取り組みを行いました。 そもそも出社していた時代では新入社員が入った時は社内にいる人たちでランチに行くのが恒例だったのでそこからの着想です。</p> <p>やることはシンプルで、決まった時間にバーチャルオフィスに集まって、ご飯食べながら雑談するだけのスタイルです。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20211202/20211202112322.png" alt="f:id:mkitagawa-312:20211202112322p:plain" width="800" height="291" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>開催ごとに参加人数は減ってきてはいますが、ある程度の人数が定着したので成功だったかなと思います。 技術的な話だったり、最近何買ったとか、やっぱり他愛もない会話する時間って楽しいですよね。</p> <p>詳細についてはこちらでも記事にしていただきました。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr%2Fn%2Fn1ce4b6166cba" title="エンジニアランチ会の気軽な会話を切り口に、相談しやすいチームへ|トレタのnote|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/toreta_hr/n/n1ce4b6166cba">note.com</a></cite></p> <h1>さいごに宣伝</h1> <p>トレタではエンジニアを積極募集中です!まずはカジュアルに話を聞きたい方もこちらからご応募いただけます! <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Ftoreta%2Fprojects" title="株式会社トレタの募集 採用・求人情報 - Wantedly" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.wantedly.com/companies/toreta/projects">www.wantedly.com</a></cite></p> mkitagawa-312 2021 年の振り返りと今後の展望 hatenablog://entry/13574176438038226737 2021-12-01T11:09:55+09:00 2021-12-01T11:09:55+09:00 初めに こんにちは。トレタ CTO の鄧(でん)です。 今年も残り少なくなってきたので、少し早いかもしれませんがこの怒涛のような一年の振り返りをさせて頂ければと思います。 プロダクト 2021 年は主に 3 本の柱に絞って開発を進めております。 O/X(Order Experience、より良い顧客体験を実現するためのメニュー / POS システム) B/X(Booking Experience、より良い顧客体験を実現するための予約管理システム) CC(飲食店向けのコンタクトセンター) それぞれ事業として違うフェーズになっていたり、技術的な制限が異なってたりするので事業を横断するときのコンテキ… <h3>初めに</h3> <p>こんにちは。トレタ CTO の鄧(でん)です。 今年も残り少なくなってきたので、少し早いかもしれませんがこの怒涛のような一年の振り返りをさせて頂ければと思います。</p> <h4>プロダクト</h4> <p>2021 年は主に 3 本の柱に絞って開発を進めております。</p> <ul> <li>O/X(<strong>O</strong>rder <strong>E</strong>xperience、より良い顧客体験を実現するためのメニュー / POS システム)</li> <li>B/X(<strong>B</strong>ooking <strong>E</strong>xperience、より良い顧客体験を実現するための予約管理システム)</li> <li>CC(飲食店向けのコンタクトセンター)</li> </ul> <p>それぞれ事業として違うフェーズになっていたり、技術的な制限が異なってたりするので事業を横断するときのコンテキストスイッチはそこそこ大変だったと記憶しております。</p> <h5>O/X</h5> <p>「2021 年は第二の創業期」という代表の思いと、「お客さまとお料理の幸せな出会いを最高に演出できるメニュー」というコンセプトを元に</p> <ul> <li>各法人、各ブランド毎にメニューの演出をフルカスタマイズする(SES)</li> <li>裏側のインフラ、サーバー、店舗スタッフが操作する管理画面などは共通のものを使うことによってコスト面とリリース速度のスケールメリットを得る(SaaS)</li> </ul> <p>というぱっと見相反する要件を実現させるため、メニュー画面とバックエンドの仕組みを分離させたヘッドレス(headless)な構成として作っており、こちらは 2020 年の後半から案を練ってましたが、今年 2021 年の 2 月から開発が活発になって一足先に導入テストなども行い、7 月 26 日ようやく一般公開できるようになりました (1)。</p> <p>技術的に工夫したところとしてはリリースのプロセスを高速化しつつ、高い信頼性を担保するために車輪を再発明せず、市販の SaaS をレゴブロックのように組み合わせて限られたスケジュールの中で高速に組み立てる手法を採用しており、おかげさまで 12 月 1 日現在、システムに登録されている店舗は 90 を超えており、利用している法人は 4 法人となっており、我々は開発手法とツールを工夫することによって小規模な開発組織を維持しながらこの一年でフロントエンドを 6 案件(各ブランドの UI x 4 + 管理コンソール x 2)、且つサーバーサイド多数 (2) をリリースしたことになります。</p> <p>ただ、タイトなスケジュールがあるとはいえ、品質やパフォーマンスを妥協することは考えてなく、テストの書きやすさを前提に設計を進める、早めにデザインと API 仕様を擦り合わせる、早めに DB 構造と分析要件を擦り合わせる、全般的に QA が情報や流れをキャッチできるようにするなど、開発手法を工夫してチームとしてのアウトプットを底上げするのを狙っております。</p> <p><strong>参考資料</strong></p> <ol> <li><a href="https://note.com/hitoshi/n/n3a8c27662f3a">弊社代表中村のブログ</a></li> <li><a href="https://tech.toreta.in/entry/2021/08/10/144917">O/X を総括しているエンジニアの北川さんの紹介記事</a></li> </ol> <h5>B/X</h5> <p>今までトレタの主力商品であった予約台帳ですが、コロナの影響もあり、正直ここ一年伸び悩んでおりました。その中で SRE やサーバーサイドエンジニアの努力もあり、直近一年間のダウンタイムは合計 9 分間、稼働率に換算すると軽く 99.99% を超えております。</p> <p>もし 2021 年を守りの一年と例えると、来年に関しては一部コロナオミクロン株の懸念もあるものの、攻めに転じる一年だと考えており、飲食業界が回復した際にちゃんとその成長を支えられるように、API 周りの改修、特にメディア連携と店舗を横断した在庫検索を強化しようと考えておりゆくゆくは O/X のようにカスタマイズしやすい予約システムを目指しております。</p> <p>技術的にはデータ連携のためのストリーム処理や検索エンジンを中心に開発を進めておりますので、もしご興味がありましたらぜひご連絡ください。</p> <p><strong>ストリーム処理</strong></p> <ul> <li><a href="https://cloud.google.com/pubsub">GCP Pub/Sub</a></li> <li><a href="https://cloud.google.com/dataflow">GCP Dataflow</a></li> </ul> <p><strong>検索エンジン</strong></p> <ul> <li><a href="https://www.algolia.com/">Algolia</a></li> </ul> <h5>CC</h5> <p>飲食店には電話でのやり取りがまだ多く残っておりますが、その負担を少しでも軽減できるようにトレタでは別途 <a href="https://aws.amazon.com/connect/">Amazon Connect</a> を通して、音声認識をベースとしたコールセンターサービスを展開しております。</p> <p>まだ発展途中の分野で、障害や誤認識がまだ多少残ってはいるものの、飲食店のサーポートツールとしては十分利用可能だと考えており、来年以降はより自然なやり取りとより高度なシナリオを実装することによって、より多くのタスクをこなせるように目指しております。</p> <h4>組織と開発手法</h4> <p>トレタでは限られた時間内で手戻りや良くない意味でのサプライズを減らすためにユースケース整理、DB 設計、API 設計、UI 設計、QA などを実装する前にすり合わせております。一部のシステムにおいては実装する前に先に API 仕様をリリースしたり、あるいはテスト仕様を先に書くような開発手法を採用しております。</p> <p>また、サービスの設計思想として、ただアプリケーションが正常に実行しているだけではなく、行動履歴や購買履歴など、データが正しく蓄積され、それを集計・分析した上で飲食店のオペレーションにフィードバックするところまでが商品価値であり、それを実現するためには早い DB 設計の段階で分析メンバーにもレビューが入るようにしており、コアなデータベースに関しては初期リリースから分析基盤や BI ツールが全て繋がった状態でリリースされておりますので、ある意味分析を前提としてシステム設計をしていると言っても過言ではないと考えております。</p> <p>B/X の紹介でも少し触れたように、トレタではリリースのプロセスを高速化しつつ、高い信頼性を担保するために車輪を再発明せず SaaS をレゴブロックのように組み合わせて限られたスケジュールの中で高速に組み立てる手法を採用しております。こちらに社内で使っている技術を幾つかリストアップしておりますので、もしご興味ありましたらぜひご感想、ご指摘などお聞かせいただければと思います。</p> <p>ご連絡お待ちしております。チームの仲間を募集しています!</p> <p><strong>フロントエンド</strong></p> <ul> <li><a href="https://www.contentful.com/">Contentful</a> - CMS</li> <li><a href="https://firebase.google.com/">Firebase</a> - データベース</li> <li><a href="https://vercel.com/">Vercel</a> - CI/CD・CDN・Serverless インフラ</li> <li>Angular / Next.js / TypeScript</li> </ul> <p><strong>サーバーサイド</strong></p> <ul> <li><a href="https://auth0.com/">Auth0</a> - 認証認可</li> <li><a href="https://stripe.com/jp">Stripe</a> - カード決済</li> <li><a href="https://cloud.google.com/run">GCP Cloud Run</a> - Serverless インフラ</li> <li><a href="https://www.datomic.com/">Datomi Cloud</a> - データベース</li> <li>Clojure / Golang / Ruby on Rails</li> </ul> <p><strong>SRE</strong></p> <ul> <li><a href="https://aws.amazon.com/eks/">AWS EKS</a> - Kubernetes</li> <li><a href="https://aws.amazon.com/ecs/">AWS ECS</a> - インフラ</li> <li><a href="https://www.datadoghq.com/">Datadog</a> - ログ収集・サーバーの監視</li> <li><a href="https://sentry.io/welcome/">Sentry</a> - エラー通知</li> </ul> <p><strong>データ分析</strong></p> <ul> <li><a href="https://cloud.google.com/bigquery">GCP BigQuery</a> - DWH</li> <li><a href="https://looker.com/">Looker</a> - BI</li> <li><a href="https://mixpanel.com/">Mixpanel</a> - ユーザーの行動分析</li> <li><a href="https://segment.com/">Segment</a> - ユーザーの行動分析</li> </ul> toreta-dev トレタO/Xの開発の裏側 hatenablog://entry/26006613793286237 2021-08-10T14:49:17+09:00 2021-08-10T16:25:58+09:00 はじめに こんにちは、Half-Vaccinatedなフロントエンドエンジニアのkitagawaです。接種2回目にビビりながら今から備えています。 この度トレタの新しいサービスとして、トレタO/X(トレタオーエックス)が正式リリースされました。O/Xは「Order Experience(注文体験)」の略で、飲食店に来店したお客様がご自身のスマートフォンで注文から決済までを行えるモバイルオーダーアプリです。 サービスの本格立ち上げから今回のリリースまで、約1年ほど主にバックエンドのマネジメントに携わってきたので、その振り返りとして開発内で行った数々の挑戦について、この記事では紹介しようと思います… <h1>はじめに</h1> <p>こんにちは、Half-Vaccinatedなフロントエンドエンジニアのkitagawaです。接種2回目にビビりながら今から備えています。</p> <p>この度トレタの新しいサービスとして、トレタO/X(トレタオーエックス)が正式リリースされました。O/Xは「Order Experience(注文体験)」の略で、飲食店に来店したお客様がご自身のスマートフォンで注文から決済までを行えるモバイルオーダーアプリです。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20210803/20210803101040.png" alt="f:id:mkitagawa-312:20210803101040p:plain" width="800" height="267" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>サービスの本格立ち上げから今回のリリースまで、約1年ほど主にバックエンドのマネジメントに携わってきたので、その振り返りとして開発内で行った数々の挑戦について、この記事では紹介しようと思います。</p> <p>サービスの詳細や背景については<a href="https://note.com/hitoshi/n/n3a8c27662f3a">代表のひとしさんのブログ</a>をぜひご一読ください。</p> <h2>トレタO/Xの特徴</h2> <p>タブレット据置機でのオーダーや、テイクアウトのモバイルオーダーなど、オーダーアプリを目にする機会は近年急激に増えてきました。</p> <p>そんなレッドオーシャンの中でトレタO/Xの最大の特徴は、導入する各飲食法人ごとにUIを完全カスタマイズした専用アプリを開発している点です。これはSaaSとして提供してきたトレタの予約台帳とは全く逆のアプローチで、予約台帳では個社ごとのカスタマイズは行わないポリシーで行っていましたが、今回は逆に完全に個社向けに作り込むアプローチをとっています。</p> <p>事例に沿ってご紹介していきます。</p> <p>最初に導入したのはワンダーテーブルさまのよなよなビアワークスで、クラフトビールのよなよなエール公式のビアレストランです。そのためアプリでは、画面のレイアウト、フォント、カラー、注文後のアクションなど、細部までよなよなビアワークスの世界観を表現するために作りこんでいます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20210803/20210803101055.png" alt="f:id:mkitagawa-312:20210803101055p:plain" width="800" height="444" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>ダイヤモンドダイニングさまの焼鳥IPPON向けアプリでは、好きな焼き鳥の組み合わせや味付けを串一本ずつ自由にカスタマイズして注文する仕組みや、各々が注文した分を会計できる個別決済など、店舗開発の時点から携わらせていただき新しい注文方式をアプリへと落とし込んでいきました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20210803/20210803101115.png" alt="f:id:mkitagawa-312:20210803101115p:plain" width="800" height="444" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これらを実現するため、開発の構成としてはフロントエンドとバックエンドの明確な分離を行いました。Shopifyなどのヘッドレスコマースを参考にし、バックエンドは注文と決済をコアにしたAPIを提供するSaaSの構成となっています。</p> <p>フロントエンドは各社ごとに個別に開発し、メニューなどのマスターデータはCMSなどを利用して各アプリごとに持たせています。導入する店舗は居酒屋やカフェ、ファミリーレストラン、ファストフードなど、業態や店舗形態に応じて様々なメニューの構造や注文フローを想定した結果、画一的なデータ構造ではそれらに対応できないと考え、マスターデータを共通バックエンドでは扱わずに各社ごとに最適なデータ構造のマスターを持つことで、柔軟なUI表現を可能にしています。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20210803/20210803101145.png" alt="f:id:mkitagawa-312:20210803101145p:plain" width="1142" height="922" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2>トレタO/Xの技術スタック</h2> <p>トレタO/Xの技術スタックは、プロトタイピングを隔てて、製品版向けに一度リプレイスを行っています。</p> <p>PoC期間のプロトタイプでは開発スピードが求められるため、フロントエンドにFlutter、バックエンドにFirebaseを採用しました。フロントは主に来店したお客様が利用するユーザー向けアプリと、店内のホールスタッフが利用するスタッフ向けアプリの2種類があります。両方ともFlutterで作っていますが、Flutter Webを使用しています。</p> <p>ユーザー向けアプリは来店時にお客様自身のスマホでQRコードを読み込んで開始してもらう流れとしているため、アプリインストールの強制は避けるためにWebアプリとしています。</p> <p>スタッフ向けアプリに関してはそういった制約はないのですが、ネイティブアプリだとアップデートごとにiOSやAndroidのマーケットへの審査が必要となってしまうため、開発スピードを優先してこちらもWebアプリとしました。ネイティブの機能が必要になったときにiOS/Androidアプリへ切り替えられるようにFlutterを採用しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20210803/20210803101200.png" alt="f:id:mkitagawa-312:20210803101200p:plain" width="1084" height="590" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>プロトタイプ後の製品版では、プロトタイピングで検証したデータ構造やドメイン境界を基に、バックエンドは各種ドメインに分割したマイクロサービスにしています。トレタでは長らくRuby on Railsを使用したモノリシックなシステム構成が主だったので、トレタとしては今回初めてのマイクロサービスを採用となりました。</p> <p>マイクロサービスは銀の弾丸ではないことは肝に命じた上で、マイクロサービスを選んだ経緯についてご紹介します。</p> <h3>メンバーの技術スタックに合わせた並行開発</h3> <p>限られた開発リソースの中でいかに早く開発できるかという点で、各自が独立して開発を進められるように、マイクロサービス単位でチームの分割を行いました。(弊害として、システム間の結合時にはコミュニケーション不足による問題も多く発生しました...)</p> <p>プログラミング言語や各技術要素の選定には、それらの特性を踏まえつつ、挙がった候補の中から何を使うかはメンバーの技術領域に合わせて選んでいます。</p> <p>例えば、Node.jsはフロントエンドのメンバーでも扱いやすいため、フロントとバックを兼任する場面で選択しています。Goについては、Rubyと違い型付きという利点があるため、社内では最近RubyからGoを選択する場面が増えています。またデータベースには、強いACID特性を持つDatomicを使うために社内ではあまり使われないClojureも使用しています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/m/mkitagawa-312/20210804/20210804123139.png" alt="f:id:mkitagawa-312:20210804123139p:plain" width="953" height="455" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3>サービスの再利用</h3> <p>トレタO/Xの開発を開始をする時点で、コロナによる飲食業界の急激な状況変化に対応すべく、トレタでは多くの新規プロダクトの開発を行い、クローズを行いました。トレタは飲食業界のVertical SaaSを目指すため、各プロジェクトで似たような機能が車輪の再発明されていた状況がありました。そこから、今後別プロジェクトでも再利用が行えるようにトレタO/Xに捉われない形で個々サービスの設計を行っています。</p> <h3>モノリシックからの脱却</h3> <p>やはりモノリシックな予約台帳での教訓としての一面があります。</p> <p>エンハンスを長く続けていくと、どうしても当初の設計とは外れた機能追加が行われてしまうものです。都度うまく負債を返却できるとよいのですが、歪みが積み重なるとがんじがらめになり、追加開発が日に日に難しくなります。</p> <p>マイクロサービスで綺麗に解決するわけではないですが、開発初期から疎結合を前提にドメイン境界を意識した設計を行っておくで、この後のエンハンスのしやすさは大きく変わってくると感じています。</p> <h1>さいごに</h1> <p>トレタO/Xはまだリリースされたばかりのプロダクトです。導入店舗数を増やしつつ、各店舗が表現したいメニューブックを実現するために、より深く入り込み洗練されたUIへの磨き込みを行っていきます。 また、トレタO/Xとトレタの予約台帳や他のサービスとも連携し、お客様が店探しをする段階から来店して退店した後までの一環のライフサイクルをトレタが提供する未来も描いています。</p> <p>そんな開発に参画してくれる開発メンバーを募集しています。各ポジションがオープンになっていますので、興味をお持ちになったらまずは気軽に話を聞きにきてください。</p> <p><a href="https://www.wantedly.com/companies/toreta/projects">株式会社トレタの採用/求人一覧 - Wantedly</a></p> mkitagawa-312 トレタでエンジニアがワイワイやっている取り組みの紹介 hatenablog://entry/26006613669259245 2020-12-24T11:09:28+09:00 2020-12-24T11:09:28+09:00 はじめに 本記事はトレタ Advent Calendar 2020の 24 日目の記事です。 こんにちは、パパ iOS エンジニアのkentaroです。最近 Flutter やりはじめました。 (ちなみに2019 年・2018 年も 12/24 に記事を書いていました。 ) トレタではテックトーク(LT 会)や勉強会等が行われています。 今回はそのようなエンジニアがワイワイやっている取り組みについての紹介です。 テックトーク(LT 会) 1 時間の枠で2〜3 人が自由に LT をする会で、週 1 回くらいのペースで実施されています。 社内でハードルが低いので、LT の練習の場としても役に立って… <h2>はじめに</h2> <p>本記事は<a href="https://qiita.com/advent-calendar/2020/toreta">トレタ Advent Calendar 2020</a>の 24 日目の記事です。</p> <p>こんにちは、パパ iOS エンジニアの<a href="https://twitter.com/kenkenken_3">kentaro</a>です。最近 Flutter やりはじめました。<br /> (ちなみに<a href="https://tech.toreta.in/entry/2019/12/24/164623">2019 年</a>・<a href="https://tech.toreta.in/entry/2018/12/24/163116">2018 年</a>も 12/24 に記事を書いていました。 )</p> <p>トレタではテックトーク(LT 会)や勉強会等が行われています。<br /> 今回はそのようなエンジニアがワイワイやっている取り組みについての紹介です。</p> <h2>テックトーク(LT 会)</h2> <p>1 時間の枠で2〜3 人が自由に LT をする会で、週 1 回くらいのペースで実施されています。<br /> 社内でハードルが低いので、LT の練習の場としても役に立っていると思っています。</p> <p>特にエンジニアに限定しているわけではなく、PM からプロジェクトマネジメントの話が聞けたりもして面白いです。</p> <p>詳細はこちらの記事にまとまっています。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr%2Fn%2Fn3ed141d93824" title="自由なテーマが学びにつながる!トレタ技術共有会「テックトーク」|トレタ_HR|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/toreta_hr/n/n3ed141d93824">note.com</a></cite></p> <p>自分は Swift について話したりしました。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fqiita.com%2Fktaguchi%2Fitems%2F0b6f62a65ba44830f7e3" title="[Swift] クロージャ(Closures)の記法 - Qiita" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://qiita.com/ktaguchi/items/0b6f62a65ba44830f7e3">qiita.com</a></cite></p> <h2>Go 勉強会</h2> <p><a href="https://twitter.com/tenntenn">@tenntenn</a>さんが講師をしてくださっている、Go を基礎から体系的に学ぶことができる勉強会です。<br /> 隔週くらいのペースで実施されています。</p> <p><a href="https://play.golang.org/">Playground</a>等を利用して実際のコードを交えて説明いただけるので勉強になります。<br /> たまにクイズが出ますが、大体<a href="https://tech.toreta.in/entry/2020/12/14/194125">サーバーサイド石谷くん</a>が指名されます。</p> <h2>RDB 勉強会</h2> <p><a href="https://twitter.com/lagenorhynque">@lagenorhynque</a>さんが講師をしてくださっている、11 月からはじまった新しい勉強会です。<br /> リレーショナルなデータの設計や各 DB 構造に落とす場合のコツとかを共有する場となっています。<br /> こちらも大体隔週ペースでの実施です。</p> <h2>輪読会</h2> <p>読みたい本があるときに、週 1 くらいのペースで開催されています。<br /> 直近は『ドメイン駆動設計入門 ボトムアップでわかる! ドメイン駆動設計の基本』でした。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.shoeisha.co.jp%2Fbook%2Fdetail%2F9784798150727" title="ドメイン駆動設計入門 ボトムアップでわかる!ドメイン駆動設計の基本 | 翔泳社" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://www.shoeisha.co.jp/book/detail/9784798150727">www.shoeisha.co.jp</a></cite></p> <p>正に DDD 入門にふさわしい良書だったのでオススメです。</p> <p>次回は<a href="https://www.amazon.co.jp/dp/B07FSBHS2V">『Clean Architecture  達人に学ぶソフトウェアの構造と設計』</a>を読もうかという話になっています。</p> <p>輪読会についてはこちらの記事に詳しく書かれています。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2020%2F12%2F11%2F163431" title="トレタでの輪読会 - トレタ開発者ブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tech.toreta.in/entry/2020/12/11/163431">tech.toreta.in</a></cite></p> <h2>アドベントカレンダー</h2> <p>12 月は大体アドベントカレンダーやってます。</p> <ul> <li><a href="https://qiita.com/advent-calendar/2020/toreta">2020</a></li> <li><a href="https://qiita.com/advent-calendar/2019/toreta">2019</a></li> <li><a href="https://qiita.com/advent-calendar/2018/toreta">2018</a></li> <li><a href="https://qiita.com/advent-calendar/2017/toreta">2017</a></li> <li><a href="https://qiita.com/advent-calendar/2016/toreta">2016</a></li> </ul> <h2>おわりに</h2> <p>得意な技術や担当するプロダクトの枠を超えて共に学べるのはとてもいいですね。<br /> 今回挙げたような取り組みが技術力向上にプラスになるのはもちろん、普段の仕事であまり接点がない人とも関わる機会にもなっていると思います。</p> <h3>トレタが気になる方は…</h3> <p>ぜひ遊びにきてください!<br /> 仲間も絶賛募集中です。<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcorp.toreta.in%2Frecruit%2Fmidcareer%2F" title="中途採用 | 株式会社トレタ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://corp.toreta.in/recruit/midcareer/">corp.toreta.in</a></cite></p> <p>HR 部門が note で発信していますので、そちらもぜひ御覧ください!<br /> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fnote.com%2Ftoreta_hr" title="トレタ_HR|note" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://note.com/toreta_hr">note.com</a></cite></p> kenkenken_3 6年目エンジニアが1年間アウトプット駆動開発をしたら"圧倒的成長"した件について hatenablog://entry/26006613659151050 2020-12-23T00:00:00+09:00 2020-12-23T00:00:13+09:00 本記事はトレタアドベントカレンダーの23日目の記事です。 始めに 皆さま、こんにちは! 念願のスプラトゥーンのサーモンランカンストを達成したハイカラスクエアのタコガール兼佐久間まゆちゃん・北条加蓮ちゃんのプロデューサーの@hiroki_tanakaです。 本記事では私が今年1年間実践してきたアウトプット駆動開発に関して振り返ります。 丁度1年前の自分がこんな記事を書いていたので、アンサーソング的な意味合いも込めて笑 tech.toreta.in アウトプット駆動開発とは 私は2020年の目標に「インプットとアウトプットのスループットを良くする」を設定しました。 この目標を立てた理由はここ1~2… <h4>本記事は<a href="https://qiita.com/advent-calendar/2020/toreta">トレタアドベントカレンダー</a>の23日目の記事です。</h4> <h1>始めに</h1> <p>皆さま、こんにちは!<br> 念願のスプラトゥーンのサーモンランカンストを達成したハイカラスクエアのタコガール兼佐久間まゆちゃん・北条加蓮ちゃんのプロデューサーの<a href="https://qiita.com/hiroki_tanaka">@hiroki_tanaka</a>です。<br> 本記事では私が今年1年間実践してきたアウトプット駆動開発に関して振り返ります。<br> 丁度1年前の自分がこんな記事を書いていたので、アンサーソング的な意味合いも込めて笑<br> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.toreta.in%2Fentry%2F2019%2F12%2F11%2F070000" title="5年目エンジニアの考えるエンジニアとして&quot;圧倒的成長&quot;する方法 - トレタ開発者ブログ" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;"></iframe><cite class="hatena-citation"><a href="https://tech.toreta.in/entry/2019/12/11/070000">tech.toreta.in</a></cite></p> <h1>アウトプット駆動開発とは</h1> <p>私は2020年の目標に「インプットとアウトプットのスループットを良くする」を設定しました。<br> この目標を立てた理由はここ1~2年程全く外部へのアウトプットを怠っていた結果、エンジニアとしての成長曲線が止まってしまった感覚があり、その成長曲線を再度動かしたかったためです。<br> そこでアウトプットを自発的に増やすためにアウトプット駆動開発を導入してみました。<br> アウトプット駆動開発を一言で言うと、アウトプットする前提でインプットするというマインドセットです。</p> <p>一般的なメリットとして、以下のようなことが挙げられます。</p> <ul> <li>アウトプットを意識することで、注意深くインプットするようになり体系的な理解に繋がる。</li> <li>アウトプットに対するフィードバックやコメントをもらうことで、より深く学ぶ事ができる。</li> <li>数年後の自分が簡単に振り返ることができる。</li> </ul> <p>そのため、私は具体的には業務・業務外問わず学んだことはすぐに何らかの形でアウトプットして発信していくこととしました。<br> アウトプットの形は様々で<a href="https://tech.toreta.in/">トレタテックブログ</a>や<a href="https://qiita.com/hiroki_tanaka">Qiita</a>での技術記事投稿から、<a href="https://note.com/toreta_hr/n/n3ed141d93824">社内勉強会</a>での発表・社外勉強会の<a href="https://gotandarb.github.io/">Gotanda.rb</a>での発表という社内外での発表もありました。<br> 他にもトリッキーなものだと読んだ本の書評をAmazonレビューを書いたり、<a href="https://tech.toreta.in/entry/2020/12/02/080000">資格取得</a>をしたり、社外の友人と<a href="https://techbookfest.org/">技術書典</a>に合同誌を出品したりしました。<br></p> <p>ただ、私はSNSを殆どやっていないのでそこからの発信は全くしていなかったです。<br> そのため、アウトプットの拡散力という意味では正直イマイチでしたが、バズることや有名になることが目的ではないのでそこはあまり気になりませんでした。<br> (今思い返すとSNSを効率的に活用した方がよりフィードバックは得られたかもです。)<br></p> <h1>実践したこと</h1> <p>そんな今年一年のアウトプット駆動開発の成果ですが、下記の通りです。<br></p> <ul> <li>トレタテックブログの記事投稿:4記事</li> <li>Qiitaの記事投稿:8記事</li> <li>Amazonへの書評レビュー:9レビュー</li> <li>技術書典への出品:1冊</li> <li>社内勉強会での発表:5回</li> <li>社外勉強会での発表:3回</li> <li>資格取得:1資格</li> </ul> <p>改めて数字として数えてみると「よくやったなぁ…」というのが率直な感想です笑<br></p> <p>これらのアウトプットは勿論、どこかの月にまとめて行ったという訳ではなく、1年間を通じて満遍なく行ってきました。<br> 強いてあげれば、上半期はQiita・トレタテックブログ・Amazonへの書評レビューといった記事投稿がメインでしたが、下半期は社外勉強会での発表がメインでした。<br> おそらく、これは最初は記事投稿といったある種、一方通行なアウトプットで満足していたのですが、 徐々にアウトプットした際に見てくださった方からのフィードバックというインタラクティブなコミュニケーションが恋しくなったためです。<br></p> <p>発表に関してはネタがなくてもまず「発表します」と手を挙げることを常に意識していました。<br> 挙手してからネタを考える方式だったので、発表1~2週間前は毎回「ネタどうしよう…」と悩んでいました。<br> 今振り返るとこの挙手してからネタ考えるというのは、アウトプット駆動開発に非常にマッチしていました。<br></p> <h1>実践して感じたこと</h1> <h4>1. 学んだ技術の深い理解</h4> <p>記事や発表共に何かしらのネタが見つかると色々試行錯誤しながら、記事や資料を作っていきます。<br> 外部へのアウトプットを意識すると「外部に発信するのだから、下手なこと言えない…」という気持ちにもなり、丁寧に調べるようになりました。<br> そうすると自然と今まで自分が知らなかったことや曖昧なままの何となくの理解だった部分もしっかり理解出来るようになりました。<br></p> <p>例えば、私は下記の発表をするまでRailsの権限管理に関して断片的な知識しかなかったのですが、まとめていく中で体系だった知識が身につきました。</p> <script async class="speakerdeck-embed" data-id="fc48ff14095a408fa3222cfde87c3da4" data-ratio="1.33333333333333" src="//speakerdeck.com/assets/embed.js"></script> <p>また、よく言われることですが、勉強会は発表者が一番勉強になるということを身に沁みて実感しました。<br> 下手でも聞く側にいるよりも発信する側にいる方が断然良いです。<br></p> <h4>2. わかりやすく説明できる力の向上</h4> <p>記事でも発表でも自分の学んだこと・経験したことを読み手や聞き手にわかりやすく伝わるように論理立ててまとめる必要があります。<br> 勿論、普段の業務の中でも例えば、Pull Requestの概要や仕様のまとめドキュメントなどわかりやすく伝えるといったことを心がけることはありますが、<br> あくまでも社内の人は前提や背景を知った上で読み取ってくれる部分も正直、小さくはないです。<br></p> <p>しかし、外部の人は前提や背景を一切知りません。<br> その人たちにわかりやすく伝わるように私は以下のことに気をつけました。<br></p> <ul> <li>「自分が知ってるのだから、相手は絶対知っている。」という思い込みを捨てる。</li> <li>言葉だけで説明せずに、サンプルコードを適度に使用する。</li> <li>記事で伝えたいことや発表で話したいことを最初に書くようにする。また、伝えたいことも最大3つまでに絞る。</li> </ul> <p>このような工夫を行った下記の発表は「わかりやすかった」というフィードバックを頂けて、かなり嬉しかったです!<br></p> <script async class="speakerdeck-embed" data-id="704512045e28434d814b624a6da99b9d" data-ratio="1.33333333333333" src="//speakerdeck.com/assets/embed.js"></script> <h4>3. 高いモチベーションの維持</h4> <p>1人で粛々と勉強していてもモチベーションを維持するのはとても難しいです。<br> (勉強の最大の敵はモチベーションの維持だと思っています。)<br></p> <p>しかし、外部にアウトプットすると記事の場合はLGTMやスターが・発表の場合は聴衆からのフィードバックが貰えるようになります。<br> これがモチベーション維持に役立ち、「自分の記事が誰かの役に立っているんだ!」や「発表後に活発な議論が起こったということは皆気になってたんだ!」と感じることが出来、かなり励みになりました!<br></p> <p>また、アウトプットすると自分の理解が深まると同時に、自分の知らなかったことも見えてくるので次はこういうことを勉強しよう・やってみようと思い、それがまたモチベーションUPに一役買いました。<br></p> <h1>まとめと今後</h1> <p>総括するとアウトプット駆動開発を通じて、成長速度は2段階くらいギアが上がったように感じたので、やってみて本当に良かったです! そのため、来年もアウトプット駆動開発は続けていきます!<br></p> <p>今年は上期は記事投稿中心・下期は発表中心のアウトプット媒体のバランスが悪かったので、来年は記事投稿と発表をバランス良く両立させていきたいです。<br> そして、外部での発表は社外勉強会だけではなくより大きなカンファレンスなどでも登壇したいです。<br> 勿論、勉強会での発表も続けていきたいです!<br></p> <p>また、自分が発表するだけでなく、他のエンジニアが発表出来る場も提供出来るようになりたいです!<br> (このアドベントカレンダーもその1つです!)<br></p> <h1>終わりに</h1> <p>正直に言うとアウトプット駆動開発はやっぱり、かなりしんどくて「休日なんだから、ダラダラしたい…」といった誘惑や「こんな記事書いても誰も見てくれないでしょ…」といったネガティブ感情に囚われたことは数え切れません。<br> ただ、業務と同じで発表(・ものによっては記事投稿も)は発表日という明確な締切があり、かなり強制力が働くのでかえって良かったのかなと思います。<br> おかげで継続する事ができ、1年間のアウトプット駆動開発を通じて圧倒的成長することが出来たと胸を張って言えます!<br> これからもこの2つの言葉を胸に来年も邁進していきます。<br></p> <blockquote><p><em>インプットは必要、でも差別化要因にならない。<br> しかし、アウトプットすることで差別化になる。<br> ~@yukihiro_matz~</em></p></blockquote> <p><br></p> <blockquote><p><em>自分と同じくらいの能力がある人間はいくらでもいる。<br> 学び続ける姿勢を止めればかつての恐竜と同じ末路をたどることになる。<br> ~『プログラマが知るべき97のこと』~</em></p></blockquote> <p><div class="hatena-asin-detail"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4873114799/hatena-blog-22/"><img src="https://m.media-amazon.com/images/I/511RPej0BNL.jpg" class="hatena-asin-detail-image" alt="プログラマが知るべき97のこと" title="プログラマが知るべき97のこと"></a><div class="hatena-asin-detail-info"><p class="hatena-asin-detail-title"><a href="https://www.amazon.co.jp/exec/obidos/ASIN/4873114799/hatena-blog-22/">プログラマが知るべき97のこと</a></p><ul><li><span class="hatena-asin-detail-label">発売日:</span> 2010/12/18</li><li><span class="hatena-asin-detail-label">メディア:</span> 単行本(ソフトカバー)</li></ul></div><div class="hatena-asin-detail-foot"></div></div></p> <h1>終わりの終わりに</h1> <p>トレタに少しでも興味を持っていただいた方がいれば、ぜひ遊びに来てくださいヾ(o・ω・)ノ<br> <a href="https://corp.toreta.in/recruit/midcareer/">仲間</a>も募集しています!<br></p> hiroki_tanaka