コネヒト開発者ブログ コネヒト開発者ブログ 2024-02-15T18:23:21+09:00 connehito Hatena::Blog hatenablog://blog/8454420450094796513 iOS/Android 開発Tips共有会 potatotips #86 を開催しました hatenablog://entry/6801883189081837348 2024-02-15T18:23:21+09:00 2024-02-15T18:23:21+09:00 こんにちは!iOSエンジニアのyoshitakaです。 先日コネヒト主催でiOS/Android 開発Tips共有会 potatotips #86 を開催しました! potatotips.connpass.com 前回コネヒトで主催した際のブログがこちらです。 tech.connehito.com 今回はオンライン/オフラインのハイブリッド開催という形を取りました。 現地参加22名、オンライン視聴も常時30名以上とたくさんの方に参加していただき感謝です。 会場の雰囲気 登壇者のスライドを一部ご紹介させていただきます。(当日xでも紹介しておりました) speakerdeck.com speake… <p>こんにちは!iOSエンジニアのyoshitakaです。</p> <p>先日コネヒト主催でiOS/Android 開発Tips共有会 potatotips #86 を開催しました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fpotatotips.connpass.com%2Fevent%2F307311%2F" title="potatotips #86 iOS/Android開発Tips共有会 (2024/02/07 19:00〜)" 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://potatotips.connpass.com/event/307311/">potatotips.connpass.com</a></cite></p> <p>前回コネヒトで主催した際のブログがこちらです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2021%2F01%2F05%2F153656" title="iOS/Android 開発Tips共有会 potatotips #72 をオンライン開催しました - コネヒト開発者ブログ" 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.connehito.com/entry/2021/01/05/153656">tech.connehito.com</a></cite></p> <p>今回はオンライン/オフラインのハイブリッド開催という形を取りました。</p> <p>現地参加22名、オンライン視聴も常時30名以上とたくさんの方に参加していただき感謝です。</p> <p><figure class="figure-image figure-image-fotolife" title="会場の雰囲気"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20240209/20240209183248.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>会場の雰囲気</figcaption></figure></p> <p>登壇者のスライドを一部ご紹介させていただきます。(<a href="https://twitter.com/connehitomarche">当日xでも紹介しておりました</a>)</p> <p><iframe id="talk_frame_1143195" class="speakerdeck-iframe" src="//speakerdeck.com/player/deea99a89ab549fb847bed6df3d144f1" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/akkie76/observationdehazimeruzhi-jian-shi">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_1143378" class="speakerdeck-iframe" src="//speakerdeck.com/player/2c543faf0fb5420b85d58c9563ea6b15" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/kubode/results-of-complete-migration-to-k2">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_1143778" class="speakerdeck-iframe" src="//speakerdeck.com/player/a49f1c87769b43bd90bfd4cf086abaa5" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/uetyo/healthkit-to-coremotion-noquan-xian-nisi-ku-ba-ku-sitahua">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_1143785" class="speakerdeck-iframe" src="//speakerdeck.com/player/3e8420f13e544c93bb212d5c5c2897a2" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/mthiroshi/flutter-secure-storagetoandroidbatukuatupunozhu-yi-dian">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_1143548" class="speakerdeck-iframe" src="//speakerdeck.com/player/d8436a67befb4b7e962ab1fa59534442" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/tsuboyan5/liveactivity-teahurinoli-yong-ti-yan-woxiang-shang-saseyou">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_1143536" class="speakerdeck-iframe" src="//speakerdeck.com/player/1ea32c3e8ea44e4c98e9849d89ad8508" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/umsys/gemini-apiwoshi-tutemiyou-at-potatotips-number-86">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_1143527" class="speakerdeck-iframe" src="//speakerdeck.com/player/94e99f7454ce4d7bbe1ebd05519fe4d3" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/swiftty/swiftlint-nikontorihiyutosuru">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_1143258" class="speakerdeck-iframe" src="//speakerdeck.com/player/55f41cf631d0427d852f813e15eb4904" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/fumiyasac0921/overviewing-tca-v1-dot-7-and-back-and-forth-with-mvvm">speakerdeck.com</a></cite></p> <p><iframe id="talk_frame_1143524" class="speakerdeck-iframe" src="//speakerdeck.com/player/b8988f89aaa64bb3870f2f1166a8ce50" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/tomorrowkey/strong-skipping-modewohazimeyou">speakerdeck.com</a></cite></p> <p>コネヒトからもiOSとAndroidでそれぞれ1名LTさせていただきました!</p> <p><iframe src="https://www.docswell.com/slide/Z6Y22X/embed?key=2024-02-08-093659" allowfullscreen="true" class="docswell-iframe" width="620" height="349" style="border: 1px solid #ccc; display: block; margin: 0px auto; padding: 0px; aspect-ratio: 620/349;"></iframe><cite class="hatena-citation"><a href="https://www.docswell.com/s/4109190/Z6Y22X-2024-02-08-093659">www.docswell.com</a></cite></p> <p><figure class="figure-image figure-image-fotolife" title="会場の雰囲気"><div class="images-row mceNonEditable"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20240209/20240209185648.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20240209/20240209185930.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20240209/20240209185946.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20240209/20240209185953.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20240209/20240209190015.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></div></figure></p> <p>当日は概ねタイムライン通りに進めることができました。</p> <p>登壇された方々にはスムーズな運営にご協力頂きありがとうございました!</p> <p>LT発表後の懇親会も盛り上がっておりました🎉</p> <p>引き続き勉強会を開催していきたいと思っておりますので、コラボして頂ける企業様がございましたらぜひお声がけ頂けると嬉しいです!</p> yoshitaka465 Draft Releaseデプロイフローを作成してみました hatenablog://entry/6801883189064097350 2024-02-07T11:21:23+09:00 2024-02-07T11:21:23+09:00 こんにちは。サーバーサイドエンジニアの岡田です みなさんはどんなデプロイフローを採用していますでしょうか?今回は自分が取り組んでいたデプロイフローのちょっとした改善を皆さんに紹介したいと思います! きっかけ 現状コネヒトではgdpを使ったデプロイ方法を採用しており、手元のコマンドからデプロイするのが標準になっています。 tech.connehito.com 元々自分はgit-pr-releaseを使ったワンクリックデプロイを以前の現場で使っており、手元でコマンドを打たなければいけないのが地味にめんどうだなあと感じていました。 そこでワンクリックでもっとラクしてデプロイしたい!と思ったのが今回の… <p>こんにちは。サーバーサイドエンジニアの岡田です</p> <p>みなさんはどんなデプロイフローを採用していますでしょうか?今回は自分が取り組んでいたデプロイフローのちょっとした改善を皆さんに紹介したいと思います!</p> <h3 id="きっかけ">きっかけ</h3> <p>現状コネヒトではgdpを使ったデプロイ方法を採用しており、手元のコマンドからデプロイするのが標準になっています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2018%2F08%2F28%2F124608" title="gdpというGo製のCLIツールを公開しました - コネヒト開発者ブログ" 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.connehito.com/entry/2018/08/28/124608">tech.connehito.com</a></cite></p> <p>元々自分はgit-pr-releaseを使ったワンクリックデプロイを以前の現場で使っており、手元でコマンドを打たなければいけないのが地味にめんどうだなあと感じていました。 そこでワンクリックでもっとラクしてデプロイしたい!と思ったのが今回のデプロイフローを考案したきっかけになります!</p> <h4 id="それ以外にも手元のコマンドからデプロイする事に感じていた課題感">それ以外にも手元のコマンドからデプロイする事に感じていた課題感</h4> <ul> <li>手元でコマンド打つ場合、打ち間違いやコマンド履歴等から意図せずデプロイしてしまう可能性</li> <li>devデプロイ〜本番リリースの間(ビルドの7〜8分)地味にブランチを切り替え辛い(リリース前にmainブランチにcheckoutが必要なため)</li> <li>新しい人が入社してきた場合にgdp&hubのインストールと設定が必要になる</li> <li>本番デプロイ後にリリースノートを公開(gdp publish)するのを忘れてしまう</li> </ul> <h4 id="そしてこのデプロイフローが生まれた背景">そしてこのデプロイフローが生まれた背景</h4> <p>当初はgit-pr-releaseを導入すれば簡単にワンクリックデプロイ出来るようになる!と考えていました。しかしいざgit-pr-releaseを導入しようと調べてみると、難しい背景がいくつか見つかりました。</p> <p><strong>ちなみにgit-pr-releaseとは</strong></p> <p><a href="https://github.com/x-motemen/git-pr-release">git-pr-release</a>とは、本番環境向けの対象ブランチへのリリースPRを作成&リリース内容を一覧にして作成してくれるgemライブラリです。導入も簡単なので使える環境であればぜひ使ってみて欲しいです</p> <p>こちらの記事が非常に参考になります!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fsongmu.jp%2Friji%2Fentry%2F2022-08-05-git-pr-release.html" title="git-pr-releaseとGitHub Actionsでワンクリックデプロイを実現する | おそらくはそれさえも平凡な日々" 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://songmu.jp/riji/entry/2022-08-05-git-pr-release.html">songmu.jp</a></cite></p> <p>まずgit-pr-releaseが想定しているブランチモデルは</p> <ul> <li>本番用のmainブランチと開発用のdevelopブランチが並走する形で、developブランチからfeatureブランチを切ってmainブランチへマージしていく、いわゆるgit flowの簡易版の様なフローが前提</li> <li>mainブランチへのマージが本番デプロイへのトリガーとして想定されている</li> <li>環境構成:本番環境(main)、dev環境(develop)</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="git-pr-releaseが想定しているブランチモデル"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204211014.png" width="1200" height="514" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>git-pr-releaseが想定しているブランチモデル</figcaption></figure></p> <p>対してコネヒトでは</p> <ul> <li>GitHub Flowを踏襲しており、本番用のmainブランチからfeatureブランチを切ってmainブランチへマージしていくフローになっている</li> <li>最新のmainブランチから切ったtagのpushが本番デプロイへのトリガーになっている</li> <li>環境構成:本番環境(最新のtag)、dev環境(featureブランチ)</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="コネヒトのブランチモデル"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204211039.png" width="1200" height="449" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>コネヒトのブランチモデル</figcaption></figure></p> <p>なのでもしgit-pr-releaseを採用する場合はブランチ戦略自体を見直す必要があり、流石にそれは影響範囲が大きすぎるかつ現実的ではありませんでした。そこで今あるブランチ戦略を変えずにもっと簡単にデプロイを実現する方法として考えたのがこの「Draft Releaseデプロイフロー」です。</p> <h2 id="そして実際にできたデプロイフローがこちら">そして実際にできたデプロイフローがこちら</h2> <p>いつものようにPRをマージします</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204211102.png" width="1200" height="833" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Slackにdev環境へのデプロイ完了とドラフトリリースの作成が通知されます</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204211207.png" width="1200" height="310" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>作成されたドラフトリリースを</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204211333.png" width="1200" height="781" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Publishすると</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204211350.png" width="1200" height="910" 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/o/okdyy/20231204/20231204211406.png" width="1200" height="883" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>tagが切られ本番デプロイが走ると言った感じです</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204212734.png" width="1200" height="451" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="ビフォーアフター">ビフォーアフター</h2> <h3 id="既存のgdpを使ったデプロイフロー">既存のgdpを使ったデプロイフロー</h3> <ol> <li>mainブランチからfeatureブランチを切る</li> <li>feature PRをmainブランチへマージ(dev環境へデプロイ)</li> <li>dev環境へのデプロイが完了したら、手元でgdpコマンドからタグを切ってpush <ol> <li>手元のmainを最新化(git checkout main &amp;&amp; git pull origin main &amp;&amp; git pull --tags)</li> <li>リリース内容確認(gdp deploy -d)</li> <li>タグを切って本番デプロイ(gdp deploy)</li> </ol> </li> <li>本番環境へのデプロイが完了したら、手元でgdpを使ってリリースノートを公開 <ol> <li>リリースノート内容確認(gdp publish -d)</li> <li>リリースノート公開(gdp publish)</li> </ol> </li> </ol> <p><figure class="figure-image figure-image-fotolife" title="デプロイフローBefore"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204211447.png" width="1200" height="523" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デプロイフローBefore</figcaption></figure></p> <h3 id="今回作成したDraft-Releaseデプロイフロー">今回作成したDraft Releaseデプロイフロー</h3> <ol> <li>mainブランチからfeatureブランチを切る</li> <li>feature PRをmainブランチへマージ(dev環境へデプロイ)</li> <li>dev環境へのデプロイが完了したら、GitHub上から下書きリリースノートの内容を確認し、問題なければ公開(合わせてタグも切られるのでそのまま本番デプロイ)</li> </ol> <p><figure class="figure-image figure-image-fotolife" title="デプロイフローAfter"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/o/okdyy/20231204/20231204211500.png" width="1200" height="797" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>デプロイフローAfter</figcaption></figure></p> <h3 id="改善ポイント">改善ポイント</h3> <ul> <li>手元でコマンド打つ場合、打ち間違いやコマンド履歴等から意図せずデプロイしてしまう可能性 <ul> <li>GUI上から確認できるので、意図せずデプロイしてしまうといったことを防げる</li> </ul> </li> <li>devデプロイ〜本番リリースの間(ビルドの7〜8分)地味にブランチを切り替え辛い(リリース前にmainブランチにcheckoutが必要なため) <ul> <li>devデプロイ後は別のブランチに切り替えてすぐ作業の続きができるように</li> </ul> </li> <li>新しい人が入社してきた場合にgdp&hubのインストールと設定が必要になる <ul> <li>今回の対応で不要に</li> </ul> </li> <li>本番デプロイ後にリリースノートを公開(gdp publish)するのを忘れてしまう <ul> <li>必ずリリースノートが作成されるフローに</li> </ul> </li> </ul> <h2 id="実際のコード">実際のコード</h2> <h3 id="呼び出し元">呼び出し元</h3> <h4 id="ドラフトリリース作成ワークフロー">ドラフトリリース作成ワークフロー</h4> <p>下記コードを各PJのリポジトリに展開するだけでドラフトリリースが作成されます!</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">push</span><span class="synSpecial">:</span> <span class="synIdentifier">branches</span><span class="synSpecial">:</span> <span class="synStatement">- </span>main <span class="synIdentifier">permissions</span><span class="synSpecial">:</span> <span class="synIdentifier">contents</span><span class="synSpecial">:</span> write <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">deploy</span><span class="synSpecial">:</span> <span class="synComment"> # デプロイ処理</span> ... <span class="synIdentifier">create_draft_release</span><span class="synSpecial">:</span> <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ success() }} <span class="synIdentifier">needs</span><span class="synSpecial">:</span> <span class="synSpecial">[</span> deploy <span class="synSpecial">]</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> <span class="synSpecial">{</span>Reusable Workflow用リポジトリ<span class="synSpecial">}</span>/reusable-workflow/.github/workflows/create_draft_release.yml@main <span class="synIdentifier">secrets</span><span class="synSpecial">:</span> <span class="synIdentifier">slack-webhook-url</span><span class="synSpecial">:</span> ${{ secrets.SLACK_WEBHOOK_URL }} </pre> <h3 id="呼び出される側">呼び出される側</h3> <h4 id="ドラフトリリース作成ワークフロー-1">ドラフトリリース作成ワークフロー</h4> <p>ドラフトリリース作成actionの実行と、Slack通知を行っています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create Draft Release <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">workflow_call</span><span class="synSpecial">:</span> <span class="synIdentifier">secrets</span><span class="synSpecial">:</span> <span class="synIdentifier">slack-webhook-url</span><span class="synSpecial">:</span> <span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">start</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Action create-draft-release <span class="synIdentifier">id</span><span class="synSpecial">:</span> create-draft-release <span class="synIdentifier">uses</span><span class="synSpecial">:</span> <span class="synSpecial">{</span>Reusable Workflow用リポジトリ<span class="synSpecial">}</span>/reusable-workflow/.github/actions/create-draft-release@main <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Notify slack of create draft release <span class="synIdentifier">uses</span><span class="synSpecial">:</span> 8398a7/action-slack@v3 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">status</span><span class="synSpecial">:</span> ${{ job.status }} <span class="synIdentifier">fields</span><span class="synSpecial">:</span> repo,workflow <span class="synIdentifier">author_name</span><span class="synSpecial">:</span> ${{ github.actor }} <span class="synIdentifier">text</span><span class="synSpecial">:</span> | ドラフトリリースが作成されました。&lt;${{ steps.create-draft-release.outputs.draft-release-link }}|${{ steps.create-draft-release.outputs.draft-release-tag }}&gt; &lt;${{ github.server_url }}/${{ github.repository }}/releases|リリース一覧&gt; <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">SLACK_WEBHOOK_URL</span><span class="synSpecial">:</span> ${{ secrets.slack-webhook-url }} </pre> <h4 id="実装時のポイント">実装時のポイント</h4> <p>Reusable Workflowとはいわゆる再利用可能なワークフローの事で、各リポジトリで共通のワークフローを実行することができます! 作成方法はシンプルで<code>on.workflow_call</code>を定義したymlファイルを作成するだけです。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">workflow_call</span><span class="synSpecial">:</span> </pre> <p>今回Reusable Workflowとして実装した理由ですが、Reusable Workflowと後述のaction.ymlの大きな違いとしては実行環境に左右されるかどうかだと考えています。</p> <p>Reusable Workflowの場合はワークフロー(1つ以上のジョブ)でしか実行できないので前後の処理や実行環境に左右されません。逆にaction.ymlではジョブの一ステップとして実行されるので、必要であれば <code>actions/checkout@v2</code> や <code>actions/setup-node@v2</code> など使って実行環境を整える必要があります。</p> <p>そして今回のDraft Release作成処理は完全に処理が独立しているので、リポジトリ毎の実行環境に左右されずに横展開しやすいReusable Workflowで実装しました</p> <p>Reusable Workflow使用時の注意点としては実行されるコードはあくまで<strong>呼び出し元で実行される</strong>ので</p> <ul> <li><code>actions/checkout</code> を使用する場合は呼び出し元のブランチにチェックアウトされる</li> <li>Reusable Workflowのリポジトリ内にあるファイルは参照できない</li> </ul> <p>と言った点に注意が必要です!上記点を解決するために次のaction.ymlへとつながります</p> <p>詳しくは公式ドキュメントを確認してください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fen%2Factions%2Fusing-workflows%2Freusing-workflows" title="Reusing workflows - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/en/actions/using-workflows/reusing-workflows">docs.github.com</a></cite></p> <h4 id="ドラフトリリース作成処理本体">ドラフトリリース作成処理本体</h4> <p>こちらがメインのドラフトリリース作成処理です。実行している処理はシンプルにGitHub CLIを使ったリリースノートの作成と削除を行なっています。(削除はリリースノートの自動生成を使いたいため一度削除→再作成しています)</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create Draft Release Action <span class="synIdentifier">description</span><span class="synSpecial">:</span> Create draft release with calver tag <span class="synIdentifier">outputs</span><span class="synSpecial">:</span> <span class="synIdentifier">draft-release-link</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> Create draft release link. <span class="synIdentifier">value</span><span class="synSpecial">:</span> ${{ steps.create-draft-release.outputs.draft-release-link }} <span class="synIdentifier">draft-release-tag</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> Create draft release tag. <span class="synIdentifier">value</span><span class="synSpecial">:</span> ${{ steps.create-draft-release.outputs.draft-release-tag }} <span class="synIdentifier">runs</span><span class="synSpecial">:</span> <span class="synIdentifier">using</span><span class="synSpecial">:</span> <span class="synConstant">&quot;composite&quot;</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Create Draft Release <span class="synIdentifier">id</span><span class="synSpecial">:</span> create-draft-release <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">TZ</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Asia/Tokyo&quot;</span> <span class="synIdentifier">GH_TOKEN</span><span class="synSpecial">:</span> ${{ github.token }} <span class="synIdentifier">shell</span><span class="synSpecial">:</span> bash <span class="synIdentifier">run</span><span class="synSpecial">:</span> | <span class="synComment"> # 最新のタグ取得</span> latest_tag=$(gh release list --repo <span class="synConstant">'${{ github.repository }}'</span> | awk -F'\\t' <span class="synConstant">'$2==&quot;Latest&quot; { print $3 }'</span>) echo <span class="synConstant">&quot;latest_tag:${latest_tag}&quot;</span> <span class="synComment"> # calver形式での次バージョンのタグ取得</span> next_tag=$(/bin/bash ${{ github.action_path }}/calver.sh $latest_tag) echo <span class="synConstant">&quot;next_tag:${next_tag}&quot;</span> <span class="synComment"> # draft release取得</span> draft_tag=$(gh release list --repo <span class="synConstant">'${{ github.repository }}'</span> | awk -F'\\t' <span class="synConstant">'$2==&quot;Draft&quot; { print $3 }'</span> | head -n 1) echo <span class="synConstant">&quot;draft_tag:${draft_tag}&quot;</span> <span class="synComment"> # draft release が既に存在していれば、削除してから再作成(リリースノートの自動生成を使いたいため)</span> if <span class="synSpecial">[</span> <span class="synConstant">&quot;$draft_tag&quot;</span> != <span class="synConstant">&quot;&quot;</span> <span class="synSpecial">]</span>; then gh release delete $draft_tag --repo <span class="synConstant">'${{ github.repository }}'</span> -y fi draft_release_link=$( gh release create $next_tag \\ --repo <span class="synConstant">'${{ github.repository }}'</span> \\ --title <span class="synConstant">&quot;Release ${next_tag}&quot;</span> \\ --latest --draft --generate-notes ) echo <span class="synConstant">&quot;draft-release-link=${draft_release_link}&quot;</span> &gt;&gt; <span class="synConstant">&quot;${GITHUB_OUTPUT}&quot;</span> echo <span class="synConstant">&quot;draft-release-tag=${next_tag}&quot;</span> &gt;&gt; <span class="synConstant">&quot;${GITHUB_OUTPUT}&quot;</span> echo $draft_release_link </pre> <h4 id="実装時のポイント-1">実装時のポイント</h4> <p>action.ymlとはいわゆるメタデータ構文と言われているもので、ワークフロー内の処理を切り出したり共通化することができます! 普段よく使っているような<code>actions/checkout</code>のようなアクションを作成できるイメージです。作成方法はこちらもシンプルで<code>action.yml</code>または<code>action.yaml</code>のファイル名で作成するだけです</p> <p>action.ymlではパブリックリポジトリにあるアクションはもちろん、同一リポジトリ内のアクションからプライベートリポジトリのアクションも実行可能です。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink> <span class="synComment"> # パブリックのaction</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/create-draft-release@main <span class="synComment"> # 同一リポジトリ内のaction</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> ./.github/actions/create-draft-release <span class="synComment"> # プライベートリポジトリのaction</span> <span class="synIdentifier">uses</span><span class="synSpecial">:</span> <span class="synSpecial">{</span>owner<span class="synSpecial">}</span>/{repo}/.github/actions/create-draft-release@main </pre> <p>詳しくは公式ドキュメントと</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdocs.github.com%2Fja%2Factions%2Fcreating-actions%2Fabout-custom-actions" title="カスタム アクションについて - GitHub Docs" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://docs.github.com/ja/actions/creating-actions/about-custom-actions">docs.github.com</a></cite></p> <p>こちらの記事が参考になりました!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.mamezou-tech.com%2Fblogs%2F2022%2F12%2F24%2Fsharing-private-actions-and-reusable-workflows%2F" title="GitHub Actions - private リポジトリの Action と再利用可能ワークフローが呼び出しが可能に | 豆蔵デベロッパーサイト" 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://developer.mamezou-tech.com/blogs/2022/12/24/sharing-private-actions-and-reusable-workflows/">developer.mamezou-tech.com</a></cite></p> <h4 id="calver形式でのタグ作成シェルスクリプト">calver形式でのタグ作成シェルスクリプト</h4> <p>このシェルスクリプトを作成した背景ですが、前提としてコネヒトでは<a href="https://calver.org/">calver</a>形式でのバージョン管理を採用しています。</p> <p>タグ作成にあたってできるだけ既存と同じようなcalver形式のタグを作成する必要があったのですが、現状これと同じようなタグを作成するツールが見つかりませんでした。そこで元々のgdpコマンドで行なっていたタグ作成ロジックをそのままコピーしてきて、このシェルスクリプトを作成することにしました。(GitHub Actionsとの相性や実行の手軽さも考えてシェルスクリプトにしました)</p> <pre class="code bash" data-lang="bash" data-unlink>#!/bin/bash set -eu ############################## ## Get next calver ## @ref &lt;https://github.com/Connehito/gdp/blob/main/format.go&gt; ############################## today=$(date +&#39;%Y%m%d&#39;) regexp=&#34;(.*)([0-9]{8})\\.(.+)&#34; version=${1:-$today} if [[ $version =~ $regexp ]]; then if [[ $today = ${BASH_REMATCH[2]} ]]; then minor=${BASH_REMATCH[3]} next=$(($minor + 1)) echo &#34;${BASH_REMATCH[1]}${today}.${next}&#34; exit 0 fi echo &#34;${BASH_REMATCH[1]}${today}.1&#34; exit 0 fi echo &#34;${today}.1&#34; exit 0</pre> <h4 id="おわりに">おわりに</h4> <p>同じような環境でデプロイフローに悩みを感じでいる方の参考になれば嬉しいです</p> okdyy AWSコスト削減の一環でecrmを使ってECRの不要イメージを削除した話 hatenablog://entry/6801883189075735823 2024-01-18T17:44:59+09:00 2024-01-18T23:19:04+09:00 こんにちは、コネヒト プラットフォームグループの@yosshiです。 昨今、円安の進行によりAWSのコスト増が無視できない状況となっています。 このような状況下で、コスト削減関連のイベントに注目が集まるなど、コスト削減への関心が徐々に高まってきているように感じます。 弊社でもさまざまな方法でコスト削減施策を進めており、今回はその一環で「ecrm」というツールを使用しECRイメージを削除した話をしたいと思います。 弊社では、コンテナのオーケストレーションにAmazon ECSを主に使用しています。 ECSにタスクをデプロイする場合は、イメージをビルドした上でAmazon ECRにpushし、その… <p>こんにちは、コネヒト プラットフォームグループの<a href="https://twitter.com/yosshi8448">@yosshi</a>です。</p> <p>昨今、円安の進行によりAWSのコスト増が無視できない状況となっています。 <br/> このような状況下で、コスト削減関連のイベントに注目が集まるなど、コスト削減への関心が徐々に高まってきているように感じます。</p> <p>弊社でもさまざまな方法でコスト削減施策を進めており、今回はその一環で「ecrm」というツールを使用しECRイメージを削除した話をしたいと思います。</p> <p>弊社では、コンテナのオーケストレーションにAmazon ECSを主に使用しています。 ECSにタスクをデプロイする場合は、イメージをビルドした上でAmazon ECRにpushし、そのECRのイメージを使用しタスクを起動しています。 ECRはイメージが増えれば増えるほど、容量に応じてコストがかかるので、この機会に対応したいと考えていました。</p> <h1 id="ECRで発生するコスト">ECRで発生するコスト</h1> <p>ECRではデータストレージの容量に応じて料金がかかります。 <br/> ap-northeast-1(アジアパシフィック(東京))のリージョンでは、GB/月当たり 0.10USDの料金が発生します。<br/> ※2024/01/17時点の情報。ECRでは上記とは別に転送コストなどが発生します。</p> <p><strong>参考:<a href="https://aws.amazon.com/jp/ecr/pricing/">https://aws.amazon.com/jp/ecr/pricing/</a></strong></p> <h1 id="ecrmとは">ecrmとは?</h1> <p>ecrmはAmazon ECRから不要なイメージを安全に削除するOSSです。<br/> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fgithub.com%2Ffujiwara%2Fecrm" title="GitHub - fujiwara/ecrm: A command line tool for managing ECR repositories." 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/fujiwara/ecrm">github.com</a></cite></p> <p>こちらのブログに詳細が書いてありますので、詳しくはこちらをご確認ください。<br/> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftechblog.kayac.com%2Fecrm-oss" title="ecrm - Amazon ECRから不要イメージを安全に削除するOSSを作った - KAYAC engineers&#39; blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://techblog.kayac.com/ecrm-oss">techblog.kayac.com</a></cite></p> <p>ECRには元々<a href="https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/LifecyclePolicies.html">ライフサイクルポリシー</a>という機能があり、世代数やイメージプッシュからの日数に基づき古いイメージを自動的に削除することができるのですが、「現在のECSタスクで使用しているもの」という観点でチェックをしてくれるわけではないので、使用中のイメージを削除してしまうリスクがあると感じていました。</p> <p>そこで調べていたところecrmの存在を知り、試してみることにしました。</p> <h1 id="試してみる">試してみる</h1> <p>インストール(macOS で brew を使う場合)</p> <pre class="code" data-lang="" data-unlink>$ brew install fujiwara/tap/ecrm</pre> <p>イメージ削除にあたり、削除対象を決めるための設定ファイルが必要になります。<br/> AWSアカウントのcredentialを適切に環境変数などで設定した状態で 以下コマンドを実行することで、アカウントに存在するECRやECS、Lambdaのリソースを元に、設定ファイル(ecrm.yaml)を自動生成してくれます。</p> <pre class="code" data-lang="" data-unlink>$ ecrm generate</pre> <p>実行すると設定ファイル(ecrm.yaml)が作成されました。<br/> クラスタやタスク定義はワイルドカードのパターンで指定でき、prefixが記号([_/-])で区切れてまとめられそうなものは、適宜まとめたパターンで自動生成してくれます。</p> <p><strong>ecrm.yaml</strong></p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">clusters</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;cluster1&gt;-* <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;cluster2&gt;-* ・ ・ <span class="synIdentifier">task_definitions</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;task_definition1&gt;-* <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;task_definition2&gt;-* <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> ・ ・ <span class="synIdentifier">lambda_functions</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;lambda_function1&gt;-* <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synIdentifier">keep_aliase</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;lambda_function2&gt;-* <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synIdentifier">keep_aliase</span><span class="synSpecial">:</span> <span class="synConstant">true</span> ・ ・ <span class="synIdentifier">repositories</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> <repository1>-* <span class="synIdentifier">expires</span><span class="synSpecial">:</span> 30d <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synIdentifier">keep_tag_patterns</span><span class="synSpecial">:</span> <span class="synStatement">- </span>latest <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> <repository2>-* <span class="synIdentifier">expires</span><span class="synSpecial">:</span> 30d <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synIdentifier">keep_tag_patterns</span><span class="synSpecial">:</span> <span class="synStatement">- </span>latest ・ ・ </pre> <p>この設定ファイル(ecrm.yaml)を弊社の都合に合わせて修正していきます。</p> <h2 id="設定した条件-削除対象外とするもの">設定した条件 (削除対象外とするもの)</h2> <ul> <li>現在使用中のイメージ <ul> <li>ECSクラスタで現在実行中のタスクに含まれるイメージ</li> <li>ECSサービスで指定されているタスク定義に含まれるイメージ</li> </ul> </li> <li>ECSタスク定義で指定されているイメージ(タスク定義最新リビジョンから<u>5世代分</u>)</li> <li>Lambda関数で指定されているイメージ(Lambda最新バージョンから<u>5世代分</u>)</li> <li><u>90日</u>以内に作成されたイメージ</li> <li>タグ名に<u>latest</u>, <u>release</u>がついているイメージ ※弊社ではリリース対象に左記のようなタグ名をつけています</li> </ul> <p>上記の下線部は可変にできるので、適宜適切な内容に置き換えてください。 設定ファイル(ecrm.yaml)は以下のようになりました</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">clusters</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;cluster1&gt;-* <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;cluster2&gt;-* ・ ・ <span class="synIdentifier">task_definitions</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;task_definition1&gt;-* <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;task_definition2&gt;-* <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> ・ ・ <span class="synIdentifier">lambda_functions</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;lambda_function1&gt;-* <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synIdentifier">keep_aliase</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> &lt;lambda_function2&gt;-* <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synIdentifier">keep_aliase</span><span class="synSpecial">:</span> <span class="synConstant">true</span> ・ ・ <span class="synIdentifier">repositories</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> <repository1>-* <span class="synIdentifier">expires</span><span class="synSpecial">:</span> 90d <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synIdentifier">keep_tag_patterns</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synType">*latest*</span> <span class="synStatement">- </span><span class="synType">*release*</span> <span class="synStatement">- </span><span class="synIdentifier">name_pattern</span><span class="synSpecial">:</span> <repository2>-* <span class="synIdentifier">expires</span><span class="synSpecial">:</span> 90d <span class="synIdentifier">keep_count</span><span class="synSpecial">:</span> <span class="synConstant">5</span> <span class="synIdentifier">keep_tag_patterns</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synType">*latest*</span> <span class="synStatement">- </span><span class="synType">*release*</span> ・ ・ </pre> <p>設定ファイル(ecrm.yaml)の編集が完了したら、どの程度イメージが削減できそうか確認します。</p> <p>削除対象は以下コマンドで確認できます。</p> <pre class="code" data-lang="" data-unlink>$ ecrm plan</pre> <p>実行したところ以下のようになりました。</p> <pre class="code" data-lang="" data-unlink>REPOSITORY | TYPE | TOTAL | EXPIRED | KEEP ---------------------------+-------------+--------------+---------------+-------------- &lt;repository1&gt; | Image | 256 (38 GB) | -187 (27 GB) | 69 (11 GB) &lt;repository2&gt; | Image | 227 (44 GB) | -209 (41 GB) | 18 (3.2 GB) &lt;repository3&gt; | Image | 92 (13 GB) | -82 (12 GB) | 10 (1.5 GB) &lt;repository4&gt; | Image | 109 (307 GB) | -57 (181 GB) | 52 (126 GB) &lt;repository5&gt; | Image | 63 (67 GB) | -52 (53 GB) | 11 (14 GB) &lt;repository6&gt; | Image | 251 (2.2 GB) | -236 (2.0 GB) | 15 (190 MB) &lt;repository7&gt; | Image | 771 (67 GB) | -760 (66 GB) | 11 (862 MB) &lt;repository8&gt; | Image | 150 (75 GB) | -135 (67 GB) | 15 (7.8 GB) &lt;repository9&gt; | Image | 65 (22 GB) | -60 (20 GB) | 5 (1.7 GB) &lt;repository10&gt; | Image | 138 (29 GB) | -128 (27 GB) | 10 (2.1 GB) &lt;repository11&gt; | Image | 116 (21 GB) | -104 (19 GB) | 12 (2.2 GB) &lt;repository12&gt; | Image | 927 (129 GB) | -910 (126 GB) | 17 (2.8 GB) &lt;repository13&gt; | Image | 284 (55 GB) | -231 (44 GB) | 53 (11 GB) &lt;repository14&gt; | Image | 57 (546 MB) | -21 (186 MB) | 36 (360 MB) </pre> <p>トータルのイメージに対して削除対象がどの程度あるか(EXPIRED)、残るイメージがどの程度あるか(KEEP)がわかります。</p> <p>実際に削除を実行する際には以下コマンドを実行します。</p> <pre class="code" data-lang="" data-unlink>$ ecrm delete</pre> <p>実行した後、実際に削除されたかどうかを確認したいので再度ecrm planを実行します。</p> <pre class="code" data-lang="" data-unlink>$ ecrm plan</pre> <p>実行結果</p> <pre class="code" data-lang="" data-unlink>REPOSITORY | TYPE | TOTAL | EXPIRED | KEEP -----------------+-------------+-------------+---------+-------------- &lt;repository1&gt; | Image | 69 (11 GB) | | 69 (11 GB) &lt;repository2&gt; | Image | 18 (3.2 GB) | | 18 (3.2 GB) &lt;repository3&gt; | Image | 10 (1.5 GB) | | 10 (1.5 GB) &lt;repository4&gt; | Image | 52 (126 GB) | | 52 (126 GB) &lt;repository5&gt; | Image | 11 (14 GB) | | 11 (14 GB) &lt;repository6&gt; | Image | 15 (190 MB) | | 15 (190 MB) &lt;repository7&gt; | Image | 11 (862 MB) | | 11 (862 MB) &lt;repository8&gt; | Image | 15 (7.8 GB) | | 15 (7.8 GB) &lt;repository9&gt; | Image | 5 (1.7 GB) | | 5 (1.7 GB) &lt;repository10&gt; | Image | 10 (2.1 GB) | | 10 (2.1 GB) &lt;repository11&gt; | Image | 12 (2.2 GB) | | 12 (2.2 GB) &lt;repository12&gt; | Image | 17 (2.8 GB) | | 17 (2.8 GB) &lt;repository13&gt; | Image | 53 (11 GB) | | 53 (11 GB) &lt;repository14&gt; | Image | 36 (360 MB) | | 36 (360 MB) </pre> <p>EXPIREDの列がブランクとなり、対象の不要イメージが削除されたことがわかります。</p> <h1 id="結果">結果</h1> <p>今回のケースでは685GBのイメージの削減に成功しました。 現時点のAWSのコストベース(GB/月当たり 0.10USD)で換算すると、月間<strong>68.5USD</strong>の削減となります。<br/> ※2024/01時点のAWS ECRのコスト</p> <h1 id="感想">感想</h1> <p>今回はecrmを使用してECRイメージを削除した話をしました。</p> <p>金額としては小さいかもしれませんが、もともと使っていないイメージに対する余分なコストだったので、このタイミングで簡単に削減できてよかったなと思います。</p> <p>コスト削減に注目が集まっている状況だと思うので、各社のコスト削減の参考になれば幸いです。</p> <p>今回は単発でイメージの削除を行いましたが、今後は自動で定期実行する仕組みなども合わせて検討していきたいと考えています。</p> yosshi8448 2023年のコネヒト開発組織を振り返る hatenablog://entry/6801883189069705374 2023-12-25T21:30:59+09:00 2023-12-25T21:32:52+09:00 こんにちは。CTOの永井(shnagai)です。 早いもので今年も残すところ数日ですね。 アドベントカレンダー最終日ということで、技術的なトピックやチームでの工夫はこれまでの記事でみんなが思う存分書いてくれたので、今回は2023年のコネヒト開発組織の1年を自分の視点で振り返っていこうと思います。 この記事は、コネヒトAdvent Calendar2023の25日目の記事です。 adventar.org 開発組織のアップデートと特に目立った技術的なトピックという構成で書いてます。 開発組織のリフレーミングと頼れる多くの仲間のジョイン それぞれのチームの関係を可視化 ピープルマネジメントをEMに集… <p>こんにちは。CTOの永井(<a href="https://twitter.com/shnagai">shnagai</a>)です。</p> <p>早いもので今年も残すところ数日ですね。</p> <p>アドベントカレンダー最終日ということで、技術的なトピックやチームでの工夫はこれまでの記事でみんなが思う存分書いてくれたので、今回は2023年のコネヒト開発組織の1年を自分の視点で振り返っていこうと思います。</p> <p>この記事は、コネヒトAdvent Calendar2023の25日目の記事です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F8994" title="コネヒト Advent Calendar 2023 - Adventar" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://adventar.org/calendars/8994">adventar.org</a></cite></p> <p>開発組織のアップデートと特に目立った技術的なトピックという構成で書いてます。</p> <ul class="table-of-contents"> <li><a href="#開発組織のリフレーミングと頼れる多くの仲間のジョイン">開発組織のリフレーミングと頼れる多くの仲間のジョイン</a><ul> <li><a href="#それぞれのチームの関係を可視化">それぞれのチームの関係を可視化</a></li> <li><a href="#ピープルマネジメントをEMに集約-エンジニアがよりプロダクト事業に集中出来る体制へ">ピープルマネジメントをEMに集約 〜エンジニアがよりプロダクト・事業に集中出来る体制へ〜</a></li> <li><a href="#3人に1人を目指して-多くの仲間のジョイン">3人に1人を目指して 多くの仲間のジョイン</a></li> </ul> </li> <li><a href="#検索システムの内製化完了">検索システムの内製化完了</a></li> <li><a href="#Lets-Go">Let’s Go</a></li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="開発組織のリフレーミングと頼れる多くの仲間のジョイン">開発組織のリフレーミングと頼れる多くの仲間のジョイン</h2> <p>FY23のスタートに合わせて、開発組織のリフレーミングを行いました。</p> <h3 id="それぞれのチームの関係を可視化">それぞれのチームの関係を可視化</h3> <p>期初のタイミングでチームトポロジーを参考に、それぞれのチームの構造を改めて可視化しました。</p> <p>自分は<a href="https://www.amazon.co.jp/%E3%83%81%E3%83%BC%E3%83%A0%E3%83%88%E3%83%9D%E3%83%AD%E3%82%B8%E3%83%BC-%E4%BE%A1%E5%80%A4%E3%81%82%E3%82%8B%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%82%92%E3%81%99%E3%81%B0%E3%82%84%E3%81%8F%E5%B1%8A%E3%81%91%E3%82%8B%E9%81%A9%E5%BF%9C%E5%9E%8B%E7%B5%84%E7%B9%94%E8%A8%AD%E8%A8%88-%E3%83%9E%E3%82%B7%E3%83%A5%E3%83%BC%E3%83%BB%E3%82%B9%E3%82%B1%E3%83%AB%E3%83%88%E3%83%B3/dp/4820729632">チームトポロジー</a>の、ストリームアラインドチームが出す価値の総量が開発組織のバリューという考えに非常に感銘を受けており、プロダクトの先にいるユーザーファーストの考えをうまく組織に当てはめた考えだなと思っています。</p> <p>ママリや自治体向け事業等の事業毎にストリームアラインドチームを配置して、事業部と密に連携しながら開発するスタイルをコネヒトでは採用しています。</p> <p>コンプリケイテッドサブシステムチームは技術的難易度の高い領域を受け持ち、プラットフォームチームでは、ストリームアラインドチームの認知負荷を減らしプラットフォームエンジニアリングでプロダクトの価値最大化に貢献することをミッションにしています。</p> <p>下記は、期初戦略で全社に説明した資料の一部です。</p> <p>※現在はメンバー増によりストリームアラインドが増えています。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/n/nagais/20231225/20231225193701.png" width="1200" height="665" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="ピープルマネジメントをEMに集約-エンジニアがよりプロダクト事業に集中出来る体制へ">ピープルマネジメントをEMに集約 〜エンジニアがよりプロダクト・事業に集中出来る体制へ〜</h3> <p>元々コネヒトでは、各開発グループにグループリーダー(以後、GL)を配置し、そのGLがチームのマネジメント4象限(ピープル/プロダクト/プロジェクト/テクノロジー)を全て担う構造にしていました。</p> <p>だいたい2,3名のチームにそれぞれGLがいる構造だったのですが、今の組織フェーズ的にはエンジニアがよりプロダクトや事業に集中する時間を作るほうが良いと判断して、ピープルマネジメント業務をチーム横断でEM 2名に集約しました。</p> <p>全てがうまくいっているわけではありませんが、ピープルマネジメントを集約したことでメンバーからはよりプロダクトや事業に集中出来ているというフィードバックをもらっており、一定うまくいっている施策だなと思っています。</p> <p>制度やロールは常に見直しながらアップデートしていくのが良いと思っており、これからも組織拡大に合わせて柔軟に見直していき、アジリティの高い開発が出来るようにしていきたいと思っています。</p> <p>きれいなことばかり書きましたが、テックリードという役割を設けるトライもしたのですが、中々イメージの言語化や定義が追いつかずに、役割を担ってくれたメンバーと対話しながら、半年でまた別のロールにしたりと頼れる同僚とともに試行錯誤しながらやっています。</p> <h3 id="3人に1人を目指して-多くの仲間のジョイン">3人に1人を目指して 多くの仲間のジョイン</h3> <p>今年は、7名の新しいエンジニアがコネヒトにジョインしてくれました。</p> <p>コネヒトでは、テックビジョンにおいて「3人に1人」という戦術を掲げており、会社としてやりたい事業を全て実現し、テクノロジーやエンジニアリングの恩恵を社内も受け、そしてエンジニア以外でもテクノロジーを使いこなせるような世界を目指しています。</p> <p>今年ジョインしてくれた仲間の職種も多岐に渡るのですが、それぞれが所属するチームや開発組織でバリューを発揮してくれており全体として活気づいています。</p> <p>社内のエンジニア比率が高まっていく中で、生成AIの技術革新もあいまってテックビジョンに掲げる世界をより実現フェーズに持っていくために新たな動きも走り始めています。</p> <p>全社向けに生成AIの活用とテクノロジーを利用した業務改善を推進する「Run with Tech 」プロジェクトで、こちらは、また、どこかの機会で紹介出来ればと思っています。</p> <p>続いて、今年特に印象的に残った技術的なトピックについても紹介していければと思います。</p> <h2 id="検索システムの内製化完了">検索システムの内製化完了</h2> <p>今年技術的なトピックで一番大きなトピックといえば検索システムの内製化が完了したことがまず挙げられます。</p> <p>これまで、SaaSの検索システムを内部のAPIから呼び出す形式で使っていたものを、フルリプレイスする形で0から検索システムを構築しました。</p> <p>時系列で見る検索システム内製化の軌跡</p> <ul> <li>2021年10月 構想開始</li> <li>2022年3月 経営承認</li> <li>2022年4月 開発開始</li> <li>2022年8月 最初の機能(新着質問順)のリプレイス完了</li> <li>2023年6月 全ての機能のリプレイス完了</li> </ul> <p>主な狙いは、検索システムを内部で持つことによるユーザー体験の強化とSaaS入れ替えによる費用削減の2点でした。</p> <p>時系列で書くとすんなり言ったように見えますが、すべての機能において既存とのABテストを行いママリにおける検索体験のユーザー毀損がないことを細かく確認しながらリプレイスを行いました。</p> <p>新着順、人気順、サジェスト、関連キーワードと多岐に渡る機能のABテストはSaaS提供と同品質をゴールにしている関係で中々にヒリヒリするもので、メインで担当していた同僚達のやり切る力には脱帽です。</p> <p>技術的な要素を少し解説すると、検索システム内製化には下記技術要素があります。</p> <ul> <li>内部APIから呼ばれる検索API(Go)</li> <li>検索エンジンにはOpenSearchを採用</li> <li>技術的難易度が一番高かったデータ同期の仕組みにはAWS GlueとStepFunctionsでニアリアルタイムな基盤を構築   <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2022%2F08%2F24%2F184911" title="ニアリアルタイムで同期される検索基盤を構築 ~AWS Glueによるデータ同期編~ - コネヒト開発者ブログ" 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.connehito.com/entry/2022/08/24/184911">tech.connehito.com</a></cite></li> </ul> <p>担当したメンバーは全員検索システムは未経験だったので、みんなでペンギン本を読んで輪読会をしたり、壁にぶつかるたびに議論して一歩ずつ前に進んでいきました。私自身もメンバーとして当初コミットしていて、検索システムは奥深くエンジニアとしてまた一つ成長できた良い機会だったと感じています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2022%2F09%2F27%2F164009" 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.connehito.com/entry/2022/09/27/164009">tech.connehito.com</a></cite></p> <p>もちろん、リプレイスをして終了ではなく、ようやく検索システムをママリの武器に出来るスタートラインに立てたので、昨今のLLM全盛の時勢も鑑みながら、キーワード検索にとらわれずよりユーザーにとってママリの検索がより使いやすいものであり続けられるように技術的な検証や新たなトライも既に始まっています。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F12%2F08%2F144649" title="「ベクトル検索 vs 全文検索」〜Amazon Bedrockの埋め込みモデルを用いたプロトタイピング〜 - コネヒト開発者ブログ" 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.connehito.com/entry/2023/12/08/144649">tech.connehito.com</a></cite></p> <p>「終わらせることを始めよう」を合言葉に最終的に3ヶ月計画を前倒しし、検索システム内製化という成果に対して社内表彰もされたプロジェクトとなりました。</p> <p>ユーザーへの価値提供という点では、まさにこれから真価を問われることになりますが、テクノロジーでプロダクトを伸ばす先端事例になるような取組みなので今後がより楽しみです。</p> <h2 id="Lets-Go">Let’s Go</h2> <p>コネヒトのテックビジョンでは、「バックエンドのシステムにGoを積極的に導入する」という戦略を掲げていおりその戦術名が「Let's Go」です。</p> <p>これまでも、前述の検索APIしかり新規のシステムでのGoの採用は何度か行ってきました。それらを通しての、組織としての知見の習得はそこそこ進んできたかと感じています。</p> <p>このLet’s Goの作戦は、<a href="https://twitter.com/suxisuxido">aboy</a>がボールを持って中心に進めてくれているのですが、今年の大きな進歩として、既存のPHPで書かれたコードベースをGoに置き換えるというプロジェクトが走り出しました。</p> <p>まずは、Goの特徴である並列処理やワーカー実装のしやすさを活かせる非同期のバッチ処理のリプレイスを実施していますが、メインシステムを適材適所でGoに置き換える動きがスタートしているのは開発組織として大きな一歩だなと感じています。</p> <p>中期戦略では、2024年中にバッチ処理を全てGoに置き換えるというチャレンジングな目標を掲げており、その実現に向けて日々開発を進めています。</p> <p>また、Let’s Go Talkという社主催のイベントも定期的に実施しており、Go好きがゆるく繋がれるようなコミュニティも作っていきたいと思っているので興味のある方は是非ご参加ください。</p> <p><a href="https://connehito.connpass.com/">&#x30B3;&#x30CD;&#x30D2;&#x30C8;&#xFF0F;Connehito Inc. - connpass</a></p> <h2 id="まとめ">まとめ</h2> <p>2024年も引き続きプロダクトや事業を伸ばしていくことに注力しつつ、テックビジョンに掲げる「Beyond a Tech Company」の実現に向けてCTOというラベルを活かして色々と策を打っていこうと思います。</p> <p>テックビジョンの存在が羅針盤からより会社の中で身近なものになっていけば、コネヒトとしてユーザーや社会に約束するありたい世界観の実現に近づいていくと自分は信じています。</p> <p>最後にですが、エンジニアはもちろん、幅広い職種で採用もしていますので興味持たれた方は是非ご応募いただきカジュアルにお話出来るとうれしいです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fconnehito%2Fjobs" 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://hrmos.co/pages/connehito/jobs">hrmos.co</a></cite></p> nagais trivyとGithub Actionsを使用しTerraform設定ファイルのセキュリティスキャンを実行する仕組みを作りました hatenablog://entry/6801883189068048194 2023-12-21T10:46:31+09:00 2023-12-21T11:49:56+09:00 この記事はコネヒトアドベントカレンダー21日目の記事です。 コネヒト Advent Calendar 2023って? コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。 コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、 ママの一歩を支えるアプリ「ママリ」などを 運営しています。 adventar.org はじめに コネヒトのプラットフォームグループでインフラ関連を担当している@yosshiです。 今年の7月に入社してから早いもので半年が経ちました。時が経つのは本当に早いですね。 今回のブログでは、セキュリティスキャンツールであるtriv… <p>この記事はコネヒトアドベントカレンダー21日目の記事です。</p> <pre class="code" data-lang="" data-unlink>コネヒト Advent Calendar 2023って? コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。 コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、 ママの一歩を支えるアプリ「ママリ」などを 運営しています。</pre> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F8994" title="コネヒト Advent Calendar 2023 - Adventar" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://adventar.org/calendars/8994">adventar.org</a></cite></p> <h1 id="はじめに">はじめに</h1> <p>コネヒトのプラットフォームグループでインフラ関連を担当している<a href="https://twitter.com/yosshi8448">@yosshi</a>です。 今年の7月に入社してから早いもので半年が経ちました。時が経つのは本当に早いですね。</p> <p>今回のブログでは、セキュリティスキャンツールである<a href="https://github.com/aquasecurity/trivy">trivy</a>を使って、自動的にIaC (Infrastructure as Code)スキャンを実行する仕組みを構築した話をしたいと思います。</p> <p>弊社ではインフラ構成をTerraform利用して管理するようにしており、それをモノレポの構成で運用しています。</p> <p>インフラリソースの作成・変更・削除をする際には必ず相互レビューを必須としているものの、人的なチェックのみに依存しているためファイルの設定ミスやセキュリティ上の見落としが潜在的なリスクとなっていました。</p> <p>これらを事前に検知するツールを調べていたところtrivyの存在を知り、今回導入に至りました。</p> <h1 id="trivyとは">trivyとは</h1> <ul> <li>コンテナイメージやアプリケーションの依存ライブラリ・OSのパッケージなどを迅速にスキャンし、セキュリティリスクを効率的に検出するセキュリティツールです。</li> <li>当初はコンテナのセキュリティ問題に焦点を当てたツールとして開発されたようですが、後にTerraformやKubernetesなどの設定ファイルのチェック機能も追加されました。</li> <li>設定ファイルのチェックでは、設定ミスやセキュリティのベストプラクティスに沿っていない構成などを検知することができます。</li> </ul> <p>参考:<a href="https://github.com/aquasecurity/trivy">https://github.com/aquasecurity/trivy</a></p> <h2 id="参考スキャン可能な対象-202312時点">(参考)スキャン可能な対象 2023/12時点</h2> <ul> <li>コンテナイメージ</li> <li>ファイルシステム</li> <li>リモートGitリポジトリ</li> <li>仮想マシンイメージ</li> <li>Kubernetes</li> <li>AWS</li> </ul> <h2 id="参考検出可能な内容-202312時点">(参考)検出可能な内容 2023/12時点</h2> <ul> <li>OSパッケージとソフトウェア依存関係(SBOM)</li> <li>既知の脆弱性(CVE)</li> <li>IaCの問題と設定ミス</li> <li>機密情報と秘密</li> <li>ソフトウェアライセンス</li> </ul> <p>上記の通りtrivyでは、Terraformのコードだけでなくさまざまなセキュリティスキャン行うことができます。</p> <p>trivy自体の詳しい説明はここでは割愛するので、詳しくは公式サイトや他の方の記事などをご確認いただけると幸いです。</p> <h1 id="Github-Actionsでの実装">Github Actionsでの実装</h1> <p>では早速ですが実装内容の説明に移りたいと思います。 今回はTerraformコードのスキャンをCIに組み込んでいます。 弊社では CIツールとしてGithub Actionsを利用しているため、今回もこちらを利用します。</p> <p>スキャン実行は、trivy公式で用意しているGithub Actions用のツール(<a href="https://github.com/aquasecurity/trivy-action">tricy-action</a>)があるので、こちらをそのまま利用しています。</p> <h2 id="背景">背景</h2> <p>まず、前提条件となる弊社のディレクトリ構造を説明します。</p> <p>弊社では、サービスで共通利用するリソース(base_system)と各サービスで利用するリソース(product_system)とでディレクトリを分けており、 product_sytem以下にはサービスごと関連するリソースが紐づいています。以下のようなイメージです。</p> <pre class="code" data-lang="" data-unlink>. ├── base_system │ ├── common │ │ └── terraform │ ├── privilege │ │ └── terraform │ . │ . └── product_system ├── (サービス1) │ └── terraform ├── (サービス2) │ └── terraform . . . </pre> <h2 id="実装方針と内容">実装方針と内容</h2> <p>実装したGithub Actionsのコードは以下の通りです。</p> <p>主に以下のことをやっています。</p> <ol> <li>シェルスクリプト(sync-updated-dirs-to-work-dir.sh)の実行 <ul> <li>mainブランチとプルリクエスト中のブランチのコードの差分を検知</li> <li>差分となっているディレクトリを作業用のディレクトリに同期</li> </ul> </li> <li>作業ディレクトリに対してスキャン実行</li> <li>検出されたスキャン結果をPRのコメントに残す。</li> </ol> <p>Github Actionsのコードは以下の通りです。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> trivy-scan <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="synSpecial">[</span>opened, reopened, synchronize<span class="synSpecial">]</span> <span class="synIdentifier">permissions</span><span class="synSpecial">:</span> <span class="synIdentifier">id-token</span><span class="synSpecial">:</span> write <span class="synIdentifier">contents</span><span class="synSpecial">:</span> read <span class="synIdentifier">pull-requests</span><span class="synSpecial">:</span> write <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">trivy_scan</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> Run Trivy Scan <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Clone repo <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v4 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> fetch origin/main for getting diff <span class="synIdentifier">run</span><span class="synSpecial">:</span> git fetch --depth <span class="synConstant">1</span> origin $GITHUB_BASE_REF <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Sync Updated Dir to Work Dir <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">GITHUB_TOKEN</span><span class="synSpecial">:</span> ${{ secrets.github_token }} <span class="synIdentifier">run</span><span class="synSpecial">:</span> | bash utils/scripts/sync-updated-dirs-to-work-dir.sh <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Trivy Scan <span class="synIdentifier">uses</span><span class="synSpecial">:</span> aquasecurity/trivy-action@master <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">scan-type</span><span class="synSpecial">:</span> <span class="synConstant">'config'</span> <span class="synIdentifier">severity</span><span class="synSpecial">:</span> <span class="synConstant">'HIGH,CRITICAL'</span> <span class="synIdentifier">scan-ref</span><span class="synSpecial">:</span> scan_work_dir <span class="synIdentifier">output</span><span class="synSpecial">:</span> trivy-scan-result.txt <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Format Trivy Scan Result <span class="synIdentifier">run</span><span class="synSpecial">:</span> | if <span class="synSpecial">[</span> -s trivy-scan-result.txt <span class="synSpecial">]</span>; then <span class="synComment"> # ファイルに内容がある場合</span> echo -e <span class="synConstant">&quot;## 脆弱性スキャン結果</span><span class="synSpecial">\n</span><span class="synConstant">&lt;details&gt;&lt;summary&gt;詳細&lt;/summary&gt;</span><span class="synSpecial">\n\n</span><span class="synConstant">\`\`\`</span><span class="synSpecial">\n</span><span class="synConstant">$(cat trivy-scan-result.txt)</span><span class="synSpecial">\n</span><span class="synConstant">\`\`\`</span><span class="synSpecial">\n</span><span class="synConstant">&lt;/details&gt;&quot;</span> &gt; formatted-trivy-result.md else <span class="synComment"> # ファイルが空の場合</span> echo -e <span class="synConstant">&quot;## 脆弱性スキャン結果</span><span class="synSpecial">\n</span><span class="synConstant">脆弱性が検知されませんでした。&quot;</span> &gt; formatted-trivy-result.md fi <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Comment PR with Trivy scan results <span class="synIdentifier">uses</span><span class="synSpecial">:</span> marocchino/sticky-pull-request-comment@v2 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">recreate</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">GITHUB_TOKEN</span><span class="synSpecial">:</span> ${{ secrets.github_token }} <span class="synIdentifier">path</span><span class="synSpecial">:</span> formatted-trivy-result.md </pre> <p>詳しく見ていきます。</p> <p>まずは変更差分となったディレクトリを調べるため、Github Actionsの中でシェルスクリプトを実行しています。</p> <p>ここでは一時的なディレクトリを用意し、プルリクエスト上で変更が発生したディレクトリの内容を作業用のディレクトリに同期するという作業を行なっています。</p> <p>Github Actionsの関連部分は以下です。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Sync Updated Dir to Work Dir <span class="synIdentifier">run</span><span class="synSpecial">:</span> | bash utils/scripts/sync-updated-dirs-to-work-dir.sh </pre> <p><code>sync-updated-dirs-to-work-dir.sh</code>はrsyncをwrapしたもので、前述の通りプルリクエスト上で変更が発生したterraformディレクトリを、スキャン実行する一時的な作業用ディレクトリに同期する処理をしています。<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup></p> <p>細かな実装は内部事情に特化したものとなっているため割愛しますが、例えば、<code>base_system/common/terraform</code>, <code>base_system/privilege/terraform</code>, <code>product_system/(サービス1)/terraform</code>のディレクトリで変更が発生している場合、このスクリプトを実行することで作業用ディレクトリ(scan_work_dir)が作成され、以下のようなディレクトリ構造となります。</p> <pre class="code" data-lang="" data-unlink>. ├── base_system │ ├── common │ │ └── terraform │ └── privilege │ │ └── terraform │ . │ . ├── product_system │ ├── (サービス1) │ │ └── terraform │ ├── (サービス2) │ │ └── terraform │ . │ . └── scan_work_dir (ここのディレクトリにtrivyによるスキャンを実行する) ├── base_system │ ├── common │ │ └── terraform │ └── privilege │ │ └── terraform └── product_sytem └── (サービス1) └── terraform </pre> <p>次に同期してきた作業用ディレクトリに対してスキャンを実行します。<br/> ここでは前述の通り、公式が用意している<a href="https://github.com/aquasecurity/trivy-action">tricy-action</a>を利用しています。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Trivy Scan <span class="synIdentifier">uses</span><span class="synSpecial">:</span> aquasecurity/trivy-action@master <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">scan-type</span><span class="synSpecial">:</span> <span class="synConstant">'config'</span> <span class="synIdentifier">severity</span><span class="synSpecial">:</span> <span class="synConstant">'HIGH,CRITICAL'</span> <span class="synIdentifier">scan-ref</span><span class="synSpecial">:</span> scan_work_dir <span class="synIdentifier">output</span><span class="synSpecial">:</span> trivy-scan-result.txt </pre> <p>オプションの内容は以下の通りです。</p> <ul> <li>scan-type: 'config' <ul> <li><code>config</code>はTerraformなどの設定ファイルをスキャンする際に使用する。</li> </ul> </li> <li>scan-ref: scan_work_dir <ul> <li>作業用ディレクトリ <code>scan_work_dir</code> を指定している。</li> </ul> </li> <li>output: trivy-scan-result.txt <ul> <li>スキャンした結果をテキストファイル<code>trivy-scan-result.txt</code>に出力する。</li> <li>このテキストファイルは次のアクションでフォーマットを整形して出力するために使用している。</li> </ul> </li> <li>serverity <ul> <li>脆弱性の深刻度で'CLITICAL'と'HIGH'を指定しています。</li> <li>ここのレベルは、CVSS<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup>によって定量化された脆弱性の深刻度をもとに設定されています。</li> </ul> </li> </ul> <p>他のオプションなどについては<a href="https://github.com/aquasecurity/trivy-action">tricy-action</a>のページをご確認ください。</p> <p>次に、前のアクションで出力したテキストファイルを見やすく整形した上で、<a href="https://github.com/marocchino/sticky-pull-request-comment">marocchino/sticky-pull-request-comment</a>を使用しプルリクエストのコメント欄に出力しています。</p> <p>プルリクエスト更新時には、既存の脆弱性に関するコメントを削除した上で新規コメントを残して欲しかったので、その点でmarocchino/sticky-pull-request-comment(recreate: trueのオプション指定)を利用することで楽に実装することができました。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># スキャンした結果を整える</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Format Trivy Scan Result <span class="synIdentifier">run</span><span class="synSpecial">:</span> | if <span class="synSpecial">[</span> -s trivy-scan-result.txt <span class="synSpecial">]</span>; then <span class="synComment"> # ファイルに内容がある場合</span> echo -e <span class="synConstant">&quot;## 脆弱性スキャン結果</span><span class="synSpecial">\n</span><span class="synConstant">&lt;details&gt;&lt;summary&gt;詳細&lt;/summary&gt;</span><span class="synSpecial">\n\n</span><span class="synConstant">\`\`\`</span><span class="synSpecial">\n</span><span class="synConstant">$(cat trivy-scan-result.txt)</span><span class="synSpecial">\n</span><span class="synConstant">\`\`\`</span><span class="synSpecial">\n</span><span class="synConstant">&lt;/details&gt;&quot;</span> &gt; formatted-trivy-result.md else <span class="synComment"> # ファイルが空の場合</span> echo -e <span class="synConstant">&quot;## 脆弱性スキャン結果</span><span class="synSpecial">\n</span><span class="synConstant">脆弱性が検知されませんでした。&quot;</span> &gt; formatted-trivy-result.md fi <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Comment PR with Trivy scan results <span class="synIdentifier">uses</span><span class="synSpecial">:</span> marocchino/sticky-pull-request-comment@v2 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">recreate</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">GITHUB_TOKEN</span><span class="synSpecial">:</span> ${{ secrets.github_token }} <span class="synIdentifier">path</span><span class="synSpecial">:</span> formatted-trivy-result.md </pre> <h2 id="スキャン結果">スキャン結果</h2> <ul> <li><p><strong>脆弱性が検知されなかった場合</strong> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yosshi8448/20231219/20231219112747.png" width="926" height="212" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p></li> <li><p><strong>脆弱性が検知された場合</strong> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yosshi8448/20231219/20231219112917.png" width="695" height="199" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yosshi8448/20231220/20231220195056.png" width="782" height="544" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p></li> </ul> <p>「詳細」部分を開くと以下のような形で表示されています。</p> <p>今回のケースだとCritical0件、High61件の脆弱性が検知されていることがわかります。</p> <pre class="code" data-lang="" data-unlink>base_system/privilege/terraform/xxx.tf (terraform) ===================================================================== Tests: 75 (SUCCESSES: 14, FAILURES: 61, EXCEPTIONS: 0) Failures: 61 (HIGH: 61, CRITICAL: 0) HIGH: IAM policy document uses sensitive action &#39;autoscaling:Describe*&#39; on wildcarded resource &#39;*&#39; ════════════════════════════════════════ You should use the principle of least privilege when defining your IAM policies. This means you should specify each exact permission required without using wildcards, as this could cause the granting of access to certain undesired actions, resources and principals. See https://avd.aquasec.com/misconfig/avd-aws-0057 ──────────────────────────────────────── base_system/privilege/terraform/xxx.tf:376 via base_system/privilege/terraform/xxx.tf:376 (aws_iam_policy.xxx.xxx) via base_system/privilege/terraform/xxx.tf:375-438 (aws_iam_policy.xxx.xxx) via base_system/privilege/terraform/xxx.tf:373-439 (aws_iam_policy.xxx) ──────────────────────────────────────── 373 resource &#34;aws_iam_policy&#34; &#34;xxx&#34; { ... 376 [ Version = &#34;2012-10-17&#34;, ... 439 } ──────────────────────────────────────── ・ ・ ・(以下省略) </pre> <p>上記の例では、重要度「HIGH」の脆弱性が検知されています。</p> <p>IAMポリシーは最小特権で付与するべきなので、ワイルドカード使うのはリスクがありますよ、という指摘のようです。</p> <h3 id="補足">補足</h3> <p>検出された脆弱性の中で、この内容は指摘する対象から除外したい、というケースもあると思います。</p> <p>その場合は<code>.trivyignore</code>というファイルをトップディレクトリに置くことで勝手に参照して検出対象から除外してくれます。</p> <p>以下のような形で記載します。</p> <p><strong>.trivyignore</strong></p> <pre class="code" data-lang="" data-unlink>AVD-AWS-0057 AVD-AWS-XXXX</pre> <h4 id="参考">(参考)</h4> <p>以下のようにtrivyignoresのオプションを利用することで、トップディレクトリに配置するだけでなく別のディレクトリに配置しているファイルを参照させたり、<code>.trivyignore</code>以外の別のファイル名を指定することもできるようです。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Trivy Scan <span class="synIdentifier">uses</span><span class="synSpecial">:</span> aquasecurity/trivy-action@master <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">scan-type</span><span class="synSpecial">:</span> <span class="synConstant">'config'</span> <span class="synIdentifier">severity</span><span class="synSpecial">:</span> <span class="synConstant">'HIGH,CRITICAL'</span> <span class="synIdentifier">scan-ref</span><span class="synSpecial">:</span> trivy_temp_dir <span class="synIdentifier">output</span><span class="synSpecial">:</span> trivy-scan-result.txt <span class="synIdentifier">trivyignores</span><span class="synSpecial">:</span> test-trivyignore </pre> <h1 id="まとめ">まとめ</h1> <p>今回はtrivyとGitHub Actionsを活用し、Terraformでのセキュリティ上のリスクを効果的に検知する仕組みを構築しました。</p> <p>今回検知されたものについては、優先度の高いものから順次改善していきたいと思います。</p> <p>また、冒頭に説明した通り、trivyでは他にも様々なものを検知してくれる機能があるので、 Terraformの設定だけでなく色々な場面での活用を検討していきたいと思います。</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> 変更が発生したディレクトリの抽出には <code>git diff origin/main --name-only</code> の結果をパースしています。<a href="#fnref:1" rev="footnote">&#8617;</a></li> <li id="fn:2"> CVSS( Common Vulnerability Scoring System ) = 共通脆弱性評価システム<a href="#fnref:2" rev="footnote">&#8617;</a></li> </ol> </div> yosshi8448 ママリiOSアプリで今年取り組んだ3つの改善について hatenablog://entry/6801883189067245530 2023-12-20T18:14:51+09:00 2023-12-20T18:14:51+09:00 「コネヒト Advent Calendar 2023」の20日目のブログです! adventar.org iOSエンジニアのyoshitakaです。 コネヒトに入社してそろそろ1年が経ちます。 コネヒトに入社してからはママリiOSアプリの開発を担当しています。 今回はママリiOSアプリで行った3つの改善をまとめました。 フォルダ構成の変更 開発中アプリの配布フローの改善 画面遷移の改善 それぞれどんな課題があったか、どんな改善をしたかを紹介します。 フォルダ構成の変更 課題に感じていたこと 既存のフォルダ構成は機能追加をする際に対象となるコードが見つけにくい状況でした。 前提 ママリiOSアプ… <p>「コネヒト Advent Calendar 2023」の20日目のブログです!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F8994" title="コネヒト Advent Calendar 2023 - Adventar" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://adventar.org/calendars/8994">adventar.org</a></cite></p> <p>iOSエンジニアのyoshitakaです。 コネヒトに入社してそろそろ1年が経ちます。</p> <p>コネヒトに入社してからはママリiOSアプリの開発を担当しています。</p> <p>今回はママリiOSアプリで行った3つの改善をまとめました。</p> <ol> <li>フォルダ構成の変更</li> <li>開発中アプリの配布フローの改善</li> <li>画面遷移の改善</li> </ol> <p>それぞれどんな課題があったか、どんな改善をしたかを紹介します。</p> <h2 id="フォルダ構成の変更">フォルダ構成の変更</h2> <h3 id="課題に感じていたこと">課題に感じていたこと</h3> <p>既存のフォルダ構成は機能追加をする際に対象となるコードが見つけにくい状況でした。</p> <h3 id="前提">前提</h3> <ul> <li>ママリiOSアプリのアーキテクチャーはMVVMを採用している</li> <li><code>View・ViewModel</code>と<code>Model</code>でモジュール分割している <ul> <li>今回改善したのは<code>View・ViewModel</code>側のフォルダ構成</li> </ul> <p> ※ モジュール分割の取り組みについてはこちらの記事を見てください。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F01%2F30%2F094949" title="ママリ iOSアプリのモジュール分割 - コネヒト開発者ブログ" 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.connehito.com/entry/2023/01/30/094949">tech.connehito.com</a></cite></p></li> </ul> <h3 id="どんな改善をしたか">どんな改善をしたか</h3> <p>変更前のフォルダ構成のイメージがこちらです。</p> <pre class="code" data-lang="" data-unlink>App ├── ViewController │ ├── 機能AのViewController │ ├── 機能AのViewController │ ├── 機能BのViewController │ └── ... ├── View │ ├── 機能AのView │ ├── 機能AのView │ ├── 機能BのView │ ├── 機能AB共通のView │ └── ... └── ViewModel ├── 機能AのViewModel ├── 機能AのViewModel ├── 機能BのViewModel └── ...</pre> <p>アーキテクチャーがわかりやすい構成になっていると思います。</p> <p>しかし、機能追加をする際に対象となるコードにたどり着くのに時間がかかっていました。</p> <p>改善案は大きく二つありました。</p> <ol> <li>現在のフォルダ構成の配下に機能ごとのフォルダを作る <ul> <li>メリット: 変更が少なく済む</li> <li>デメリット: 機能ごとのコードが分散する</li> </ul> </li> <li>App配下に機能ごとのフォルダ作り、機能フォルダ内でさらにViewとViewModelのフォルダを作る <ul> <li>メリット: 機能ごとのコードがまとまる</li> <li>デメリット: フォルダ構成の変更が大きい</li> </ul> </li> </ol> <p>機能改善する際ViewとViewModelをセットで修正することがよくあるので、<code>2</code>のApp配下で機能ごとにフォルダ分けをすることにしました。</p> <p>改善したフォルダ構成イメージがこちらです。</p> <pre class="code" data-lang="" data-unlink>App ├── Features │ ├── 機能A │ │ ├── View:ViewController・DataSource・View置き場 │ │ └── ViewModel │ ├── 機能B │ ├── 機能C │ └── ... └── Common:共通化されたView・ViewModel置き場 ├── View └── ViewModel</pre> <p>複数の機能で使われているものはCommonフォルダにまとめるようにしました。</p> <p>フォルダ構成は好みもあるかと思いますが、整理したことで不要なファイルもお掃除でき、良い改善だったと思います。</p> <h2 id="開発中アプリの配布フローの改善">開発中アプリの配布フローの改善</h2> <h3 id="課題に感じていたこと-1">課題に感じていたこと</h3> <p>特定の開発中ブランチから開発中アプリのを配布する際に、CIでは実行できず、常にローカルでfastlaneを実行する必要がありました。</p> <h3 id="前提-1">前提</h3> <ul> <li>CI/CDはBitriseとfastlaneを使っている</li> <li>開発中アプリはFirebase Distributionを使い社内に配布している</li> <li>Firebase Distributionへの配布方法は以下の2つ <ul> <li>PullRequestをメインブランチにマージすると自動で配布される</li> <li>ローカルでfastlaneを実行して配布する</li> </ul> </li> </ul> <h3 id="どんな改善をしたか-1">どんな改善をしたか</h3> <p>CIでfastlaneのアプリ配布のワークフローを実行できるようにすることで、ローカルでのfastlane実行を不要にしました。</p> <p>トリガーはGitHub Actionsを使い、ブランチを指定して実行するとBitriseのアプリ配布のワークフローが実行されるようにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20231219/20231219092137.png" width="1200" height="433" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>改善方法は別の記事にまとめております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F03%2F27%2F091612" title="GitHub ActionsからマニュアルトリガーでBitriseを動かす方法 - コネヒト開発者ブログ" 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.connehito.com/entry/2023/03/27/091612">tech.connehito.com</a></cite></p> <p>この改善により、ローカル環境に依存しないアプリ配布ができるようになり、開発スピードのUPに繋がっています。</p> <h2 id="画面遷移の改善">画面遷移の改善</h2> <h3 id="課題に感じていたこと-2">課題に感じていたこと</h3> <p>アプリ全体の各ViewConrollerに画面生成と遷移のロジックが実装されていて、コードの見通しが悪く、ViewControllerの肥大化要因にもなっていました。</p> <h3 id="前提-2">前提</h3> <ul> <li>画面遷移のアーキテクチャーは採用していない</li> <li>各ViewControllerの任意の箇所でViewControllerの生成と遷移処理を行っている</li> </ul> <h3 id="どんな改善をしたか-2">どんな改善をしたか</h3> <p>画面生成と遷移処理のロジックをなるべく1箇所にまとめる仕組みを作りました。</p> <p>具体的には画面生成と遷移処理部分で以下のような変更をしました。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">let</span> <span class="synIdentifier">viewController</span> <span class="synIdentifier">=</span> QuestionContainerViewController.instantiate( id<span class="synSpecial">:</span> <span class="synType">question.id</span> ) navigationController?.pushViewController(viewController, animated<span class="synSpecial">:</span> <span class="synType">true</span>) </pre> <p>↓</p> <pre class="code lang-swift" data-lang="swift" data-unlink>pushScreen(screen<span class="synSpecial">:</span> .questionContainer( questionId<span class="synSpecial">:</span> <span class="synType">question.id</span> ) </pre> <p><code>Screen</code>という<code>enum</code>を作り、<code>Screen</code>の値を元に<code>ViewController</code>を生成するようにしました。</p> <pre class="code lang-swift" data-lang="swift" data-unlink><span class="synPreProc">enum</span> <span class="synIdentifier">Screen</span> { <span class="synStatement">case</span> questionContainer( questionId<span class="synSpecial">:</span> <span class="synType">Int</span> ) <span class="synIdentifier">...</span> } <span class="synPreProc">extension</span> <span class="synIdentifier">UIViewController</span> { <span class="synPreProc">func</span> <span class="synIdentifier">makeViewController</span>(from screen<span class="synSpecial">:</span> <span class="synType">Screen</span>) <span class="synSpecial">-&gt;</span> <span class="synType">UIViewController</span> { <span class="synStatement">switch</span> screen { <span class="synStatement">case</span> .questionContainer(<span class="synPreProc">let</span> <span class="synIdentifier">questionId</span>)<span class="synSpecial">:</span> <span class="synType">return</span> QuestionContainerViewController.instantiate( id<span class="synSpecial">:</span> <span class="synType">questionId</span> ) <span class="synIdentifier">...</span> } } <span class="synPreProc">func</span> <span class="synIdentifier">pushScreen</span>(screen<span class="synSpecial">:</span> <span class="synType">Screen</span>) { <span class="synPreProc">let</span> <span class="synIdentifier">viewController</span> <span class="synIdentifier">=</span> makeViewController(from<span class="synSpecial">:</span> <span class="synType">screen</span>) <span class="synIdentifier">self</span>.navigationController?.pushViewController(viewController, animated<span class="synSpecial">:</span> <span class="synType">true</span>) } } </pre> <p>画面遷移のアーキテクチャーパターンの採用も検討しましたが、実装コストとメリットが見合わず、今回はよりライトにできる方法を採用しました。</p> <p>画面遷移のアーキテクチャー検討について以前外部イベントで発表した資料がありますので、興味がある方はご覧ください。</p> <p><iframe src="https://www.docswell.com/slide/KQ8XME/embed?key=2023-07-25-100851" allowfullscreen="true" class="docswell-iframe" width="620" height="405" style="border: 1px solid #ccc; display: block; margin: 0px auto; padding: 0px; aspect-ratio: 620/405;"></iframe><cite class="hatena-citation"><a href="https://www.docswell.com/s/4109190/KQ8XME-2023-07-25-100851">www.docswell.com</a></cite></p> <p>この改善は現在も段階的に置き換えを進めているところですが、すでに実装完了している部分だけでもコードの見通しが良くなり、またテストコードも書きやすくなりました。</p> <h1 id="まとめ">まとめ</h1> <p>どの改善も日々の開発業務のスピードを上げることができていると実感する部分が多くありました。</p> <p>来年はより難易度の高い改善にも取り組めたらと思っております!</p> yoshitaka465 ドメインモデリングを通じて起きたチームの変化 ~ytake氏のワークショップ~ hatenablog://entry/6801883189066134155 2023-12-13T16:31:04+09:00 2023-12-13T16:31:04+09:00 この記事はコネヒト Advent Calendarのカレンダー 13日目の記事です。 adventar.org 2023/10/06にytakeさんをお招きして、社内メンバーを対象にドメインモデリングの講義を開催しました。 今回は当日の様子と、その後の変化について紹介したいと思います。 現時点では、定量的に測れる変化までは至っていませんが、業務上定性的な変化が起きていると感じています。 今回、本記事で例示するのは以下の2つです。 チーム内でモデルと命名の話題が増えた 「境界づけられたコンテキスト」を基準に設計・実装に反映する 本記事は、以下の構成でお送りします。 ワークショップの目的 コンテキ… <p>この記事はコネヒト Advent Calendarのカレンダー 13日目の記事です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F8994" title="コネヒト Advent Calendar 2023 - Adventar" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://adventar.org/calendars/8994">adventar.org</a></cite></p> <p>2023/10/06に<a href="https://twitter.com/ex_takezawa">ytake</a>さんをお招きして、社内メンバーを対象にドメインモデリングの講義を開催しました。 今回は当日の様子と、その後の変化について紹介したいと思います。</p> <p>現時点では、定量的に測れる変化までは至っていませんが、業務上定性的な変化が起きていると感じています。 今回、本記事で例示するのは以下の2つです。</p> <ul> <li>チーム内でモデルと命名の話題が増えた</li> <li>「境界づけられたコンテキスト」を基準に設計・実装に反映する</li> </ul> <p>本記事は、以下の構成でお送りします。</p> <ul class="table-of-contents"> <li><a href="#ワークショップの目的">ワークショップの目的</a><ul> <li><a href="#コンテキストギャップ埋める方法を身につけリリースまでの機動力を高める">コンテキストギャップ埋める方法を身につけ、リリースまでの機動力を高める</a></li> <li><a href="#技術コミュニティと新しい学びのループを作る">技術コミュニティと新しい学びのループを作る</a></li> </ul> </li> <li><a href="#講義の内容">講義の内容</a><ul> <li><a href="#タイムライン">タイムライン</a></li> <li><a href="#参加者内訳">参加者内訳</a></li> </ul> </li> <li><a href="#当日の様子">当日の様子</a><ul> <li><a href="#座学">座学</a></li> <li><a href="#イベントストーミング">イベントストーミング</a></li> </ul> </li> <li><a href="#その後の変化">その後の変化</a><ul> <li><a href="#チーム内でモデルと命名の話題が増えた">チーム内でモデルと命名の話題が増えた</a></li> <li><a href="#境界づけられたコンテキストを基準に設計実装に反映する">「境界づけられたコンテキスト」を基準に設計・実装に反映する</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <h2 id="ワークショップの目的">ワークショップの目的</h2> <p>ワークショップの目的は、以下の2点です。</p> <ol> <li>コンテキストギャップ埋める方法を身につけ、リリースまでの機動力を高める</li> <li>技術コミュニティと新しい学びのループを作る</li> </ol> <h3 id="コンテキストギャップ埋める方法を身につけリリースまでの機動力を高める">コンテキストギャップ埋める方法を身につけ、リリースまでの機動力を高める</h3> <p>日々の業務では、個人個人が持つ情報差分や専門職種の特性などから作られるコミュニケーションのコンテキストに違いが生まれます。 その違いが大きいと、コミュニケーションのコストが高くなったり、思わぬ手戻りが発生することで、ボトルネックとなってしまうことがあります。 どの程度のボトルネックかは、個人個人の暗黙的な感覚に依存してしまうため、 まずはワークショップを通じた現状認識と課題の発見を目的としました。</p> <h3 id="技術コミュニティと新しい学びのループを作る">技術コミュニティと新しい学びのループを作る</h3> <p>コネヒトにはスマイル制度という「技術コミュニティになくてはならない開発組織をつくる」をコンセプトにしたアウトプット支援制度があります。 インプットとアウトプットのループを作ることで、コネヒトも技術コミュニティもWin-Winな関係を築いていく狙いがあります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech-vision.connehito.com%2Fprogram%2Fsmile.html" title="スマイル制度 · Connehito Tech Vision" 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://tech-vision.connehito.com/program/smile.html">tech-vision.connehito.com</a></cite></p> <p>これまでは研修参加や書籍の購入など、個人で利用されるケースが多かったのですが、複数人で同じことを学び、発信する事例としてこの制度を利用しました。 本制度の多様な活用事例を生み出すことで、より技術コミュニティとWin-Winになること2つめの目的と設定しています。</p> <h2 id="講義の内容">講義の内容</h2> <p>当日の講義は、座学とワークショップを組み合わせた形式で、合計4時間の内容でした。参加者は事前に目的を共有し希望者を募りました。 ytakeさんに参加者と予定時間を相談の上、半日で収まる内容にチューニングしていただきました。</p> <h3 id="タイムライン">タイムライン</h3> <p><strong>座学(120min)</strong></p> <ul> <li>14:00 ~ 14:05: イベント概要おさらい</li> <li>14:05 ~ 14:15: 自己紹介/チェックインタイム</li> <li>14:15 ~ 15:00: ytakeさん自己紹介・座学</li> <li>15:00 ~ 15:10: 休憩</li> <li>15:10 ~ 16:00: 座学</li> <li>16:00 ~ 16:10: 休憩</li> </ul> <p><strong>イベントストーミング形式ワークショップ(120min)</strong></p> <ul> <li>16:10 ~ 17:00: ワークショップ</li> <li>17:00 ~ 17:10: 休憩</li> <li>17:10 ~ 17:50: ワークショップ</li> <li>17:50 ~ 18:00: チェックアウト</li> </ul> <h3 id="参加者内訳">参加者内訳</h3> <ul> <li>エンジニア:12名</li> <li>デザイナー:1名</li> <li>PdM/PMM: 2名</li> </ul> <p>なお、イベントストーミングは、Alberto Brandolini氏が考案した協働的にドメインモデルを発見していく手法です。 <a href="https://www.eventstorming.com/">EventStorming</a>のサイトを参照すると以下のような記載があります。</p> <blockquote><p>The adaptive nature of EventStorming allows sophisticated cross-discipline conversation between stakeholders with different backgrounds, delivering a new type of collaboration beyond silo and specialisation boundaries.</p> <p>翻訳:EventStorming の適応的な性質により、異なる背景を持つ関係者間で専門分野を超えた洗練された会話が可能になり、サイロや専門分野の境界を超えた新しいタイプのコラボレーションが実現します。`</p></blockquote> <p>今回はこのようなコラボレーションを実現するために、エンジニアとPdM/PMM、デザイナーの3つの職種のメンバーに参加してもらい、この方式を採用しました。</p> <h2 id="当日の様子">当日の様子</h2> <h3 id="座学">座学</h3> <p>当日は以下の内容を中心にお話ししていただきました。</p> <ul> <li>ドメインモデルとはなに?</li> <li>分析するための考え方</li> <li>ユースケース</li> </ul> <p>座学の間も適宜質問を受けていただき、理解を深めながらお聞きすることができました。 ここで教えていただいた「コンテキストの境界」が今後の変化に繋がっていきます。</p> <p><figure class="figure-image figure-image-fotolife" title="わいわいと講義を受けています"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231212/20231212171358.png" width="1200" height="903" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>わいわいと講義を受けています</figcaption></figure></p> <h3 id="イベントストーミング">イベントストーミング</h3> <p>今回はママリアプリで起こる出来事を題材にイベントストーミングを開催しました。 みんなでわいわいと、出来事や関連する要素を書き出していき、ytakeさんのファシリテーションを受けながら、認識の違いを発見していきました。</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/katsutomu0124/20231212/20231212171836.png" width="1200" height="911" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>グルーピングしたものをもとに議論しています</figcaption></figure></p> <p>付箋の色分けは以下の通りです。</p> <ul> <li>画面やUIが絡むようなもの: 青</li> <li>システム的なもの: ピンク</li> <li>サービスの仕様、重要な出来事: オレンジ</li> </ul> <p><figure class="figure-image figure-image-fotolife" title="当日のサンプル"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231212/20231212172926.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>当日のサンプル</figcaption></figure></p> <h2 id="その後の変化">その後の変化</h2> <p>このようにモデリングを利用し、認識の違いを発見する方法を学んだことで起きたエピソードを2つ紹介します。</p> <h3 id="チーム内でモデルと命名の話題が増えた">チーム内でモデルと命名の話題が増えた</h3> <p>ミーティングで話す用語は、どのように整理すると自然なのかというコミュニケーションが以前より増え、用語集を作る動きがチーム内で生まれています。 筆者の所属するチームが動画コンテンツの改修を担当しているため、まずは動画施策の用語の整理から始めています。</p> <p><figure class="figure-image figure-image-fotolife" title="用語集の整理"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231212/20231212172414.png" width="1200" height="683" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>用語集の整理</figcaption></figure></p> <p>職種を問わず共同作業ができるように、Notionのデータベースを利用しています。</p> <h3 id="境界づけられたコンテキストを基準に設計実装に反映する">「境界づけられたコンテキスト」を基準に設計・実装に反映する</h3> <p>コネヒトが管理する動画には、外部サイトへのリンクがあるものないものがあります。この違いをRestAPIリソースとして個別に分けるかの議論がなされました。</p> <p><figure class="figure-image figure-image-fotolife" title="miroで整理した様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231212/20231212172537.png" width="933" height="705" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>miroで整理した様子</figcaption></figure></p> <p>ワークショップ開催前は、設計においてアクターが誰かという話題は出てこなかったのですが、ワークショップを通じて、「コンテキストの境界」が重要であることを学んだことで、 この議論では、アクターが同じため同一のリソースとして定義しておこうと判断することができるようになりました。</p> <p>この他にも</p> <ul> <li>チームの担当領域に絞りイベントストーミングを実施する</li> <li>特定施策のユースケースを書き出してみる</li> </ul> <p>など少しずつモデリング実施する機会が増えてきています。 それらの事例も、今後の変化に繋がっていくと思うので、また別の機会に紹介したいと思います。</p> <h2 id="まとめ">まとめ</h2> <p>今回のワークショップでは、まずはモデリングのエッセンスを学ぶことにフォーカスしておりました。 本来の目的であるリリースの機動力を高める目的の実現には、まだまだ課題が多くあり、モデリングの実践とコードへの設計・実装への反映を定期的に繰り返す必要があると感じています。</p> <p>ytakeさんから「目の前のものに騙されないように本質はなにか分析しましょう!100回くらい分析を繰り返しましょう!」というアドバイスをいただきました。 今後はより日常的にモデリングを実践できるように、常にMiro<a href="#f-74d2efd1" id="fn-74d2efd1" name="fn-74d2efd1" title="コネヒトではオンラインホワイトボードツールとして利用しています。https://miro.com/ja/">*1</a>を開いてミーティングに参加しようと思います。</p> <p><strong>目指せモデリング100回!</strong></p> <p>最後となりますが、ytakeさん講義の開催ありがとうございました! モデリングにお悩みの場合、最初の一歩として、ytakeさんに相談することをおすすめしたいと思います。</p> <div class="footnote"> <p class="footnote"><a href="#fn-74d2efd1" id="f-74d2efd1" name="f-74d2efd1" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">コネヒトではオンラインホワイトボードツールとして利用しています。<a href="https://miro.com/ja/">https://miro.com/ja/</a></span></p> </div> katsutomu0124 レコメンドで使用する類似アイテムをAmazon Bedrockとitem2vecで計算・比較検証してみた hatenablog://entry/6801883189064775682 2023-12-11T14:10:31+09:00 2023-12-11T14:10:31+09:00 みなさんこんにちは。MLエンジニアのたかぱい(@takapy0210)です。 最近、久しぶりに機動戦士ガンダムSEEDを見直しました。(来年には劇場版の公開もあります) 地球連合軍第7機動艦隊に所属するパイロットであるムウさんの 「君は出来るだけの力を持っているだろう?なら、出来ることをやれよ」 というセリフが好きです。 相手をリスペクトしつつ、でもお前はもっとできるだろ?という期待も込もった、良い言葉だなと感じます。 さて本日は、レコメンドで使用頻度の高い類似アイテムの計算処理を2パターンで実施し、どんな差分がでるのか?を検証した結果をお話ししようと思います。 この記事はコネヒト Adven… <p>みなさんこんにちは。MLエンジニアのたかぱい(<a href="https://twitter.com/takapy0210">@takapy0210</a>)です。</p> <p>最近、久しぶりに<a href="https://ja.wikipedia.org/wiki/%E6%A9%9F%E5%8B%95%E6%88%A6%E5%A3%AB%E3%82%AC%E3%83%B3%E3%83%80%E3%83%A0SEED">機動戦士ガンダムSEED</a>を見直しました。(来年には<a href="https://www.gundam-seed.net/freedom/">劇場版</a>の公開もあります)</p> <p>地球連合軍第7機動艦隊に所属するパイロットであるムウさんの</p> <p><strong>「君は出来るだけの力を持っているだろう?なら、出来ることをやれよ」</strong></p> <p>というセリフが好きです。<br/> 相手をリスペクトしつつ、でもお前はもっとできるだろ?という期待も込もった、良い言葉だなと感じます。</p> <p>さて本日は、レコメンドで使用頻度の高い類似アイテムの計算処理を2パターンで実施し、どんな差分がでるのか?を検証した結果をお話ししようと思います。</p> <p>この記事は<a href="https://adventar.org/calendars/8994">コネヒト Advent Calendarのカレンダー</a> 10日目の記事です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F8994" title="コネヒト Advent Calendar 2023 - Adventar" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://adventar.org/calendars/8994">adventar.org</a></cite></p> <hr /> <p><span style="font-size: 130%">目次</span></p> <ul class="table-of-contents"> <li><a href="#背景">背景</a></li> <li><a href="#Amazon-Bedrockの埋め込みモデルでベクトルを取得する">Amazon Bedrockの埋め込みモデルでベクトルを取得する</a></li> <li><a href="#item2vecで計算したベクトルを取得する">item2vecで計算したベクトルを取得する</a></li> <li><a href="#比較検証結果">比較検証結果</a><ul> <li><a href="#パターン1ディズニーランドの情報が知りたいユーザー">パターン1:ディズニーランドの情報が知りたいユーザー</a></li> <li><a href="#パターン2帝王切開後の身体への影響が気になるユーザー">パターン2:帝王切開後の身体への影響が気になるユーザー</a></li> <li><a href="#パターン3-ドラム式洗濯機を買うか迷っているユーザー">パターン3: ドラム式洗濯機を買うか迷っているユーザー</a></li> <li><a href="#パターン4離乳食の2回食をいつから始めるか気になっているユーザー">パターン4:離乳食の2回食をいつから始めるか気になっているユーザー</a></li> <li><a href="#パターン5オムツのサイズアップをいつからすれば良いか悩んでいるユーザー">パターン5:オムツのサイズアップをいつからすれば良いか悩んでいるユーザー</a></li> </ul> </li> <li><a href="#考察">考察</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <hr /> <h2 id="背景">背景</h2> <p>コネヒトの運営するコミュニティサービスママリでは、様々な部分でレコメンデーション機能が提供されています。 今後もレコメンデーションロジックの改善を継続的に行っていく中で、類似するアイテムをどのように計算するのか?は重要な課題の1つです。</p> <p>類似アイテムを計算する1つのHowとして、昨今話題になっているLLMを使う方法が考えられます。<br/> LLMを用いることで様々なEmbedding(ベクトル)を取得することができ、このベクトルを用いることでアイテム間の類似度を計算することができます。</p> <p>本記事では以下の2パターンで算出したベクトルを用いて、類似アイテム(ここで言うアイテム=質問)を抽出し、比較・考察してみようと思います。</p> <ul> <li>Amazon Bedrockの埋め込みモデルで取得したベクトル</li> <li>item2vecで計算したベクトル</li> </ul> <p>前半でそれぞれのベクトル取得方法を簡単に説明し、後半では実際にいくつかの質問を用いてどのような類似アイテムが算出できるのか?を見ていこうと思います。</p> <h2 id="Amazon-Bedrockの埋め込みモデルでベクトルを取得する">Amazon Bedrockの埋め込みモデルでベクトルを取得する</h2> <p>Amazon Bedrock(以下、Bedrock)とは、テキスト生成AIをはじめとする基盤モデル (Foundation Model) を提供するAWSのサービスです。Bedrockで使用できる基盤モデルには、Amazon自身が開発提供するTitanやAnthropicのテキスト生成AIであるClaudeなどがあります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Faws.amazon.com%2Fjp%2Fbedrock%2F" title="基盤モデルによる生成系 AI アプリケーションの構築 - Amazon Bedrock - 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/bedrock/">aws.amazon.com</a></cite></p> <p>以下のようなコードで、Bedrockの埋め込みモデルを使ってテキストのベクトルを取得することができます。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> json <span class="synPreProc">import</span> boto3 bedrock_runtime_client = boto3.client(<span class="synConstant">'bedrock-runtime'</span>, region_name=<span class="synConstant">&quot;ap-northeast-1&quot;</span>) <span class="synStatement">def</span> <span class="synIdentifier">get_bedrock_embedding</span>(input_str, bedrock_runtime_client): bedrock_body = { <span class="synConstant">&quot;inputText&quot;</span>: input_str } body_bytes = json.dumps(bedrock_body).encode(<span class="synConstant">'utf-8'</span>) response = bedrock_runtime_client.invoke_model( accept=<span class="synConstant">&quot;*/*&quot;</span>, body=body_bytes, contentType=<span class="synConstant">&quot;application/json&quot;</span>, modelId=<span class="synConstant">&quot;amazon.titan-embed-text-v1&quot;</span>, ) response_body = json.loads(response.get(<span class="synConstant">&quot;body&quot;</span>).read()) embedding = response_body.get(<span class="synConstant">&quot;embedding&quot;</span>) <span class="synStatement">return</span> embedding text = <span class="synConstant">&quot;帝王切開で出産、退院時に痛み止めもらった方、いつぐらいまで飲んでましたか?&quot;</span> vec = get_bedrock_embedding(input_str=text, bedrock_runtime_client=bedrock_runtime_client) </pre> <p>今回、Bedrockで取得したEmbeddingはOpenSearchに格納し、その機能を利用して類似質問を抽出しています。 OpenSearchへの格納方法などは、以下のブログに書いていますので、興味のある方はこちらもご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F12%2F08%2F144649" title="「ベクトル検索 vs 全文検索」〜Amazon Bedrockの埋め込みモデルを用いたプロトタイピング〜 - コネヒト開発者ブログ" 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.connehito.com/entry/2023/12/08/144649">tech.connehito.com</a></cite></p> <h2 id="item2vecで計算したベクトルを取得する">item2vecで計算したベクトルを取得する</h2> <p>item2vecとは、自然言語処理におけるword2vecの概念をアイテム推薦などに適用したものです。<br/> word2vecは単語をベクトルとして表現し、これらのベクトルを使って単語間の意味的な関係を捉えることができますが、item2vecでは、この考えを商品や映画、曲などの「アイテム」に適用することができます。<a href="#f-311945bb" id="fn-311945bb" name="fn-311945bb" title="ITEM2VEC: Neural item embedding for collaborative filtering">*1</a></p> <p>今回はママリの質問閲覧ログデータを用いてitem2vecの学習を行いました。学習には<a href="https://radimrehurek.com/gensim/">gensim</a>ライブラリを用いています。</p> <p>学習に使用したデータは以下のようなイメージです。</p> <table> <thead> <tr> <th> user_id </th> <th> question_id </th> <th> event_dt </th> </tr> </thead> <tbody> <tr> <td> 100 </td> <td> 19064176 </td> <td> 2023-10-15 </td> </tr> <tr> <td> 100 </td> <td> 19073732 </td> <td> 2023-10-16 </td> </tr> <tr> <td> 100 </td> <td> 19037730 </td> <td> 2023-10-16 </td> </tr> <tr> <td> 101 </td> <td> 18892007 </td> <td> 2023-10-15 </td> </tr> <tr> <td> 101 </td> <td> 18891679 </td> <td> 2023-10-16 </td> </tr> <tr> <td> ... </td> <td> ... </td> <td> ... </td> </tr> </tbody> </table> <p>まずは、このデータをgensimのモデルに入力できるように、系列データに変更していきます。</p> <pre class="code lang-python" data-lang="python" data-unlink>series_df = pd.DataFrame(df.groupby([<span class="synConstant">'user_id'</span>, <span class="synConstant">'event_dt'</span>])[<span class="synConstant">'question_id'</span>].apply(<span class="synIdentifier">list</span>)).reset_index() series_df = series_df.rename(columns={<span class="synConstant">'question_id'</span>: <span class="synConstant">'order_question'</span>}) </pre> <p>こうすることで、以下の様なDataFrameを取得することができます。</p> <table> <thead> <tr> <th> user_id </th> <th> event_dt </th> <th> order_question </th> </tr> </thead> <tbody> <tr> <td> 100 </td> <td> 2023-10-15 </td> <td> [19084888, 18932714, 18925535 …] </td> </tr> <tr> <td> 100 </td> <td> 2023-10-16 </td> <td> [19060467, 19055834, 19047868 …] </td> </tr> <tr> <td> 101 </td> <td> 2023-10-15 </td> <td> [19096491, 19011148, 19095622 …] </td> </tr> <tr> <td> 101 </td> <td> 2023-10-15 </td> <td> [18921942, 18921942, 18921939 …] </td> </tr> <tr> <td> ... </td> <td> ... </td> <td> ... </td> </tr> </tbody> </table> <p>最後にこのデータをgensimに渡し、item2vecモデルの学習を行います。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> multiprocessing <span class="synPreProc">from</span> gensim.models <span class="synPreProc">import</span> Word2Vec corpus = series_df[<span class="synConstant">'order_question'</span>].values.tolist() cpu_count = multiprocessing.cpu_count() model = Word2Vec( corpus, vector_size=<span class="synConstant">50</span>, window=<span class="synConstant">5</span>, hs=<span class="synConstant">1</span>, min_count=<span class="synConstant">1</span>, sg=<span class="synConstant">1</span>, workers=cpu_count, seed=<span class="synConstant">42</span> ) </pre> <p>類似アイテムの抽出は以下の様に行うことができます。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synStatement">for</span> i <span class="synStatement">in</span> model.wv.most_similar(target_q_id, topn=<span class="synConstant">3</span>): <span class="synIdentifier">print</span>(f<span class="synConstant">&quot;類似質問ID:{i[0]}, 類似度:{round(i[1], 4)}&quot;</span>) </pre> <h2 id="比較検証結果">比較検証結果</h2> <p>上記2種類のロジックで取得したベクトルを用いて、どのような類似質問が取得できるのか類似度TOP3を取得して比較します。(今回はコサイン類似度を用いています)<br/> 「入力クエリ欄」に記載した質問内容に興味のあるユーザーに対して、どんなものが推薦されるのか?という想定で検証してみます。(Bedrockの場合は入力クエリがテキスト、item2vecの場合入力クエリは質問IDになりますが、ここでは分かりやすいようにテキストで統一して記述します)</p> <p>※以下で掲示している質問文は一部改変しております</p> <h3 id="パターン1ディズニーランドの情報が知りたいユーザー">パターン1:ディズニーランドの情報が知りたいユーザー</h3> <p><strong>入力クエリ</strong></p> <blockquote><p>ディズニー詳しい方や最近行った方回答お願いします! 10月中旬の平日にディズニーランドへ行くのですが、朝何時から開園並んで何時に入園できましたか? ハロウィンのパレードはプレミアアクセスを購入予定ですが、何時までに入れば買える可能性ありますか?</p></blockquote> <p><strong>取得結果</strong></p> <p><em>Bedrockの類似アイテムTOP:3</em></p> <blockquote><p>①:10月ディズニーインパについて。 今平日でも入園待ちですごいことになっているみたいですね。10時とかだとスムーズに入園できそうですが、その頃だともうプライオリティパスは取れないですかね?取れても夜の時間とかでしょうか? ハニーハントかモンスターズインクを取る予定です!</p> <p>②:10/3ディズニー、平日、ハロウィンの情報です!どなたかの参考になれば嬉しいです。6:30ランド到着で一般前から10列目くらいで入場できました。プライオリティパスは入場してすぐで10:10~11:10の回でした!ハモカラ1時間前に地蔵で前から3列目、うちはそのままスプブ待ちでチップ、デール停車位置で前から2列目とれました。</p> <p>③:皆さんディズニー行く時何時に行きますか?シーもハロウィンだと混むんでしょうか…? 何かYouTubeみて旦那が、7時半には並んだ方がいい!とか言ってますが、子連れでその時間から並ぶって無理あるでしょと思うのですが…笑朝イチじゃないとショーやプライオリティパスもすぐなくなってしまいますか? 来週水曜にディズニーシーに行く予定です。。 全然詳しくないので教えて頂きたいです。</p></blockquote> <p><em>item2vecの類似アイテムTOP:3</em></p> <blockquote><p>①:皆さんの意見を聞かせてください! 何年振りかにディズニーランドに行きます!2歳と4歳の子供を連れていきます! 13日は平日なので多少は空いてる?!チケット料金が安いです。 14日だった場合は親が一緒にこれるので私と旦那の負担が少ない?休日なので混んでる&amp;チケット料金が高いです。 皆さんだったらどちらに行きますか?? 1つ気になるのが13日がシーが早く閉まる?のでランドが混みそうと言う事です。。 ディズニー初心者過ぎて…皆さんの力を貸してください。</p> <p>②:ここ何年もディズニーに行ってなく、最後に行ったのは10年近く前、、、 その頃は紙のファストパスの時代でしたが今はアプリでとる?んですよね? 5歳3歳1歳の子供連れで久しぶりのディズニーで不安すぎます。 ディズニーランドに行くのですが1日の流れどのようにするのがいいですかね? まったくの無知なのでこうした方がいいよなどあったら教えてほしいです!</p> <p>③:ここ数日でディズニー行った方教えて下さい! 平日で混んでますか?乗り物の待ち時間どのくらいでしょうか?コロナ禍のガラガラ時以降行ってなくて。。</p></blockquote> <h3 id="パターン2帝王切開後の身体への影響が気になるユーザー">パターン2:帝王切開後の身体への影響が気になるユーザー</h3> <p><strong>入力クエリ</strong></p> <blockquote><p>帝王切開の方にお聞きしたいです。もうすぐ産後1ヶ月になります。骨盤ガタガタで尾てい骨も痛いし足あげるのも痛いし、ゆっくりしか歩けません。みなさんは骨盤ベルトしていましたか?</p></blockquote> <p><strong>取得結果</strong></p> <p><em>Bedrockの類似アイテムTOP:3</em></p> <blockquote><p>①:臨月になりお尻と足の付け根痛いです。 頭がおりてきていたり、靭帯が緩んだりと原因はいろいろあるみたいですが、これは出産が近いのかな…?? とにかく痛みに対しては骨盤ベルトで抑えてます。 同じように股関節の痛みを感じた方、どの位の期間で出産になりましたか??</p> <p>②:赤ちゃんがもう産まれても大丈夫な状態らしく、たくさん歩くように言われました。 昨日と今日、いつもより多めに歩いたのですが、先ほどから恥骨がいつもにまして割れるような痛みがきてしまいました。 その前から恥骨は痛かったのですが、歩くのも困難になるほどです。それに加えて、股関節も痛いです。 昨日の検診では赤ちゃん、まだ全然降りてきてないと言われましたが赤ちゃんがおりてきてるんでしょうか?</p> <p>③:産後50日過ぎましたが、恥骨がまだ痛過ぎます。いつになったら解放されますか? 妊娠後期から恥骨激痛で歩くのやっとで、産んだら解放されるかと思いきや、全然痛い・・・ 骨盤矯正は、先週から行き始めましたが、産後も恥骨痛あった人、いつから無くなりましたか?</p></blockquote> <p><em>item2vecの類似アイテムTOP:3</em></p> <blockquote><p>①:出生後、しばらく哺乳瓶で、その後スムーズに直母授乳に移れますか? 25日の月曜日に帝王切開にて出産しました。 術後の私の回復具合、我が子の体調などなどいろんなことを考慮して、しばらくは哺乳瓶でミルクまたは搾乳した母乳をあたえていくことになり、その方法で1週間近く経ちました。 最初の3日間はミルクを使用、4日目以降搾乳した母乳のみ飲ませています。 同じように、最初は哺乳瓶でその後母乳に移られた方、どうでしたか??特に抵抗なく直母に移れるのもでしょうか?</p> <p>②:帝王切開で出産、退院時に痛み止めもらった方いつぐらいまで飲んでましたか?</p> <p>③:出産して入院中なんですが、特に足首から下のむくみがすごく、象の足みたいになってるんですがしばらくはこんな感じなんですかね?</p></blockquote> <h3 id="パターン3-ドラム式洗濯機を買うか迷っているユーザー">パターン3: ドラム式洗濯機を買うか迷っているユーザー</h3> <p><strong>入力クエリ</strong></p> <blockquote><p>縦型洗濯機からドラム式洗濯機に乗り換えた方にお聞きしたいです! 電気代と水道代はどのくらい高くなりましたか? 大体で構わないので教えてください。</p></blockquote> <p><strong>取得結果</strong></p> <p><em>Bedrockの類似アイテムTOP:3</em></p> <blockquote><p>①:現在、縦型の洗濯機を使っていますが、ドラム式に買い替えることにしました。 明日、電気屋さんに行くのですが、 全く無知なので、おすすめの洗濯機を教えてください! また金額も20万ぐらいかな…と思っているのですが、どのぐらいするのでしょうか?</p> <p>②:みなさんは洗濯機って縦型使ってますか? それともドラム式洗濯乾燥機ですか? うちは縦型ですが、ドラム式ほしいです。でも高い...</p> <p>③:洗濯機買って5年経つんですが、縦型で容量が8キロ。 子どもが2人になり洗濯物が増えて、週末は1日に2回まわすこともよくあります。 容量が少ないのがストレスで買い替えたい気持ちもあるのですが、まだ5年だしな〜と迷っています。 もし次買うならドラム式がいいですが高い。。。ドラム式を使われてる方は洗濯後乾燥までされてる方が多いんですかね?縦型、ドラムどちらがおすすめですかね??皆さんどちら使われてるかも教えて欲しいです☺️</p></blockquote> <p><em>item2vecの類似アイテムTOP:3</em></p> <blockquote><p>①:マイホームを購入した時にサービスでつけてもらったオプションや家具家電など参考にさせてもらいたいので教えて下さい</p> <p>②:コンビニで2,000円の買い物をするのに、 ①楽天カードで支払い ②楽天ペイで支払い(クレカからチャージ) どちらがお得なんでしょうか?</p> <p>③:年少1人、1歳1人のお子様が居るご家庭の月の食費を教えてください!また旦那さんのお昼ご飯代も込みかどうかも教えて貰えるとありがたいです</p></blockquote> <h3 id="パターン4離乳食の2回食をいつから始めるか気になっているユーザー">パターン4:離乳食の2回食をいつから始めるか気になっているユーザー</h3> <p><strong>入力クエリ</strong></p> <blockquote><p>生後5ヶ月半から離乳食を始めました。 2回食にするのは離乳食が始まって2ヶ月後である7ヶ月半くらいかな?と思っていたんですが、6ヶ月の時点で2回食検討中、またはされている話も聞いたりします。 みなさん2回食はいつから始められましたか?</p></blockquote> <p><strong>取得結果</strong></p> <p><em>Bedrockの類似アイテムTOP:3</em></p> <blockquote><p>①:離乳食についてです。 みなさんいつから2回食に移行しましたか? 5ヶ月から始めてもう1ヶ月と1週間が過ぎました。調べると2回食を始めるのはだいたい7ヶ月からか、始めて1ヶ月経ったらと見ます。 もちろん子どもの様子によって進めていくものだとは思いますが、2回食にするのがすごく気が乗らなくて笑</p> <p>②:6ヶ月から離乳食を始めた方、いつから2回食にしましたか?5ヶ月で始める時より早めに2回食にした方がいいとかあるんでしょうか、、進め方がよく分からず困っています。</p> <p>③:5ヶ月から離乳食始めました。 今7週目で(6ヶ月の2週目)いつから2回食を始めるか悩んでます。今日から始めようかな、、と。 みなさん2回目はいつ頃から、何時頃あげてましたか?</p></blockquote> <p><em>item2vecの類似アイテムTOP:3</em></p> <blockquote><p>①:10ヶ月検診で肥満気味と言われました。 男の子で身長73センチの11キロです。離乳食を始めたのが6ヶ月後半からで遅く、量もあまり食べなかったのですが、1ヶ月くらい前から本格的に3回食にして量は小鉢2つ程度です。量は測ったことなかったのですが、 今回測ってみたら25ml+30ml+50mlの冷凍パックしたものを解凍し味噌汁に野菜をプラスしたおかゆとじゃがいもと野菜を潰してコンソメで味付けのポテトサラダで、105g?食べているのかなと思いました。ただ、毎回このくらい食べる時もあれば半分食べたかなくらいで残す時もあります。 本やネットには離乳食の間隔は4時間あけて18時以降は食べさせないと書いてあり、先生には1度に量を食べれなければ間食としてご飯をあげてくださいと言われましたが、その場合はあける時間間隔は無視してもいいのでしょうか。 正直、間食をあげるタイミングもわからないです。 10ヶ月のお子さんがいらっしゃる方は、毎食どのくらい食べているのでしょうか?</p> <p>②:まだ自分でコップを持って飲めない赤ちゃん、コップ飲みの練習ってどうやりましたか? 現在生後6ヶ月で、生後5ヶ月の頃から離乳食デビューに合わせて麦茶でのコップ飲みを始めました。 ダイソーで売っているトレーニングコップを使用していて、取っ手の部分を握ったりはしますがそのまま自分の口に持っていくのはまだできません。なので私がコップを持って飲ませていますが、麦茶がたくさん口に入るのかむせてしまったりしてイマイチ上手くあげられません。 自分で持って飲んでむせたり溢したりしてだんだんコップに慣れるイメージなのですが、親が飲ませていて練習になるんでしょうか? 1ヶ月続けてもなんの進展もないので不安になってしまいました。 ぜひ教えてください。</p> <p>③:離乳食をあげてる時間教えてください。 現在生後6ヶ月で5ヶ月の頃から離乳食あげてますがまだ一回食です。 離乳食はモリモリ食べすぎてるくらいなのですが、二回食にするのがめんどくさくて… でもそろそろ二回食を考えなきゃな〜と思ってるので、何時に離乳食をあげているのかみなさんの離乳食スケジュール教えてください。 三回食をもう始めている場合は今後のために三回食の時間も教えて欲しいです。 うちは今10時に離乳食をあげています。 離乳食とミルクはバラバラです。</p></blockquote> <h3 id="パターン5オムツのサイズアップをいつからすれば良いか悩んでいるユーザー">パターン5:オムツのサイズアップをいつからすれば良いか悩んでいるユーザー</h3> <p><strong>入力クエリ</strong></p> <blockquote><p>オムツのサイズアップについてです。 生後1ヶ月で、まだ新生児サイズを使っているのですが、太ももにかなり跡がついています。 ですが、お腹周りはゆるゆるです。 サイズアップした方がいいのかな?と思い、試しに試供品のSサイズを使ってみたら、太ももはぴったりですがお腹の方はオムツがかなり上まで来てゆるゆるです。 このような場合でもサイズアップしたほうがいいのでしょうか?</p></blockquote> <p><strong>取得結果</strong></p> <p><em>Bedrockの類似アイテムTOP:3</em></p> <blockquote><p>①:オムツの新生児サイズからSサイズへのサイズアップっていつ頃でしたか? また、どんな感じになったらサイズアップした方が良いのか教えて下さい。</p> <p>②:オムツのサイズについて質問させてください! 生後2ヶ月の男の子がいます。 私の母乳外来受診のため、授乳の様子も見てもらえるということで息子も一緒に受診したのですが、付けているオムツが小さいからサイズアップしたほうがいいと言われました。 体重は約5800gでテープのSサイズを使ってます。 最近ようやく太もも周りがちょうどよくなってきたかなぁと自分的には思ってた矢先助産師さんに小さいと言われたのですが、やはりMサイズへ変えるべきでしょうか?</p> <p>③:オムツのサイズ別の使用量について。 2400gで生まれ、1ヶ月半新生児用を使用しメリーズファーストプレミアムを5袋使いました。 1ヶ月半となり、4000gになったのでオムツをSサイズにしました! 皆さんはSサイズ、Mサイズ、Lサイズをそれぞれ何ヶ月頃から使用し、何袋使いましたか?? もちろん、赤ちゃんの体型それぞれなのは承知で参考までに。 また、何ヶ月の何キロからSサイズorMサイズからテープオムツをパンツオムツにしましたか??</p></blockquote> <p><em>item2vecの類似アイテムTOP:3</em></p> <blockquote><p>①:生後1ヶ月と半月です。 夜間の授乳が5~6時間くらい初めて空いたのですが、こうやって徐々に夜の時間を延ばしていってもいいんですかね? 最近、日中はずっと起きています。。。早めに昼夜の感覚をつけたいと思ってます!</p> <p>②:生後1ヶ月 4キロです。 アマゾンセールでおむつを買いたいです。 今使っているものの次のサイズをストックしておくつもりです。 6キロからのMサイズを購入しますがテープタイプとパンツタイプだとどちらが良いのでしょうか?</p> <p>③:現在生後1ヶ月、まもなく2ヶ月になる息子の育児について相談させてください。ご機嫌に起きている時で、あやすのも授乳も終えている時は皆さんどうされてますか? ご機嫌で1人でおしゃべりしてる時はそのままベッドやハイローチェアで眠るのを待っていても良いのでしょうか?</p></blockquote> <h2 id="考察">考察</h2> <p>それぞれのパターンについて簡単にまとめてみます。</p> <table> <thead> <tr> <th> パターン </th> <th> Bedrock </th> <th> item2vec </th> </tr> </thead> <tbody> <tr> <td> 1:ディズニーランドの情報が知りたいユーザー </td> <td>「プレミアアクセス」や「時間状況」などの単語表現をうまく拾い、類似したアイテムが推薦されている </td> <td> ディズニーランド全般に関する幅広い情報が推薦されている</td> </tr> <tr> <td> 2:帝王切開後の身体への影響が気になるユーザー </td> <td>骨盤や股関節の痛みといった、身体への影響に関連するアイテムが推薦されている</td> <td>どちらかと言えば帝王切開に関連したアイテムが推薦されている</td> </tr> <tr> <td> 3:ドラム式洗濯機を買うか迷っているユーザー </td> <td>洗濯機を選ぶ基準やおすすめの洗濯機に関連するアイテムが推薦されており、購入を検討しているユーザーには有用そう</td> <td>家庭生活全般(主にお金関連)に関連するアイテムが推薦されている </td> </tr> <tr> <td> 4:離乳食の2回食をいつから始めるか気になっているユーザー </td> <td>離乳食の2回食移行に関連したアイテムが推薦されている</td> <td>赤ちゃんの健康管理や飲み物の練習など、子育ての幅広い側面をカバーしたアイテムが推薦されている </td> </tr> <tr> <td> 5:オムツのサイズアップをいつからすれば良いか悩んでいるユーザー </td> <td>オムツのサイズに重点を置いたアイテムが推薦されている</td> <td>パターン4の時と同様に子育ての幅広い側面をカバーしたアイテムが推薦されている</td> </tr> </tbody> </table> <p>Bedrockによる推薦は、細かいテキスト内容部分も考慮された類似アイテムが計算できていそうです。(テキストをベクトル化して類似度を計算しているので当たり前と言えば当たり前ですが)<br/> 一方でitem2vecによる推薦は、ドンピシャな推薦というよりは、同一トピック内ではあるものの、もう少し幅広いアイテムが計算されていそうです。</p> <h2 id="おわりに">おわりに</h2> <p>本記事では、Bedrockとitem2vecを用いて類似アイテムの計算と比較検証を行ってみました。</p> <p>レコメンデーションには大きく分けて「探索 (Exploration)」と「活用 (Exploitation)」があると言われています。</p> <p>探索とは、何らかのアルゴリズムによってユーザの嗜好に最も合うであろうアイテムが決定される中で、あえて嗜好に最も合うもの"ではない"アイテムを推薦することを言います。 探索の主な目的は新しい知見を得ることです。これにより、ユーザーの好みや興味の変化に適応することができます。<br/> 逆に、現時点で最もユーザの嗜好に合うと計算されたアイテムをそのまま推薦することを活用と言います。<br/> 一般的にこの2つはトレードオフの関係になっています。</p> <p>今回の考察結果から、item2vecの推薦はユーザーの興味トピックとしては近いが、そのトピックの中から幅広いアイテムを抽出しているため、どちらかというと探索向けの推薦として利用できそうだなと感じました。<br/> 一方でBedrockによる推薦は、興味を持った具体的なアイテムと似ているものをピンポイントで推薦するため、活用向けの推薦に利用できそうだなと感じました。</p> <p>今後も推薦の目的や状況に合わせたレコメンデーションロジックの検証・改善を繰り返し、より良いユーザー体験を届けていきたいと思います。</p> <div class="footnote"> <p class="footnote"><a href="#fn-311945bb" id="f-311945bb" name="f-311945bb" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://ieeexplore.ieee.org/abstract/document/7738886">ITEM2VEC: Neural item embedding for collaborative filtering</a></span></p> </div> taxa_program 「ベクトル検索 vs 全文検索」〜Amazon Bedrockの埋め込みモデルを用いたプロトタイピング〜 hatenablog://entry/6801883189064199136 2023-12-08T14:46:49+09:00 2023-12-08T14:46:49+09:00 ※ この記事は、AWS (Amazon Web Services) の技術支援を受けて執筆しています。 はじめに この記事はコネヒトアドベントカレンダー 8日目の記事です。 コネヒト Advent Calendar 2023って? コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。 コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、 ママの一歩を支えるアプリ「ママリ」などを 運営しています。 adventar.org こんにちは!コネヒトの機械学習エンジニア y.ikenoueです。 突然ですがみなさん、Amazon Bedrockをご存… <p>※ この記事は、AWS (Amazon Web Services) の技術支援を受けて執筆しています。<br></p> <h2 id="はじめに">はじめに</h2> <p>この記事はコネヒトアドベントカレンダー 8日目の記事です。</p> <pre class="code" data-lang="" data-unlink>コネヒト Advent Calendar 2023って? コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。 コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、 ママの一歩を支えるアプリ「ママリ」などを 運営しています。</pre> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F8994" title="コネヒト Advent Calendar 2023 - Adventar" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://adventar.org/calendars/8994">adventar.org</a></cite></p> <hr /> <p>こんにちは!コネヒトの機械学習エンジニア <a href="https://tech.connehito.com/archive/author/ashenOne">y.ikenoue</a>です。<br> 突然ですがみなさん、Amazon Bedrockをご存知でしょうか。<br></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Faws.amazon.com%2Fjp%2Fbedrock%2F" title="基盤モデルによる生成系 AI アプリケーションの構築 - Amazon Bedrock - 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/bedrock/">aws.amazon.com</a></cite></p> <p>Amazon Bedrock(以下、Bedrock)は、テキスト生成AIをはじめとする基盤モデル (Foundation Model)<a href="#f-33f4f9a7" id="fn-33f4f9a7" name="fn-33f4f9a7" title="「基盤モデル」... 広範な用途に使用することのできる大規模なAIモデルを指します。">*1</a>を提供するAWSのサービスです。単に基盤モデルを呼び出して出力結果を得るだけにとどまらず、ファインチューニングやナレッジベースの構築によるRAG(検索拡張生成, Retrieval Augmented Generation)<a href="#f-7ebab1b9" id="fn-7ebab1b9" name="fn-7ebab1b9" title="「RAG(検索拡張生成, Retrieval Augmented Generation)」... 検索技術を用いて生成AIの出力結果を改善する技術です。">*2</a>の実現など、基盤モデルに関する様々な操作を統一的なAPIから利用することができます。Bedrockで使用できる基盤モデルには、Amazon自身が開発提供するTitanの他、Anthropicのテキスト生成AIであるClaude、Stability AIの画像生成AIであるStable Diffusionなど、多様な用途をカバーしたモデルが含まれます。<br></p> <p><br> さらに、Bedrockが提供する基盤モデルの中には埋め込みモデル<a href="#f-318069c1" id="fn-318069c1" name="fn-318069c1" title="「埋め込みモデル」... データをベクトルに変換するためのモデルを指します。">*3</a>がラインナップされています。埋め込みモデルは、画像や自然言語といったデータを特定の次元のベクトルに変換するためのモデルです。埋め込みモデルによって生み出されたベクトルは元のデータの特徴を反映したものとなり、「機械学習モデルに入力する特徴量として用いる」「ベクトル同士の類似性を計算することで検索やレコメンデーション用途に使う」といった活用が考えられます。<br> <br> そこで本日は、Bedrockの埋め込みモデルを用いたテキストのベクトル化を実践します。さらに、埋め込みモデルによって生成されたベクトルの活用例として「ベクトル検索」<a href="#f-cd0ec8c9" id="fn-cd0ec8c9" name="fn-cd0ec8c9" title="「ベクトル検索」... ベクトル同士の距離(類似性)を計算することで検索を実現する手法です">*4</a>システムのプロトタイピングを行います。具体的には、Bedrock埋め込みモデルによって生成されたベクトルデータによるベクトル検索の実行結果と従来の全文検索<a href="#f-7e03dfc9" id="fn-7e03dfc9" name="fn-7e03dfc9" title="「全文検索」... テキストデータの中から特定のキーワードを含む文書を検索する手法です。">*5</a>の実行結果を比較することで、埋め込みモデルの性能やベクトル検索の特徴に関する理解を深めることを目的とします。</p> <p>※ 「手っ取り早くベクトル検索と全文検索の比較結果を見たい!」という方は、<a href="#start_comparison">こちら</a>からお読みください!<br></p> <p>※ 当ブログでは、過去にも埋め込みモデルや検索システムに関する記事を公開しています。興味のある方は、下記のリンクからご覧ください。<br></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F11%2F14%2F221416" title="ChatGPTに社内文書に基づいた回答を生成させる仕組みを構築しました - コネヒト開発者ブログ" 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.connehito.com/entry/2023/11/14/221416">tech.connehito.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2022%2F03%2F18%2F092540" title="AWS OpenSearchでの技術検証をスムーズにしたTIPS - コネヒト開発者ブログ" 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.connehito.com/entry/2022/03/18/092540">tech.connehito.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2022%2F09%2F27%2F164009" 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.connehito.com/entry/2022/09/27/164009">tech.connehito.com</a></cite></p> <h2 id="プロトタイピングの動機">プロトタイピングの動機</h2> <p>まずは、今回のプロトタイピングを行うに至った動機についてご説明します。<br> <br> 弊社コネヒトでは、母親向けのQ&amp;Aを主なコンテンツとするママリというコミュニティサービスを運営しています。<br> ママリには毎月十万件以上の質問が新たに投稿されており、これらの大量の質問データの中からユーザーが求めるものを見つけ出すための検索システムは、AWSのOpenSearch Serviceを用いて自社で構築しています。<a href="#f-b8ae667c" id="fn-b8ae667c" name="fn-b8ae667c" title="https://tech.connehito.com/entry/2022/09/16/165655">*6</a>現在の検索システムでは、単語の出現頻度に基づく検索アルゴリズムである全文検索をベースとした手法を採用しているのですが、ここにクエリと文書の意味的な類似性に基づく検索を可能とするベクトル検索技術を取り入れることで、ユーザーの検索体験の改善につなげたいという狙いがあります。<br> <br> またママリでは、質問のレコメンデーション<a href="#f-db99389c" id="fn-db99389c" name="fn-db99389c" title="https://speakerdeck.com/takapy/komiyuniteisabisuniokerurekomendesiyonfalsebian-qian-tomlpaipurainnituite">*7</a>や検閲を行う機械学習モデル<a href="#f-ab63b2ff" id="fn-ab63b2ff" name="fn-ab63b2ff" title="https://tech.connehito.com/entry/2022/03/24/173719">*8</a>を運用しています。埋め込みモデルによって獲得したベクトルをこれらのモデルの特徴量として活用することで、モデルの精度向上が期待できるのではないかという仮説をもっています。<br> <br> このように、ママリにおいて埋め込みモデルの活用範囲は非常に広いと考えています。<br> さらに、弊社ではインフラにAWSを採用しているため、AWS上の他のサービスとの連携を考慮するとBedrockは有力な選択肢となります。<br> <br> 以上のような背景から、まずはBedrockの埋め込みモデルの性能や使い勝手を確認するための検証第一弾として、今回のプロトタイピングを実施することになりました。<br> <br></p> <h2 id="プロトタイピングの概要">プロトタイピングの概要</h2> <p><br> 続いて、今回のプロトタイピングの概要についてご説明します。 <br> 繰り返しになりますが、この記事ではベクトル検索と全文検索を行った際に、両者の検索結果にどのような違いが生じるのかを比較することで、両者の検索手法の強み・弱みを可能な限り明らかにしていきます。<br> この際、ベクトル検索を実行するために必要なベクトルデータは、Bedrockの埋め込みモデルによって生成します。ベクトル検索の実行結果の妥当性を通して、埋め込みモデルの性能に対する理解も同時に深めていきます。<br> <br> 埋め込み及び検索の対象となるデータにはママリの実データを使用します。具体的には、2023年10月に投稿された十万件以上の質問文を対象とします。<br></p> <p>以降は、プロトタイピングの手順を以下3ステップに分け、順番にご説明します。<br> <br></p> <ul> <li>ステップ① Bedrockの埋め込みモデルによるベクトルの生成 <ul> <li>Bedrockの埋め込みモデルを用いて、ママリの質問データ(2023年10月文)をベクトル化します。</li> </ul> </li> <li>ステップ② OpenSearch Serviceによる検索システムの構築 <ul> <li>ママリの質問文及びステップ①で生成したベクトルデータをAWSのOpenSearch Serviceのマネージド型ドメインに格納することで検索システムを構築します。</li> </ul> </li> <li>ステップ③ 「全文検索」と「ベクトル検索」の実行と検索結果の比較 <ul> <li>ステップ②で構築した検索システムを用いて、ママリの質問文に対する「全文検索」と「ベクトル検索」を実行します。そして、両者の検索結果にどのような違いが生じるのかを定性的に比較します。</li> </ul> </li> </ul> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/a/ashenOne/20231207/20231207230000.png" width="1200" height="978" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="ステップ-Bedrockの埋め込みモデルによるベクトルの生成">ステップ① Bedrockの埋め込みモデルによるベクトルの生成</h2> <p><br> 最初のステップとして、Bedrockの埋め込みモデルを用いたテキストのベクトル化を行います。 <br> まずは、埋め込みモデルの選定基準についてご説明します。 2023年12月8日現在、Bedrockにはテキストデータを対象とした埋め込みモデルとして下記3種類が提供されています。</p> <table> <thead> <tr> <th style="text-align:center;"> モデル名 </th> <th style="text-align:center;"> 開発元 </th> <th style="text-align:center;"> 対応言語 </th> <th style="text-align:center;"> コンテキスト長 </th> <th style="text-align:center;"> 埋め込み次元 </th> <th style="text-align:center;"> 東京リージョンにおける利用可否<br>(2023/12/8時点) </th> </tr> </thead> <tbody> <tr> <td style="text-align:center;"> <a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-embedding-models.html">Titan Embeddings G1 - Text</a> </td> <td style="text-align:center;"> Amazon </td> <td style="text-align:center;"> 多言語<br>(日本語含む) </td> <td style="text-align:center;"> 8000 </td> <td style="text-align:center;"> 1536 </td> <td style="text-align:center;"> ○ </td> </tr> <tr> <td style="text-align:center;"> <a href="https://aws.amazon.com/marketplace/pp/prodview-qd64mji3pbnvk?sr=0-4&amp;ref_=beagle&amp;applicationId=AWSMPContessa">Embed English</a> </td> <td style="text-align:center;"> Cohere </td> <td style="text-align:center;"> 英語 </td> <td style="text-align:center;"> 512 </td> <td style="text-align:center;"> 1024 </td> <td style="text-align:center;"> ☓ </td> </tr> <tr> <td style="text-align:center;"> <a href="https://aws.amazon.com/marketplace/pp/prodview-b4mpgdxvpa3v6?sr=0-2&amp;ref_=beagle&amp;applicationId=AWSMPContessa">Embed Multilingual</a> </td> <td style="text-align:center;"> Cohere </td> <td style="text-align:center;"> 多言語<br>(日本語含む) </td> <td style="text-align:center;"> 512 </td> <td style="text-align:center;"> 1024 </td> <td style="text-align:center;"> ☓ </td> </tr> </tbody> </table> <p>※ マルチモーダルモデルである<a href="https://docs.aws.amazon.com/bedrock/latest/userguide/titan-multiemb-models.html">Titan Multimodal Embeddings G1</a>は比較の対象外としています。<br> <br> 3つのモデルのうち、「Embed English」(Cohere)は対応言語が英語のみとなっていることから、日本語データを対象とする今回のプロトタイピングでは選択肢から外れました。<br> <br> 残った2つのモデルに関しては、主にコンテキスト長の差を重視し「Titan Embeddings G1 - Text」(Amazon)を選択しました。コンテキスト長は、埋め込みモデルが入力として考慮することのできる最大のトークン数を指します。「Embed Multilingual」(Cohere)のコンテキスト長は512までとなっていますが、ママリの質問データにはこれを超えるものが存在することから、コンテキスト長が最大8000トークンまでと余裕のある「Titan Embeddings G1 - Text」 (Amazon)が適しているという判断になります。<br></p> <p>次に、BedrockのAPIを呼び出し、テキストをベクトル表現に変換するためのコードをご紹介します。<br> プログラミング言語には、Pythonを使用しています。<br></p> <pre class="code lang-python" data-lang="python" data-unlink> <span class="synPreProc">import</span> json <span class="synPreProc">import</span> boto3 <span class="synComment"># Bedrock runtimeに接続するためのclientを生成</span> bedrock_runtime_client = boto3.client(<span class="synConstant">'bedrock-runtime'</span>, region_name=<span class="synConstant">&quot;ap-northeast-1&quot;</span>) <span class="synStatement">def</span> <span class="synIdentifier">bedrock_embedding</span>(input_str): bedrock_body = { <span class="synConstant">&quot;inputText&quot;</span>: input_str } body_bytes = json.dumps(bedrock_body).encode(<span class="synConstant">'utf-8'</span>) <span class="synComment"># 埋め込みモデルの呼び出し</span> response = bedrock_runtime_client.invoke_model( accept=<span class="synConstant">&quot;*/*&quot;</span>, body=body_bytes, contentType=<span class="synConstant">&quot;application/json&quot;</span>, modelId=<span class="synConstant">&quot;amazon.titan-embed-text-v1&quot;</span>, ) <span class="synStatement">return</span> json.loads(response.get(<span class="synConstant">&quot;body&quot;</span>).read()).get(<span class="synConstant">&quot;embedding&quot;</span>) </pre> <p>コードの要点を取り上げて解説します。<br></p> <p>はじめに、Bedrockのモデルを呼び出す際は、<code>bedrock-runtime</code>に接続したclientが持つ<code>invoke_model()</code>というメソッドを使用します。このメソッドには、モデルの入力とするテキストをキー<code>"inputText"</code>に対応する値として設定したJSON形式のデータを渡す必要があります。<br> 使用するモデルの種類は、引数<code>modelId</code>に与えます。先述の通り、今回のプロトタイプでは「Titan Embeddings G1 - Text」(Amazon)を使用するため、このモデルのIDに該当する<code>"amazon.titan-embed-text-v1"</code>を指定しています。<br> なお、モデル名とIDの正式な対応関係は下記のコードで出力されるモデル一覧から知ることができます。</p> <pre class="code lang-python" data-lang="python" data-unlink>bedrock = boto3.client(service_name=<span class="synConstant">'bedrock'</span>) <span class="synIdentifier">print</span>(bedrock.list_foundation_models()) </pre> <p><code>invoke_model()</code>メソッドのレスポンスには<code>"embedding"</code>という要素が含まれており、これがテキストの埋め込み結果に該当するベクトルデータです。ちなみに、このレスポンスには<code>"embedding"</code>の他に、元のテキストのトークン長が格納された<code>"inputTextTokenCount"</code>が含まれているため、この値を参照することで埋め込みモデルの利用にかかったコストを個別のデータ単位で正確に把握することができます。</p> <h2 id="ステップ-OpenSearch-Serviceによる検索システムの構築">ステップ② OpenSearch Serviceによる検索システムの構築</h2> <p>続いて、AWSのOpenSearch Serviceを用いた検索システムの構築についてご説明します。 このステップでは、特にベクトル検索を実行するためのインデックスの構築方法に焦点を当てています。</p> <p>なお、このステップのプログラムを実行するには、事前にOpenSearch Serviceのドメインを構築しておく必要がありますが、分量が非常に長くなるためこの記事では説明の対象外とします。ドメインの構築方法については、代わりに下記の公式ドキュメントを御覧ください。</p> <ul> <li><a href="https://docs.aws.amazon.com/ja_jp/opensearch-service/latest/developerguide/createupdatedomains.html">Amazon OpenSearch Service ドメインの作成と管理</a></li> </ul> <p>以下は、全文検索とベクトル検索の両者に対応したインデックスを作成するためのPythonコードです。実装にはopensearch-pyというライブラリを用いています。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># 必要なライブラリのインポート</span> <span class="synPreProc">from</span> opensearchpy <span class="synPreProc">import</span> OpenSearch, RequestsHttpConnection <span class="synPreProc">from</span> opensearchpy.helpers <span class="synPreProc">import</span> bulk <span class="synPreProc">from</span> requests_aws4auth <span class="synPreProc">import</span> AWS4Auth region = <span class="synConstant">'ap-northeast-1'</span> service = <span class="synConstant">'es'</span> credentials = boto3.Session().get_credentials() awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token) host = <span class="synConstant">'XXX.ap-northeast-1.es.amazonaws.com'</span> <span class="synComment"># XXXには、事前に構築したドメインのエンドポイントを記入します</span> port = <span class="synConstant">443</span> <span class="synComment"># OpenSearch Serviceに接続するためのクライアントを生成</span> client = OpenSearch( hosts = [{<span class="synConstant">'host'</span>: host, <span class="synConstant">'port'</span>: port}], http_auth = awsauth, use_ssl = <span class="synIdentifier">True</span>, verify_certs = <span class="synIdentifier">True</span>, connection_class = RequestsHttpConnection, timeout=<span class="synConstant">1000</span> ) <span class="synComment"># ドキュメントとベクトルが格納された辞書データを受け取り、インデックスに登録する関数</span> <span class="synStatement">def</span> <span class="synIdentifier">add_documents_to_index</span>(client, index_name, document_dict): actions = [] <span class="synStatement">for</span> i, document_id <span class="synStatement">in</span> <span class="synIdentifier">enumerate</span>(document_dict): actions.append({ <span class="synConstant">&quot;_op_type&quot;</span>: <span class="synConstant">&quot;index&quot;</span>, <span class="synConstant">&quot;_index&quot;</span>: index_name, <span class="synConstant">&quot;_id&quot;</span>: document_id, <span class="synConstant">&quot;_source&quot;</span>: { <span class="synConstant">&quot;document&quot;</span>: document_dict[document_id][<span class="synConstant">&quot;document&quot;</span>], <span class="synComment"># 元のテキストデータを追加</span> <span class="synConstant">&quot;embedding&quot;</span>: document_dict[document_id][<span class="synConstant">&quot;embedding&quot;</span>] <span class="synComment"># ベクトルデータを追加</span> } }) <span class="synStatement">if</span> i % <span class="synConstant">100</span> == <span class="synConstant">0</span>: <span class="synComment"># データをひとまとめにして一括で登録</span> bulk(client, actions) actions = [] <span class="synStatement">else</span>: bulk(client, actions) </pre> <p>上記のコードでは、<code>”document”</code>フィールドにオリジナルのテキストデータ、<code>”embedding”</code>フィールドにテキストの埋め込み表現を追加しており、それぞれのフィールドを全文検索、ベクトル検索の実行時に使用します。<br> <br></p> <p>また、データの登録時には関数<code>bulk</code>を用いて複数件のデータを一度にまとめて登録することで、処理にかかる時間を削減しています。一方で、あまりに多くのデータを一度に転送しようとすると<code>bulk</code>の実行時にエラーが発生することがあるため、ここではデータを100件ごとに区切って処理を実行しています。(一度に登録できるデータ件数の上限は一つのデータあたりの容量などに依存します。)</p> <h2 id="ステップ-全文検索とベクトル検索の実行と検索結果の比較">ステップ③ 「全文検索」と「ベクトル検索」の実行と検索結果の比較</h2> <p>最後に、構築したシステムを用いて検索を実行します。<br> 以下は、全文検索、ベクトル検索を実行するためのPythonコードです。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># OpenSearch Serverlessに構築したVector Storeに対してクエリを実行する関数</span> <span class="synStatement">def</span> <span class="synIdentifier">search</span>(query_str, search_type, size=<span class="synConstant">5</span>): <span class="synStatement">if</span> search_type == <span class="synConstant">&quot;vector&quot;</span>: <span class="synComment"># 検索クエリを埋め込み表現に変換</span> vec, _ = bedrock_embedding(query_str, bedrock_runtime_client) <span class="synComment"># ベクトル検索用のDSLを定義</span> query = { <span class="synConstant">&quot;size&quot;</span>: size, <span class="synConstant">&quot;query&quot;</span>: { <span class="synConstant">&quot;knn&quot;</span>: { <span class="synConstant">&quot;embedding&quot;</span>: { <span class="synConstant">&quot;vector&quot;</span>: vec, <span class="synConstant">&quot;k&quot;</span>: <span class="synConstant">10</span> } } } } <span class="synStatement">else</span>: <span class="synComment"># 全文検索用のDSLを定義</span> query = { <span class="synConstant">&quot;size&quot;</span>: size, <span class="synConstant">&quot;query&quot;</span>: { <span class="synConstant">&quot;match&quot;</span>: { <span class="synConstant">&quot;document&quot;</span>: query_str } } } <span class="synComment"># 検索を実行</span> results = client.search(index=index_name, body=query) <span class="synStatement">return</span> results </pre> <p>関数<code>search</code>では、引数<code>search_type</code>に”vector”を指定した場合はベクトル検索、それ以外の場合は全文検索用のDSL (ドメイン固有言語: Domain Specific Language) を用いるように条件分岐を行っています。<br></p> <p>またベクトル検索を行うには、事前に検索クエリの埋め込み表現を獲得する必要があります。そこで、ステップ①で掲載した関数<code>bedrock_embedding</code>を使ってテキストデータをベクトルに変換する処理を実行しています。<br></p> <p>それでは、いよいよ準備が整ったので、全文検索とベクトル検索の検索結果を比較していきましょう。<br> 今回の比較検証では、以下5つのケースを取り上げてそれぞれの検索結果に対する考察を行います。<br></p> <ul> <li>ケース1. クエリが一つの単語で構成される場合</li> <li>ケース2. クエリが複数の単語で構成される場合</li> <li>ケース3. クエリに固有名詞を含む場合</li> <li>ケース4. クエリに誤字を含む場合</li> <li>ケース5. クエリに文章を使用した場合</li> </ul> <h3 id="ケース1-クエリが一つの単語で構成される場合"><a id="start_comparison">ケース1. クエリが一つの単語で構成される場合</a></h3> <p>検索クエリ: <code>保育園</code></p> <p>まずは、検索クエリが一つの単語で構成される場合の例を見ていきます。以下は、<code>保育園</code>を検索クエリとして用いた場合の検索結果です。</p> <table> <thead> <tr> <th style="text-align:center;">検索ランキング</th> <th style="text-align:left;">全文検索</th> <th style="text-align:left;">ベクトル検索</th> </tr> </thead> <tbody> <tr> <td style="text-align:center;">1</td> <td style="text-align:left;">【愛知県瀬戸市の<code>保育園</code>について】瀬戸市の<code>保育園</code>について教えてください <strong>(...以下略...)</strong></td> <td style="text-align:left;"><code>保育園</code>ってどのくらい前から探すものですか??</td> </tr> <tr> <td style="text-align:center;">2</td> <td style="text-align:left;">親の都合で<code>保育園</code>を8月いっぱいで退園させてしまいました <strong>(...以下略...)</strong></td> <td style="text-align:left;">八幡西区でベビーカー置き場のある<code>保育園</code>はありますか?</td> </tr> <tr> <td style="text-align:center;">3</td> <td style="text-align:left;">千葉県香取市の<code>保育園</code>についてまんまる<code>保育園</code>、香西<code>保育園</code>、たまつくり<code>保育園</code>どんな感じか <strong>(...以下略...)</strong></td> <td style="text-align:left;">滋賀県愛荘町にお住まいの方!<code>保育園</code>の応募なんですが、この辺り出身ではないので <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">4</td> <td style="text-align:left;">【通勤途中の<code>保育園</code>と家の近くの<code>保育園</code>、どちらに預けるべきかについて】モヤモヤしてます <strong>(...以下略...)</strong></td> <td style="text-align:left;">山形市内から天童市内周辺まででおすすめの<code>保育園</code>はありますか?0歳児クラスがあるところ <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">5</td> <td style="text-align:left;"><code>保育園</code>についてです。今生後7ヶ月の娘が<code>保育園</code>に通っているのですが、3回食にならないと保育 <strong>(...以下略...)</strong></td> <td style="text-align:left;">通園て何が必要ですか?<code>保育園</code>です。</td> </tr> </tbody> </table> <p>※ 質問文の全体を掲載すると文章が長くなりすぎてしまう場合があるため、質問文の意味や検索クエリとの関連性が正しく伝わる程度に一部の情報を省略して掲載しています。<br></p> <p>両者ともに上位五件の検索結果には<code>保育園</code>という単語が含まれており、正常に検索が行われていると言えそうです。<code>保育園</code>という単語だけではユーザーがどういう情報を求めているのかを判断することが難しいため、検索結果の良し悪しについては、今回のケースではこれ以上の評価することができません。<br></p> <p>そのうえで両者の検索結果の違いに着目すると、以下のような傾向が見受けられます。</p> <ul> <li>全文検索では、質問内に<code>保育園</code>というワードが複数個含まれている質問が上位に並んでいる</li> <li>ベクトル検索では、<code>保育園</code>というワードを含みつつも、質問の全体の文章量が少ない質問が上位に並んでいる</li> </ul> <p>全文検索では、クエリと文書の一致度を計算する際に、クエリと文書の単語の出現頻度を考慮しているため、クエリと文書の単語の出現頻度が高いほど検索結果の上位に位置づけられる傾向があるためこういった結果が生まれたと推察できます。<br></p> <p>一方、ベクトル検索では、単語の出現頻度ではなくクエリと文書の意味的な類似性に基づき検索スコアが計算されます。今回のケースでは、クエリが<code>保育園</code>という一単語のみで構成されているため、文章が長くなるほど(<code>保育園</code>以外の情報が加わるほど)、<code>保育園</code>からは意味が離れていく作用が働いていると考えられます。<br></p> <h3 id="ケース2-クエリが複数の単語で構成される場合">ケース2. クエリが複数の単語で構成される場合</h3> <p>検索クエリ: <code>保育園 お弁当</code></p> <p>続いて、クエリ内に複数の単語が含まれる場合を比較しましょう。以下は、ケース1の<code>保育園</code>に一単語を追加した<code>保育園 お弁当</code>を検索クエリとして用いた場合の検索結果です。</p> <table> <thead> <tr> <th style="text-align:center;">検索ランキング</th> <th style="text-align:left;">全文検索</th> <th style="text-align:left;">ベクトル検索</th> </tr> </thead> <tbody> <tr> <td style="text-align:center;">1</td> <td style="text-align:left;">年少さんのお弁当の量ってどんなかんじですか?<code>保育園</code>で初めて<code>お弁当</code>あり <strong>(...以下略...)</strong></td> <td style="text-align:left;"><code>保育園</code>の園外保育で給食の方、<code>お弁当</code>はどうされていますか?</td> </tr> <tr> <td style="text-align:center;">2</td> <td style="text-align:left;">19日に<code>保育園</code>の<code>お弁当</code>の日があります。<code>お弁当</code>のおかずについてです。 <strong>(...以下略...)</strong></td> <td style="text-align:left;"><code>幼稚園</code> <code>お弁当</code>オススメおかずありますか?</td> </tr> <tr> <td style="text-align:center;">3</td> <td style="text-align:left;"><code>保育園</code>の行事で<code>お弁当</code>を持たせる時 普段給食なので<code>お弁当</code>は作らないので <strong>(...以下略...)</strong></td> <td style="text-align:left;"><code>保育園</code>の遠足の<code>お弁当</code>。ほんっと下手くそで泣きそうです <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">4</td> <td style="text-align:left;">年長 遠足の<code>お弁当</code> 今度、<code>保育園</code>の遠足で<code>お弁当</code>持っていきます <strong>(...以下略...)</strong></td> <td style="text-align:left;"><code>幼稚園</code>の<code>お弁当</code>は何入れてますか?</td> </tr> <tr> <td style="text-align:center;">5</td> <td style="text-align:left;"><code>保育園</code>に入ってから初めての遠足が明後日あります!初めての<code>お弁当</code>です <strong>(...以下略...)</strong></td> <td style="text-align:left;"><code>保育園</code>や<code>幼稚園</code>から帰宅後、家でも<code>おやつやジュース</code>を食べますか?</td> </tr> </tbody> </table> <p>基本的な検索結果の傾向はケース1と似通っているものの、ベクトル検索の結果に興味深い点があります。それは、検索ランキングの二位と四位に<code>保育園</code>ではなく<code>幼稚園</code>を含む文書が出現したことです。これは、<code>保育園</code>と<code>幼稚園</code>が意味的に類似しているため、ベクトル検索では<code>保育園</code>と<code>幼稚園</code>を同じような意味を持つ単語として扱っているものと考えられます。<br></p> <p>また、検索ランキングの五位に<code>お弁当</code>がなく<code>おやつ</code>や<code>ジュース</code>が含まれる質問があることも同様の理由であると言えそうです。</p> <h3 id="ケース3-クエリに固有名詞を含む場合">ケース3. クエリに固有名詞を含む場合</h3> <p>検索クエリ: <code>多摩総合医療センター</code></p> <p>ここからは、検索クエリが特殊な特徴をもつ場合の比較検証を行います。まずは、検索クエリに固有名詞を含む場合です。固有名詞は一般名詞と比べて文書内の出現頻度が低い傾向にあり、その言葉の意味を埋め込みモデルが学習することは困難です。そうなると、ベクトル検索では固有名詞を含む文書を正しく検索することが難しいのではないかと考えられますが、どういう結果が生まれるか見ていきましょう。<br></p> <p>以下は、検索クエリに<code>多摩総合医療センター</code>という特定の病院名を与えた場合の例です。</p> <table> <thead> <tr> <th style="text-align:center;">検索ランキング</th> <th style="text-align:left;">全文検索</th> <th style="text-align:left;">ベクトル検索</th> </tr> </thead> <tbody> <tr> <td style="text-align:center;">1</td> <td style="text-align:left;">榊原記念病院、<code>多摩総合医療センター</code>、どちらで分娩するか悩んでいます。<strong>(...以下略...)</strong></td> <td style="text-align:left;">広島県西条の八本松にある高橋ホームクリニックご存知の方に質問です。<strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">2</td> <td style="text-align:left;"> - </td> <td style="text-align:left;">吹田徳洲会病院でご出産された方いらっしゃいませんか? <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">3</td> <td style="text-align:left;"> - </td> <td style="text-align:left;">神奈川県海老名市付近の婦人科検診と不妊治療が行える病院を探しています <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">4</td> <td style="text-align:left;"> - </td> <td style="text-align:left;">仙台市太白区付近でセミオープンおすすめの病院を教えてください。<strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">5</td> <td style="text-align:left;"> - </td> <td style="text-align:left;">熊本県合志市でおすすめの婦人科教えてください <strong>(...以下略...)</strong></td> </tr> </tbody> </table> <p>全文検索では、<code>多摩総合医療センター</code>について記述されたデータがヒットしています。(<code>多摩総合医療センター</code>を含む質問は一件しか存在しなかったため、検索結果はこれのみ)<br></p> <p>一方で、ベクトル検索を用いた場合の検索結果には<code>多摩総合医療センター</code>を含む文書は上位五件以内には現れませんでした。検索結果として並んだ質問を詳細に見ていくと、地域名と病院を含むという共通点が見受けられるため、<code>多摩総合医療センター</code>と意味的に類似する質問がヒットしているとは言えそうですが、直接的に<code>多摩総合医療センター</code>という病院名を含むデータを上位に位置づけることはできませんでした。<br> <br> 一つの例を確認しただけでは結論づけるのは尚早ですが、特定の施設や商品等、固有名詞による検索を必要とするケースでは、ベクトル検索より全文検索のほうが求める検索結果を得られやすい傾向にあるのかもしれません。</p> <h3 id="ケース4-クエリに誤字を含む場合">ケース4. クエリに誤字を含む場合</h3> <p>検索クエリ: <code>子供 正確</code></p> <p>続いて、クエリに誤字を含む場合の検索結果を比較します。 ここでは、『<code>子供 性格</code>と打つつもりが、<code>子供 正確</code>と打ち間違えてしまった』ケースを想定して検索結果を比較します。</p> <table> <thead> <tr> <th style="text-align:center;">検索ランキング</th> <th style="text-align:left;">全文検索</th> <th style="text-align:left;">ベクトル検索</th> </tr> </thead> <tbody> <tr> <td style="text-align:center;">1</td> <td style="text-align:left;">すぐに測れる体温計でおすすめありませんか? <br> <strong>(...中略...)</strong> <br> 非接触型体温計もあるのですが、全く<code>正確</code>に測れません?</td> <td style="text-align:left;">小学一年生になる息子がいます。小規模の小学校でクラスに7人しか男の子がいません。息子は<code>おとなしい</code> <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">2</td> <td style="text-align:left;">小学校一年生の子供にGPSを持たせようと思いますが、<code>正確</code>に場所とか分かるGPS <strong>(...以下略...)</strong></td> <td style="text-align:left;">子供って、なんて純粋で<code>優しい</code>んだろう…私は子供を <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">3</td> <td style="text-align:left;">【犬猫のGPSトラッカーについて】犬猫用のGPS 常に<code>正確</code>な位置を携帯アプリで表示 <strong>(...以下略...)</strong></td> <td style="text-align:left;">幼稚園の先生から見て満3歳児クラスの子でこの子はまだお母さんといた方が良いのではと思う子って <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">4</td> <td style="text-align:left;">おでことかにあててピッと一瞬で体温が測れる体温計って<code>正確</code>に測れているんでしょうか? <strong>(...以下略...)</strong></td> <td style="text-align:left;">息子は口が悪くなったり<code>怒りっぽい</code>一方で、<code>優しく穏やか</code>な時もあります。 <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">5</td> <td style="text-align:left;">【おすすめの体温計について】医療関係者の方 なるべく早くて<code>正確</code>なおすすめの体温計 <strong>(...以下略...)</strong></td> <td style="text-align:left;">男の子は<code>優しいよ</code>って言われたけど <strong>(...以下略...)</strong></td> </tr> </tbody> </table> <p>はじめに全文検索の結果を見てみると、<code>正確</code>を含む質問が並んでいることからクエリとした投げた<code>子供 正確</code>にそのまま素直に一致するデータを返していることがわかります。<br> <br> 一方、興味深いことに、ベクトル検索の結果には子どもの<code>性格</code>に関する質問が上位に並んでいます。<br> 更に、これらの質問は<code>性格</code>という直接的に一致するキーワードを含んでいるわけではなく<code>おとなしい</code>や<code>怒りっぽい</code>等、<code>性格</code>と共起性が高いワードを含む文書たちとなっています。ここからは仮説ですが、埋め込みモデルの学習データにも<code>子供 性格</code>を<code>子供 正確</code>と間違えて使用していた文書が多く含まれていたことが、こういった結果を引き起こしたと可能性があります。その場合、<code>子供 正確</code>という言葉と「子供がおとなしい」「子供が怒りっぽい」といった子供の性格を表す表現が意味的に類似性が高いものとして学習されていると考えても不思議ではありません。<br></p> <p>このように、ベクトル検索を用いることでたとえクエリに誤字が含まれていたとしても、クエリ内の他のキーワードとの文脈が考慮されることによって、ユーザーが本来求めている情報を検索結果として返すことができる可能性が示唆されました。</p> <h3 id="ケース5-クエリに文章を使用した場合">ケース5. クエリに文章を使用した場合</h3> <p>検索クエリ: <code>先日、4歳の子供に「サンタさんはお父さんなの?」と聞かれました。本当のことを言うべきでしょうか?</code><br></p> <p>最後に、キーワードではなく文章をクエリとして使用した場合の検索結果を比較します。ベクトル検索は、文章の文脈を含む意味的な類似性を計算することができるという性質から、文章をクエリとして使用した場合にこそ全文検索にはない強みを発揮するのではないかと考えています。<br> また、テキスト生成AIの応用技術として注目を浴びているRAG (検索拡張生成, Retrieval Augmented Generation)では、ユーザーが入力した文章をそのまま検索クエリとして使用することが多いため、このケースはRAGの実践を見据えた検証としても重要であると言えるでしょう。<br> <br></p> <p>以下は、<code>先日、4歳の子供に「サンタさんはお父さんなの?」と聞かれました。本当のことを言うべきでしょうか?</code>という短い文章を検索クエリとして用いた場合の検索結果です。</p> <table> <thead> <tr> <th style="text-align:center;">検索ランキング</th> <th style="text-align:left;">全文検索</th> <th style="text-align:left;">ベクトル検索</th> </tr> </thead> <tbody> <tr> <td style="text-align:center;">1</td> <td style="text-align:left;">小1男子なんですが、まだサンタさん信じてるんですけどその方が珍しいんですかね?友達に<code>サンタはお父さんとお母さんやで</code>って言われたみたいで ほんまなん?って聞かれました...真実を教える年頃ですか? <strong>(...以下略...)</strong></td> <td style="text-align:left;">5歳年長の娘が<code>「サンタってほんとはお父さんとお母さんじゃない?」</code>と言い出しました、、 <br> <strong>(...中略...)</strong> <br> あんまり嘘つくのもよくないかな下の子もいるしあと3〜4年は信じてもらってたいです</td> </tr> <tr> <td style="text-align:center;">2</td> <td style="text-align:left;">あと2ヶ月でサンタさん来ますね!?うちはもう欲しいものが決まってて、サンタさんにこれ貰おうねーとよく話すのですが、<strong>(...以下略...)</strong></td> <td style="text-align:left;">小1男子なんですが、まだサンタさん信じてるんですけどその方が珍しいんですかね?友達に<code>サンタはお父さんとお母さんやで</code>って言われたみたいで ほんまなん?って聞かれました...真実を教える年頃ですか?|5歳年長の娘が<code>「サンタってほんとはお父さんとお母さんじゃない?」</code>と言い出しました、、? <br> <strong>(...中略...)</strong> <br> あんまり嘘つくのもよくないかな 下の子もいるしあと3〜4年は信じてもらってたいです</td> </tr> <tr> <td style="text-align:center;">3</td> <td style="text-align:left;">孫に対して(3歳)言うこと聞かないと機嫌悪くなり <br> <strong>(...中略...)</strong> <br> 旦那のお父さんは。子供って中々ご飯食べてくれない時どうしてますか?</td> <td style="text-align:left;">【クリスマスプレゼントについて】ちょっと時期早めの話題ですが…6歳前後のお子さんをお持ちの方に質問です。 <br> <strong>(...中略...)</strong> <br> 子どもの夢は壊したくないから<code>サンタさんいないんだよっていいたくはないし</code> ...</td> </tr> <tr> <td style="text-align:center;">4</td> <td style="text-align:left;">〈サンタさんについて〉少し早いですが..サンタさんからのプレゼント?を何歳くらいからやりましたか?直接サンタさんから渡される感じではなく子供達が寝た後、枕元に置くプレゼント?です</td> <td style="text-align:left;">【旦那がサンタについて嘘をついたことについて】旦那と息子と3人でたわいもない会話をしながら食卓を囲んでいた時に今年のクリスマスプレゼント何が欲しいーの?サンタさんにお願いしようね!っと私が言うと、旦那が笑いながら息子に対して<code>サンタなんか居ない</code>からと言いました。 <strong>(...以下略...)</strong></td> </tr> <tr> <td style="text-align:center;">5</td> <td style="text-align:left;">5歳年長の娘が<code>「サンタってほんとはお父さんとお母さんじゃない?」</code>と言い出しました、、 <br> <strong>(...中略...)</strong> <br> あんまり嘘?つくのもよくないかな 下の子もいるしあと3〜4年は信じてもらってたいです <strong>(...以下略...)</strong></td> <td style="text-align:left;">子供がもう、サンタさんの話してる笑 サンタさんくる?!って何回も聞いてくる <strong>(...以下略...)</strong></td> </tr> </tbody> </table> <p>検索クエリと同じく「サンタさんは実在しないことを子供に正直に伝えるべきか」をテーマとした質問に該当するものとしては、全文検索は1位と5位の2件、ベクトル検索は1位〜4位の4件がランクインしています。この後、6位以降の結果も見渡したところ「サンタさんは実在しないことを子供に正直に伝えるべきか」をテーマとした質問は、ベクトル検索の実行結果により多いという印象を受けました。<br> <br></p> <p>やはり、クエリが長くなるほど (クエリに込められる意味の情報量が増すほど) ベクトル検索はその強みを発揮する傾向があると言えそうです。<br></p> <h3 id="ベクトル検索と全文検索-比較結果のまとめ">「ベクトル検索」と「全文検索」 比較結果のまとめ</h3> <p>ここまでの検証で得られた結果を改めて整理します。<br></p> <ul> <li>キーワードベースのクエリを用いて検索を実行した場合、全文検索ではそのクエリを文書内に多く含むものが上位に並ぶ傾向があり、ベクトル検索では文書の長さが短いものが上位に並ぶ傾向が見受けられた。</li> <li>誤字を含むクエリを用いて検索を実行した場合、ベクトル検索は誤字ではなく本来検索者が入力しようとしていた情報に関連性の高い文書を検索結果として返すことができる可能性が示唆された。</li> <li>固有名詞を含むクエリを用いて検索を実行した場合、ベクトル検索ではその固有名詞を含む文書を上位に位置づけることが困難な場合があることが示唆された。</li> <li>文章をクエリとして用いて検索を実行した場合、ベクトル検索では文章の文脈を考慮することで全文検索以上にユーザーが求める検索結果を返すことができるという可能性が示唆された。</li> </ul> <p>今回の比較検証ではケースごとに1つのクエリの結果しかご紹介していないため、ベクトル検索と全文検索の検索結果の傾向を一概に判断することはできません。そのうえで、クエリに誤字を含む場合やクエリが文章である場合など、ベクトル検索が全文検索より優れた結果を返す場面が存在することが示唆されたと言えるでしょう。<br></p> <h2 id="おわりに">おわりに</h2> <p>以上、この記事ではAmazon Bedrockの埋め込みモデルを用いたベクトル検索システムのプロトタイピング、及びベクトル検索と全文検索の検索結果の比較検証を行ってきました。<br></p> <p>近年は「ハイブリッド検索」という検索手法に注目が集まっており、全文検索とベクトル検索の検索スコアをRRF<a href="#f-2dd6dffd" id="fn-2dd6dffd" name="fn-2dd6dffd" title="「RRF (Reciprocal Rank Fusion)」 ... 複数の検索スコアを統合するアルゴリズムの一つです。">*9</a>などのアルゴリズムによって統合することで、両者のメリットを活かした検索結果を得ることのできる可能性があります。そこで、今後は「ハイブリッド検索」システムのプロトタイピングにも取り組んでいく所存です。<br></p> <p>また、今回のプロトタイピング結果から、Bedrockの埋め込みモデルによって獲得されたテキストのベクトル表現の有用性が確認できたため、今後は検索以外の用途にも活用を広げていきたいと考えています。コネヒト開発者ブログでは、今後も積極的に技術検証の結果を発信していきますので、ぜひ引き続きご注目ください!<br> <br> それでは、今回の記事が皆さんのお役に立てれば幸いです。お読みいただきありがとうございました!<br></p> <h2 id="コネヒトCTO永井からのコメント">コネヒトCTO永井からのコメント</h2> <p>生成AI分野の進歩は、目覚ましいものがあると日々感じています。コネヒトでは、これまでAWSのサービスを基盤として、事業を着実に成長させてきました。AWSの堅牢で柔軟なインフラは、私たちのプロダクト開発において不可欠なものになっています。<br> <br> そして、今回のBedrockを用いた検証結果には、この技術を用いることがユーザーの検索体験の改善につながるという手応えがありました。さらに将来的には、今回検証したベクトル検索技術とテキスト生成AIを組み合わせて使うことで、これまでにない新たなユーザー体験を提供することができる可能性も感じています。今後もコネヒトではスピード感をもってプロダクトに実装し実験を繰り返しながら、より良いユーザー体験を創造していきたいと考えています。</p> <div class="footnote"> <p class="footnote"><a href="#fn-33f4f9a7" id="f-33f4f9a7" name="f-33f4f9a7" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">「基盤モデル」... 広範な用途に使用することのできる大規模なAIモデルを指します。</span></p> <p class="footnote"><a href="#fn-7ebab1b9" id="f-7ebab1b9" name="f-7ebab1b9" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text">「RAG(検索拡張生成, Retrieval Augmented Generation)」... 検索技術を用いて生成AIの出力結果を改善する技術です。</span></p> <p class="footnote"><a href="#fn-318069c1" id="f-318069c1" name="f-318069c1" class="footnote-number">*3</a><span class="footnote-delimiter">:</span><span class="footnote-text">「埋め込みモデル」... データをベクトルに変換するためのモデルを指します。</span></p> <p class="footnote"><a href="#fn-cd0ec8c9" id="f-cd0ec8c9" name="f-cd0ec8c9" class="footnote-number">*4</a><span class="footnote-delimiter">:</span><span class="footnote-text">「ベクトル検索」... ベクトル同士の距離(類似性)を計算することで検索を実現する手法です</span></p> <p class="footnote"><a href="#fn-7e03dfc9" id="f-7e03dfc9" name="f-7e03dfc9" class="footnote-number">*5</a><span class="footnote-delimiter">:</span><span class="footnote-text">「全文検索」... テキストデータの中から特定のキーワードを含む文書を検索する手法です。</span></p> <p class="footnote"><a href="#fn-b8ae667c" id="f-b8ae667c" name="f-b8ae667c" class="footnote-number">*6</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://tech.connehito.com/entry/2022/09/16/165655">https://tech.connehito.com/entry/2022/09/16/165655</a></span></p> <p class="footnote"><a href="#fn-db99389c" id="f-db99389c" name="f-db99389c" class="footnote-number">*7</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://speakerdeck.com/takapy/komiyuniteisabisuniokerurekomendesiyonfalsebian-qian-tomlpaipurainnituite">https://speakerdeck.com/takapy/komiyuniteisabisuniokerurekomendesiyonfalsebian-qian-tomlpaipurainnituite</a></span></p> <p class="footnote"><a href="#fn-ab63b2ff" id="f-ab63b2ff" name="f-ab63b2ff" class="footnote-number">*8</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://tech.connehito.com/entry/2022/03/24/173719">https://tech.connehito.com/entry/2022/03/24/173719</a></span></p> <p class="footnote"><a href="#fn-2dd6dffd" id="f-2dd6dffd" name="f-2dd6dffd" class="footnote-number">*9</a><span class="footnote-delimiter">:</span><span class="footnote-text">「RRF (Reciprocal Rank Fusion)」 ... 複数の検索スコアを統合するアルゴリズムの一つです。</span></p> </div> ashenOne ドメイン駆動設計入門の輪読会をやってました! hatenablog://entry/6801883189064110935 2023-12-06T17:58:46+09:00 2023-12-07T09:43:52+09:00 「コネヒト Advent Calendar 2023」の7日目のブログです! コネヒト Advent Calendar 2023って? コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。 コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、 ママの一歩を支えるアプリ「ママリ」などを 運営しています。 adventar.org こんにちは。サーバーサイドエンジニアをしている高橋です。 以前からDDDに興味がありましたが、なんとなくしか理解しておらずそんな時に 「ドメイン駆動設計入門」の本をお勧めされました。 DDDの考え方は正解がないものなの… <p>「コネヒト Advent Calendar 2023」の7日目のブログです!</p> <pre class="code" data-lang="" data-unlink>コネヒト Advent Calendar 2023って? コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。 コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、 ママの一歩を支えるアプリ「ママリ」などを 運営しています。</pre> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F8994" title="コネヒト Advent Calendar 2023 - Adventar" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://adventar.org/calendars/8994">adventar.org</a></cite></p> <hr /> <p>こんにちは。サーバーサイドエンジニアをしている高橋です。</p> <p>以前からDDDに興味がありましたが、なんとなくしか理解しておらずそんな時に 「<a href="https://amzn.asia/d/c73h3eU">ドメイン駆動設計入門</a>」の本をお勧めされました。 DDDの考え方は正解がないものなので、メンバーとワイワイしながら読んでみたいと思いこの輪読会を開催しました。</p> <p>社内でメンバー募集をした時、サーバーサイドエンジニアに限らずインフラ、Androidエンジニア、機械学習エンジニアなど様々な領域の方が手を挙げてくれました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/N/NahomiTakahashi/20231204/20231204173619.png" width="1052" height="314" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>既に知見がある方もいれば、DDDに興味があるけどわからないという方もいました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/N/NahomiTakahashi/20231206/20231206175149.png" width="998" height="124" 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/N/NahomiTakahashi/20231206/20231206175227.png" width="1200" height="205" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <hr /> <h2 id="輪読会の流れ">輪読会の流れ</h2> <p>全15章あり、輪読会を開催したのは11月上旬からで、できれば年内に終わらせたいと思い、2章ずつの合計7回で終わるように設計しました。</p> <p>2章ずつ読んできてもらい、以下の内容を事前にmiroの付箋に記入してもらいます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/N/NahomiTakahashi/20231206/20231206175437.png" width="1154" height="422" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>輪読会では以下のように進めました。</p> <ol> <li>順番に付箋を読み上げていく</li> <li>深掘りしたい付箋に一人2票投票する</li> <li>票が多かった付箋について深掘りして理解を高める</li> </ol> <p>1時間の輪読会なので、なるべく議論できる時間を作りたいと思い、付箋は事前に書いてきてもらうようにしました。 深掘りタイムでは「自分も同じようにどういうことなんだろうと思った」という意見が多くあったので、DDDに知見のあるメンバーに質問して、疑問点を解消していきました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/N/NahomiTakahashi/20231204/20231204215140.png" width="1200" height="273" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>具体的にどのような深掘りをしていたか一部紹介します。</p> <p>本の例にあったものは、名前の登録時のバリデーションについて以下のように記載されていました。</p> <p>名前にはn文字以上の入力が必須 ⇒ エンティティで実装する</p> <p>同じ名前は登録できない ⇒ ドメインサービスで実装する</p> <p><strong>ほみ</strong>「名前が何文字以上かはエンティティに記載するのに、重複しているかどうかはエンティティに持たせると不自然になるのはなぜか説明できない」</p> <p><strong>高谷さん</strong> 「本だと「コードで書くと不自然になるから」という説明だったが、あんまりしっくりこなかった」</p> <p><strong>柳村さん</strong> 「インスタンス単体で解決可能かどうかじゃないですかね。たとえば高谷さんに高谷さんって他に存在していますかって聞いても分からないですよね。国の台帳かなんか調べないと分からない。」</p> <p><strong>aboさん</strong> 「値オブジェクトの値はチェックできるが、エンティティの存在チェックは自分でできないという理解をした。」</p> <p>この会話だと柳村さんの説明でメンバーがなるほどなるほどと納得している様子でした。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/N/NahomiTakahashi/20231206/20231206175400.png" width="882" height="326" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>DDDは誰かが答えを持っている訳ではないので、メンバー同士の対話を通してより理解を深められたと思いました。</p> <h2 id="メンバーの声">メンバーの声</h2> <p>現状半分まで輪読会を終えて、メンバーに以下のようなアンケートを取ってみました。</p> <p><strong>この輪読会にどのようなことを期待して参加しましたか。</strong></p> <ul> <li>ドメイン駆動設計完全に理解した状態になる。</li> <li>開発をする上でそこまで意識することは正直ないが、概念として押さえておきたいと思ったから。</li> <li>ドメイン駆動設計の大まかな理解と、可能であれば実務に取り入れていきたい。</li> <li>DDDについての理解を深める。</li> <li>モデリングとの実装のつながりを理解する。</li> <li>他の方と設計に関する知見交流ができると思ったため。</li> </ul> <p><strong>前半戦を終え、現時点で感じていること(難易度等)教えてください。</strong></p> <ul> <li>実装パターンの話は理解が進んでいる一方で、肝心のドメイン周りは大丈夫?という感じ。でもこの本のテーマ的に、実装パターンから理解してドメイン駆動設計の本丸に進むのを怖くなくするみたいな感じっぽいので、それでいうといい感じ。</li> <li>業務上直接ドメインを意識することは少ないので、十分に理解した状態には至っていないが、参加メンバーに具体例を出してもらうことで納得できる部分はあり、ほんの少しずつだが理解が進んできている気がする。</li> <li>設計面はDDDの1側面ではあるので、モデリングとセットで学びたい。</li> <li>現行のコードに落とし込んでいくには慎重になったほうがよさそう。</li> <li>ドメイン駆動設計を実現するための実装パターンが紹介されてはいるもののOOPのデザインパターンのお話しなど他の知識を知らないとよくわからないみたいな部分も少しあるので知見ない方は理解しずらしかもなと思ってました。</li> </ul> <p><strong>後半の輪読会でもっとこうした方がいいとかあれば教えてください。</strong></p> <ul> <li>議事録をいい感じにみんなで取りたいわね。</li> <li>メンバーで時間いっぱい議論に使えているので、良い会が進められている気がする。</li> <li>本から学びつつ、ママリの事業で実践するにはという会話を増やせるとよさそう。</li> <li>そのためには一定の図解が必要で、miroで図解しながら話してもおもろいかもですね。</li> <li>輪読会自体の時間でも良いですし輪読会が終わった後でも良いですが改めて全体で深掘りして聞いてみたいことなどあればお話しできる時間や話せる場所があれば良いのかなと思った。同期的でなくともnotionに非同期で質問書くとでも全然あり。</li> </ul> <h2 id="感想">感想</h2> <p>DDDはバックエンド領域の考え方と思っておりましたが、あくまでそれは実装方法の話であり、ドメインを意識して作っていくという意味ではどの領域のエンジニアにも必要なことだと改めて感じました。</p> <p>比較的初心者でも読みやすいとは思いつつも、DDDの考え方に慣れていないと結構つまずく部分はありました。このような輪読会を通して、他の方の意見でより理解が深まったと思います。後半戦は年内に終わる予定ですので、引き続きやっていきたいと思っております。</p> NahomiTakahashi FlutterについてDroidKaigi2023に登壇してました hatenablog://entry/6801883189063117788 2023-12-02T11:41:33+09:00 2023-12-02T11:41:33+09:00 この記事はコネヒトアドベントカレンダー 2日目の記事です。 コネヒト Advent Calendar 2023って? コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。 コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、 ママの一歩を支えるアプリ「ママリ」などを 運営しています。 adventar.org 初めまして、コネヒトでAndroidエンジニアとして開発しております中島(id:nacatl)です。 開発経歴で言うと、Android→Flutter→Androidという出戻りエンジニアです。 7月にジョインしてから右往左往してたら、… <p>この記事はコネヒトアドベントカレンダー 2日目の記事です。</p> <pre class="code" data-lang="" data-unlink>コネヒト Advent Calendar 2023って? コネヒトのエンジニアやデザイナーやPdMがお送りするアドベント カレンダーです。 コネヒトは「家族像」というテーマを取りまく様々な課題の解決を 目指す会社で、 ママの一歩を支えるアプリ「ママリ」などを 運営しています。</pre> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fadventar.org%2Fcalendars%2F8994" title="コネヒト Advent Calendar 2023 - Adventar" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://adventar.org/calendars/8994">adventar.org</a></cite></p> <hr /> <p>初めまして、コネヒトでAndroidエンジニアとして開発しております中島(<a href="http://blog.hatena.ne.jp/nacatl/">id:nacatl</a>)です。 開発経歴で言うと、Android→Flutter→Androidという出戻りエンジニアです。 7月にジョインしてから右往左往してたら、もう世間では師走になっており困惑しております。</p> <p>今回のブログでは、去る9月14日〜16日にかけて開催されました DroidKaigi2023において「<strong><strong>Flutterにおけるアプリ内課金実装 -Android/iOS 完全なる統一-</strong></strong>」と題しまして登壇したことについて、遅ればせながら少し補足などお話させていただきます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2023.droidkaigi.jp%2F" title="DroidKaigi 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://2023.droidkaigi.jp/">2023.droidkaigi.jp</a></cite></p> <p>当日の様子などはこちらのブログをご覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F09%2F14%2F193300" title="DroidKaigi 2023 Day1 参加レポート - コネヒト開発者ブログ" 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.connehito.com/entry/2023/09/14/193300">tech.connehito.com</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F09%2F15%2F193138" title="DroidKaigi 2023 Day2 参加レポート - コネヒト開発者ブログ" 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.connehito.com/entry/2023/09/15/193138">tech.connehito.com</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F09%2F19%2F153452" title="DroidKaigi 2023 Day3 参加レポート - コネヒト開発者ブログ" 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.connehito.com/entry/2023/09/19/153452">tech.connehito.com</a></cite></p> <ul class="table-of-contents"> <li><a href="#Flutterにおけるアプリ内課金実装--AndroidiOS-完全なる統一-">Flutterにおけるアプリ内課金実装 -Android/iOS 完全なる統一-</a><ul> <li><a href="#なぜFlutterの登壇だったのか">なぜFlutterの登壇だったのか</a></li> <li><a href="#セッションの補足について">セッションの補足について</a></li> </ul> </li> <li><a href="#FlutterからJetpackComposeへ">FlutterからJetpackComposeへ</a><ul> <li><a href="#宣言的UI">宣言的UI</a></li> <li><a href="#MaterialDesign">MaterialDesign</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> </ul> <h2 id="Flutterにおけるアプリ内課金実装--AndroidiOS-完全なる統一-"><strong><strong>Flutterにおけるアプリ内課金実装 -Android/iOS 完全なる統一-</strong></strong></h2> <p>nacatl名義にて、Day 2の<strong>ArcticFox</strong>にて 12:00~12:40 の40分間で登壇いたしました。</p> <p>動画の方もDroidKaigiのYoutubeチャンネルにて、先日無事公開していただきました。 開催の準備から動画の公開まで色々実行していただきましたこと、運営の方々にはこの場を借りて感謝申し上げます。</p> <p><iframe id="talk_frame_1077087" class="speakerdeck-iframe" src="//speakerdeck.com/player/8b2d15b7276f471a88f40ae34d32b706" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/nacatl/ioswan-quan-narutong">speakerdeck.com</a></cite></p> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/iyf00Vci2oE?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="DroidKaigi 2023 - [JA] Flutterにおけるアプリ内課金実装 -Android/iOS 完全なる統一- | nacatl"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=iyf00Vci2oE">www.youtube.com</a></cite></p> <p>AndroidネイティブではなくFlutterの話、かつまさかの<a href="https://2023.droidkaigi.jp/timetable/494965/">荒木佑一さんのセッション</a>と同じ時間帯ということで伽藍とするだろうとも予想していたのですが、多くの方々に聞いていただきとても嬉しく思いました。</p> <h3 id="なぜFlutterの登壇だったのか">なぜFlutterの登壇だったのか</h3> <p>今回の登壇内容は、表題の通りFlutterというマルチプラットフォームのフレームワークについての発表でした。 ただ先に言っておきますが、コネヒトでは現状Flutterは利用しておりません。</p> <p>このことは発表内でも述べていますが、冒頭にも書いた通り中島がコネヒトにジョインしたのは7月であって、実はDroidKaigiへセッションを投稿した時点ではまだ以前の職場である<a href="https://info.studyplus.co.jp/">スタディプラス株式会社</a>に所属していました。 そのため、セッション内容もStudyplusの開発における内容で投稿したことが理由です。 発表内容に関しても退職後にも快く協力していただき、改めましてこの場にて感謝申し上げます。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.studyplus.co.jp%2Fentry%2F2023%2F09%2F25%2F100114" title="DroidKaigi 2023に参加しました - Studyplus Engineering Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://tech.studyplus.co.jp/entry/2023/09/25/100114">tech.studyplus.co.jp</a></cite> Studyplusからもセッションについて紹介していただいております</p> <h3 id="セッションの補足について">セッションの補足について</h3> <p>セッションの本筋に関しましては、Flutterの課金実装に関して自分の知見を余さず発表できたと自負しています。 ただ、最後にまとめとして話したことについて、一言この場で補足いたします。</p> <blockquote><p>アプリ内課金も含めてFlutterによる完全なる統一は目指せる</p></blockquote> <p>資料にも小さく書いてありますが、「目指すべきか」どうかは各プロダクトの事情によると認識しています。 これは「目指すことが可能である」ことが重要だと思っており、Flutterによってモバイルアプリプロダクトにおける技術選択の幅が確実に広がっていることが肝要です。 Studyplusもこの恩恵に授かったプロダクトのひとつです。</p> <p>FlutterはモバイルアプリだけでなくWebアプリの開発にも利用できるフレームワークの一つとして、今後も発展していくだろうと期待しています。</p> <h2 id="FlutterからJetpackComposeへ">FlutterからJetpackComposeへ</h2> <p>ここまで読んでいただいた方にはおそらく、「コネヒトで使ってないんじゃ、転職して知見リセットして仕事してるの?」と思われた方もいらっしゃるかと思います。</p> <p>これに関しては半分その通りで半分違うという答えになります。</p> <h3 id="宣言的UI">宣言的UI</h3> <p>確かに現状のコネヒトではFlutterそのものは採用していませんが、宣言的UIを用いて開発した経験は活きていると認識しています。 昨今、モバイルアプリの開発では宣言的UIの採用が進んでおり、Android開発では <a href="https://io.google/2019/">Google I/O 2019</a> にてJetpack Compose、iOSでも <a href="https://developer.apple.com/videos/wwdc2019/">WWDC 2019</a> でSwiftUIが発表されて、それぞれ既に数年が経っています。</p> <p>コネヒトの開発するモバイルアプリ「ママリ」においても、それぞれの導入が進んでおります。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.android.com%2Fjetpack%2Fcompose%3Fhl%3Dja" title="UI アプリ開発ツールキット Jetpack Compose - Android デベロッパー  |  Android Developers" 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://developer.android.com/jetpack/compose?hl=ja">developer.android.com</a></cite></p> <p><iframe width="560" height="315" src="https://www.youtube.com/embed/JcZqtBy9MT8?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen title="Google I/O&#39;19 - Chet Haase Interview on Jetpack Compose"></iframe><cite class="hatena-citation"><a href="https://www.youtube.com/watch?v=JcZqtBy9MT8">www.youtube.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.apple.com%2Fdocumentation%2FSwiftUI" title="SwiftUI | Apple Developer 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://developer.apple.com/documentation/SwiftUI">developer.apple.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.apple.com%2Fvideos%2Fplay%2Fwwdc2019%2F204%2F" title="Introducing SwiftUI: Building Your First App - WWDC19 - Videos - Apple Developer" 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://developer.apple.com/videos/play/wwdc2019/204/">developer.apple.com</a></cite></p> <h3 id="MaterialDesign">MaterialDesign</h3> <p>また、FlutterはGoogleの後発UIツールキットという立場からか、MaterialDesign、特にMaterialDesign3(以下M3)の導入もAndroidネイティブと同じかそれ以上に進んでいる印象を持っています。 2023/11/16にリリースされたFlutter 3.16では、M3がデフォルト設定になっています。</p> <blockquote><p>Throughout the year we’ve worked on completing support for Material 3, the latest version of the Material Design design system. Flutter’s Material widgets now fully support Material 3 and, in Flutter 3.16, Material 3 is now the default style.</p></blockquote> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmedium.com%2Fflutter%2Fmajor-steps-this-year-on-the-journey-to-multiplatform-development-b9218b17f0f7" title="Major steps this year on the journey to multiplatform development" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://medium.com/flutter/major-steps-this-year-on-the-journey-to-multiplatform-development-b9218b17f0f7">medium.com</a></cite></p> <p>M3の知見はAndroidでもそのまま適用できるので、コネヒトでもデザイナーの方々と色々知見を共有し合いながら開発を進められていると感じております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fm3.material.io%2F" title="Material Design" 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://m3.material.io/">m3.material.io</a></cite></p> <h2 id="おわりに">おわりに</h2> <p>今回は、DroidKaigi2023にてFlutterについて登壇したこと、そこから派生して、転職を経てFlutterの知見をどうAndroidネイティブに活かしているかについて軽く紹介させていただきました。</p> <p>これからも、Flutterで培った宣言的UIやMaterialDesignの知見を基に、コネヒトのAndroidエンジニアとしてママリをより良いアプリにしていきたい所存です。</p> nacatl 明示的メモリ管理が引き起こす問題とガベージコレクションの解法 hatenablog://entry/6801883189060496883 2023-11-21T15:44:09+09:00 2023-11-21T18:03:40+09:00 こんにちは!バックエンドエンジニアのjunyaUです。 社会人になって2回目の冬が来ましたが、どんなに寒くても暖房だけはつけない派です⛄️ 今回は明示的メモリ管理が引き起こす問題と、ガベージコレクションがどうやってこの問題に対処するのかについて書いていこうと思います〜! はじめに 動機 結論 そもそもメモリ管理って? プロセスのメモリ領域 テキストセグメント データセグメント スタックセグメント メモリ管理とは ヒープ領域の役割 メモリの枯渇問題 明示的メモリ管理とは 概要 明示的解放が引き起こす問題 メモリリーク ダングリングポインタ 二重解放 自動的メモリ管理 概要 ガベージコレクション … <p>こんにちは!バックエンドエンジニアのjunyaUです。</p> <p>社会人になって2回目の冬が来ましたが、どんなに寒くても暖房だけはつけない派です⛄️</p> <p>今回は明示的メモリ管理が引き起こす問題と、ガベージコレクションがどうやってこの問題に対処するのかについて書いていこうと思います〜!</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a><ul> <li><a href="#動機">動機</a></li> <li><a href="#結論">結論</a></li> </ul> </li> <li><a href="#そもそもメモリ管理って">そもそもメモリ管理って?</a><ul> <li><a href="#プロセスのメモリ領域">プロセスのメモリ領域</a><ul> <li><a href="#テキストセグメント">テキストセグメント</a></li> <li><a href="#データセグメント">データセグメント</a></li> <li><a href="#スタックセグメント">スタックセグメント</a></li> </ul> </li> <li><a href="#メモリ管理とは">メモリ管理とは</a><ul> <li><a href="#ヒープ領域の役割">ヒープ領域の役割</a></li> <li><a href="#メモリの枯渇問題">メモリの枯渇問題</a></li> </ul> </li> </ul> </li> <li><a href="#明示的メモリ管理とは">明示的メモリ管理とは</a><ul> <li><a href="#概要">概要</a></li> <li><a href="#明示的解放が引き起こす問題">明示的解放が引き起こす問題</a><ul> <li><a href="#メモリリーク">メモリリーク</a></li> <li><a href="#ダングリングポインタ">ダングリングポインタ</a></li> <li><a href="#二重解放">二重解放</a></li> </ul> </li> </ul> </li> <li><a href="#自動的メモリ管理">自動的メモリ管理</a><ul> <li><a href="#概要-1">概要</a></li> <li><a href="#ガベージコレクション">ガベージコレクション</a><ul> <li><a href="#ゴミの判別メカニズム">ゴミの判別メカニズム</a></li> </ul> </li> <li><a href="#ガベージコレクションの種類">ガベージコレクションの種類</a><ul> <li><a href="#マークフェーズ">マークフェーズ</a></li> <li><a href="#スイープフェーズ">スイープフェーズ</a></li> </ul> </li> </ul> </li> <li><a href="#ガベージコレクションの解法">ガベージコレクションの解法</a><ul> <li><a href="#明示的解放の問題が発生する要因">明示的解放の問題が発生する要因</a><ul> <li><a href="#オブジェクトの活性を判断する視点">オブジェクトの活性を判断する視点</a></li> <li><a href="#オブジェクトの解放を判断する視点">オブジェクトの解放を判断する視点</a></li> </ul> </li> <li><a href="#なぜGCではこれらの問題が起きないのか">なぜGCではこれらの問題が起きないのか</a></li> <li><a href="#ソフトウェア工学的観点の影響">ソフトウェア工学的観点の影響</a></li> <li><a href="#GCの制約">GCの制約</a><ul> <li><a href="#パフォーマンスオーバーヘッド">パフォーマンスオーバーヘッド</a></li> <li><a href="#メモリ使用量の増加">メモリ使用量の増加</a></li> <li><a href="#実行タイミングが予想できない">実行タイミングが予想できない</a></li> <li><a href="#生きているオブジェクトが使われるとは限らない">生きているオブジェクトが使われるとは限らない</a></li> </ul> </li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#参考資料">参考資料</a><ul> <li><a href="#書籍">書籍</a></li> <li><a href="#サイト">サイト</a></li> </ul> </li> </ul> <h1 id="はじめに">はじめに</h1> <h2 id="動機">動機</h2> <p>元々、明示的メモリ管理を強いられるC言語を触っていた経験が少しありました。</p> <p>メモリリークなどの、明示的メモリ管理による問題にちょくちょく遭遇していたので、 なぜ起こるのかはなんとなく把握しているつもりでしたが、明確に言語化することはできませんでした。</p> <p>しかし、最近読んだ「ガベージコレクション: 自動的メモリ管理を構成する理論と実装」という本の中で明示的メモリ管理に対する言及がされている部分がありました。 この本を読んだ上でこの問題について考えてみると言語化できそうだなと思ったので、得た知識と自分の見解をまとめてみようと思いこの記事を書くことにしました。</p> <h2 id="結論">結論</h2> <p>明示的メモリ管理が引き起こす、メモリリーク、二重解放、ダングリングポインタなどの問題は、メモリのライフサイクルに関する視点の違いから生じます。 オブジェクトが生きているかどうかの判断は、グローバルな視点で判断される必要があるのに対して、メモリの解放はローカルスコープで判断することを強いられます。 この視点のギャップが問題を引き起こします。</p> <p>一方で、ガベージコレクションは明示的メモリ管理とは異なり、グローバルな視点からオブジェクトの生存状態を判断して自動的に解放を行うため、これらの問題を解決します。</p> <p>次節から、両者の特徴や問題点に触れながら、ガベージコレクションがどのように問題を解決するのかを考えていこうと思います。</p> <h1 id="そもそもメモリ管理って">そもそもメモリ管理って?</h1> <h2 id="プロセスのメモリ領域">プロセスのメモリ領域</h2> <p><figure class="figure-image figure-image-fotolife" title="プロセスのメモリ領域の簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231121/20231121114939.png" width="1200" height="1162" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>プロセスのメモリ領域の簡略図</figcaption></figure> メモリ管理を理解するには、まずプロセスのメモリ領域についての基本を知っておく必要があります。</p> <p>プロセスのメモリ領域は、大きく3つのセグメントに分かれます。</p> <h3 id="テキストセグメント">テキストセグメント</h3> <p>テキストセグメントは、プログラムの機械語命令が格納される領域です。</p> <p>この領域は読み取り専用で、プログラムの実行コードが含まれています。</p> <h3 id="データセグメント">データセグメント</h3> <p>データセグメントは、プログラムのデータを格納するための領域で、主に三つの領域に分けられます。</p> <ul> <li><strong>データ領域</strong> : 初期化されたグローバル変数や静的変数が格納されます。</li> <li><strong>BSS領域</strong> : 初期化されていないグローバル変数や静的変数が格納されます。</li> <li><strong>ヒープ領域</strong> : プログラムの実行時に、動的に確保されるメモリが配置されます。サイズは実行時に動的に変化します。</li> </ul> <h3 id="スタックセグメント">スタックセグメント</h3> <p>スタックセグメントは、関数のローカル変数や引数などが格納される領域です。</p> <p>この領域は、LIFOの原則に基づいてデータの格納と開放が行われます。</p> <h2 id="メモリ管理とは">メモリ管理とは</h2> <p>メモリ管理は、プログラムにおけるヒープ領域のメモリ割り当てと解放を適切に行うプロセスを指します。このプロセスは、メモリリソースを効率的に使用し、プログラムのパフォーマンスと安定性を維持するために不可欠です。</p> <h3 id="ヒープ領域の役割">ヒープ領域の役割</h3> <p>ヒープ領域は、プログラム実行時に動的にメモリを確保するために使用されるメモリ領域です。</p> <p>例えば、事前に要素数のわからない配列やクラスのインスタンスは静的にサイズを決定することができないため、ヒープ領域にメモリが割り当てられます。</p> <p>一方、静的にサイズが決まる場合は、ヒープ以外の領域に割り当てられます。</p> <h3 id="メモリの枯渇問題">メモリの枯渇問題</h3> <p>ヒープ領域のメモリは、スタック領域とは異なり、自動的に解放されません。</p> <p>メモリは有限なので、不要になったヒープ領域のメモリを解放せずに割り当てを行なっていると、メモリの枯渇が発生して、新たなメモリ割り当てが不可能になる可能性があります。</p> <p>これはパフォーマンスの低下や、クラッシュを引き起こす原因になります。</p> <p>これを防ぐためには、不要になったメモリを適切に解放する必要があります。メモリ管理は、このようなメモリのライフサイクルを適切に管理し、システムリソースを効率的に利用するために重要な役割を果たします。</p> <h1 id="明示的メモリ管理とは">明示的メモリ管理とは</h1> <h2 id="概要">概要</h2> <p>明示的メモリ管理とは、プログラマが手動でメモリの割り当てと解放を管理するプロセスのことです。この方法では、プログラマはメモリを必要とする際に明示的に割り当てを行い、不要になったメモリを手動で解放します。</p> <p>C言語では、<code>malloc()</code>を使用してヒープ領域から指定されたサイズのメモリを割り当てます。使い終わったメモリは<code>free()</code>を使って解放します。</p> <p>以下にその一例を示します。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synType">int</span> <span class="synIdentifier">main</span>() { <span class="synType">int</span> *array = <span class="synIdentifier">malloc</span>(<span class="synStatement">sizeof</span>(<span class="synType">int</span>) * <span class="synConstant">10</span>); <span class="synStatement">if</span> (array == <span class="synConstant">NULL</span>) { <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;malloc failed</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>); <span class="synStatement">return</span> <span class="synConstant">1</span>; } <span class="synComment">// array を操作</span> <span class="synIdentifier">free</span>(array); <span class="synStatement">return</span> <span class="synConstant">0</span>; } </pre> <p>このように、プログラマが明示的に<code>free()</code>を使ってメモリを解放する必要があるのが、明示的メモリ管理の特徴です。freeを呼ぶのを忘れたり、不適切な場所で呼んでしまうと様々な問題が発生してしまいます。</p> <p>このプロセスにおいて、<strong><code>free()</code></strong>を適切に呼び出す責任はプログラマにあります。もし <strong><code>free()</code></strong> の呼び出しを忘れるか、不適切なタイミングで呼び出すと、様々な問題が発生してしまいます。</p> <h2 id="明示的解放が引き起こす問題">明示的解放が引き起こす問題</h2> <h3 id="メモリリーク">メモリリーク</h3> <p>メモリリークは、割り当てられたメモリが適切に解放されない場合に発生します。</p> <p>これにより、使用されなくなったメモリがプログラムの実行中に解放されずに残り、徐々にメモリ使用量が増加していきます。長時間実行されるようなプログラムでは特にこれが問題となり、最終的に新たなメモリ割り当てが不可能になることもあります。</p> <h3 id="ダングリングポインタ">ダングリングポインタ</h3> <p>ダングリングポインタは、すでに解放されたメモリ領域を指し続けるポインタのことを指します。</p> <p>以下の例では、メモリが解放後にそのメモリ領域へのアクセスを試みる状況を示しています。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synType">int</span> <span class="synIdentifier">main</span>() { <span class="synType">int</span> *ptr = <span class="synIdentifier">malloc</span>(<span class="synStatement">sizeof</span>(<span class="synType">int</span>)); <span class="synComment">// メモリ割り当て</span> *ptr = <span class="synConstant">10</span>; <span class="synIdentifier">free</span>(ptr); <span class="synComment">// メモリ解放</span> <span class="synComment">// 解放後のメモリへのアクセス(不正な操作)</span> *ptr = <span class="synConstant">42</span>; <span class="synStatement">return</span> <span class="synConstant">0</span>; } </pre> <p>解放されたメモリへのアクセスは、予期しない動作やデータの破壊を引き起こす可能性があります。</p> <h3 id="二重解放">二重解放</h3> <p>二重解放は、同じメモリ領域が複数回解放されることを指します。</p> <p>これは、プログラムの異なる部分で既に解放されたメモリを再度解放しようとするときに発生します。二重解放はメモリの破壊やプログラムの予期しない動作を引き起こす可能性があり、安定性やセキュリティの問題を生じさせることがあります。</p> <p>明示的メモリ管理では、プログラマがメモリのライフサイクルを正確に追跡し、適切なタイミングでのみ解放を行う責任を負います。不要なメモリを即座に解放できるという利点がありますが、この手法は誤った使用によりメモリリーク、ダングリングポインタ、二重解放などの様々な問題を引き起こすリスクを伴います。</p> <h1 id="自動的メモリ管理">自動的メモリ管理</h1> <h2 id="概要-1">概要</h2> <p>自動的メモリ管理とは、プログラマが明示的にメモリ解放を行わなくても、専用のプロセスが自動的に不要と判断したメモリを解放するシステムを指します。</p> <p>この方法は、JavaやPython、Goなどの高級言語で採用されています。</p> <p>例えば、<a href="https://tech-vision.connehito.com/strategy/lets-go.html">弊社が推進しているGo</a>でヒープメモリを割り当てる場合は次のようにします。</p> <pre class="code lang-go" data-lang="go" data-unlink><span class="synStatement">package</span> main <span class="synStatement">import</span> <span class="synConstant">&quot;fmt&quot;</span> <span class="synStatement">type</span> MyStruct <span class="synStatement">struct</span> { Field <span class="synType">int</span> } <span class="synStatement">func</span> createStruct() *MyStruct { <span class="synComment">// new を使って MyStruct の新しいインスタンスを作成</span> ms := <span class="synStatement">new</span>(MyStruct) ms.Field = <span class="synConstant">10</span> <span class="synStatement">return</span> ms <span class="synComment">// 関数の外部に返すことでヒープ割り当てが発生する</span> } <span class="synStatement">func</span> main() { ms := createStruct() fmt.Println(ms) } </pre> <p>この例では、<strong><code>createStruct()</code></strong> 内で <strong><code>MyStruct</code></strong> のインスタンスをヒープに割り当てていますが、プログラマはそのメモリを手動で解放する必要はありません。</p> <p>代わりに、専用のプロセスが自動的に使われないと判断したメモリを解放してくれます。この専用のプロセスのことをガベージコレクションと呼びます。</p> <h2 id="ガベージコレクション">ガベージコレクション</h2> <p>ガベージコレクション(GC)は、プログラムにおいて不要になったメモリを自動的に特定し、解放するシステムです。これにより、プログラマはメモリの手動解放をする必要がなくなり、メモリ管理の負担が大幅に軽減されます。</p> <p>GCのプロセスでは、アプリケーションコードを実行する部分を「ミューテータ」と呼びます。ミューテータは、プログラムの実行中にメモリを割り当て、使用します。</p> <p>一方で、GC自体のコードを実行する部分、つまり不要になったメモリを特定し解放する部分は「コレクタ」と呼ばれます。</p> <p>メモリの割り当てられたオブジェクトは、プログラムにおいて使用される間は「生きている」と見なされます。コレクタは、プログラムの実行中に「生きていない」と判断されたオブジェクト、すなわち「ゴミ」と見なされるオブジェクトを特定し、メモリから解放します。</p> <p>では、コレクタはどのようにしてゴミを判断するのでしょうか?</p> <h3 id="ゴミの判別メカニズム">ゴミの判別メカニズム</h3> <p>GCは、<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/j/junya555/20231121/20231121122019.png" width="1200" height="832" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ポインタ到達可能性の簡略図</figcaption></figure> 上図において、ルート集合からアクセス可能なオブジェクト(例えば<code>ObjectA</code>)は生きているとみなせます。</p> <p><code>ObjectB</code>と<code>ObjectC</code>はルート集合から直接アクセスできないものの、<code>ObjectA</code>を介して間接的にアクセス可能なため、これらは生きているとみなされます。一方で、<code>ObjectD</code>はどの生きているオブジェクトからも参照されていないため、ゴミと判断され回収の対象となります。</p> <h2 id="ガベージコレクションの種類">ガベージコレクションの種類</h2> <p>GCには、主に以下の4つの基本的なタイプがあります。</p> <ul> <li>マークスイープGC</li> <li>コピーGC</li> <li>マークコンパクトGC</li> <li>参照カウントGC</li> </ul> <p>その他のGCアルゴリズムは、通常これらのGCのどれかを組み合わせたGCとなります。</p> <p>先程せっかくGoに触れたので、ここではGoで使われているマークスイープGCについて軽く紹介します。マークスイープGCは「マークフェーズ」と「スイープフェーズ」という2つのフェーズに分かれています。</p> <h3 id="マークフェーズ">マークフェーズ</h3> <p><figure class="figure-image figure-image-fotolife" title="マークフェーズの簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231121/20231121121647.png" width="1200" height="751" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>マークフェーズの簡略図</figcaption></figure> マークフェーズでは、ルート集合から到達可能なオブジェクトにマークをつけます。</p> <p>マークされたオブジェクトは、「生きている」と見なされます。</p> <p>上図ではオブジェクトの中にマークをつけていますが、外部に管理表(ビットマップ)を持たせてそこでマークを管理する方法もあります。</p> <h3 id="スイープフェーズ">スイープフェーズ</h3> <p>スイープフェーズでは、マークのついていないオブジェクト。つまり「ゴミ」とみなされるオブジェクトのメモリを解放します。全ての解放が終了すると、生きているオブジェクトのマークはクリアされ、新しいサイクルが始まります。</p> <p>このようにマークスイープGCでは、生きているオブジェクトを特定し、それ以外を解放するため、これを「間接的GC」と呼びます。今回の主題ではないのでかなり省いた説明をしていますが、機会があれば別の記事として書こうかなと思います。</p> <h1 id="ガベージコレクションの解法">ガベージコレクションの解法</h1> <h2 id="明示的解放の問題が発生する要因">明示的解放の問題が発生する要因</h2> <p>前置きが長くなりましたが、ここからが本題になります。</p> <p>まずは、明示的メモリ解放が直面する主要な問題であった、メモリリーク、ダングリングポインタ、そして二重解放がなぜ起こるのかをみていきます。</p> <p>これらの原因を理解するためには、オブジェクトの活性とメモリ解放の判断における視点の違いを考慮する必要があります。</p> <h3 id="オブジェクトの活性を判断する視点">オブジェクトの活性を判断する視点</h3> <p>オブジェクトの活性(生きているかどうか、もう不要かどうか)を判断する際には、プログラム全体のグローバルのコンテキストを考慮する必要があります。</p> <p>ヒープに割り当てられたオブジェクトは、スタック領域に格納されるローカル変数とは異なり、宣言されたスコープ外でも生存し続け、明示的に解放されるまでメモリを占有します。</p> <p>このため、特定のスコープでそのオブジェクトが使用されていなくても、どこか一つのスコープでも使われていれば、そのオブジェクトは生きていると判断されます。</p> <h3 id="オブジェクトの解放を判断する視点">オブジェクトの解放を判断する視点</h3> <p>明示的な解放の際には、プログラマはその時点でのローカルスコープに基づいてメモリ解放を判断しなければなりません。</p> <p><strong><code>free()</code></strong>などの関数は特定のスコープ内で呼び出されるため、そのスコープの情報のみが判断基準となります。このローカルな視点に基づいた解放判断は、プログラム全体のコンテキストを考慮せずに行われるため、オブジェクトの活性判断のグローバルな視点との間にギャップが生じます。</p> <p>この視点のギャップが、メモリリークやダングリングポインタ、二重解放といった問題の原因となります。</p> <p>この問題を示したコードの簡単な例を以下に示します。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synType">void</span> <span class="synIdentifier">local_scope</span>(<span class="synType">int</span> *ptr) { <span class="synComment">// 他のスコープでまだ使われることを知らずに ptr を解放する。</span> <span class="synIdentifier">free</span>(ptr); } <span class="synType">int</span> <span class="synIdentifier">main</span>() { <span class="synComment">// ヒープ上に整数のためのメモリを割り当てる。</span> <span class="synType">int</span> *ptr = (<span class="synType">int</span> *)<span class="synIdentifier">malloc</span>(<span class="synStatement">sizeof</span>(<span class="synType">int</span>)); *ptr = <span class="synConstant">10</span>; <span class="synIdentifier">local_scope</span>(ptr); <span class="synComment">// ローカルスコープ内で解放されたことを知らずに ptr を使用する。</span> <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;pointer value: </span><span class="synSpecial">%d\n</span><span class="synConstant">&quot;</span>, *ptr); <span class="synComment">// 未定義の振る舞いになる。</span> <span class="synStatement">return</span> <span class="synConstant">0</span>; } </pre> <p>この例では、<code>local_scope()</code>が関数内でメモリを解放してしまうため、プログラムの他の部分でそのポインタが参照されると問題が発生します。</p> <p>このコードは小さいので判断できますが、プログラムの規模が大きくなると、ファイルやモジュールが分割されることによって、このギャップの問題はもっと顕著になることが予測できます。</p> <h2 id="なぜGCではこれらの問題が起きないのか">なぜGCではこれらの問題が起きないのか</h2> <p>GCが明示的メモリ管理の問題に効果的である理由は、単に自動的にメモリを解放することだけでではありません。プログラム全体を俯瞰するグローバルな視点に基づいてオブジェクトの解放を判断することにあります。</p> <p>GCはプログラムの実行状態を全体的に分析し、到達可能性のアルゴリズムに基づいて、活性のあるオブジェクトを維持しつつ、不要になったオブジェクトを特定して解放します。</p> <p>これによって、すべてのオブジェクトに対して、プログラムのどの部分からもアクセスできないというグローバルな条件を満たした時点でのみ、解放が行われます。</p> <p>このように、GCはプログラマがローカルスコープの情報に基づいて行う判断を、プログラム全体の状態を基に行います。これにより、メモリリーク、ダングリングポインタ、二重解放といった問題を根本的に解消することが可能になります。また、プログラムが大規模化し、複数のモジュールやファイルに分割されても、GCの効果は維持されます。</p> <h2 id="ソフトウェア工学的観点の影響">ソフトウェア工学的観点の影響</h2> <p>他にもメモリ管理の違いは、ソフトウェア工学的観点の影響を与えます。</p> <p>ソフトウェア工学において、モジュール間のコミュニケーションは最小限に抑えるということは重要な原則の一つです。この原則は、モジュール間の結合度を低く保ち、他のモジュールに依存を減らし、ソフトウェアの変更や拡張を容易にします。</p> <p>しかし、明示的メモリ管理はこの原則に反し、「暗黙的なインターフェースの複雑化」という問題を引き起こします。</p> <p>以下の例を元に考えてみます。</p> <pre class="code lang-c" data-lang="c" data-unlink><span class="synComment">// メモリの所有権について暗黙的に知っておく必要がある関数</span> <span class="synType">void</span> <span class="synIdentifier">process_and_free</span>(<span class="synType">int</span> *data) { <span class="synIdentifier">printf</span>(<span class="synConstant">&quot;データ処理中: </span><span class="synSpecial">%d\n</span><span class="synConstant">&quot;</span>, *data); <span class="synIdentifier">free</span>(data); } <span class="synComment">// メモリを割り当てて、呼び出し元が解放することを期待する関数</span> <span class="synType">int</span>* <span class="synIdentifier">create_data</span>() { <span class="synType">int</span> *data = (<span class="synType">int</span>*)<span class="synIdentifier">malloc</span>(<span class="synStatement">sizeof</span>(<span class="synType">int</span>)); *data = <span class="synConstant">42</span>; <span class="synComment">// 何かの値を設定する。</span> <span class="synStatement">return</span> data; } <span class="synType">int</span> <span class="synIdentifier">main</span>() { <span class="synComment">// インターフェースは、所有権が呼び出し側に渡ることが暗黙的です。</span> <span class="synType">int</span> *new_data = <span class="synIdentifier">create_data</span>(); <span class="synComment">// インターフェースは、誰がメモリを解放するべきかについて暗黙的であり、</span> <span class="synComment">// ドキュメントや規約の必要性を生み出します。</span> <span class="synIdentifier">process_and_free</span>(new_data); <span class="synStatement">return</span> <span class="synConstant">0</span>; } </pre> <p>この例では、<code>create_data()</code>がメモリを割り当ててポインタを返しますが、 この関数を使用する際、呼出し側はこのポインタのメモリを解放する必要があります。</p> <p>すなわち、メモリの所有権が関数の呼出し元に暗黙的に移譲されるわけですが、このルールは関数のシグネチャから明確に読み取ることができず、メモリリークのリスクを増加させます。</p> <p>また、<code>process_and_free()</code>では、内部でメモリ解放しますが、その事実もシグネチャからは読み取れません。そのため呼び出し側はその関数の内部を知らなければ、解放済みのメモリのポインタを再度使用してしまう可能性があります。</p> <p>これらの問題に対処するためには、ドキュメントやコメントなどで補足する必要がありますが、これはインターフェースを不必要に複雑化させ、エラーのリスクや認知負荷が増加させます。加えて、暗黙のルールを理解しなければコードが理解できないとなると、モジュールの再利用性は制限されてしまいます。</p> <p>一方、自動メモリ管理(GC)はこのような問題を回避できます。</p> <p>これは、GCがメモリ管理の責任をプログラマから引き受けることによって、インターフェースからメモリ管理の複雑さを取り除くからです。</p> <p>明示的メモリ管理の場合、メモリを管理するためのコードがあちこちに散りばめられますが、GCの場合は、メモリ管理をするためのコードを書く必要がなくなり、再利用性も改善され保守性が高くなるメリットがあります。</p> <h2 id="GCの制約">GCの制約</h2> <p>ここまでGCを使うことによる利点を述べてきましたが、万能薬というわけではなく、いくつかの制約を考慮する必要があります。</p> <h3 id="パフォーマンスオーバーヘッド">パフォーマンスオーバーヘッド</h3> <p>GCのプロセスがパフォーマンスに影響を与えることがあります。</p> <p>特にFull GCと呼ばれるタイプのGCでは、GC実行中に全てのアプリケーションスレッドが停止してしまうことがあり、これは「Stop The World」と呼ばれる現象になります。</p> <p>他の種類のGCでは「Stop The World」は発生しなくても、パフォーマンス低下を引き起こすことがあります。</p> <h3 id="メモリ使用量の増加">メモリ使用量の増加</h3> <p>GCは不要なオブジェクトを即座に解放しないので、メモリ使用量が増えることがあります。特にメモリリソースが限られた環境だとこれが問題になります。</p> <h3 id="実行タイミングが予想できない">実行タイミングが予想できない</h3> <p>GCがいつ実行されるかは予測できないため、特定のタイミングでのメモリ解放を保証することができません。これは、リソースが限られた環境や、厳格なリソース管理が必要なアプリケーションでは問題になる可能性があります。</p> <h3 id="生きているオブジェクトが使われるとは限らない">生きているオブジェクトが使われるとは限らない</h3> <p>GCは、オブジェクトの生存状態を判断する際に「到達可能性」を基準にしますが、これはオブジェクトが実際に使われているかどうかを必ずしも意味しません。つまり、プログラム内で参照されているが、実際にはもう使用されていない「生きているが使われていない」オブジェクトが存在する可能性があります。</p> <p>このようなオブジェクトは、GCによって回収されないため、メモリを不必要に占有し続けることになります。</p> <p>他にもまだ考慮すべき制約はたくさんありますが、これらの制約はGCの利点とのトレードオフになります。GCを使用する際はこれらの制約を理解しておくことが重要になります。</p> <h1 id="まとめ">まとめ</h1> <p>明示的メモリ管理における問題の多くは、オブジェクトの活性はグローバルなスコープで判断されるのにもかかわらず、メモリ解放はローカルスコープで判断しなければならないという判断の視点のギャップにありました。</p> <p>GCは、メモリ解放の決定をグローバルスコープで行い、自動で解放することで明示的メモリの問題の解決を図ります。 それに加えて、モジュールの再利用性も向上し、メンテナンスのしやすいプログラムも書きやすくなるのでした。</p> <p>しかし、GCは完璧な解決策ではなく、さまざまな制約が存在します。 これらの制約の理解し、コードがどのメモリ領域に格納されるのかを考慮しながらコードを書くことが大事なんだな〜と思いました。</p> <p>メモリ管理って面白い!( ^∀^)</p> <h1 id="参考資料">参考資料</h1> <h2 id="書籍">書籍</h2> <ul> <li>Richard Jones, ガベージコレクション 自動的メモリ管理を構成する理論と実装, 翔泳社, 2016</li> <li>Noam Nisan, コンピューターシステムの理論と実装 —モダンなコンピュータの作り方, オライリージャパン, 2015</li> </ul> <h2 id="サイト">サイト</h2> <ul> <li><a href="https://tip.golang.org/doc/gc-guide">https://tip.golang.org/doc/gc-guide</a></li> </ul> junya555 ChatGPTに社内文書に基づいた回答を生成させる仕組みを構築しました hatenablog://entry/6801883189058380615 2023-11-14T22:14:16+09:00 2023-11-14T22:14:16+09:00 はじめに はじめまして、8月にコネヒトに入社したy.ikenoueです。 突然ですがみなさん、生成AIは使っておりますでしょうか? ChatGPTやStable Diffusionといった代表的な生成AIの発表から約1年が経過し、そろそろブームも落ち着くかと思っていたのですが、つい先日もOpenAI DevDayにてChatGPTに関する様々なアップデートが発表されるなど、相変わらず目まぐるしい日々が続いていますね。 弊社における生成AIの活用状況はというと、以前に下記の記事にて、Slack上でChatGPTと会話できる環境を社内提供しているという取り組みをご紹介しました。 tech.conn… <h2 id="はじめに">はじめに</h2> <p>はじめまして、8月にコネヒトに入社したy.ikenoueです。</p> <p>突然ですがみなさん、生成AIは使っておりますでしょうか? ChatGPTやStable Diffusionといった代表的な生成AIの発表から約1年が経過し、そろそろブームも落ち着くかと思っていたのですが、つい先日もOpenAI DevDayにてChatGPTに関する様々なアップデートが発表されるなど、相変わらず目まぐるしい日々が続いていますね。</p> <p>弊社における生成AIの活用状況はというと、以前に下記の記事にて、Slack上でChatGPTと会話できる環境を社内提供しているという取り組みをご紹介しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F05%2F24%2F182929" title="LangChainとOpenAI APIを組み合わせて、文脈を考慮して会話できるSlack Botを作った話 - コネヒト開発者ブログ" 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.connehito.com/entry/2023/05/24/182929">tech.connehito.com</a></cite></p> <p>本日は、上記の社内ツールに新たに追加した「社内文書の参照機能」についてご紹介します。</p> <h2 id="社内文書の参照機能の概要と開発動機">「社内文書の参照機能」の概要と開発動機</h2> <p>まずは「社内文書の参照機能」の概要と開発にいたった動機についてご説明します。</p> <p>「社内文書の参照機能」は、その名の通り、ChatGPTが社内に存在する文書を参照したうえで、回答を生成する機能となっております。 というのも、ChatGPTを始めとする大規模言語モデルは、学習時に用いたデータ以外の知識を持っていません。そのため例えば、「経費を精算するにはどういった手順を踏む必要がありますか?」という質問を投げかけたとしても、ChatGPTは一般的な経費精算手順について述べるのみで、特定の企業における正しい経費精算の手順を詳細に回答することは不可能となっています。「社内で発生するQAをChatGPTに回答してもらおう」といった発想はChatGPTを業務で活用するうえで真っ先に思い浮かぶ案の一つかと思いますが、上記の例に代表されるように、質問者が求める情報を正確に提供することができないという場面は非常に多いです。</p> <p>このような課題に対する解決策として「検索拡張生成 (RAG: Retrieval Augmented Generation)」という技術が知られています。(以降「RAG」と略します。) RAGは、生成AI技術に対して検索技術を掛け合わせることで、本来の生成AIが知り得ない情報に関する回答を可能にする技術です。 ここで、簡素な図を使ってRAGについて説明します。</p> <p><figure class="figure-image figure-image-fotolife" title="RAG概要"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/a/ashenOne/20231113/20231113104615.png" width="1200" height="537" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>RAG概要</figcaption></figure></p> <p>上記のように、RAGでは、ユーザーが発信したメッセージを生成AIに渡す前に、文書の検索を実行するステップが発生します。(上図②) 検索ステップでは、予め準備された文書の中からユーザーが発信したメッセージと関連性が高い(≒ユーザーが求める回答を生成するために役立つ可能性が高いと考えられる)文書の抽出を試みます。 そして、見つけ出した文書をユーザーが発信したメッセージと一緒に生成AIに与えることで、生成AIが生成する回答の信頼性を高めることができるという仕組みになっています。</p> <p>この度実装した「社内文書の参照機能」では、以上に述べたRAGの技術を用いて、社内制度やナレッジが書かれた文書の検索とそれらの文書に基づく回答生成を実現しています。 特に今回は、可能な限り低コストで本機能を実現することを重視しており、Azure Cognitive SearchやOpenAIの文章埋め込みモデルといったRAGを実践する際に用いられることの多い有料サービスを使用しない方法を採用しています。低コストで本機能を実現したいと考えている皆さんのお役に立つ内容になっているかと思いますので、参考になれば幸いです。</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/a/ashenOne/20231114/20231114121904.png" width="1200" height="797" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このシステムによって実行される処理は、「A. 定期バッチ」として行うもの(構成図上部)、「アプリケーション」の実行時に行うもの(構成図下部)の2つに大別されます。以降は、これらを分けてご説明します。</p> <p>「A. 定期バッチ」では、検索を実行するために必要となる社内文書の取得及び検索用インデックスの構築を行います。今回は、検索手法としてベクトル検索を採用しているため、事前準備として文書のベクトル化を行いベクトルインデックスを構築する必要があります。</p> <p>「B. アプリケーション」には、ユーザーから受け取ったメッセージに基づく関連文書の検索やOpenAIのAPIを通したChatGPTとのやり取り、ユーザーへの回答送信などを実行します。また、アプリケーションの起動時には、「A. 定期バッチ」で作成された検索用のオブジェクトを読み込みます。</p> <p>当記事では、「社内文書の参照機能」に特有な下記の3つの手順に焦点を当て、それぞれで実行する処理の中身についてご説明します。</p> <p>「A. 定期バッチ」</p> <ul> <li>手順① 社内文書のベクトル化 (構成図A-2に該当)</li> </ul> <p>「B. アプリケーション」</p> <ul> <li>手順② ユーザーのメッセージに関連する文書の抽出 (構成図B-1〜B-3に該当)</li> <li>手順③ ChatGPTに与えるプロンプトの構築 (構成図B-4に該当)</li> </ul> <h3 id="手順-社内文書のベクトル化-構成図A-2に該当">手順① 社内文書のベクトル化 (構成図A-2に該当)</h3> <p>手順①では、社内のマニュアルやナレッジが記載された文書をベクトル化する際の処理についてご説明します。これは、手順②の「ユーザーのメッセージによる関連する文書の抽出」においてベクトル検索を行うための事前準備にあたります。</p> <p>はじめに、文書をベクトル化するためのアルゴリズムとしては、HuggingFaceにて公開されているモデル<code>multilingual-e5-small</code> を採用しました。2023年11月時点では、文書をベクトル化するためのモデルの主要な候補としてOpenAI製の<code>text-embedding-ada-002</code> がありますが、今回は下記の点を考慮して<code>multilingual-e5-small</code> の採用を決定しました。</p> <ul> <li>最初はなるべくコストを掛けずに機能の実現可能性・実用性を検証したい</li> <li><a href="https://huggingface.co/spaces/mteb/leaderboard">ベンチマーク</a>において、<code>multilingual-e5</code> の性能が<code>text-embedding-ada-002</code> と大差ないことが報告されている。(※)</li> </ul> <p>※ <code>multilingual-e5</code>には、モデルサイズ別にsmall / base / largeの3つのバリエーションがありますが、largeサイズの性能はベンチマークによって<code>text-embedding-ada-002</code>を上回っています。</p> <p>以下に、<code>multilingual-e5-small</code>によるベクトル化を実行するためのコードを記載します。実装には、PythonライブラリのLlamaIndexを用いています。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># 手順①-1. モジュールのインポート</span> <span class="synPreProc">import</span> torch <span class="synPreProc">from</span> langchain.embeddings <span class="synPreProc">import</span> HuggingFaceEmbeddings <span class="synPreProc">from</span> llama_index <span class="synPreProc">import</span> Document, ServiceContext, StorageContext, VectorStoreIndex, set_global_service_context <span class="synPreProc">from</span> llama_index.node_parser <span class="synPreProc">import</span> SimpleNodeParser <span class="synPreProc">from</span> llama_index.text_splitter <span class="synPreProc">import</span> TokenTextSplitter <span class="synPreProc">from</span> llama_index.vector_stores <span class="synPreProc">import</span> SimpleVectorStore <span class="synComment"># 手順①-2. 埋め込みモデルに関する設定</span> EMBEDDING_DEVICE = <span class="synConstant">&quot;cuda&quot;</span> <span class="synStatement">if</span> torch.cuda.is_available() <span class="synStatement">else</span> <span class="synConstant">&quot;mps&quot;</span> <span class="synStatement">if</span> torch.backends.mps.is_available() <span class="synStatement">else</span> <span class="synConstant">&quot;cpu&quot;</span> <span class="synComment"># 埋め込みモデルの計算を実行する機器</span> embed_model = HuggingFaceEmbeddings(model_name=<span class="synConstant">&quot;intfloat/multilingual-e5-small&quot;</span>, model_kwargs={<span class="synConstant">'device'</span>: EMBEDDING_DEVICE}) node_parser = SimpleNodeParser(text_splitter=TokenTextSplitter(separator=<span class="synConstant">&quot; &quot;</span>, chunk_size=<span class="synConstant">512</span>, chunk_overlap=<span class="synConstant">64</span>)) service_context = ServiceContext.from_defaults(embed_model=embed_model, node_parser=node_parser) set_global_service_context(service_context) <span class="synComment"># 手順①-3. 社内ドキュメントをLlamaIndexのDocument型のデータに変換</span> llamaindex_documents = [] <span class="synComment">## ※ 下記の変数original_documentsは、社内ドキュメントを要素とするリスト型の変数であり、</span> <span class="synComment">## 各要素は社内ドキュメントの「タイトル」「本文」「URL」が格納された辞書型のデータとします。</span> <span class="synStatement">for</span> document_dict <span class="synStatement">in</span> original_documents: llamaindex_documents.append(Document( text=document_dict[<span class="synConstant">&quot;本文&quot;</span>], metadata={ <span class="synConstant">&quot;タイトル&quot;</span>: document_dict[<span class="synConstant">&quot;タイトル&quot;</span>], <span class="synConstant">&quot;URL&quot;</span>: document_dict[<span class="synConstant">&quot;URL&quot;</span>], }, excluded_embed_metadata_keys=[<span class="synConstant">&quot;url&quot;</span>] )) <span class="synComment"># 手順①-4. 文書のベクトル化を実行し、文書検索用のオブジェクトを構築</span> index = VectorStoreIndex.from_documents(llamaindex_documents) <span class="synComment"># 手順①-5. 構築した検索用のオブジェクトを保存</span> persist_dir = <span class="synConstant">&quot;./my_storage&quot;</span> index.storage_context.persist(persist_dir=persist_dir) </pre> <p>コードの内容について一部解説します。</p> <p>手順①-2のブロックでは、ベクトル化を実行する際の設定として「ベクトル化を実行するモデルの種類」「計算デバイス」「テキスト分割方法」を決めています。特に「テキスト分割方法」では、今回使用するモデル<code>multilingual-e5-small</code> の対応シーケンス長が最大512トークンであることから、各文書をあらかじめ512トークン以下の粒度で分割するよう設定しています。</p> <p>手順①-3のブロックでは、オリジナルの社内文書をLlamaIndexが求めるデータ型 (Document型)に変換する処理を行っています。特に<code>excluded_embed_metadata_keys=["url"]</code>の部分では、metadataに指定したデータの内、ベクトル化の対象外とするものを指定することができます。このように、「何らかの用途で後から参照する可能性はあるが、検索の用途では意味をなさないためベクトル化の対象からは外したい」といったデータは、ベクトル化の対象から除外することをおすすめします。</p> <p>手順①-4のブロックでは、ベクトルインデックスを中心とする検索用のオブジェクトを生成します。 文書のベクトル化処理もこのセクションで実行しているので、元の文書量が膨大な場合は処理にそこそこの時間を必要とします。(参考までに、弊社の全文書に対してm2チップを搭載したノートPCで処理を実行した場合、約3時間ほどの時間を要しました。)</p> <h3 id="手順-ユーザーのメッセージに関連する文書の抽出-構成図B-1B-3に該当">手順② ユーザーのメッセージに関連する文書の抽出 (構成図B-1〜B-3に該当)</h3> <p>ここからは、ChatGPTとやり取りを行うアプリケーションの動作時に実行する処理となります。手順②では、ユーザーから受け取ったメッセージと関連性が高い社内文書を見つけ出す処理についてご説明します。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment">## 〜 コードが重複するため省略 〜</span> <span class="synComment">## 手順①-1及び2の「埋め込みの設定」処理をここで実行</span> <span class="synComment">## 〜 コードが重複するため省略 〜</span> <span class="synComment"># 手順②-1. モジュールのインストール</span> <span class="synPreProc">from</span> llama_index <span class="synPreProc">import</span> load_index_from_storage <span class="synPreProc">from</span> llama_index.retrievers <span class="synPreProc">import</span> VectorIndexRetriever <span class="synComment"># 手順②-2. 手順①で構築した検索用オブジェクトのロード</span> persist_dir = <span class="synConstant">&quot;./my_storage&quot;</span> storage_context = StorageContext.from_defaults(persist_dir=persist_dir) loaded_index = load_index_from_storage(storage_context) <span class="synComment"># 手順②-3. ベクトル検索を行い、関連性の高い上位5つの文書を抽出</span> retriever = VectorIndexRetriever(index=loaded_index, similarity_top_k=<span class="synConstant">5</span>) <span class="synComment">## ※ 下記の変数user_messageには、ユーザが入力した文字列が格納されているものとします</span> retrieved_text_list = retriever.retrieve(user_message) </pre> <p>手順②-2では、手順①で保存したデータを読み込むことで、検索用のオブジェクトをメモリ上に展開しています。</p> <p>手順②-3では、ベクトル検索を行うことで、ユーザーから受け取ったメッセージと関連性が高い社内文書を見つけ出す処理を行います。注意点として、<code>retriever.retrieve(user_message)</code>の部分では、ユーザから受け取ったテキストに対するベクトル化処理を行っています。ここで使用するモデルは、手順①で社内文書をベクトル化した際と同じモデルである必要があります。</p> <h3 id="手順-ChatGPTに与えるプロンプトの構築-構成図B-4に該当">手順③ ChatGPTに与えるプロンプトの構築 (構成図B-4に該当)</h3> <p>最後に、手順②で抽出した文章をユーザーから受け取ったメッセージに追加することで、ChatGPTに与えるプロンプトを作成します。</p> <p>まずはコードを記載します。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># 手順③-1. 手順②で抽出したドキュメントをユーザーから受け取ったメッセージの末尾に追加</span> reference_documents = <span class="synConstant">&quot;&quot;</span> <span class="synStatement">for</span> retrieved_text <span class="synStatement">in</span> retrieved_text_list: <span class="synComment"># 検索にヒットした社内ドキュメントの内容及びタイトルを取得</span> document_content = retrieved_text.node.text document_title = retrieved_text.node.metadata[<span class="synConstant">&quot;タイトル&quot;</span>] reference_documents += f<span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">## 文書「{document_title}」</span><span class="synSpecial">\n</span><span class="synConstant">{document_content}</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span> <span class="synComment"># ユーザーのメッセージに、関連文書を追加</span> user_message += f<span class="synConstant">&quot;</span><span class="synSpecial">\n\n</span><span class="synConstant">ただし、下記の文書を参考にして回答してください。</span><span class="synSpecial">\n</span><span class="synConstant">{reference_documents}&quot;</span> </pre> <p>上記の処理を実行することで、ChatGPTに与えるプロンプトは以下のような文字列になります。</p> <pre class="code bash" data-lang="bash" data-unlink>{ユーザーから受け取ったオリジナルのメッセージ} ただし、下記の文書を参考にして回答してください。 ## 文書「{文書1のタイトル}」 {文書1の本文} ## 文書「{文書2のタイトル}」 {文書2の本文} ## 文書「{文書3のタイトル}」 {文書3の本文} ## 文書「{文書4のタイトル}」 {文書4の本文} ## 文書「{文書5のタイトル}」 {文書5の本文}</pre> <p>このように、ユーザーのメッセージに加えてそれに関連する社内文書を渡すことで、ChatGPTが社内独自のマニュアル・ナレッジに基づいた回答を生成することが可能となります。</p> <h2 id="社内文書の参照機能の使用例">「社内文書の参照機能」の使用例</h2> <p>それでは、今回の機能搭載によりChatGPTの回答に具体的にどのような変化が生じるようになったのか、実際の例をお見せします。</p> <p>以下では「社員が書籍購入補助制度の有無について尋ねる」という場面を想定し、</p> <ul> <li>before: オリジナルのChatGPTをそのまま使用した場合</li> <li>after: 「社内文書の参照機能」を使用した場合</li> </ul> <p>として、それぞれの回答例を比較しています。</p> <h3 id="before-オリジナルのChatGPTをそのまま使用した場合">before: オリジナルのChatGPTをそのまま使用した場合</h3> <p>まずは、「社内文書の参照機能」を使用しない場合の回答結果です。</p> <p><figure class="figure-image figure-image-fotolife" title="回答例 -before-"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/a/ashenOne/20231113/20231113104843.png" width="1000" height="668" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>回答例 -before-</figcaption></figure></p> <p>そもそも「コネヒト(弊社)の制度について知りたい」という意図をChatGPTに伝えていないため、ChatGPTの回答は一般的な書籍購入補助制度について述べるにとどまっています。また、弊社の書籍購入補助制度の詳細は外部に公開されているものでもないため、その点でもオリジナルのChatGPTに正確な回答を期待することはできない質問となっています。</p> <h3 id="after-社内文書の参照機能を使用した場合">after: 「社内文書の参照機能」を使用した場合</h3> <p>続いて、今回新たに搭載した「社内文書の参照機能」を使用した場合の回答例です。(社外に公開していない情報を含んでいるため、マスクしている箇所があります。)</p> <p>※ 下記のユーザーメッセージ内に含まれる「コネヒト」という文字列は今回搭載した機能を動かすためのトリガーの役割を果たすのみで、ChatGPTに送信するメッセージからは除外する処理をアプリ側で行っています。</p> <p><figure class="figure-image figure-image-fotolife" title="回答例 -after-"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/a/ashenOne/20231113/20231113104918.png" width="809" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>回答例 -after-</figcaption></figure></p> <p>回答内で言及されている”スキルアップ支援制度”は弊社の書籍購入補助制度の名称です。弊社固有の名称を用いて制度に関する具体的な説明が行われていることから、実際の文書の内容に基づく回答が生成されていることがわかります。(弊社ではドキュメントの管理にNotionを使用しているため、ChatGPTの回答の末尾にNotionページへのリンクを追記することで、回答のソースとなった文書にアクセスできるような導線を引いています。)</p> <h2 id="おわりに">おわりに</h2> <p>以上、当記事ではChatGPTに社内文書を参照させたうえで正確な回答を生成させる機能の実現例をご紹介しました。</p> <p>この機能は社内でリリースしてから約2ヶ月が経過するのですが、嬉しいことに「新入社員のオンボーディングに使いたい」や「社内制度に関するQA一次対応に使いたい」など、具体的なケースでの活用を希望する声があがっています。</p> <p>弊社では、今後もベクトル検索や生成AIといった技術を積極的に活用することで社内の業務効率化やプロダクトの提供価値向上に挑戦していきます。その際は、得られた知見を同様に記事化してご紹介しますので、引き続き当ブログに注目いただけると幸いです!</p> ashenOne カオナビ/スターフェスティバル/リンケージ/コネヒトで合同LT&交流会を開催しました! hatenablog://entry/6801883189055484594 2023-11-02T19:54:22+09:00 2023-11-02T19:54:22+09:00 こんにちは。コネヒトでAndroidエンジニアをしている@katsutomuです。 2023/10/18に株式会社カオナビ、スターフェスティバル株式会社、株式会社リンケージの皆様と、オフラインの合同LT&交流会を開催しました! このイベントは、社内だけでは得られないような、各社の生な事例や課題をワイワイと話して、学び合うことを主題にクローズドな形式で開催をしています。 4月に開催した前回の参加企業に加え、リンケージさんにもお声がけして、4社合同での開催となりました。今回はイベントの様子をおすそ分けできればと思います。 カオナビさんのイベントブログ www.wantedly.com 前回のイベン… <p>こんにちは。コネヒトでAndroidエンジニアをしている<a href="https://twitter.com/katsutomu">@katsutomu</a>です。</p> <p>2023/10/18に<a href="https://corp.kaonavi.jp/">株式会社カオナビ</a>、<a href="https://stafes.co.jp/">スターフェスティバル株式会社</a>、<a href="https://linkage-inc.co.jp/">株式会社リンケージ</a>の皆様と、オフラインの合同LT&amp;交流会を開催しました!</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231102/20231102171417.jpg" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>このイベントは、社内だけでは得られないような、各社の生な事例や課題をワイワイと話して、学び合うことを主題にクローズドな形式で開催をしています。</p> <p>4月に開催した前回の参加企業に加え、リンケージさんにもお声がけして、4社合同での開催となりました。今回はイベントの様子をおすそ分けできればと思います。</p> <p><strong>カオナビさんのイベントブログ</strong></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Fkaonavi%2Fpost_articles%2F867959" title="2023/10/18(水) 合同LT勉強会を開催しました | カルチャー" 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/kaonavi/post_articles/867959">www.wantedly.com</a></cite></p> <p><strong>前回のイベントブログ</strong></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F05%2F01%2F181712" 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.connehito.com/entry/2023/05/01/181712">tech.connehito.com</a></cite></p> <h2 id="前回からのアップデート">前回からのアップデート</h2> <p>今回はイベントにテーマを設けるTryを実施しました。</p> <p><strong>「交流会での話題が、探り探りになる」、「LTで何を話せば/話していいかわからない」</strong>ということが、過去2回の反省点としてあがったことが理由です。</p> <p>テーマはいくつか候補が上がりましたが、オープンな場だと話題にあげづらい<strong>「開発生産性」</strong>を選びました。</p> <h2 id="イベントの概要">イベントの概要</h2> <p>各社からのLTと交流会が主な内容になっています。</p> <ul> <li>オープニング <ul> <li>18:30 - 18:40: 諸注意・会場案内</li> <li>18:40 - 19:30: 交流会</li> </ul> </li> <li>LT <ul> <li>19:30 - 19:50: LT1~4</li> <li>19:50 - 20:00: 休憩</li> <li>20:00 - 20:20: LT5~8</li> <li>20:20 - 20:30: 飛び入り</li> </ul> </li> <li>クロージング(30min) <ul> <li>20:45 - 21:15: LTの感想話したり、片付けたり</li> <li>21:15 - 21:30: クロージング</li> </ul> </li> </ul> <h2 id="交流会">交流会</h2> <p>話題に困らないように、トークテーマシートを用意しており、最初にシートから選んで話し始めるグループも多くありました。ひとつひとつの話題は「開発生産性」で話したいことを、コネヒト社内のメンバーから募集した内容を載せています。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231102/20231102171258.png" width="1200" height="673" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>最初こそ探り探りでしたが、次第に打ち解けていきました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231102/20231102172235.jpg" width="1200" height="800" 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/k/katsutomu0124/20231102/20231102172222.jpg" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="LT枠">LT枠</h2> <p>オンラインでの登壇も含み、各社から2名ずつ合計8名の方に話していただきました。 生産性への取り組みに関する各社の事例や、狭義の生産性に着目しビルドトラップに陥る罠など、見識を広げるお話も多く、非常に参考になりました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231102/20231102173623.jpg" width="1200" height="800" 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/k/katsutomu0124/20231102/20231102173457.jpg" width="1200" height="800" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>テーマを決めた時には、どんなLTが集まるか不安はありましたが、限定的な場だからこそ話せるクローズドイベントならではな内容もあり、嬉しく思っています。</p> <h2 id="実施後のアンケート">実施後のアンケート</h2> <p>継続的なアップデートのために、毎回参加した弊社メンバーにアンケートを実施しています。 設問は以下の通りです。</p> <ul> <li>今回のイベントに期待してたことを教えてください</li> <li>LTで期待していたことは、得られましたか</li> <li>交流会で期待していたことは、得られましたか</li> <li>LTや交流の中で、持ち帰ってTryしたい他社の事例はありましたか</li> <li>また次回あったら参加したいですか</li> </ul> <p>「開発生産性」というキーワードと当日の内容にギャップを感じたメンバーもおり、運営側として学びを感じる結果となりまいた。</p> <h3 id="アンケート結果">アンケート結果</h3> <h4 id="今回のイベントに期待してたこと抜粋">今回のイベントに期待してたこと(抜粋)</h4> <ul> <li>開発生産性のアップが期待できそうな取り組み事例</li> <li>他社の皆さんとの交流、LTでの情報収集</li> <li>友達を増やす!</li> </ul> <h4 id="定量評価">定量評価</h4> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20231102/20231102174818.png" width="1002" height="446" 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/k/katsutomu0124/20231102/20231102174847.png" width="1004" height="468" 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/k/katsutomu0124/20231102/20231102174905.png" width="994" height="404" 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/k/katsutomu0124/20231102/20231102175020.png" width="1004" height="412" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h4 id="具体的なお声抜粋">具体的なお声(抜粋)</h4> <ul> <li>生産性が落ちるパターンの解説LTが勉強になりました</li> <li>振り返りの振り返りはtryしてみたいと思った</li> <li>Tryというか、そもそも開発生産性とは?という内容に着目したLTが多かったので、きちんと考えるきっかけになったのが良かった。</li> <li>テーマに対する自分の抱くイメージとLTの話題にギャップが激しく「どちらとも言えない」にしています</li> </ul> <h2 id="まとめ">まとめ</h2> <p>テーマ設定とトークシートを用意したことで、交流会が盛り上がったことは安心しています。社内でトークシートの内容を募集した際も、色々な聞きたいことが集まり、社外と情報交換したいことは多くありそうだと感じているので、今後もアップデートしながら継続開催していきたいと思います。</p> <p>反面、テーマから抱くイメージとギャップを感じさせてしまったなど反省もあるので、次回のイベントに活かしていきたいと考えています。</p> <p>ご協力いただいた各社様に改めて感謝いたします。また次回よろしくお願いします!</p> katsutomu0124 配列と連結リストの線形探索における計算速度とキャッシュメモリの重要性 hatenablog://entry/6801883189050807581 2023-10-16T11:59:09+09:00 2023-10-16T11:59:09+09:00 こんにちは!バックエンドエンジニアのjunyaUと申します。 最近はコンビニに売っている生ラムネにハマっていて、1日2袋のペースで食べ続けていましたが、ここ数日コンビニに置いていなくて悲しい思いをしています。 今回は配列と連結リストおいて、線形探索の計算速度はどちらが速いのか、キャッシュメモリがどう関わっているのかについて考えていこうと思います〜! はじめに なぜ記事を書こうと思ったのか 結論 配列と連結リスト 配列の特徴 ランダムアクセス可能 削除と挿入操作時のオーバーヘッドが大きい 固定長 連結リストの特徴 ランダムアクセスはできない 削除と挿入時のオーバーヘッドが小さい 可変長 比較表 … <p>こんにちは!バックエンドエンジニアのjunyaUと申します。</p> <p>最近はコンビニに売っている生ラムネにハマっていて、1日2袋のペースで食べ続けていましたが、ここ数日コンビニに置いていなくて悲しい思いをしています。</p> <p>今回は配列と連結リストおいて、線形探索の計算速度はどちらが速いのか、キャッシュメモリがどう関わっているのかについて考えていこうと思います〜!</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a><ul> <li><a href="#なぜ記事を書こうと思ったのか">なぜ記事を書こうと思ったのか</a></li> <li><a href="#結論">結論</a></li> </ul> </li> <li><a href="#配列と連結リスト">配列と連結リスト</a><ul> <li><a href="#配列の特徴">配列の特徴</a><ul> <li><a href="#ランダムアクセス可能">ランダムアクセス可能</a></li> <li><a href="#削除と挿入操作時のオーバーヘッドが大きい">削除と挿入操作時のオーバーヘッドが大きい</a></li> <li><a href="#固定長">固定長</a></li> </ul> </li> <li><a href="#連結リストの特徴">連結リストの特徴</a><ul> <li><a href="#ランダムアクセスはできない">ランダムアクセスはできない</a></li> <li><a href="#削除と挿入時のオーバーヘッドが小さい">削除と挿入時のオーバーヘッドが小さい</a></li> <li><a href="#可変長">可変長</a></li> </ul> </li> <li><a href="#比較表">比較表</a></li> </ul> </li> <li><a href="#線形探索">線形探索</a><ul> <li><a href="#線形探索について">線形探索について</a></li> <li><a href="#どちらの方が計算コストが低いのか">どちらの方が計算コストが低いのか</a><ul> <li><a href="#配列はarrayではなくvectorを使う">配列はarrayではなくvectorを使う</a></li> <li><a href="#コンパイラがループを最適化しないようにした">コンパイラがループを最適化しないようにした</a></li> <li><a href="#結果">結果</a></li> </ul> </li> </ul> </li> <li><a href="#キャッシュメモリ">キャッシュメモリ</a><ul> <li><a href="#キャッシュメモリの概要">キャッシュメモリの概要</a><ul> <li><a href="#補足--記憶階層とは">補足 : 記憶階層とは?</a></li> </ul> </li> <li><a href="#なぜ配列の方がヒット率が高いのか">なぜ配列の方がヒット率が高いのか</a><ul> <li><a href="#空間的局所性">空間的局所性</a></li> <li><a href="#時間的局所性">時間的局所性</a></li> </ul> </li> </ul> </li> <li><a href="#おわりに">おわりに</a><ul> <li><a href="#結論-1">結論</a></li> </ul> </li> </ul> <h1 id="はじめに">はじめに</h1> <h2 id="なぜ記事を書こうと思ったのか">なぜ記事を書こうと思ったのか</h2> <p>最近、個人開発で物理メモリ管理の実装をする機会がありました。</p> <p>その中で、配列を線形探索する処理があったのですが、</p> <p>「連結リストと配列を線形探索する時に、どちらの方が計算速度が速いんだろう」</p> <p>とふと疑問に思ったのがきっかけです。</p> <p>この問題を調べて勉強していくうちに、キャッシュメモリの存在が両者の計算速度の違いに関わっていることがわかりました。</p> <p>そこで、配列と連結リストのデータ構造の違い、キャッシュメモリがどのようなものなのかについて触れながら両者の線形探索における計算速度の違いを見ていこうと思います!</p> <p>なお、ここでの連結リストは単方向の連結リストとします。</p> <h2 id="結論">結論</h2> <p>配列と連結リストは共に線形探索の時間計算量がO(n)ですが、計算速度は配列の方が速いです。</p> <p>これは、キャッシュメモリのヒット率が配列の方が高いからです。</p> <p>連結リストはメモリ上で非連続的に配置される可能性がありますが、配列はメモリ上に連続で配置されることが保証されています。</p> <p>連続してメモリ上に配置されているので、キャッシュラインに配列の他の要素が含まれる可能性が高いため、キャッシュヒット率が連結リストよりも高くなるのです。</p> <p>これを理解するには配列と連結リストの構造の違い、キャッシュメモリの概要や特性を知る必要があるので、次節から詳しく見ていきたいと思います。</p> <h1 id="配列と連結リスト">配列と連結リスト</h1> <h2 id="配列の特徴">配列の特徴</h2> <p>私がプログラミングを勉強し始めた頃、配列は「複数のデータを入れることができる箱」と学びました。</p> <p>しかしそれだけでは、配列がどのような実態を持っているのかを掴めないので、もう少し具体的に定義すると、</p> <p>「メモリ上に連続して配置された、同じ種類のデータの集合」と言えます。 <figure class="figure-image figure-image-fotolife" title="メモリと配列の簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231016/20231016011852.png" width="1200" height="310" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>メモリと配列の簡略図</figcaption></figure> 上の図は、5つの要素を持つ配列を示しています。各要素のサイズは8バイトと仮定しています。</p> <p>メモリに連続して配置されているので、要素のサイズ分アドレスがずれて要素が配置されていることがわかるかと思います。</p> <p>連続して配置することによって、いくつかの特性や利点が生まれます。</p> <h3 id="ランダムアクセス可能">ランダムアクセス可能</h3> <p>ランダムアクセスは、インデックスを用いて任意の要素に直接アクセスする操作を指します。</p> <p>例えば、<strong><code>array[2]</code></strong>のような操作と考えれば馴染み深いと思います。</p> <p>このような直接アクセスが可能なのは、配列の要素がメモリ上に連続して配置されているためです。</p> <p>アクセスのメカニズムは単純で、<strong><code>array</code></strong>という変数には配列の先頭アドレスが格納されており、このアドレスに「要素のサイズ × インデックス」を加えることで、目的の要素のアドレスを瞬時に計算することができます。</p> <p><strong><code>array[3]</code></strong> というアクセスがあった場合:</p> <pre class="code" data-lang="" data-unlink>0x10000 + (8 × 3) = 0x10018</pre> <p>この計算でアドレスを求めることによって、O(1)の時間計算量で該当の要素にアクセスすることができます。</p> <p>配列のインデックスが0から始まる理由も、このメモリ上の連続した配置と密接に関連しています。「なぜ0から?」と初めは誰もが疑問に感じると思いますが、メモリの観点から考えると、アドレス計算を効率化するためであることが明白になります。</p> <h3 id="削除と挿入操作時のオーバーヘッドが大きい">削除と挿入操作時のオーバーヘッドが大きい</h3> <p>配列の末尾に対して、挿入や削除をする場合の時間計算量はO(1)です。</p> <p>しかし先頭や中間に挿入や削除を行う場合、追加の時間計算量がかかります。 <figure class="figure-image figure-image-fotolife" title="配列からindexが2の要素を削除する簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231016/20231016012147.png" width="1200" height="430" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>配列からindexが2の要素を削除する簡略図</figcaption></figure> 上図は、indexが2の要素を削除した場合の配列を示しています。</p> <p>削除すると、その位置に空きが生まれます。この空きを埋めるために、削除した要素以降の要素を全て1つ前にずらす必要が出てきます。この操作は、特に配列が大きい場合、大きなオーバーヘッドが発生します。</p> <p>今回のケースではindex3とindex4の要素を1つ前にずらすという操作が必要となります。</p> <p>このようなオーバーヘッドを考慮すると、頻繁に削除や挿入が行われるケースでは、配列を使うのではなく、他のデータ構造を使うべきだと言えます。</p> <h3 id="固定長">固定長</h3> <p>C, C++, Java, および Go などの配列は固定長であり、一度確保したサイズを途中で変更することはできません。主な理由は、配列が連続したメモリ領域に配置されているためです。</p> <p>拡張をしようとしても、配列の直後のメモリには別のデータが配置されている可能性があります。</p> <p>また、仮に配列を後に拡張するために余分にメモリを確保するとしても、それが後で使用されるかどうか予測することが難しく、メモリの浪費につながります。</p> <p>なので、データ長を動的に変えたい場合、配列は適したデータ構造ではありません。</p> <h2 id="連結リストの特徴">連結リストの特徴</h2> <p>連結リストは、複数のデータを管理するという観点では配列と同じですが、そのデータ構造は全く違います。</p> <p>配列はデータを連続したメモリ領域に確保しますが、連結リストはそのような連続性を保証しません。 <figure class="figure-image figure-image-fotolife" title="単方向連結リストとメモリの簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231016/20231016012835.png" width="1200" height="394" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>単方向連結リストとメモリの簡略図</figcaption></figure> 上図を見ると、それぞれの要素が非連続に配置されていることがわかります。</p> <p>連結リストの各要素(ノードとも呼ばれる)はデータと、次の要素への参照(ポインタ)を持っています。このポインタをたどることで、次の要素にアクセスすることができ、非連続でも複数データを管理することができます。</p> <p>このように非連続で配置することによって、いくつかの特性や利点が生まれます。</p> <h3 id="ランダムアクセスはできない">ランダムアクセスはできない</h3> <p>連結リストは、特定の要素への直接アクセスが配列と比べると非効率です。</p> <p>配列では<strong><code>array[3]</code></strong>のように特定のインデックスを指定して、O(1)の時間計算量でその要素にアクセスできます。</p> <p>一方、連結リストではデータが連続したメモリ領域に格納されていないため、ベースアドレスを使用してインデックス計算で要素のアドレスを直接特定(ランダムアクセス)することはできません。</p> <p>n番目の要素にアクセスするためには、連結リストの先頭から、指定された位置に達するまで各要素のポインタを逐次たどる必要があります。</p> <p>この操作はO(n)の時間計算量がかかることになります。</p> <p>そのため、特定の要素への高速なアクセスが必要な場合は、連結リストよりも配列を使用する方が適切です。</p> <h3 id="削除と挿入時のオーバーヘッドが小さい">削除と挿入時のオーバーヘッドが小さい</h3> <p>配列では先頭や中間への要素の挿入や削除に大きなオーバーヘッドがかかりますが、連結リストではこれらの操作をより小さいオーバーヘッドで行うことができます。 <figure class="figure-image figure-image-fotolife" title="連結リストからNode2を削除する簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231016/20231016013028.png" width="1200" height="892" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>連結リストからNode2を削除する簡略図</figcaption></figure> 上図ではNode2を削除していますが、配列のように削除したindex以降の要素をずらすという操作は必要ありません。</p> <p>Node1が持っていたNode2へのポインタをNode3へのポインタに付け替えるだけで削除の操作は完了します。</p> <p>このように、頻繁に削除や挿入が行われるケースは配列よりも連結リストの方が適しています。</p> <h3 id="可変長">可変長</h3> <p>連結リストは可変長のデータ構造です。</p> <p>配列と違い、データを連続して配置する必要はないので、拡張が必要な場合は都度空いているメモリにNodeを割り当て、連結リストを拡張することができます。</p> <p>動的なデータ長を扱う場合、配列よりも連結リストの方が適したデータ構造と言えます。</p> <h2 id="比較表">比較表</h2> <p>最後にそれぞれの特徴をまとめた表を置いておきます。</p> <table> <thead> <tr> <th> 特徴/操作 </th> <th> 配列 </th> <th> 連結リスト </th> </tr> </thead> <tbody> <tr> <td> メモリの配置 </td> <td> 連続 </td> <td> 非連続 </td> </tr> <tr> <td> ランダムアクセス </td> <td> O(1)(高速) </td> <td> O(n)(低速) </td> </tr> <tr> <td> 先頭や中間への要素の挿入・削除 </td> <td> O(n)(低速、要素をずらす必要がある) </td> <td> O(1)(高速、ポインタを更新するだけ) </td> </tr> <tr> <td> メモリ使用効率 </td> <td> 低い(固定長) </td> <td> 高い(可変長) </td> </tr> <tr> <td> サイズ変更 </td> <td> 困難/不可能(再確保とコピーが必要) </td> <td> 容易(要素の追加・削除でサイズ変更) </td> </tr> </tbody> </table> <h1 id="線形探索">線形探索</h1> <h2 id="線形探索について">線形探索について</h2> <p>今回の主題である、連結リストと配列において線形探索する時に、どちらの方が計算コストが少ないのかという問題を考える前に、線形探索について軽く触れておこうと思います。</p> <p>線型探索は検索のアルゴリズムの1つで、 連結リストや配列に入ったデータに対する検索を行うにあたって、 先頭から順に比較を行いそれが見つかれば終了するといったアルゴリズムです。</p> <p>優秀なアルゴリズムですが、先頭から片っ端に探していくので「馬鹿サーチ」とも呼ばれているみたいです。(ひどい…😭)</p> <p>ちなみに連結リストの節で</p> <blockquote><p>n番目の要素にアクセスするためには、連結リストの先頭から、指定された位置に達するまで各要素のポインタを逐次たどる必要があります。</p></blockquote> <p>このように、ランダムアクセスではなく先頭から逐次辿る必要があると述べましたが、まさにこれが線形探索となります。</p> <h2 id="どちらの方が計算コストが低いのか">どちらの方が計算コストが低いのか</h2> <p>線形探索自体の時間計算量はO(n)となりますが、実際の実行速度はデータ構造によって異なります。</p> <p>配列と連結リストでは、どちらの方が速く探索できるのかを、実際にコードを書いて検証してみます。</p> <p>なお今回はC++を使うことにします。</p> <pre class="code lang-cpp" data-lang="cpp" data-unlink><span class="synPreProc">#include </span><span class="synConstant">&lt;iostream&gt;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;forward_list&gt;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;random&gt;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;algorithm&gt;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;chrono&gt;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;vector&gt;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;numeric&gt;</span> <span class="synPreProc">#include </span><span class="synConstant">&lt;cmath&gt;</span> <span class="synComment">// Generate random data for testing</span> <span class="synType">template</span>&lt;<span class="synType">typename</span> Container&gt; <span class="synType">void</span> <span class="synIdentifier">generate_data</span>(Container &amp;c, <span class="synType">size_t</span> size) { <span class="synConstant">std</span>::<span class="synType">random_device</span> rd; <span class="synConstant">std</span>::<span class="synType">mt19937</span> <span class="synIdentifier">mt</span>(<span class="synIdentifier">rd</span>()); <span class="synConstant">std</span>::<span class="synType">uniform_int_distribution</span>&lt;<span class="synType">int</span>&gt; <span class="synIdentifier">dist</span>(<span class="synConstant">1</span>, <span class="synConstant">1000000</span>); c.<span class="synIdentifier">resize</span>(size); <span class="synConstant">std</span>::<span class="synIdentifier">generate</span>(c.<span class="synIdentifier">begin</span>(), c.<span class="synIdentifier">end</span>(), [&amp;mt, &amp;dist]() { <span class="synStatement">return</span> <span class="synIdentifier">dist</span>(mt); }); } <span class="synType">template</span>&lt;<span class="synType">typename</span> Container&gt; <span class="synType">void</span> <span class="synIdentifier">linear_search_test</span>(<span class="synType">const</span> Container &amp;c, <span class="synType">const</span> <span class="synConstant">std</span>::<span class="synType">string</span> &amp;type) { <span class="synType">const</span> <span class="synType">int</span> repeat_count = <span class="synConstant">200</span>; <span class="synConstant">std</span>::<span class="synType">vector</span>&lt;<span class="synType">long</span> <span class="synType">long</span>&gt; times; <span class="synStatement">for</span> (<span class="synType">int</span> i = <span class="synConstant">0</span>; i &lt; repeat_count; ++i) { <span class="synType">auto</span> start = <span class="synConstant">std</span>::<span class="synConstant">chrono</span>::<span class="synType">system_clock</span>::<span class="synIdentifier">now</span>(); <span class="synComment">// Perform a dummy operation to prevent loop optimization</span> <span class="synType">int</span> sum = <span class="synConstant">0</span>; <span class="synStatement">for</span> (<span class="synType">const</span> <span class="synType">auto</span> &amp;val: c) { sum += val; } <span class="synType">auto</span> end = <span class="synConstant">std</span>::<span class="synConstant">chrono</span>::<span class="synType">system_clock</span>::<span class="synIdentifier">now</span>(); <span class="synType">auto</span> elapsed = <span class="synConstant">std</span>::<span class="synConstant">chrono</span>::<span class="synIdentifier">duration_cast</span>&lt;<span class="synConstant">std</span>::<span class="synConstant">chrono</span>::<span class="synType">microseconds</span>&gt;(end - start); times.<span class="synIdentifier">push_back</span>(elapsed.<span class="synIdentifier">count</span>()); } <span class="synType">double</span> mean_time = <span class="synConstant">std</span>::<span class="synIdentifier">accumulate</span>(times.<span class="synIdentifier">begin</span>(), times.<span class="synIdentifier">end</span>(), <span class="synConstant">0.0</span>) / repeat_count; <span class="synType">double</span> sq_sum = <span class="synConstant">std</span>::<span class="synIdentifier">inner_product</span>(times.<span class="synIdentifier">begin</span>(), times.<span class="synIdentifier">end</span>(), times.<span class="synIdentifier">begin</span>(), <span class="synConstant">0.0</span>); <span class="synType">double</span> stdev_time = <span class="synConstant">std</span>::<span class="synIdentifier">sqrt</span>(sq_sum / repeat_count - mean_time * mean_time); <span class="synType">double</span> cv = stdev_time / mean_time * <span class="synConstant">100</span>; <span class="synConstant">std</span>::<span class="synIdentifier">cout</span> &lt;&lt; <span class="synConstant">&quot;Type: &quot;</span> &lt;&lt; type &lt;&lt; <span class="synConstant">&quot;</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>; <span class="synConstant">std</span>::<span class="synIdentifier">cout</span> &lt;&lt; <span class="synConstant">&quot;Average time: &quot;</span> &lt;&lt; mean_time &lt;&lt; <span class="synConstant">&quot;μs</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>; <span class="synConstant">std</span>::<span class="synIdentifier">cout</span> &lt;&lt; <span class="synConstant">&quot;Time stddev: &quot;</span> &lt;&lt; stdev_time &lt;&lt; <span class="synConstant">&quot;μs</span><span class="synSpecial">\n</span><span class="synConstant">&quot;</span>; <span class="synConstant">std</span>::<span class="synIdentifier">cout</span> &lt;&lt; <span class="synConstant">&quot;Coefficient of variation: &quot;</span> &lt;&lt; cv &lt;&lt; <span class="synConstant">&quot;%</span><span class="synSpecial">\n\n</span><span class="synConstant">&quot;</span>; } <span class="synType">int</span> <span class="synIdentifier">main</span>() { <span class="synType">const</span> <span class="synType">size_t</span> data_size = <span class="synConstant">10000000</span>; <span class="synConstant">std</span>::<span class="synType">vector</span>&lt;<span class="synType">int</span>&gt; vec; <span class="synConstant">std</span>::<span class="synType">forward_list</span>&lt;<span class="synType">int</span>&gt; <span class="synType">list</span>(data_size); <span class="synIdentifier">generate_data</span>(vec, data_size); <span class="synIdentifier">generate_data</span>(<span class="synType">list</span>, data_size); <span class="synIdentifier">linear_search_test</span>(vec, <span class="synConstant">&quot;Vector&quot;</span>); <span class="synIdentifier">linear_search_test</span>(<span class="synType">list</span>, <span class="synConstant">&quot;Forward List&quot;</span>); <span class="synStatement">return</span> <span class="synConstant">0</span>; } </pre> <p>これは、1000万個の要素を持つ連結リストと配列に対して、線形探索を200回ずつ行いそれぞれにかかった時間を計測するコードです。</p> <p>結果の前に、計測するにあたって考慮した点を軽く紹介していこうと思います。</p> <h3 id="配列はarrayではなくvectorを使う">配列はarrayではなくvectorを使う</h3> <p>今回は、配列の表現に、<code>std::array</code>ではなく<code>std::vector</code>を使うようにしました。</p> <p>これは、連結リストと同じメモリ領域を使うためです。</p> <p><code>std::array</code>と<code>std::vector</code>はどちらも配列を表すデータ構造ですが、使われるメモリ領域が異なります。</p> <p><code>std::array</code>はコンパイル時にサイズが確定するのでスタック領域に割り当てられますが、</p> <p><code>std::vector</code>はヒープ領域に割り当てられます。</p> <p>それぞれのメモリ領域に軽く触れておくと :</p> <ul> <li><strong>スタック領域</strong>は、通常は関数の呼び出しや局所変数の保存に利用されるメモリ領域で、アクセス速度が速い</li> <li><strong>ヒープ領域</strong>は、動的にデータを割り当てる際に使用されるメモリ領域で、メモリアクセスの速度はスタックに比べて遅い</li> </ul> <p>となっています。</p> <p>この検証では、「メモリ領域の違い」ではなく「メモリの連続性」に焦点を当てています。</p> <p>なので、配列と連結リストで使用するメモリ領域を揃えることで、メモリの連続性による影響を強調できるようにしました。</p> <h3 id="コンパイラがループを最適化しないようにした">コンパイラがループを最適化しないようにした</h3> <p>線形探索に該当するループの部分で<code>sum</code>という変数に加算を行っています。</p> <p>これは、何もループ内に処理を書かなければ、この部分の実行はする必要がないとコンパイラに判断されその部分は省略(最適化)されてしまうため、最適化されないようにダミーの処理を追加しています。</p> <h3 id="結果">結果</h3> <p>こちらが検証に使用したマシンのスペックです。</p> <ul> <li><strong>CPU</strong>: Apple M2 Max</li> <li><strong>RAM</strong>: 64GB</li> <li><strong>OS</strong>: Ventura 13.4</li> </ul> <p>このマシンでそれぞれ200回実行した結果は次のようになりました。</p> <pre class="code lang-cpp" data-lang="cpp" data-unlink><span class="synStatement">Type</span>: Vector Average time: <span class="synConstant">47886.5</span>μs Time stddev: <span class="synConstant">1139.39</span>μs Coefficient of variation: <span class="synConstant">2.3794</span>% <span class="synStatement">Type</span>: Forward List Average time: <span class="synConstant">60295.2</span>μs Time stddev: <span class="synConstant">1518.22</span>μs Coefficient of variation: <span class="synConstant">2.5180</span>% </pre> <p>結果から明らかなのは、配列(Vector)と連結リスト(Forward List)の平均実行時間には約12,000マイクロ秒の差があるという点です。また、両方のデータ構造における係数の変動(Coefficient of Variation)が低いことから、このデータは一定の信頼性を持っていると判断できます。</p> <p>(マシンやリソースの状況によって結果は異なりますが)</p> <p>では、なぜ時間計算量が両方ともO(n)であるにも関わらず、このような実行時間の差が生まれるのでしょうか?</p> <p>これは、データのメモリ配置に関わる「キャッシュ効率」が大きな要因の1つとして挙げられます。</p> <p>連続したメモリ領域に配置された配列は、キャッシュメモリにおいて高いヒット率を示すため、全体として計算コストが削減されるのです。</p> <p>なぜ配列の方がヒット率が高くなるのでしょうか?</p> <p>ここで登場したキャッシュメモリが持つこれらの性質と、なぜヒット率が異なるのかを次の章で詳しく見ていきます。</p> <h1 id="キャッシュメモリ">キャッシュメモリ</h1> <h2 id="キャッシュメモリの概要">キャッシュメモリの概要</h2> <p><figure class="figure-image figure-image-fotolife" title="データAをメインメモリからフェッチするときの簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231016/20231016013736.png" width="1200" height="680" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>データAをメインメモリからフェッチするときの簡略図</figcaption></figure></p> <p>キャッシュメモリは、CPU(処理装置)とメインメモリ(主記憶装置)の中間に位置する、高速なデータアクセスが可能な小さな記憶装置です。</p> <p>ノイマン型アーキテクチャのコンピュータでは、CPUとメインメモリの処理性能には性能差があります。</p> <p>いくらCPUの性能を高めようと、そのCPUの処理性能にメインメモリのアクセス速度が追いつけません。 そのため、メインメモリからデータをフェッチする速度の遅さがボトルネックとなり、システム全体の処理速度も遅くなります。</p> <p>この現象は「ノイマンボトルネック」と呼ばれています。</p> <p>キャッシュメモリは、両者の処理性能差を埋め合わせることでこの問題の解決を図ります。</p> <p>具体的には、処理装置がアクセスしたデータやアドレスなどをキャッシュメモリに一時的に保持しておくことで、次に同じデータやアドレスにアクセスがあった場合、メインメモリではなくキャッシュメモリから迅速にデータをフェッチできます。</p> <p>これにより、メインメモリへのアクセスが削減され、データフェッチの効率の向上、高速化が見込めます。</p> <p>キャッシュメモリはL1,L2,L3といった複数のレベルを持っています。</p> <p>記憶階層において、このレベルの数字が小さいほどCPUに近く高速になります。</p> <h3 id="補足--記憶階層とは">補足 : 記憶階層とは?</h3> <p><figure class="figure-image figure-image-fotolife" title="記憶階層の簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231016/20231016013943.png" width="1200" height="791" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>記憶階層の簡略図</figcaption></figure></p> <p>CPUにとって、望ましい記憶装置は大容量で高速にアクセスできるものです。</p> <p>しかし、実際の記憶装置は大容量であるほどアクセス速度が遅くなり、小容量であるほど高速になるというトレードオフがあるので、望むような記憶装置は現状作ることができません。</p> <p>そこで図のように記憶装置を階層化し、各階層を下位の階層のキャッシュとして用いることで、CPUから見た場合、あたかも大容量で高速な1つの記憶装置であるかのように振舞います。</p> <p>このように階層化された記憶システムを、記憶階層と言います。</p> <h2 id="なぜ配列の方がヒット率が高いのか">なぜ配列の方がヒット率が高いのか</h2> <p>配列が連結リストと比べてキャッシュメモリのヒット率が高くなる理由は、その連続したメモリ構造とキャッシュメモリのデータ取得方法に密接な関連があります。</p> <p>キャッシュメモリは「キャッシュライン」という一定の単位でデータを一時的に保持しています。</p> <p>キャッシュラインのサイズは、プロセッサのアーキテクチャやモデルによって異なりますが、32、64、または128バイトが一般的です。</p> <p>メインメモリからデータAをフェッチする際、キャッシュメモリはデータAだけでなく、その近傍のデータも合わせて保持します。したがって、次に近傍のデータにアクセスする際はヒットする確率が高くなります。 <figure class="figure-image figure-image-fotolife" title="array[0]のデータをフェッチする簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20231016/20231016014155.png" width="1200" height="662" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>array[0]のデータをフェッチする簡略図</figcaption></figure></p> <p>例として、上図の<strong><code>array[0]</code></strong>の要素をフェッチする場合を考えてみます。</p> <p><strong><code>array[0]</code></strong>をメモリからフェッチする際、その近傍のデータもキャッシュライン(ここでは例として32バイト)としてキャッシュメモリに一時保存します。</p> <p>すると、4つ全ての要素がキャッシュメモリに保持され、次にこれらの要素にアクセスする際は、メインメモリよりも高速なキャッシュメモリからデータをフェッチすることができます。</p> <p>これは、配列のメモリ上での連続した配置と、キャッシュメモリがキャッシュライン単位で近傍のデータも一緒に保存する性質とが組み合わさって実現されます。</p> <p>一方で、連結リストではメモリ上に非連続に配置されているため、キャッシュミスが配列に比べて頻発します。</p> <p>これが、配列と連結リストの間でキャッシュヒット率が異なる理由です。</p> <p>また、このキャッシュラインによる近傍データの同時フェッチの考え方は、「空間的局所性」というコンピューティングの性質に基づいています。</p> <h3 id="空間的局所性">空間的局所性</h3> <p>空間的局所性は、一度アクセスされたデータの近傍が近いうちに再びアクセスされる可能性が高い、というプログラムの実行特性を指します。</p> <p>キャッシュメモリの設計において、キャッシュラインがこの特性に基づいています。</p> <p>具体的には、一度データをフェッチすると、その近くにあるデータも一緒にキャッシュに取り込むことで、次のアクセスが高速になる可能性が高くなります。</p> <h3 id="時間的局所性">時間的局所性</h3> <p>時間的局所性は本例では触れられていませんが、こちらも重要な特性ですので補足として紹介しておきます。</p> <p>時間的局所性とは、一度アクセスされたデータが近いうちに再度アクセスされる可能性が高いという特性を指します。</p> <p>webブラウザにおいて、「戻る」ボタンを押す行動もこの一例となります。</p> <p>ブラウザは、この操作を高速に行うため、以前アクセスしたページのデータをキャッシュしており、これは時間的局所性に基づいたものになります。</p> <h1 id="おわりに">おわりに</h1> <h2 id="結論-1">結論</h2> <p>配列が連結リストと比較して、線形探索の際のメモリアクセスのコストが低いことがわかりました。</p> <p>この違いは、配列のほうがキャッシュメモリに対して高いヒット率を示すためです。</p> <p>キャッシュメモリが高いヒット率を持つ背後には、キャッシュメモリが「空間的局所性」の原理に基づいて設計されていること、及び配列のデータがメモリ上で連続して配置される特性があります。</p> <p>具体的には、キャッシュメモリはキャッシュラインと呼ばれる単位で近傍のデータも一緒に保持し、これによって近くのデータにアクセスする際もキャッシュから高速にデータを取得することができるので、結果的に配列の線形探索の方が早くなったのでした。</p> <p>このようにハードウェアの特性や概念を知ることで、コンピュータリソースをより効率的に扱えるようになることにつながります。</p> <p>Web開発においては、「業務で使わないから」という理由で低レイヤーやアルゴリズムの勉強が軽視されることがありますが、こういった抽象化されている部分を勉強することは、</p> <p>普段使っている技術への理解が深まりスキルの向上につながると思うので、引き続きこういった内容のブログを発信していこうと思っています!</p> <p>また、キャッシュメモリは他にもデータの一貫性を保つための「キャッシュコヒーレンシ」など、面白い仕組みがまだまだあるので、機会があればこちらもまた書いていこうと思います〜!</p> junya555 コネヒト株式会社は PHP Conference Japan 2023 にゴールドスポンサーとして参加しました!当日の様子をご紹介します! hatenablog://entry/6801883189049965020 2023-10-13T11:59:56+09:00 2023-10-13T11:59:56+09:00 こんにちは @ryoです! コネヒトは今回 PHP Conference Japan 2023にゴールドスポンサーとして協賛してブースを出展させていただきました。 本ブログでは当日の様子や感想などをご紹介していきたいと思います! PHP Conference Japan 2023 とは PHP Conference 2023 は2023年10月8日(日)に開催された国内最大級のPHPイベントです! phpcon.php.gr.jp ブースの出展 今回コネヒトはブースを出展させていただきました。 ブースの内容は以前ブログで紹介したママリドリルとシステム開発に関するアンケートです。 tech.co… <p>こんにちは <a href="https://twitter.com/ryo23526217">@ryo</a>です!</br></p> <p>コネヒトは今回 PHP Conference Japan 2023にゴールドスポンサーとして協賛してブースを出展させていただきました。</br> 本ブログでは当日の様子や感想などをご紹介していきたいと思います!</p> <h2 id="PHP-Conference-Japan-2023-とは">PHP Conference Japan 2023 とは</h2> <p>PHP Conference 2023 は2023年10月8日(日)に開催された国内最大級のPHPイベントです!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fphpcon.php.gr.jp%2F2023%2F" title="PHP Conference Japan 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://phpcon.php.gr.jp/2023/">phpcon.php.gr.jp</a></cite></p> <h2 id="ブースの出展">ブースの出展</h2> <p>今回コネヒトはブースを出展させていただきました。</br> ブースの内容は以前ブログで紹介したママリドリルとシステム開発に関するアンケートです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F10%2F04%2F122344" title="【告知】コネヒトはPHP Conference Japan 2023にゴールドスポンサーとして協賛します! - コネヒト開発者ブログ" 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.connehito.com/entry/2023/10/04/122344#%E3%83%96%E3%83%BC%E3%82%B9%E3%82%92%E5%87%BA%E5%B1%95%E3%81%97%E3%81%BE%E3%81%99">tech.connehito.com</a></cite></p> <p>ママリドリルやアンケートにお答えいただいた方には、ノベルティとしてママリオリジナルデザインのチロルチョコをお渡ししました!</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/ryoutakaya3623/20231012/20231012234813.jpg" width="675" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>今年のPHPerKaigi 2023でブースを出展していた事もあり前回の反省点や経験を活かしスムーズに事前準備を進める事ができました。</p> <h2 id="当日の様子">当日の様子</h2> <p>当日は大田区産業プラザPiOで行われました。</br>スポンサーブースを出展する場所とセッションを発表する場所が一体となった空間だったのでとても広かったですが、全てのスポンサー様のブースが一箇所に集まっていて準備の段階で熱気が高くとてもワクワクしました!!</p> <p>そして開場してからはたくさんの方がコネヒトのブースに来てくれました。本当にありがとうございます! <figure class="figure-image figure-image-fotolife" title="ブースでおもてなしをするコネヒトメンバー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/ryoutakaya3623/20231012/20231012235142.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ブースにておもてなしをするコネヒトメンバー</figcaption></figure></p> <p>ブースに来ていただいた参加者の皆様とママリついてのお話しや現在のシステム開発の事についてお互いの事情などをたくさんお話しさせていただくことができました!</br> 他にもコネヒトメンバーはセッションを聞きにいったり他のスポンサーブースへお邪魔したりとても有意義な時間を過ごすことが出来て楽しかったです!</p> <p>そして皆様から回答を頂いたアンケート結果はこちらです。</p> <h3 id="使っているPHPの主なバージョンは">使っているPHPの主なバージョンは?</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/ryoutakaya3623/20231012/20231012164653.jpg" width="900" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>8.2.x系 47票</li> <li>8.x 71票</li> <li>7系 74票</li> <li>5系 21票</li> </ul> <h3 id="好きなエディタは">好きなエディタは?</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/r/ryoutakaya3623/20231012/20231012164721.jpg" width="900" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <ul> <li>Visual Studio Code 106票</li> <li>PhpStorm 81票</li> <li>Vim 25票</li> <li>Emacs 5票</li> <li>サクラエディタ 10票</li> <li>秀丸エディタ 8票</li> <li>その他7</li> </ul> <p>個人的には好きなエディタはPhpStormがトップになるかなと予想していましたが汎用性抜群のVisual Studio Codeがトップになりました!Visual Studio Code強し。</p> <h2 id="感想">感想</h2> <p>今回もたくさんの参加者の皆様と交流することができ満足したカンファレンスとなりました!!</br> これからもコネヒトはPHPコミュニティへの貢献を続けていきたい思います!</br> <a href="https://twitter.com/ryo23526217">@ryo</a>個人の感想としては今回のPHP Conference Japan 2023にプロポーザルを出したのですが残念ながら不採択だったので次回はリベンジできるように頑張りたいと思います!</p> ryoutakaya3623 【告知】コネヒトはPHP Conference Japan 2023にゴールドスポンサーとして協賛します! hatenablog://entry/820878482972420850 2023-10-04T12:23:44+09:00 2023-10-04T12:23:44+09:00 こんにちは!高谷です。普段は主にPHPを書いています。 本日は弊社が参加するPHP Conference Japan 2023にスポンサーとして参加するお知らせです。 コネヒトはPHP Conference Japan 2023に協賛いたします! コネヒトではメインプロダクトである「ママリ」を始めとして開発のメイン言語としてPHPを活用しており、フレームワークとしてはCakePHPを採用しています。 そんなPHPを愛用しているコネヒトですがこの度ゴールドスポンサーとして協賛させていただくことになりました。 phpcon.php.gr.jp イベント概要 開催日 2023年10月8日(日) 場所… <p>こんにちは!高谷です。普段は主にPHPを書いています。</p> <p>本日は弊社が参加するPHP Conference Japan 2023にスポンサーとして参加するお知らせです。</p> <h3 id="コネヒトはPHP-Conference-Japan-2023に協賛いたします">コネヒトはPHP Conference Japan 2023に協賛いたします!</h3> <p>コネヒトではメインプロダクトである「ママリ」を始めとして開発のメイン言語としてPHPを活用しており、フレームワークとしてはCakePHPを採用しています。</br> そんなPHPを愛用しているコネヒトですがこの度ゴールドスポンサーとして協賛させていただくことになりました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fphpcon.php.gr.jp%2F2023%2F" title="PHP Conference Japan 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://phpcon.php.gr.jp/2023/">phpcon.php.gr.jp</a></cite></p> <h3 id="イベント概要">イベント概要</h3> <ul> <li><strong>開催日</strong> 2023年10月8日(日)</li> <li><strong>場所</strong> 大田区産業プラザPiO</li> <li><strong>主催</strong> PHPカンファレンス 2023 実行委員会</li> <li><strong>公式HP</strong> <a href="https://phpcon.php.gr.jp/2023/">https://phpcon.php.gr.jp/2023/</a></li> <li><strong>タイムテーブル</strong> <a href="https://fortee.jp/phpcon-2023/timetable">https://fortee.jp/phpcon-2023/timetable</a></li> </ul> <h3 id="ブースを出展します">ブースを出展します!</h3> <p>今年はコネヒトは企業ブースも出します! 企画としては</p> <p><strong>1. あなたは何問正解できる!? ママリドリル</strong></p> <p>(ママリドリルについてはこちらからどうぞ)</p> <p><a href="https://tech.connehito.com/entry/2023/03/31/170555#ママリドリルって何">&#x30B3;&#x30CD;&#x30D2;&#x30C8;&#x682A;&#x5F0F;&#x4F1A;&#x793E;&#x306F; PHPerKaigi 2023 &#x306B;&#x30B7;&#x30EB;&#x30D0;&#x30FC;&#x30B9;&#x30DD;&#x30F3;&#x30B5;&#x30FC;&#x3068;&#x3057;&#x3066;&#x53C2;&#x52A0;&#x3057;&#x307E;&#x3057;&#x305F;&#xFF01;&#x30B9;&#x30DD;&#x30F3;&#x30B5;&#x30FC;&#x30D6;&#x30FC;&#x30B9;&#x6E96;&#x5099;&#x3068;&#x5F53;&#x65E5;&#x306E;&#x69D8;&#x5B50;&#x3092;&#x3054;&#x7D39;&#x4ECB;&#x3057;&#x307E;&#x3059;&#xFF01; - &#x30B3;&#x30CD;&#x30D2;&#x30C8;&#x958B;&#x767A;&#x8005;&#x30D6;&#x30ED;&#x30B0;</a></p> <p><strong>2. システム開発に関するアンケート</strong></p> <p>を予定しております!</p> <p>ママリドリルとアンケートにお答えいただくとノベルティのチロルチョコがもらえます! 可愛いチロルチョコになっているので、ぜひぜひご参加ください🙌</p> <h3 id="最後に">最後に</h3> <p>コネヒトでは共に開発を進めてくれる頼れるPHPerを積極募集中です!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fhrmos.co%2Fpages%2Fconnehito%2Fjobs%2F00b" title="【PHP】「質とスピード」を意識して中長期でユーザーに向き合いたいバックエンドエンジニア募集 | コネヒト株式会社" 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://hrmos.co/pages/connehito/jobs/00b">hrmos.co</a></cite></p> ryoutakaya3623 SOCIインデックスによるECSのデプロイ時間短縮について検証しました hatenablog://entry/820878482970852395 2023-10-03T14:13:20+09:00 2023-10-03T14:13:20+09:00 こんにちは。2023年7月に入社しました、開発部プラットフォームグループ インフラエンジニアの @yosshi です。今回はSOCIインデックスによるECSのデプロイ時間の短縮効果について検証を行ったので、その検証内容と結果を共有したいと思います。 デプロイ時間短縮の方法は様々あると思いますが、SOCIインデックスはイメージやデプロイフローに手を入れずに、比較的簡単に取り入れられそうだったので導入を検討しました。 SOCI(Seekable OCI)インデックスとは SOCIインデックスとは、イメージの遅延読み込み(非同期読み込み)を可能にする技術です。イメージ全体をダウンロードする前にコンテ… <p>こんにちは。2023年7月に入社しました、開発部プラットフォームグループ インフラエンジニアの <strong><a href="https://twitter.com/yosshi8448">@yosshi</a></strong> です。今回はSOCIインデックスによるECSのデプロイ時間の短縮効果について検証を行ったので、その検証内容と結果を共有したいと思います。</p> <p>デプロイ時間短縮の方法は様々あると思いますが、SOCIインデックスはイメージやデプロイフローに手を入れずに、比較的簡単に取り入れられそうだったので導入を検討しました。</p> <h1 id="SOCISeekable-OCIインデックスとは">SOCI(Seekable OCI)インデックスとは</h1> <p>SOCIインデックスとは、イメージの遅延読み込み(非同期読み込み)を可能にする技術です。イメージ全体をダウンロードする前にコンテナイメージから個々のファイルを抽出できるようにし、コンテナを高速に起動することができます。</p> <p>既存のコンテナイメージにあるファイルのインデックス (SOCI インデックス) を作成することによって機能します。</p> <p>利用条件</p> <ul> <li>コンテナイメージが<span style="color: #d32f2f"><code>x86_64</code></span>もしくは<span style="color: #d32f2f"><code>ARM64</code></span>アーキテクチャであること</li> <li>Linuxプラットフォームバージョンが<span style="color: #d32f2f"><code>1.4.0</code></span>であること</li> </ul> <p>参考記事</p> <ul> <li><strong><a href="https://docs.aws.amazon.com/AmazonECS/latest/userguide/container-considerations.html?trk=3d9bf291-787b-4280-bb00-c4a8e441a748">Container images - Amazon ECS</a></strong></li> <li><strong><a href="https://aws.amazon.com/jp/about-aws/whats-new/2023/07/aws-fargate-container-startup-seekable-oci/">AWS Fargate で Seekable OCI を使用してコンテナ起動の高速化を実現</a></strong></li> </ul> <h2 id="SOCIインデックスの適用方法">SOCIインデックスの適用方法</h2> <p>SOCIインデックスを作成する方法は、公式で用意されているAWS SOCI Index Builderを使用する方法と、手動で作成する方法の2パターンがありますが、今回はより簡単に導入できそうなAWS SOCI Index Builderを使用する方法を選択しました。</p> <p>AWS SOCI Index Builderは、 AWS クラウド内のコンテナイメージのインデックスを作成するためのサーバーレスソリューションで、公式よりCloudFormaitonテンプレートが用意されています。</p> <p>今回はこのCloudFormationテンプレートを利用し、Terraformで適用していきます。<br/> 以下のようなイメージです。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudformation_stack&quot;</span> <span class="synConstant">&quot;soci_index_builder&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;soci-index-builder&quot;</span> <span class="synIdentifier">template_url</span> = <span class="synConstant">&quot;https://aws-quickstart.s3.us-east-1.amazonaws.com/cfn-ecr-aws-soci-index-builder/templates/SociIndexBuilder.yml&quot;</span> &lt;span style=<span class="synConstant">&quot;color: #d32f2f&quot;</span>&gt;<span class="synComment">#公式で用意されているSOCIのテンプレートのパス&lt;/span&gt;</span> <span class="synIdentifier">capabilities</span> = <span class="synSpecial">[</span><span class="synConstant">&quot;CAPABILITY_IAM&quot;</span><span class="synSpecial">]</span> <span class="synIdentifier">parameters</span> = <span class="synSpecial">{</span> <span class="synConstant">&quot;SociRepositoryImageTagFilters&quot;</span> = <span class="synConstant">&quot;&lt;リポジトリ名&gt;:&lt;タグ名&gt;&quot;</span> <span class="synComment">#SOCIインデックスの適用範囲のフィルターをかける</span> <span class="synSpecial">}</span> } </pre> <h2 id="AWS-SOCI-Index-Builderのアーキテクチャ">AWS SOCI Index Builderのアーキテクチャ</h2> <p>EventBridge・Lambda・IAM・CloudWatchなどのリソースが作成されます。<br/> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yosshi8448/20230926/20230926194415.png" width="838" height="432" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> 引用: <em><a href="https://aws-ia.github.io/cfn-ecr-aws-soci-index-builder/">https://aws-ia.github.io/cfn-ecr-aws-soci-index-builder/</a></em></p> <p><strong>SOCIインデックス作成の流れ</strong></p> <ol> <li>ECRイメージアクションイベントを検出しフィルタリング用のAWS Lambdaを呼び出す。</li> <li>CloudFormationのパラメータで提供されたフィルタに一致するイメージアクションイベントをフィルタリング。</li> <li>一致するイメージのSOCIインデックスを生成し、ECRレジストリのイメージリポジトリにインデックスをプッシュバックする。</li> </ol> <p>作成される各リソースの詳細を知りたい方は引用元のリンク先をご確認ください。</p> <h1 id="検証方法">検証方法</h1> <p>SOCIインデックスの検証にあたり、今回2段階で実施しました。</p> <ol> <li><p>SOCIインデックスの効果検証</p> <ul> <li>実際のDev(開発)環境に適用する前に、まずはSOCIインデックスに本当に効果がありそうか調べました。</li> <li>現行のDev環境と同一のECSクラスター内にSOCIインデックス適用済みの別サービスを立て現行と起動時間の比較をしています。</li> </ul> </li> <li>現行の開発環境での動作検証 <ul> <li>検証1でSOCIインデックスの一定の効果が見られたので、実際に使用している開発環境にSOCIインデックスを適用し比較しました。</li> </ul> </li> </ol> <p>それでは検証内容について詳しく説明していきます。</p> <h2 id="前提条件">前提条件</h2> <p>弊社では以下環境を使用しています 。</p> <ul> <li>ECS on Fargate</li> <li>AWSリソースの反映:Terraform</li> <li>ECSのデプロイ:<a href="https://github.com/kayac/ecspresso">ecspresso</a></li> </ul> <h2 id="検証1SOCIインデックスの効果検証">検証1:SOCIインデックスの効果検証</h2> <p>まずはDev環境の同一クラスター内にSOCIインデックス適用済み別サービスを立てて現行との起動時間を比較しました。</p> <p>リソースの作成はTerraformで行っています。まずは必要なリソースを作成していきます。</p> <p><strong>ECRリポジトリ</strong></p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_ecr_repository&quot;</span> <span class="synConstant">&quot;example_soci_test&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;example-soci-test&quot;</span> <span class="synType">image_scanning_configuration</span> <span class="synSpecial">{</span> <span class="synIdentifier">scan_on_push</span> = <span class="synConstant">true</span> <span class="synSpecial">}</span> } </pre> <p><strong>SOCI Index BuilderのCloudFormationテンプレート</strong><br/> example-soci-testリポジトリの全てにSOCIインデックスを適用するようフィルターを設定しています。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudformation_stack&quot;</span> <span class="synConstant">&quot;soci_index_builder&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;soci-index-builder&quot;</span> <span class="synIdentifier">template_url</span> = <span class="synConstant">&quot;https://aws-quickstart.s3.us-east-1.amazonaws.com/cfn-ecr-aws-soci-index-builder/templates/SociIndexBuilder.yml&quot;</span> <span class="synIdentifier">capabilities</span> = <span class="synSpecial">[</span><span class="synConstant">&quot;CAPABILITY_IAM&quot;</span><span class="synSpecial">]</span> <span class="synIdentifier">parameters</span> = <span class="synSpecial">{</span> <span class="synConstant">&quot;SociRepositoryImageTagFilters&quot;</span> = <span class="synConstant">&quot;example-soci-test:*&quot;</span> <span class="synSpecial">}</span> } </pre> <p>Terraformを使用し作成したリソースを適用します。</p> <pre class="code bash" data-lang="bash" data-unlink>$ terraform apply</pre> <p>次に作成したECRリポジトリにイメージをプッシュします。</p> <p>Dockerfileは従来のDev環境と同じものを使用しています。</p> <p>イメージのビルド</p> <pre class="code bash" data-lang="bash" data-unlink>$ docker build . -t example-soci-test:latest</pre> <p>リポジトリにイメージをプッシュできるように、イメージにタグ付け</p> <pre class="code bash" data-lang="bash" data-unlink>$ docker tag example-soci-test:latest xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/example-soci-test:latest</pre> <p>ECRにイメージをプッシュ</p> <pre class="code bash" data-lang="bash" data-unlink>$ docker push xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/example-soci-test:latest</pre> <p>AWSコンソール上でECRの対象リポジトリの画面から、作成したイメージに対して、アーティファクトタイプがSoci Index, Image Indexのイメージが別途作成されていることがわかります。この作成されたSOCIインデックス適用済みのイメージを使用します。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yosshi8448/20230929/20230929170026.png" width="1200" height="352" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>次にこのSOCIインデックス適用済みのイメージを使用しECS環境へデプロイします。</p> <p>弊社ではデプロイツールとしてecspressoを使用しているため、ecspressoを利用しローカルからデプロイしたいと思います。(ecspressoを使用していない場合は別の方法でのデプロイを実施してください)</p> <p>ecspresso initで既存のDev環境の設定ファイルをローカルに持ってきます。</p> <pre class="code bash" data-lang="bash" data-unlink>$ ecspresso init \ --config config.yaml \ --region ap-northeast-1 \ --cluster &lt;Dev環境のクラスター名&gt; \ --service &lt;Dev環境のサービス名&gt;</pre> <p>実行することでローカルに3つのファイルが作成されます。</p> <ul> <li>config.yaml</li> <li>ecs-service-def.json</li> <li>ecs-task-def.json</li> </ul> <p>作成されたファイルの内容を今回テストする内容に書き換えます。</p> <ul> <li>config.yaml</li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">region</span><span class="synSpecial">:</span> ap-northeast-1 <span class="synIdentifier">cluster</span><span class="synSpecial">:</span> example-cluster<span class="synComment"> #Dev環境と同じクラスターを使用</span> <span class="synIdentifier">service</span><span class="synSpecial">:</span> example-soci-test-service<span class="synComment"> #SOCIインデックス適用するためのサービス</span> <span class="synIdentifier">service_definition</span><span class="synSpecial">:</span> ecs-service-def.json <span class="synIdentifier">task_definition</span><span class="synSpecial">:</span> ecs-task-def.json <span class="synIdentifier">timeout</span><span class="synSpecial">:</span> <span class="synConstant">&quot;10m0s&quot;</span> </pre> <ul> <li>ecs-service-def.json</li> </ul> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">capacityProviderStrategy</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">base</span>&quot;: <span class="synConstant">1</span>, &quot;<span class="synStatement">capacityProvider</span>&quot;: &quot;<span class="synConstant">FARGATE_SPOT</span>&quot;, &quot;<span class="synStatement">weight</span>&quot;: <span class="synConstant">1</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, &quot;<span class="synStatement">deploymentConfiguration</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">deploymentCircuitBreaker</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">enable</span>&quot;: <span class="synConstant">false</span>, &quot;<span class="synStatement">rollback</span>&quot;: <span class="synConstant">false</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">maximumPercent</span>&quot;: <span class="synConstant">200</span>, &quot;<span class="synStatement">minimumHealthyPercent</span>&quot;: <span class="synConstant">100</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">deploymentController</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">type</span>&quot;: &quot;<span class="synConstant">ECS</span>&quot; <span class="synSpecial">}</span>, &quot;<span class="synStatement">desiredCount</span>&quot;: <span class="synConstant">1</span>, &quot;<span class="synStatement">enableECSManagedTags</span>&quot;: <span class="synConstant">false</span>, &quot;<span class="synStatement">enableExecuteCommand</span>&quot;: <span class="synConstant">false</span>, &quot;<span class="synStatement">healthCheckGracePeriodSeconds</span>&quot;: <span class="synConstant">0</span>, &quot;<span class="synStatement">launchType</span>&quot;: &quot;&quot;, &quot;<span class="synStatement">networkConfiguration</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">awsvpcConfiguration</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">assignPublicIp</span>&quot;: &quot;<span class="synConstant">ENABLED</span>&quot;, &quot;<span class="synStatement">securityGroups</span>&quot;: <span class="synSpecial">[</span> &lt;<span class="synError">security</span>-<span class="synError">group</span>&gt; #現行で使用しているセキュリティグループを利用 <span class="synSpecial">]</span>, &quot;<span class="synStatement">subnets</span>&quot;: <span class="synSpecial">[</span> &lt;<span class="synError">subnet</span>&gt; #現行で使用している<span class="synError">subnet</span>を利用 <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span>, &quot;<span class="synStatement">pendingCount</span>&quot;: <span class="synConstant">0</span>, &quot;<span class="synStatement">platformFamily</span>&quot;: &quot;<span class="synConstant">Linux</span>&quot;, &quot;<span class="synStatement">platformVersion</span>&quot;: &quot;<span class="synConstant">LATEST</span>&quot;, &quot;<span class="synStatement">propagateTags</span>&quot;: &quot;<span class="synConstant">NONE</span>&quot;, &quot;<span class="synStatement">runningCount</span>&quot;: <span class="synConstant">0</span>, &quot;<span class="synStatement">schedulingStrategy</span>&quot;: &quot;<span class="synConstant">REPLICA</span>&quot; <span class="synSpecial">}</span> </pre> <ul> <li>ecs-task-def.json</li> </ul> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">containerDefinitions</span>&quot;: <span class="synSpecial">[</span> <span class="synSpecial">{</span> &quot;<span class="synStatement">cpu</span>&quot;: <span class="synConstant">0</span><span class="synError">,</span> <span class="synError"> ]</span>, &quot;<span class="synStatement">essential</span>&quot;: <span class="synConstant">true</span>, &quot;<span class="synStatement">image</span>&quot;: &quot;<span class="synConstant">xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/example-soci-test:latest</span>&quot;, &quot;<span class="synStatement">name</span>&quot;: &quot;<span class="synConstant">example-soci-test</span>&quot;<span class="synError">,</span> <span class="synError"> }</span> ], &quot;<span class="synStatement">cpu</span>&quot;: &quot;<span class="synConstant">256</span>&quot;, &quot;<span class="synStatement">executionRoleArn</span>&quot;: &lt;<span class="synError">execution</span>-<span class="synError">role</span>-<span class="synError">arn</span>&gt;, #現行で使用している<span class="synError">execution</span> <span class="synError">role</span>を利用 &quot;<span class="synStatement">family</span>&quot;: &quot;<span class="synConstant">example-soci-test-task</span>&quot;, &quot;<span class="synStatement">ipcMode</span>&quot;: &quot;&quot;, &quot;<span class="synStatement">memory</span>&quot;: &quot;<span class="synConstant">512</span>&quot;, &quot;<span class="synStatement">networkMode</span>&quot;: &quot;<span class="synConstant">awsvpc</span>&quot;, &quot;<span class="synStatement">pidMode</span>&quot;: &quot;&quot;, &quot;<span class="synStatement">requiresCompatibilities</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">FARGATE</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">taskRoleArn</span>&quot;: &lt;<span class="synError">task</span>-<span class="synError">role</span>-<span class="synError">arn</span>&gt; #現行で使用している<span class="synError">task</span> <span class="synError">role</span>を利用 } </pre> <p>ファイルの作成が完了したら、ローカルからecspressoで適用していきます</p> <pre class="code bash" data-lang="bash" data-unlink>$ ecspresso deploy --config &lt;config.yamlのパス&gt;</pre> <p>反映されたことが確認できたら、環境の準備が整っているので起動のためのテストしていきます。</p> <p>以下のシェルスクリプトを用意し、現在のDev環境とSOCIインデックス適用済みの環境でそれぞれ実行することで、どの程度起動時間に差があるのか確認します。</p> <pre class="code shell" data-lang="shell" data-unlink>CLUSTER=example-cluster #現行のDev環境のクラスター TASKDEF={テスト対象のタスク定義} REGION=ap-northeast-1 NUM_OF_TASKS=1 TASKS=$(aws ecs list-tasks \ --cluster $CLUSTER \ --family $TASKDEF \ --region $REGION \ --query &#34;taskArns[:${NUM_OF_TASKS}]&#34; \ --output text) aws ecs describe-tasks \ --tasks $TASKS \ --region $REGION \ --cluster $CLUSTER \ --query &#34;tasks[] | reverse(sort_by(@, &amp;createdAt)) | [].[{startedAt: startedAt, createdAt: createdAt, taskArn: taskArn}]&#34; \ --output table</pre> <h2 id="実行結果">実行結果</h2> <p>SOCIインデックス適用前(現行のDev環境):1分05秒</p> <pre class="code bash" data-lang="bash" data-unlink>--------------------------------------------------------------------------------------------------------------------- | DescribeTasks | +-----------+-------------------------------------------------------------------------------------------------------+ | createdAt | 2023-08-14T12:39:13.360000+09:00 | | startedAt | 2023-08-14T12:40:18.763000+09:00 | | taskArn | arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:task/example-cluster/xxxxxxxxxxxxxxxxxxxxxxxx | +-----------+-------------------------------------------------------------------------------------------------------+</pre> <p>SOCIインデックス適用済みの環境:36秒<span style="color: #d32f2f">(29秒の短縮)</span></p> <pre class="code bash" data-lang="bash" data-unlink>--------------------------------------------------------------------------------------------------------------------- | DescribeTasks | +-----------+-------------------------------------------------------------------------------------------------------+ | createdAt | 2023-08-14T19:42:05.469000+09:00 | | startedAt | 2023-08-14T19:42:41.185000+09:00 | | taskArn | arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:task/example-cluster/xxxxxxxxxxxxxxxxxxxxxxx | +-----------+-------------------------------------------------------------------------------------------------------+</pre> <p>30秒弱起動時間が短縮されたことがわかります。</p> <h2 id="検証2現行のDev環境にSOCIインデックスを適用">検証2:現行のDev環境にSOCIインデックスを適用</h2> <p>上記のテストである程度効果がありそうなことがわかったので、次に実際にDev環境に対してSOCIインデックスを適用していきます。</p> <p>弊社ではブランチ環境をDev環境にデプロイする際、<span style="color: #d32f2f"><code>branch_deploy</code></span>のイメージタグを使用しています。 よって今回は<span style="color: #d32f2f"><code>branch_deploy</code></span>のイメージタグを使用している全ての対象に対してSOCIインデックスを適用していきます。</p> <p>再度Indexbuilderのフィルターに今回の対象を追加していきます。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudformation_stack&quot;</span> <span class="synConstant">&quot;soci_index_builder&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;soci-index-builder&quot;</span> <span class="synIdentifier">template_url</span> = <span class="synConstant">&quot;https://aws-quickstart.s3.us-east-1.amazonaws.com/cfn-ecr-aws-soci-index-builder/templates/SociIndexBuilder.yml&quot;</span> <span class="synIdentifier">capabilities</span> = <span class="synSpecial">[</span><span class="synConstant">&quot;CAPABILITY_IAM&quot;</span><span class="synSpecial">]</span> <span class="synIdentifier">parameters</span> = <span class="synSpecial">{</span> <span class="synConstant">&quot;SociRepositoryImageTagFilters&quot;</span> = <span class="synConstant">&quot;example-soci-test:*, *:branch_deploy&quot;</span> <span class="synSpecial">}</span> } </pre> <p>Dev環境での検証は、Github Actionsのデプロイ時間をもとに計測していきます。</p> <ul> <li>Github Actionsの内容 <br/> ※ 弊社で適用している内容から必要な項目のみ抜粋して記載しています。</li> </ul> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Manually deploy to development <span class="synIdentifier">on</span><span class="synSpecial">:</span> workflow_dispatch <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">IMAGE_TAG</span><span class="synSpecial">:</span> ${{ (github.ref == <span class="synConstant">'refs/heads/main'</span> <span class="synType">&amp;&amp;</span> github.sha) || <span class="synConstant">'branch_deploy'</span> }} <span class="synIdentifier">AWS_ROLE_ARN</span><span class="synSpecial">:</span> &lt;aws_role_arn&gt; <span class="synIdentifier">permissions</span><span class="synSpecial">:</span> <span class="synIdentifier">id-token</span><span class="synSpecial">:</span> write <span class="synIdentifier">contents</span><span class="synSpecial">:</span> read <span class="synIdentifier">actions</span><span class="synSpecial">:</span> read <span class="synIdentifier">issues</span><span class="synSpecial">:</span> write <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">build</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">outputs</span><span class="synSpecial">:</span> <span class="synIdentifier">ref_link</span><span class="synSpecial">:</span> ${{ env.REF_LINK }} <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Checkout <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Configure AWS credentials <span class="synIdentifier">uses</span><span class="synSpecial">:</span> aws-actions/configure-aws-credentials@v2 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">role-to-assume</span><span class="synSpecial">:</span> ${{ env.AWS_ROLE_ARN }} <span class="synIdentifier">aws-region</span><span class="synSpecial">:</span> ap-northeast-1 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Login to Amazon ECR <span class="synIdentifier">id</span><span class="synSpecial">:</span> login-ecr <span class="synIdentifier">uses</span><span class="synSpecial">:</span> aws-actions/amazon-ecr-login@v1 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Build, tag, and push image to Amazon ECR <span class="synIdentifier">if</span><span class="synSpecial">:</span> github.ref <span class="synType">!=</span> <span class="synConstant">'refs/heads/main'</span> <span class="synIdentifier">env</span><span class="synSpecial">:</span> <span class="synIdentifier">DOCKER_BUILDKIT</span><span class="synSpecial">:</span> <span class="synConstant">1</span> <span class="synIdentifier">ECR_REGISTRY</span><span class="synSpecial">:</span> ${{ steps.login-ecr.outputs.registry }} <span class="synIdentifier">ECR_REPOSITORY</span><span class="synSpecial">:</span> &lt;リポジトリ名&gt; <span class="synIdentifier">run</span><span class="synSpecial">:</span> | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG --build-arg GITHUB_ACCESS_TOKEN=${{ secrets.MACHINE_USER_GITHUB_ACCESS_TOKEN }} . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Build, tag, and push image to Amazon ECR for main <span class="synIdentifier">if</span><span class="synSpecial">:</span> github.ref == <span class="synConstant">'refs/heads/main'</span> <span class="synComment"> # branch_depoloyタグ以外の処理が書いてあるので省略</span> <span class="synComment"> # ...</span> <span class="synIdentifier">deploy</span><span class="synSpecial">:</span> <span class="synIdentifier">needs</span><span class="synSpecial">:</span> build <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Checkout <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Configure AWS credentials <span class="synIdentifier">uses</span><span class="synSpecial">:</span> aws-actions/configure-aws-credentials@v2 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">role-to-assume</span><span class="synSpecial">:</span> ${{ env.AWS_ROLE_ARN }} <span class="synIdentifier">aws-region</span><span class="synSpecial">:</span> ap-northeast-1 <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> kayac/ecspresso@v2 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">version</span><span class="synSpecial">:</span> v2.0.3 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Deploy to Amazon ECS <span class="synIdentifier">run</span><span class="synSpecial">:</span> | ecspresso deploy --config &lt;configファイルのパス&gt; </pre> <h2 id="検証結果">検証結果</h2> <p>弊社のとあるアプリケーションを例に、SOCIインデックス適用前と後でそれぞれ直近8回のデプロイ時間を計測し比較しました。</p> <p>SOCIインデックス適用前のデプロイ時間</p> <table> <thead> <tr> <th> 1 </th> <th> 2 </th> <th> 3 </th> <th> 4 </th> <th> 5 </th> <th> 6 </th> <th> 7 </th> <th> 8 </th> </tr> </thead> <tbody> <tr> <td> 3分44秒 </td> <td> 3分14秒 </td> <td> 3分50秒 </td> <td> 2分40秒 </td> <td> 3分53秒 </td> <td> 4分14秒 </td> <td> 3分47秒 </td> <td> 4分18秒 </td> </tr> </tbody> </table> <p>SOCIインデックス適用後のデプロイ時間</p> <table> <thead> <tr> <th> 1 </th> <th> 2 </th> <th> 3 </th> <th> 4 </th> <th> 5 </th> <th> 6 </th> <th> 7 </th> <th> 8 </th> </tr> </thead> <tbody> <tr> <td> 3分12秒 </td> <td> 2分37秒 </td> <td> 3分13秒 </td> <td> 3分02秒 </td> <td> 3分27秒 </td> <td> 2分55秒 </td> <td> 2分58秒 </td> <td> 2分35秒 </td> </tr> </tbody> </table> <p>※ Github Actions内の”deploy”部分の時間を記載しています。</p> <p>8回分のデプロイ時間を平均すると以下のようになりました。</p> <ul> <li>SOCIインデックス適用前:3分42秒</li> <li>SOCIインデックス適用後:2分59秒</li> <li>短縮した時間:<strong><span style="color: #d32f2f">43秒</span></strong></li> </ul> <p>時間のばらつきはあるものの、平均すると上記の通りデプロイ時間を短縮できたことになります。</p> <p>実際のDev環境でも一定の短縮効果があることがわかりました。</p> <h1 id="補足">補足</h1> <p> Lambdaのランタイムについて</p> <ul> <li>公式のCloudFormationテンプレートにより作成されるLambdaのランタイムがGo.1.xを使用しており、このサポートが2023年12月31日に終了するので、この点に関しては注意が必要そうです。</li> <li>弊社でも将来的に新しいランタイムへの移行を計画しています。</li> </ul> <h1 id="おわりに">おわりに</h1> <ul> <li>今回SOCIインデックスを試してみて、割と簡単に導入できた上に、一定の効果があったので、簡易的なデプロイ時間短縮の手段としてはアリだと思いました。</li> <li>Dev環境での動作が問題なさそうなことがわかったので、順次本番環境への適用も検討していきたいと思います。</li> <li>また一方で、なかなか大幅な改善とはならなかったので、改めて他の手段(Dockerイメージの改善等)も併せて検討していく必要があると感じました。</li> <li>プラットフォームグループでは引き続きデプロイフローの改善をはじめ、様々な開発環境の改善に向け取り組んでいきたいと思います。</li> </ul> yosshi8448 第3回 リーン開発の現場輪読会 リーン開発のテクニックや戦略についてワイワイ編 hatenablog://entry/820878482971061303 2023-10-02T14:15:40+09:00 2023-10-02T14:15:40+09:00 こんにちは!コネヒト歴ちょうど2年になったWebエンジニアの古市(@takfjp)です。最近は岩盤浴にハマっています。サウナは熱すぎて苦手なのですが、岩盤浴だとほどよい高温でじんわり汗をかきながら全身を温められてリラックスできるので、これで季節の変わり目を乗り切れそうです。 第3回輪読会 今回は、社内有志で実施している「リーン開発の現場 カンバンによる大規模プロジェクトの運営」の輪読会の様子をお届けします!第3回となる今回は書籍の第2部である「テクニックを詳しく見る」にフォーカスし、やってみたいことや内容についての感想をワイワイ語り合いました。 これまでの様子は以下の記事をご覧ください。 第1… <p>こんにちは!コネヒト歴ちょうど2年になったWebエンジニアの古市(<a href="https://twitter.com/takfjp">@takfjp</a>)です。最近は岩盤浴にハマっています。サウナは熱すぎて苦手なのですが、岩盤浴だとほどよい高温でじんわり汗をかきながら全身を温められてリラックスできるので、これで季節の変わり目を乗り切れそうです。</p> <h2 id="第3回輪読会">第3回輪読会</h2> <p>今回は、社内有志で実施している「<a href="https://www.amazon.co.jp//dp/427406932X">リーン開発の現場 カンバンによる大規模プロジェクトの運営</a>」の輪読会の様子をお届けします!第3回となる今回は書籍の第2部である「テクニックを詳しく見る」にフォーカスし、やってみたいことや内容についての感想をワイワイ語り合いました。 これまでの様子は以下の記事をご覧ください。</p> <p><strong><a href="https://tech.connehito.com/draft/entry/6ao24VpqnwkXDApagES7uFpWRLc">&#x7B2C;1&#x56DE; &#x30EA;&#x30FC;&#x30F3;&#x958B;&#x767A;&#x306E;&#x73FE;&#x5834;&#x8F2A;&#x8AAD;&#x4F1A; &#x6280;&#x8853;&#x8AB2;&#x984C;&#x306B;&#x3064;&#x3044;&#x3066;&#x30EF;&#x30A4;&#x30EF;&#x30A4;&#x7DE8; - &#x30B3;&#x30CD;&#x30D2;&#x30C8;&#x958B;&#x767A;&#x8005;&#x30D6;&#x30ED;&#x30B0;</a></strong></p> <p><strong><a href="https://tech.connehito.com/entry/2023/09/12/103623">&#x7B2C;2&#x56DE; &#x30EA;&#x30FC;&#x30F3;&#x958B;&#x767A;&#x306E;&#x73FE;&#x5834;&#x8F2A;&#x8AAD;&#x4F1A; &#x30D7;&#x30ED;&#x30BB;&#x30B9;&#x6539;&#x5584;&#x3084; WIP &#x306B;&#x3064;&#x3044;&#x3066;&#x30EF;&#x30A4;&#x30EF;&#x30A4;&#x7DE8; - &#x30B3;&#x30CD;&#x30D2;&#x30C8;&#x958B;&#x767A;&#x8005;&#x30D6;&#x30ED;&#x30B0;</a></strong></p> <p>第1部では主に、著者が携わったプロジェクトにおいてリーン開発の手法をどう実践したか・それによってどんな良い効果や学びをもたらしたかについて書かれていますが、第2部ではこれまで書籍内で触れられたリーン開発に役立つ手法・テクニックについてメインに扱われています。 今回も参加者が事前に第2部17章〜21章を読み、Miroの付箋を使って「思ったこと」「あるある」「やってみたい」という3つの分類で気づきや感想についてワイワイ話し合いました。また、自分が気になった付箋にスタンプやコメントでリアクションするようにもしました。 <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/takflife/20230927/20230927145409.png" width="778" height="567" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="輪読会でワイワイと話したこと">輪読会でワイワイと話したこと</h2> <p>今回も上記のボードから特に話したいことをピックアップしたり、各自が気になった付箋について深掘りしつつ語り合いました。 第2部は主に使用する各種テクニックや戦略についてフォーカスして書かれているため、それらについてどう取り組むか?という議論が進んでいきました。</p> <h3 id="ペアプロやテスト駆動開発は学習コストが高そうそれによって得られる効果を増やすには">ペアプロやテスト駆動開発は学習コストが高そう。それによって得られる効果を増やすには?</h3> <ul> <li>テスト駆動開発は新機能ではなく、改修などで部分的に小さく始めていく。</li> <li>ペアプロはハードル低いけどTDDは習慣にしていかないと身につかないかも。</li> <li>当たり前品質に高くコミットする人に誰かがなるか、外部から呼んでくる。 <ul> <li>TDDに強い人から「どうやって強くなっていったのか」なども聞きたい。</li> </ul> </li> </ul> <h3 id="テスト自動化戦略について">テスト自動化戦略について</h3> <ul> <li>そもそもテストしづらい構成になっているパターンがあり、つらい。 <ul> <li>長大なControllerなど、動いているがテストを書きづらいコードをうまく分割していくところに辛さがありそう。</li> <li>機能やリポジトリ単位でテストを書きづらいコードが集中している。そこにみんなで力を合わせてテストを集中的に追加していくことで、その後の工程がスムーズになりそう。</li> </ul> </li> </ul> <h3 id="プランニングポーカーの粒度について">プランニングポーカーの粒度について</h3> <ul> <li>メンバー間で見積もる際、ポイントの粒度で±1pt(ストーリーポイント)の認識を全員で合わせるのは難しく時間がかかる。</li> <li>S、M、L程度の粒度であれば直感的に見積もることができそう。</li> <li>技術者間で時間単位で見積もると、1時間程度の認識のずれが生まれることもあるが、S, M, Lの粒度で十分そう。</li> <li>粒度に関わらず、プランニングポーカーを通して、タスクについての認識齟齬をなくしていくのは大事。</li> </ul> <h2 id="記入された付箋の数々">記入された付箋の数々</h2> <p>時間内で全てについて議論はできなかったものの、各章について以下のような意見・感想がそれぞれのメンバーから出たので紹介いたします。</p> <h3 id="第17章-アジャイルとリーンの概要">第17章 アジャイルとリーンの概要</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/takflife/20230927/20230927152112.png" width="972" height="722" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第18章-テスト自動化の戦略">第18章 テスト自動化の戦略</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/takflife/20230927/20230927152238.png" width="661" height="703" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第19章-プランニングポーカーによる見積もり">第19章 プランニングポーカーによる見積もり</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/takflife/20230927/20230927152346.png" width="544" height="740" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第20章-因果関係図">第20章 因果関係図</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/takflife/20230927/20230927152520.png" width="615" height="654" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第21章-最後に伝えたいこと">第21章 最後に伝えたいこと</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/takflife/20230927/20230927152605.png" width="449" height="466" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="最後に">最後に</h3> <p>本を実際に輪読する会は今回が最後でしたが、これまで話し合ったなかで良さそうだと感じた取り組みを組織内で実践してみて、どんな学びがあったか振り返る会が予定されています。次回はその振り返りの様子をブログでお届けいたします。 最後まで読んでいただきありがとうございました!</p> takflife 仮説→実験→検証→学び...プロダクト開発のループを実現するために行っていること hatenablog://entry/820878482971295354 2023-09-28T18:06:38+09:00 2023-09-28T18:06:38+09:00 こんにちは! @TOC 、@takapy です。 最近は期の終わりも近づいてきたので、普段利用しているREALFORCEのキーボードを大掃除しました。めっちゃ綺麗になって気分も一新できたので、定期的にやらねばと思いました。いつもありがとうREALFORCE! さて、今回はプロダクト開発においてMobius Outcome Deliveryの思想を取り入れた話をご紹介しようと思います。 私は7月にこのMobius Outcome Delivery研修を受け、実際にこの考えをプロダクト開発に取り入れてみることでプロダクト開発をする上で感じていた課題感を解消できるヒントになるのではないか、と思いまし… <p>こんにちは! <a href="https://twitter.com/toc_toc05">@TOC</a> 、<a href="https://twitter.com/takapy0210">@takapy</a> です。</p> <p>最近は期の終わりも近づいてきたので、普段利用しているREALFORCEのキーボードを大掃除しました。めっちゃ綺麗になって気分も一新できたので、定期的にやらねばと思いました。いつもありがとうREALFORCE!</p> <p>さて、今回はプロダクト開発においてMobius Outcome Deliveryの思想を取り入れた話をご紹介しようと思います。</p> <p>私は7月にこのMobius Outcome Delivery研修を受け、実際にこの考えをプロダクト開発に取り入れてみることでプロダクト開発をする上で感じていた課題感を解消できるヒントになるのではないか、と思いました。</p> <p>Mobius Outcome Deliveryとは何なのか、またその思想を実際にプロダクト開発においてどう取り入れたのかの具体的な事例について興味ある方の参考になれば幸いです。</p> <hr /> <p><span style="font-size: 130%">目次</span></p> <ul class="table-of-contents"> <li><a href="#Mobius-Outcome-Deliveryとは">Mobius Outcome Deliveryとは</a></li> <li><a href="#当時の課題感何がやりたかったのか">当時の課題感、何がやりたかったのか</a></li> <li><a href="#解決に向けて方法の模索">解決に向けて、方法の模索</a></li> <li><a href="#Mobius-Outcome-Deliveryを実現するためにどうやってNotionを活用したのか">Mobius Outcome Deliveryを実現するために、どうやってNotionを活用したのか</a><ul> <li><a href="#Discoverテーマを作成する">Discover:テーマを作成する</a></li> <li><a href="#Options-Deliver実験を作成する">Options, Deliver:実験を作成する</a></li> <li><a href="#Decideもっと作るのか別の問題を見つけるのか">Decide:もっと作るのか、別の問題を見つけるのか</a><ul> <li><a href="#1-もっと作るという判断をした場合">1. もっと作るという判断をした場合</a></li> <li><a href="#2-別の問題を見つけるという判断をした場合">2. 別の問題を見つけるという判断をした場合</a></li> </ul> </li> <li><a href="#まとめると">まとめると</a></li> <li><a href="#補足-実験ドキュメントについて">補足: 実験ドキュメントについて</a><ul> <li><a href="#ステータス管理を容易に">ステータス管理を容易に</a></li> <li><a href="#実験のサマリを把握しやすく">実験のサマリを把握しやすく</a></li> </ul> </li> </ul> </li> <li><a href="#カンバンを作成する上で工夫した点">カンバンを作成する上で工夫した点</a><ul> <li><a href="#利用者はできるだけ流れに沿うだけで利用できるようにする">利用者はできるだけ流れに沿うだけで利用できるようにする</a></li> <li><a href="#目的や思想使い方のドキュメントを用意する">目的や思想、使い方のドキュメントを用意する</a></li> </ul> </li> <li><a href="#実際に運用してみてどうだったか">実際に運用してみてどうだったか</a></li> <li><a href="#今後の展望">今後の展望</a></li> </ul> <hr /> <h1 id="Mobius-Outcome-Deliveryとは">Mobius Outcome Deliveryとは</h1> <p><figure class="figure-image figure-image-fotolife" title="Mobius Navigator"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928132322.png" width="710" height="536" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Mobius Navigator(<a href="https://www.mobiusloop.com/">https://www.mobiusloop.com/</a>)</figcaption></figure></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.mobiusloop.com%2F" title="Mobius Navigator" 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.mobiusloop.com/">www.mobiusloop.com</a></cite></p> <p>Mobius Outcome Deliveryは価値を生み出すためのナビゲーターであり、戦略からデリバリーまでを繋ぎ、見えるようにするフレームワークです。「最小限のアウトプットで最大限のアウトカムを達成する」ことをゴールとしており、研修でも度々 <code>アウトカム(価値)</code> という単語が登場しました。 アイデアをいかにシンプルに小さく実験するか、を問いながら上図のようなメビウスの輪の形状をしたMobius Navigator(メビウスナビゲーター)に沿ってプロダクト開発を進めていきます。</p> <p>プロダクト開発のループを大きく<strong>Discover</strong>, <strong>Decide</strong>(<strong>Options</strong>), <strong>Deliver</strong>の3セクションで表現しており、プロダクト開発における羅針盤的なものになり得るフレームワークになっております。</p> <p>弊社では「プロダクト開発は作って終わりではない」とよく言われています。Mobius Outcome Deliveryはその言葉を表現できるフレームワークである点が一番気に入っています。 ユーザーに届けて終わりではなく、届けた結果どうなったのか、そこから学びを得て次に何をやるのか、を意識できるようになっており、プロダクト改善におけるループを辿っていけるフレームワークとなっております。</p> <h1 id="当時の課題感何がやりたかったのか">当時の課題感、何がやりたかったのか</h1> <p>弊社では現在、<strong>アジリティ</strong>が全社におけるキーワードとなっており、チームでもいかに小さく実験していけるか、が関心ごととして強くなっています。</p> <p>その際に「仮説を立て、実験し、検証する」のサイクルを回しているのですが、下記のような課題感を感じておりました。</p> <ul> <li><strong>各実験が点になっていて繋がっていないのではないか、繋がっていたとしてもそれが見えていないのではないか</strong></li> <li><strong>なんとなく進みが遅いと感じるが、どこがボトルネックになっているのか特定がしづらいのではないか</strong></li> <li><strong>やったことがチーム内に閉じていて、その知見をチーム間や将来の誰かのために活かせられてないのではないか</strong></li> </ul> <p>当時、<a href="https://www.oreilly.co.jp//books/9784873119250/">プロダクトマネジメント本</a>の輪読会を行ったメンバーとプロダクト開発におけるプロセスの改善を行おうと思ったときに課題感を擦り合わせたのですが、上記3点がほぼ同じ課題感として挙がっており、「<strong>仮説→実験→結果→学び→仮説 … のループを回したい、それを見える化したい</strong>」という共通の思いが一致したので、これを実現できる方法を模索することになりました。</p> <h1 id="解決に向けて方法の模索">解決に向けて、方法の模索</h1> <p>Mobius Outcome Deliveryが仮説→実験→結果→学び→仮説 … のループをうまく表現していると思ったので、この研修で得たものを共有し、どうやってこの思想を表現できるか、プロダクト開発に取り入れられるか、を考えてみました。</p> <p>そこでこの思想をプロダクト開発に取り入れるためのツールとして、 Miro など色々な方法を模索したのですが、Notionのプロジェクト管理テンプレートが一番やりたいことを実現できるのではないか、と思いました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2Fhelp%2Fguides%2Fcategory%2Fproject-management" title="Project management" 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.notion.so/help/guides/category/project-management">www.notion.so</a></cite></p> <p>Notionのプロジェクト管理は1つのプロジェクトに対して複数のタスクが紐づく形で構成されています。</p> <p><figure class="figure-image figure-image-fotolife" title="Notion Projectについて"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928130430.png" width="622" height="647" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Notion Projectについて</figcaption></figure></p> <p>これを弊社のプロダクト開発に応用して、<strong>1つのやりたいこと、達成したい価値に対して実験を紐づけること</strong>でやりたいことができるのではないか、と考えました。</p> <p>実際に作成したものをもとにどう実現したのかを具体的に説明します。</p> <h1 id="Mobius-Outcome-Deliveryを実現するためにどうやってNotionを活用したのか">Mobius Outcome Deliveryを実現するために、どうやってNotionを活用したのか</h1> <p>以下Mobius Outcome Deliveryを実現する上でポイントになる点に絞って説明していきます。</p> <h2 id="Discoverテーマを作成する">Discover:テーマを作成する</h2> <p><figure class="figure-image figure-image-fotolife" title="Discoverの道のり"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928142158.png" width="1200" height="905" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Discoverの道のり</figcaption></figure></p> <p>まずは上図の左側、Discoverの部分です。ここでは「なぜやるのか、誰に向けてやるのか」といった問題設定を行い、それを解決した際に得られるアウトカム、を決定します。</p> <p>この部分をNotion Projectsにおける <code>プロジェクト</code> として作成します。これを自分達は <code>テーマ</code> と呼ぶようにしました。</p> <p>データベースから新規テーマを作成すると下記のようなテンプレートが表示されます。</p> <p><figure class="figure-image figure-image-fotolife" title="テーマのテンプレート"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928130646.png" width="1200" height="463" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>テーマのテンプレート</figcaption></figure></p> <p>このテンプレートを埋めていくことでMobius Outcome Deliveryの左側Discoverを辿っていく形式になります。</p> <h2 id="Options-Deliver実験を作成する">Options, Deliver:実験を作成する</h2> <p><figure class="figure-image figure-image-fotolife" title="Options, Deliverの道のり"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928142535.png" width="1200" height="905" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Options, Deliverの道のり</figcaption></figure></p> <p>実際にやりたいことが定まったら、実現するための実験を考えていきます。</p> <p>どうやったら一番シンプルに簡単に実験できるかを考え、やることが決まったら実験を作成します。</p> <p><figure class="figure-image figure-image-fotolife" title="実験の作成"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928130745.png" width="1200" height="373" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実験の作成</figcaption></figure></p> <p>実験を作成するボタンを押すと、このテーマに紐づいた実験が作成されます。</p> <p>実験はテーマが紐づいた状態で作成されるので、この実験は何を実現するためのものなのか、といった紐付けが見えるようになります。</p> <p>このドキュメントは弊社では実験ドキュメントと呼ばれており、どうやって実験するのか、この実験で得た学び、などを記載できるようになっています。</p> <p>この実験ドキュメントについては後の章で詳しく説明します。</p> <p><figure class="figure-image figure-image-fotolife" title="テーマと実験の紐付け"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928130811.png" width="483" height="539" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>テーマと実験の紐付け</figcaption></figure></p> <p>実験が終わり、学びを得たら、次の一手を決めていきます。</p> <h2 id="Decideもっと作るのか別の問題を見つけるのか">Decide:もっと作るのか、別の問題を見つけるのか</h2> <p><figure class="figure-image figure-image-fotolife" title="Decideの道のり"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928143848.png" width="1200" height="905" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Decideの道のり</figcaption></figure></p> <p>実験をしても必ずしもうまくいくとは限りません。やってみて実際に得た反応をもとにもっと作っていくのか、はたまた別の問題を見つけるのか、といった選択をする必要があります。</p> <p>これはMobius Outcome DeliveryでいうDecideのフェーズであり、ここで分岐が発生します。 この次への繋がりが一番実現したかった部分であり、今回のこだわったポイントでもあります。</p> <p>私たちはこの繋がりをNotionのリレーションを用いて表現しました。</p> <h3 id="1-もっと作るという判断をした場合">1. もっと作るという判断をした場合</h3> <p>例えば実際にユーザーに使ってもらい反応をみた際に、ニーズはありそうだが、少し別の形で実装をしてみたい、と思ったとします。Mobius Navigatorでいうところの再度Createのループに入るイメージです。</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/toc8/20230928/20230928144005.png" width="1200" height="905" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>もっと作る判断をした場合</figcaption></figure></p> <p>この時は実験ドキュメントで「次の実験を作成する」ボタンを押します。 <figure class="figure-image figure-image-fotolife" title="次につながる実験を作成する"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928130948.png" width="712" height="163" 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/t/toc8/20230928/20230928131017.png" width="432" height="533" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>次につながる実験を作成する</figcaption></figure></p> <p>これを押すと2回目の実験が作成され、 <code>前の実験</code> プロパティに1回目の実験とのリレーションが作成されます。</p> <p>このリレーションにより2回目の実験は<strong>何を根拠に生まれた実験なのか、の繋がりを表現できるようになりました</strong>。 <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/toc8/20230928/20230928131049.png" width="1200" height="369" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実験の繋がり</figcaption></figure></p> <h3 id="2-別の問題を見つけるという判断をした場合">2. 別の問題を見つけるという判断をした場合</h3> <p>実際にユーザーに届けてみるとニーズがないことがわかったり、実験をすることで別の問題を見つけることもあります。次に繋がる実験だったということですね。</p> <p>この時Mobius Navigatorでいうところの再度Discoverのループに入るイメージです。 <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/toc8/20230928/20230928144052.png" width="1200" height="905" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>別の問題を見つけた場合</figcaption></figure></p> <p>テーマのドキュメントに戻るとネクストアクションが設定されています。ここで「次のテーマを作成する」ボタンを押すと、次に繋がるテーマが作成されます。 <figure class="figure-image figure-image-fotolife" title="次に繋がるテーマを作成する"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928131152.png" width="813" height="263" 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/t/toc8/20230928/20230928131149.png" width="744" height="439" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>次に繋がるテーマを作成する</figcaption></figure></p> <p>このリレーションが作成することで、テーマ間での繋がりも作成されます。</p> <p>こうすることで下図のように1つ1つのループにおける繋がりも表現できるようになりました。 <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/toc8/20230928/20230928143612.png" width="851" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ループが繋がっていくイメージ</figcaption></figure></p> <h2 id="まとめると">まとめると</h2> <p>テーマと実験の関係図をまとめると下記のようになります。</p> <p><figure class="figure-image figure-image-fotolife" title="テーマと実験の関係図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928131439.png" width="1200" height="288" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>テーマと実験の関係図</figcaption></figure></p> <p>また、一連の流れをMobius Navigatorのループに当てはめると下記のようになります。</p> <p><figure class="figure-image figure-image-fotolife" title="Mobius Navigatorに沿ってやること"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928131612.png" width="1200" height="848" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Mobius Navigatorに沿ってやること</figcaption></figure></p> <p>このようにNotionを利用することで、Mobius Outcome Deliveryのフローをできるだけ簡単に、かつテンプレートに沿っていくことで実現できるような仕組みを作成しました。</p> <p>最終的に作成したものはコネヒトカンバンと命名し、PdMを中心に展開をしていきました。</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/toc8/20230928/20230928131432.png" width="991" height="488" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>コネヒトカンバン爆誕</figcaption></figure></p> <h2 id="補足-実験ドキュメントについて"><strong>補足:</strong> 実験ドキュメントについて</h2> <p>ここで、先程説明をスキップした実験ドキュメントの詳細について改めて紹介します。</p> <p>上記で説明した「テーマ」に紐づく形で、「実験ドキュメント」が作成されます。</p> <p>前述した通り、この実験ドキュメントは1つのテーマに対して複数紐づくものとなっています。また、実験同士の繋がり(前後関係)も表現することができます。</p> <p>実験ドキュメントのテンプレートは、以前のブログでも紹介した「A/Bテスト標準化テンプレート」を土台として、少しブラッシュアップして作成しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2022%2F07%2F29%2F170913" title="A/Bテスト標準化へ取り組んだ話 - コネヒト開発者ブログ" 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.connehito.com/entry/2022/07/29/170913">tech.connehito.com</a></cite></p> <p>テンプレートに入れ込んだ最終的な項目は以下のようなものです。</p> <ul> <li>実験の背景</li> <li>実験の概要</li> <li>モニタリング指標</li> <li>実験後のアクションプラン</li> <li>実験結果</li> </ul> <p>以降ではブラッシュアップしたポイントを中心に紹介します。</p> <h3 id="ステータス管理を容易に">ステータス管理を容易に</h3> <p>冒頭で述べた通り、課題感の1つに「<strong>なんとなく進みが遅いと感じるが、どこがボトルネックになっているのか特定がしづらいのではないか</strong>」というものがありました。</p> <p>そこで</p> <ul> <li>各実験は今どのステータスなのか</li> <li>ステータスが暫く変わっていないのであれば、何が原因で変わっていないのか</li> </ul> <p>などの現状を知り、そこでボトルネックを共有して解決を図れるような仕組みにできるよう、ステータス管理を簡単に行えるようにしました。</p> <p>Notionで以下のようなテンプレートを用意しておき、「開発開始」や「検証開始」などのボタンを押すだけで、ステータスが変更されるようにしました。(ボタンのロジックなどは後述)</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/toc8/20230928/20230928131429.png" width="730" height="520" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ステータス変更を簡単にするためのボタン</figcaption></figure></p> <p>テンプレートの末尾には、実験終了したタイミングでステータス更新 &amp; 結果記入欄が表示されるボタンも追加しています。</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/toc8/20230928/20230928131426.png" width="728" height="156" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実験が終了した際のボタン</figcaption></figure></p> <h3 id="実験のサマリを把握しやすく">実験のサマリを把握しやすく</h3> <p>実験ドキュメントは現在の状態を把握できるだけでなく「過去に何がうまくいって、何がうまくいかなかったのかを振り返ることができる」ドキュメントとしても効果を発揮します。</p> <p>過去の実験を一覧で見たときに「どの実験がどんな目的で行われて、結果はどうだったのか」がパッと分かるとハッピーですよね。</p> <p>そこで、実験のサマリをNotionのページプロパティに持たせることで、一覧で見たときに逐一ドキュメントの中身を開かずとも大まかな内容を知ることができるようにしました。</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/toc8/20230928/20230928131424.png" width="656" height="163" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実験サマリプロパティのテンプレート</figcaption></figure></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/toc8/20230928/20230928131420.png" width="1200" height="685" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>実験一覧</figcaption></figure></p> <h1 id="カンバンを作成する上で工夫した点">カンバンを作成する上で工夫した点</h1> <p>以上が大まかなカンバンの仕組みと詳細になります。</p> <p>今回このコネヒトカンバンを作成するにあたって、実際に使ってもらうようにする工夫をいくつか行ったのでご紹介します。</p> <h2 id="利用者はできるだけ流れに沿うだけで利用できるようにする">利用者はできるだけ流れに沿うだけで利用できるようにする</h2> <p>今回カンバンを作成するにあたって、とにかく流れに沿えば自動的にメビウスループに乗っているような設計を心がけました。</p> <p>その際に役に立ったのがNotionのボタン機能です。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.notion.so%2Fhelp%2Fguides%2Fautomatically-generate-blocks-pages-with-buttons" title="Automatically generate blocks, pages and more with the click of a button" 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.notion.so/help/guides/automatically-generate-blocks-pages-with-buttons">www.notion.so</a></cite></p> <p>この機能を使うことで、ボタンをクリックした時にプロパティの操作やページ作成などを自動で行ってくれます。</p> <p>例えば「次のテーマを作成する」ボタンはクリックすると下記のような動作を自動でおこなってくれます。</p> <p>操作するページのステータスを <code>完了</code> にする→繋がりを紐づけてページを作成する→新規ページを開く</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/toc8/20230928/20230928131417.png" width="622" height="465" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ボタンを使ったプロパティの変更設定</figcaption></figure></p> <p>このように利用者はボタンを押すだけで自然とループに乗っている状態を目指すことで利用側のハードルをできるだけ下げるようにしました。</p> <h2 id="目的や思想使い方のドキュメントを用意する">目的や思想、使い方のドキュメントを用意する</h2> <p>今回カンバンを作るにあたって、目的や思想の設計を入念に行いました。</p> <p>実際にこのカンバンを作った人はいいのですが、利用する人たちはやりたいことなどを理解しないと「これをやる目的ってなんだろう…」といった状態になってしまいます。</p> <p>なので、このカンバンの目的やどういった思想のもとで作成しているのかを書いたドキュメントを用意しておきました。</p> <p><figure class="figure-image figure-image-fotolife" title="カンバンの目的を書いたドキュメントの一部"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230928/20230928131413.png" width="775" height="723" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>カンバンの目的を書いたドキュメントの一部</figcaption></figure></p> <p>ただ、用意しているだけでちゃんと読んでもらえるとは限りません。そこは読んでもらう工夫だったり、仕組み化を今後も考える必要があると思っています。</p> <h1 id="実際に運用してみてどうだったか">実際に運用してみてどうだったか</h1> <p>PdMを中心に実際に利用してもらい、数ヶ月が経ちました。実際に使ってみた感想をアンケートで取ってみたので一部ご紹介します。</p> <blockquote><p>全チームの状況が一箇所で見れるのが最高</p> <p>ステータスが俯瞰でみれるのがけっこう好き</p></blockquote> <p>特に繋がりの部分はポジティブな意見が多かったです</p> <blockquote><p>前の繋がりがあるから文脈思い出しやすい(あーこのゴールに向かう施策ね〜的な)</p> <p>前後の繋がりが圧倒的にわかりやすくなった!</p> <p>みんな書いてるけど、つながりがみえるようになったのはとても良い!</p></blockquote> <p>嬉しい声もありつつ、下記のような課題感もあらためて発見できました。</p> <blockquote><p>説明されると理解できるが、自分だけだと構造や使い方を理解するのにハードルがあった</p> <p>実験というワードが強く、1回きりの施策の時(実験じゃない場合)に書いていいのかちょい悩むことがある</p> <p>前後のつながりが見えるようになったのはとても良いが、まだ運用して時間が経っていないので本当に効果的かは長い目で見たい</p></blockquote> <p>ある程度想定はしていたり、対策をしたつもりでしたが、やはりこの辺は難しいですね。</p> <p>浸透に関しては辛抱強く、でも楽しみながらみんなで続けていって、より良いものになるようにしていきたいと思ってます!</p> <h1 id="今後の展望">今後の展望</h1> <p>このカンバンを導入して、約2ヶ月ほどが経過しました。 色々書きましたが、まだ道半ばであり実験的な取り組みです。</p> <p>改めて自分達がやりたかったこと、達成したかったことができてるかと言われると、できつつあるけど、まだまだこれから、と言う状態です。なので、今後もブラッシュアップしていきたいと思っています。</p> <p>例えば、実験で得た知見をPdMやチーム間で共有できるような仕組みを用意したり、もっと過去の繋がりを遡って追加したり…などなど。</p> <p>やりたいことがたくさんありすぎ状態ですが、同時にワクワクもしているのでこの取り組みを今後も続けていって、Mobius Outcome Deliveryの知見と実践を積んでいきたいと思います。</p> toc8 並行処理と並列処理の違いをコンテキストから考える hatenablog://entry/820878482970420256 2023-09-25T13:06:05+09:00 2023-09-30T22:37:36+09:00 こんにちは!バックエンドエンジニアのjunyaUと申します。 実は9月入社で入社してから1ヶ月も経っていない(執筆当時)のですが、テックブログを書かせていただけるのは本当にありがたい環境だな〜とヒシヒシと感じております。 最近、プライベートではもっぱら自作OSの開発をしており、時間があっという間に溶けてしまいご飯をちゃんと食べれていないのが悩みです。 今回は並行処理と並列処理の違いをコンテキストの観点から考えて、両者がどのように違うのかを考察していこうと思います〜! はじめに なぜ記事を書こうと思ったのか 数年前、私が学生だった頃に初めてGoを触り、そこで「並行処理」という言葉を初めて耳にしま… <p>こんにちは!バックエンドエンジニアのjunyaUと申します。</p> <p>実は9月入社で入社してから1ヶ月も経っていない(執筆当時)のですが、テックブログを書かせていただけるのは本当にありがたい環境だな〜とヒシヒシと感じております。</p> <p>最近、プライベートではもっぱら自作OSの開発をしており、時間があっという間に溶けてしまいご飯をちゃんと食べれていないのが悩みです。</p> <p>今回は並行処理と並列処理の違いをコンテキストの観点から考えて、両者がどのように違うのかを考察していこうと思います〜!</p> <h1 id="はじめに"><strong>はじめに</strong></h1> <h2 id="なぜ記事を書こうと思ったのか">なぜ記事を書こうと思ったのか</h2> <p>数年前、私が学生だった頃に初めてGoを触り、そこで「並行処理」という言葉を初めて耳にしました。その言葉を初めて聞いたとき、「プログラムを同時に動かすことができるのか!」とワクワクしましたが、並行処理について調べてみると、</p> <ul> <li>実は高速に切り替えて実行しているだけで本当に同時に実行していない</li> <li>並列処理と並行処理があり、並列処理は本当に同時に実行している</li> </ul> <p>というような内容が書かれており、当時は全く理解できずぼんやりとしたまま考えるのをやめてしまいました。</p> <p>時は過ぎ、私はOSの自作やコンピューターサイエンスの勉強にハマりました。</p> <p>そこで、プロセスの並行処理を調べたり自分で実装したりするうちに並行処理と並列処理への解像度が少し上がりました。</p> <p>ネットで並列処理と並行処理の違いを調べてみても、この問題を考える上で重要なコンテキストやコンテキストスイッチの概念に触れている記事が少なかったので、今回はコンテキストの観点から両者の違いを考えていこうと思います。</p> <h2 id="並行処理と並列処理の違いの結論">並行処理と並列処理の違いの結論</h2> <p>結論から述べると、プロセスAとプロセスBの実行に際して、</p> <ul> <li>並行処理は、特にシングルコアの場合、1つのコアがAとBのコンテキストを高速に入れ替えながらプロセスを実行する方式</li> <li>並列処理は、複数コアを使って、AとBのプロセスを真に同時に実行する方式</li> </ul> <p>となります。</p> <p>この違いを理解するためには、<strong>コンテキストの概念の理解</strong>は不可欠です。</p> <p>次節からコンテキストに焦点を当てて並行処理と並列処理の違いを見ていきます。</p> <h1 id="プロセスのコンテキストとは">プロセスのコンテキストとは?</h1> <h2 id="コンテキストの定義">コンテキストの定義</h2> <p>コンテキストとは、プロセスの現在の実行状態を表す情報の集合を指します。</p> <p>これは、プロセスが中断された後、その状態から継続して実行できるようにするための「状態のスナップショット」であると言えます。</p> <p>実際には、プロセスの実行状態を保存するためのデータは様々ありますが、重要でわかりやすいデータとしては、以下のものがあります。</p> <ul> <li><strong>プログラムカウンタ</strong> : 次に実行する命令が格納されているアドレス</li> <li><strong>スタックポインタ</strong> : 変数や一時的な計算結果など、プログラムの実行に必要なデータが格納されているアドレス</li> <li><strong>フラグレジスタ</strong>:条件分岐や算術命令の結果に基づくフラグの値</li> </ul> <p>これらのデータの多くがCPUのレジスタに格納されています。</p> <p>なので、コンテキストとは<strong>レジスタの内容</strong> と考えることができます。</p> <p>では、このレジスタについて詳しく見ていきます。</p> <h2 id="レジスタとは">レジスタとは?</h2> <p><figure class="figure-image figure-image-fotolife" title="CPUとメモリの簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20230925/20230925101755.png" width="1187" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>CPUとメモリの簡略図</figcaption></figure></p> <p>レジスタはCPUの内部に存在する、高速にアクセス可能である小さな記憶領域です。</p> <p>CPUはメインメモリに直接アクセスするよりもレジスタへのアクセスの方が高速であるため、頻繁に使用されるデータや、実行中の命令の情報は一時的にレジスタに格納されます。</p> <p>CPUの動作は、基本的に「命令のフェッチ」と「命令の実行」の繰り返しで、フェッチする際はメモリからデータや命令を取得し、レジスタに格納され、レジスタの値から処理が行われます。</p> <p>このレジスタには先ほど述べた、プログラムカウンタやデータなどが格納されているわけですが、</p> <p>もしプロセスAの実行中に、プロセスBのデータのアドレスやプログラムカウンタの情報でレジスタの内容を上書きした場合、どうなるでしょうか?</p> <p>プロセスAの実行中にも関わらずいきなりプロセスBの実行に切り替わってしまいます。</p> <p>これが、並行処理で行われていることの核心となる部分です。</p> <p>次の節で詳しく見ていきます。</p> <h1 id="並行処理とは"><strong>並行処理とは?</strong></h1> <h2 id="並行処理の定義">並行処理の定義</h2> <p>並行処理の定義は、「1つのCPUコアに対して複数のコンテキストを高速に入れ替えながらプロセスを実行する方式」と冒頭で述べました。</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/j/junya555/20230925/20230925102011.png" width="1200" height="368" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>メモリの簡略図(アドレスや配置は説明用なのでデタラメです)</figcaption></figure></p> <p>上の簡単な図をもとに考えてみます。</p> <p><strong>プロセスAのコンテキスト</strong></p> <ul> <li>プログラムカウンタ: 0x1000</li> <li>スタックポインタ: 0x3000</li> </ul> <p><strong>プロセスBのコンテキスト</strong></p> <ul> <li>プログラムカウンタ: 0x7000</li> <li>スタックポインタ: 0x9000</li> </ul> <p>プロセスAが実行され始めた時、レジスタにはプログラムカウンタの0x1000、スタックポインタの0x3000を始めとした様々な値が保存されています。</p> <p>ここで、プロセスAの実行中に現在のレジスタの値をどこかに退避させ、プロセスBのコンテキストをレジスタに格納すると、プロセスAの処理が中断され、プロセスBに処理が切り替わります。</p> <p>これを連続して高速に行うとどうなるでしょうか?</p> <p>CPUから見ると、複数のプロセスを高速に1つずつ処理しているだけなのですが、</p> <p>人間から見ると、プロセスAとプロセスBが同時に動いているように見えると思います。</p> <p>これが並行処理なのです。</p> <h2 id="コンテキストスイッチ">コンテキストスイッチ</h2> <p><figure class="figure-image figure-image-fotolife" title="コンテキストスイッチの簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20230925/20230925102529.png" width="1200" height="780" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>コンテキストスイッチの簡略図</figcaption></figure></p> <p>並行処理について調べていると「コンテキストスイッチ」というワードを目にすることがあると思います。当時の私にとってはこれが難しく理解できませんでした。</p> <p>ですが、コンテキストスイッチの説明は既に上の節で述べられています。</p> <blockquote><p>プロセスAの実行中に現在のレジスタの値をどこかに退避させ、プロセスBのコンテキストをレジスタに格納すると、プロセスAの処理が中断され、プロセスBに処理が切り替わります。</p></blockquote> <p>この部分です。レジスタの値をプロセスAのコンテキストからプロセスBのコンテキストに切り替えるという処理がコンテキストスイッチとなります。</p> <p>もう少し砕いた言い方をすると、<strong>コンテキストスイッチ = レジスタの値の入れ替え</strong></p> <p>ということがいえます。</p> <p>また、高速にコンテキストスイッチを行うことが並行処理といえます。</p> <h1 id="並列処理とは"><strong>並列処理とは?</strong></h1> <h2 id="並列処理の定義">並列処理の定義</h2> <p>並列処理の定義は、「並列処理は複数コアを使って、AとBのコンテキストをそれぞれ異なるコアに格納し、複数のプロセスを真に同時に実行する方式」と冒頭で述べました。</p> <p>並行処理は1つのCPUコアが複数のプロセスを切り替えながら実行するものなので、実際には同時実行していませんが、並列処理は真に同時に実行しています。</p> <p>なぜ並列処理は真に同時に実行することができるのでしょうか?</p> <h2 id="コンテキストとマルチコア">コンテキストとマルチコア</h2> <p>並列処理の真髄は、CPUが複数のコンテキストを同時に扱える能力にあります。</p> <p>コンテキストの複数保持を実現してくれるのがマルチコアの存在です。</p> <p>CPUコアはコアごとにそれぞれレジスタを持っており、複数のコンテキストを同時に保持、実行することができます。</p> <p><figure class="figure-image figure-image-fotolife" title="プロセスAとプロセスBを並列実行している簡略図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/j/junya555/20230925/20230925103146.png" width="1200" height="855" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>プロセスAとプロセスBを並列実行している簡略図</figcaption></figure></p> <p>並行処理はコア1つに対してAとBを同時に実行させようとしていたので、高速にAとBをコンテキストスイッチする必要があったのに対して、並列処理は複数のコアを用いるのでコンテキストスイッチをすることなくそれぞれのコアにコンテキストを保持させて真に同時に実行することができます。</p> <h1 id="まとめ"><strong>まとめ</strong></h1> <p>並行処理は1つのコアに高速にコンテキストを切り替えて実行していく方式で、</p> <p>並列処理は複数コアが、同時に実行したいプロセスのコンテキストをそれぞれ保持して同時に実行をしていく方式でした。コンテキストからのアプローチで見ると思っていたより理解しやすいのではないでしょうか。</p> <p>実際には、どちらの処理方式も単独で使用されることは少なく、一般的にはこれらを組み合わせて利用されます。今回は、それぞれの方式の懸念点やパフォーマンス上のトレードオフ、最適な使い所には触れていませんが、これらのテーマは非常に奥が深いので、機会があればまた書きたいなと思います。</p> <p>なお、今回の説明はわかりやすさを重視して簡略化している点もあるため、その点をご了承ください。</p> junya555 Lambda も Chatbot も SNS も使わない!EventBridge だけで CloudWatch Alarm のメッセージを加工し Slack へ post する方法 hatenablog://entry/820878482967443706 2023-09-21T16:57:25+09:00 2023-09-21T16:57:25+09:00 こんにちは。開発部プラットフォームグループでインフラエンジニアをしている @sasashuuu です。先日、Custom notifications are now available for AWS Chatbot で AWS Chatbot でのカスタム通知が行えるアップデートが発表されました。 実はこの方法以外にも、AWS EventBridge だけで CloudWatch Alarm のメッセージをさらに柔軟に加工して Slack へ post する方法があることをご存知でしょうか。Chatbot のアップデートが発表される前に当社で導入していたのですが、SNS や Chatbot … <p>こんにちは。開発部プラットフォームグループでインフラエンジニアをしている <strong><a href="https://twitter.com/sasashuuu">@sasashuuu</a></strong> です。先日、<strong><a href="https://aws.amazon.com/jp/about-aws/whats-new/2023/09/custom-notifications-aws-chatbot/">Custom notifications are now available for AWS Chatbot</a></strong> で AWS Chatbot でのカスタム通知が行えるアップデートが発表されました。 実はこの方法以外にも、AWS EventBridge だけで CloudWatch Alarm のメッセージをさらに柔軟に加工して Slack へ post する方法があることをご存知でしょうか。Chatbot のアップデートが発表される前に当社で導入していたのですが、SNS や Chatbot を組み合わせたアーキテクチャを組まなくても良い他、 Lambda のランタイムの管理や実装の煩雑さのリスクなども減るので意外と活用できる方法なのではないかと思っております(実は当時 Chatbot を利用した上でメッセージ加工ができないかを模索した結果、できないことを知りこの方法に辿り着きました。そしてその方法を紹介しようとこのブログの8割ほどを執筆し終えた時に Chatbot のアップデートが発表され、なんとも言えない気持ちになったことは内緒です...)。この記事ではその方法についてご紹介したいと思います!</p> <h2 id="BeforeAfter">Before/After</h2> <p>まずメッセージの Before/After からお見せします。</p> <p>ECS のタスク数が一定時間に必要な数から基準を下回った際に発生するアラートを例にご紹介します。</p> <h3 id="Before">Before</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230914/20230914193025.png" width="1200" height="718" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>Before の状態は Chatbot を通じて post されたデフォルトのメッセージとなっています。内容は充実しているのですが、Slack 通知をする上では情報過多な印象で、見慣れていなければどういった内容のアラートなのかを瞬時に把握し、対応するのはハードルが高いような印象でした。</p> <h3 id="After">After</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230914/20230914193043.png" width="1200" height="526" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>After の状態は EventBridge を通じて加工されたデフォルトのメッセージとなっています。</p> <p>メッセージの変更内容ですが、以下のような点を工夫しました。</p> <ul> <li>端的に何を示すアラートなのかを日本語で表現</li> <li>弊社の現場において Slack で通知する上で不要と判断した情報を除去</li> <li>アラートに対するアクションを促せるような文言を追加</li> </ul> <h2 id="アーキテクチャ概要">アーキテクチャ概要</h2> <p>タイトルや冒頭でも触れていましたが、<strong><a href="https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-what-is.html"><strong>AWS EventBridge</strong></a></strong> を使用します。</p> <p>重要となる構成要素は以下のラインナップです。</p> <ul> <li><strong><a href="https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-event-patterns.html"><strong>イベントパターン</strong></a></strong> <ul> <li>AWS で発生したイベントを検知するためのパターン</li> </ul> </li> <li><strong><a href="https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-rules.html"><strong>ルール</strong></a></strong> <ul> <li>AWS サービスのイベントを検知しターゲットへ送信するためのリソース</li> </ul> </li> <li><strong><a href="https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-transform-target-input.html"><strong>入力トランスフォーマー</strong></a></strong> <ul> <li>ターゲットへ渡すためのイベント(テキスト)をカスタマイズできる機能</li> </ul> </li> <li><strong><a href="https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-targets.html"><strong>ターゲット</strong></a></strong> <ul> <li>対象のイベントおよびパターンにマッチした際に送信先となるエンドポイント</li> </ul> </li> <li><strong><a href="https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-api-destinations.html">API Destination</a></strong> <ul> <li>ターゲットに HTTP エンドポイントを指定できる機能</li> </ul> </li> </ul> <p>上記を組み合わせて作成した処理の流れをざっくりと説明すると以下のようになります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230921/20230921164355.png" width="1200" height="517" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="実装手順">実装手順</h2> <p>ここからは具体的な実装手順について解説します。構築時はコンソールでの作業を中心に進めていましたが、ここでは最終的に IaC へ落とし込んだ Terraform のコードをメインに解説させていただきますのでご了承ください。</p> <h3 id="Slack-App-を作成">Slack App を作成</h3> <p>Slack App が必要になるので、なければ作成をしてください。</p> <p><a href="https://api.slack.com/apps">https://api.slack.com/apps</a> へアクセス後、下記の動線から作成が行えます。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230914/20230914193310.png" width="1200" height="691" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="Incomming-Webhooks-の作成">Incomming Webhooks の作成</h3> <p><a href="https://api.slack.com/messaging/webhooks"><strong>Incomming Webhooks</strong></a> を使用しますので、作成し取得しておきます。</p> <p>下記の動線から作成&amp;取得可能です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230914/20230914193336.png" width="1200" height="409" 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/sasashuuu/20230914/20230914193352.png" width="1200" height="633" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="書き込み権限の追加">書き込み権限の追加</h3> <p>Slack App へ chat:write の権限を追加します。</p> <p>下記の動線から設定可能です。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230914/20230914193413.png" width="1200" height="493" 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/sasashuuu/20230921/20230921164843.png" width="1200" height="635" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="Terraform-の実装">Terraform の実装</h3> <p>下記はルールに関する定義です。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_rule&quot;</span> <span class="synConstant">&quot;demo_alert_alarm&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert-alarm&quot;</span> <span class="synIdentifier">event_pattern</span> = <span class="synIdentifier">jsonencode</span>( <span class="synSpecial">{</span> <span class="synIdentifier">source</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;aws.cloudwatch&quot;</span> <span class="synSpecial">]</span>, <span class="synIdentifier">detail-type</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;CloudWatch Alarm State Change&quot;</span> <span class="synSpecial">]</span>, <span class="synIdentifier">resources</span> = <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synIdentifier">prefix</span> = <span class="synConstant">&quot;arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, <span class="synIdentifier">detail</span> = <span class="synSpecial">{</span> <span class="synIdentifier">state</span> = <span class="synSpecial">{</span> <span class="synIdentifier">value</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;ALARM&quot;</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> ) <span class="synSpecial">}</span> </pre> <p>マッチさせたい条件を HCL に記述し、event_pattern の値に定義しています。</p> <p>下記はターゲットに関する定義です。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_target&quot;</span> <span class="synConstant">&quot;demo_alert_alarm&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">arn</span> = aws_cloudwatch_event_api_destination.demo_alert.arn <span class="synIdentifier">role_arn</span> = aws_iam_role.demo_alert.arn <span class="synIdentifier">rule</span> = aws_cloudwatch_event_rule.demo_alert_alarm.id <span class="synType">input_transformer</span> <span class="synSpecial">{</span> <span class="synIdentifier">input_paths</span> = <span class="synSpecial">{</span> <span class="synIdentifier">account</span> = <span class="synConstant">&quot;$.account&quot;</span> <span class="synIdentifier">alarmName</span> = <span class="synConstant">&quot;$.detail.alarmName&quot;</span> <span class="synIdentifier">description</span> = <span class="synConstant">&quot;$.detail.configuration.description&quot;</span> <span class="synIdentifier">reason</span> = <span class="synConstant">&quot;$.state.reason&quot;</span> <span class="synIdentifier">region</span> = <span class="synConstant">&quot;$.region&quot;</span> <span class="synIdentifier">time</span> = <span class="synConstant">&quot;$.time&quot;</span> <span class="synSpecial">}</span> <span class="synIdentifier">input_template</span> = <span class="synConstant">&lt;&lt;EOF</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;attachments&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;color&quot;: &quot;#E01D5A&quot;,</span> <span class="synConstant"> &quot;blocks&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;text&quot;: {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*&lt;alarmName&gt; - ECSの必要なタスク数が足りていません*&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;fields&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*CloudWatch Alarm名:*\n&lt;alarmName&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*CloudWatch Alarmの詳細:*\n *&lt;https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/&lt;alarmName&gt;?|AWS Console URL&gt;*&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*AWSアカウント:*\n&lt;account&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*リージョン:*\n&lt;region&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*発生日時:*\n&lt;time&gt;&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;text&quot;: {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;アラートの内容に注意してください。\\n必要であれば *&lt;https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ&gt;* から「 &lt;alarmName&gt; 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *&lt;https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針&gt;* を参考に今回の対応記録も残しましょう。)&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant">}</span> <span class="synConstant"> EOF</span> <span class="synSpecial">}</span> } </pre> <p>input_transformer では、local.input_path_settings で local values の EventBridge の変数用の定義を指定し、input_template では post する Slack のメッセージのヒアドキュメントを定義しています。このヒアドキュメントの中で、EventBridge の変数を <hoge> のように使用し、メッセージで展開が行えます。</p> <p>下記はターゲットに付随するリソースに関する定義です。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_api_destination&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">connection_arn</span> = aws_cloudwatch_event_connection.demo_alert.arn <span class="synIdentifier">http_method</span> = <span class="synConstant">&quot;POST&quot;</span> <span class="synIdentifier">invocation_endpoint</span> = <span class="synConstant">&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;</span> <span class="synComment"># webhook URL はここでは管理しない</span> <span class="synIdentifier">invocation_rate_limit_per_second</span> = <span class="synConstant">300</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synType">lifecycle</span> <span class="synSpecial">{</span> <span class="synIdentifier">ignore_changes</span> = <span class="synSpecial">[</span> invocation_endpoint <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_connection&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">authorization_type</span> = <span class="synConstant">&quot;API_KEY&quot;</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synType">auth_parameters</span> <span class="synSpecial">{</span> <span class="synType">api_key</span> <span class="synSpecial">{</span> <span class="synIdentifier">key</span> = <span class="synConstant">&quot;Authorization&quot;</span> <span class="synIdentifier">value</span> = <span class="synConstant">&quot;dummy&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> </pre> <p>aws_cloudwatch_event_api_destination, aws_cloudwatch_event_connection では イベントの送信先と接続設定に関する設定を行なっています。aws_cloudwatch_event_api_destination の invocation_endpoint には Webhook URL の値が設定されます。aws_cloudwatch_event_connection の auth_parameters.api_key の設定は使用しないため、適当な文字列の設定で大丈夫です。また、注意点としてここでは説明をわかりやすくするために、invocation_endpoint をマスクした値でハードコードしているような実装になっていますが、セキュアな情報となりますのでこの辺りの管理や参照などは適宜安全な方法で行ってください。</p> <p>最後は IAM に関する定義です。</p> <p>EventBridge が ApiDestination へイベントを送信するための権限などを設定しています。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_iam_role&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">assume_role_policy</span> = <span class="synIdentifier">jsonencode</span>(<span class="synSpecial">{</span> <span class="synConstant">&quot;Statement&quot;</span> : <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synConstant">&quot;Action&quot;</span> : <span class="synConstant">&quot;sts:AssumeRole&quot;</span>, <span class="synConstant">&quot;Effect&quot;</span> : <span class="synConstant">&quot;Allow&quot;</span>, <span class="synConstant">&quot;Principal&quot;</span> : <span class="synSpecial">{</span> <span class="synConstant">&quot;Service&quot;</span> : <span class="synConstant">&quot;events.amazonaws.com&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, <span class="synConstant">&quot;Version&quot;</span> : <span class="synConstant">&quot;2012-10-17&quot;</span> <span class="synSpecial">}</span>) <span class="synIdentifier">managed_policy_arns</span> = <span class="synSpecial">[</span> aws_iam_policy.demo_alert.arn, <span class="synSpecial">]</span> <span class="synIdentifier">max_session_duration</span> = <span class="synConstant">3600</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synSpecial">}</span> <span class="synType">resource</span> <span class="synConstant">&quot;aws_iam_policy&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synIdentifier">policy</span> = <span class="synIdentifier">jsonencode</span>(<span class="synSpecial">{</span> <span class="synConstant">&quot;Statement&quot;</span> : <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synConstant">&quot;Action&quot;</span> : <span class="synSpecial">[</span> <span class="synConstant">&quot;events:InvokeApiDestination&quot;</span> <span class="synSpecial">]</span>, <span class="synConstant">&quot;Effect&quot;</span> : <span class="synConstant">&quot;Allow&quot;</span>, <span class="synConstant">&quot;Resource&quot;</span> : <span class="synSpecial">[</span> <span class="synConstant">&quot;arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:api-destination/demo-alert/*&quot;</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, <span class="synConstant">&quot;Version&quot;</span> : <span class="synConstant">&quot;2012-10-17&quot;</span> <span class="synSpecial">}</span>) <span class="synSpecial">}</span> </pre> <p>ここまで解説した内容の実装は、CloudWatch Alarm の state だと ALARM に関連する内容のものとなっています。ですが、実際にはアラートが復旧した場合の OK 通知も必要となる場合がほとんどでしょう。そのため下記のリソースは state ごとに 1セットで作成するので必要に応じて追加してください。(その他は共用で利用)</p> <ul> <li>aws_cloudwatch_event_rule</li> <li>aws_cloudwatch_event_target</li> </ul> <p>最終的に出来上がった全体のコードはこちらになります。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_rule&quot;</span> <span class="synConstant">&quot;demo_alert_alarm&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert-alarm&quot;</span> <span class="synIdentifier">event_pattern</span> = <span class="synIdentifier">jsonencode</span>( <span class="synSpecial">{</span> <span class="synIdentifier">source</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;aws.cloudwatch&quot;</span> <span class="synSpecial">]</span>, <span class="synIdentifier">detail-type</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;CloudWatch Alarm State Change&quot;</span> <span class="synSpecial">]</span>, <span class="synIdentifier">resources</span> = <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synIdentifier">prefix</span> = <span class="synConstant">&quot;arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, <span class="synIdentifier">detail</span> = <span class="synSpecial">{</span> <span class="synIdentifier">state</span> = <span class="synSpecial">{</span> <span class="synIdentifier">value</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;ALARM&quot;</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> ) <span class="synSpecial">}</span> <span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_rule&quot;</span> <span class="synConstant">&quot;demo_alert_ok&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert-ok&quot;</span> <span class="synIdentifier">event_pattern</span> = <span class="synIdentifier">jsonencode</span>( <span class="synSpecial">{</span> <span class="synIdentifier">source</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;aws.cloudwatch&quot;</span> <span class="synSpecial">]</span>, <span class="synIdentifier">detail-type</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;CloudWatch Alarm State Change&quot;</span> <span class="synSpecial">]</span>, <span class="synIdentifier">resources</span> = <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synIdentifier">prefix</span> = <span class="synConstant">&quot;arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, <span class="synIdentifier">detail</span> = <span class="synSpecial">{</span> <span class="synIdentifier">state</span> = <span class="synSpecial">{</span> <span class="synIdentifier">value</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;OK&quot;</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> ) <span class="synSpecial">}</span> <span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_target&quot;</span> <span class="synConstant">&quot;demo_alert_alarm&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">arn</span> = aws_cloudwatch_event_api_destination.demo_alert.arn <span class="synIdentifier">role_arn</span> = aws_iam_role.demo_alert.arn <span class="synIdentifier">rule</span> = aws_cloudwatch_event_rule.demo_alert_alarm.id <span class="synType">input_transformer</span> <span class="synSpecial">{</span> <span class="synIdentifier">input_paths</span> = <span class="synSpecial">{</span> <span class="synIdentifier">account</span> = <span class="synConstant">&quot;$.account&quot;</span> <span class="synIdentifier">alarmName</span> = <span class="synConstant">&quot;$.detail.alarmName&quot;</span> <span class="synIdentifier">description</span> = <span class="synConstant">&quot;$.detail.configuration.description&quot;</span> <span class="synIdentifier">reason</span> = <span class="synConstant">&quot;$.state.reason&quot;</span> <span class="synIdentifier">region</span> = <span class="synConstant">&quot;$.region&quot;</span> <span class="synIdentifier">time</span> = <span class="synConstant">&quot;$.time&quot;</span> <span class="synSpecial">}</span> <span class="synIdentifier">input_template</span> = <span class="synConstant">&lt;&lt;EOF</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;attachments&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;color&quot;: &quot;#E01D5A&quot;,</span> <span class="synConstant"> &quot;blocks&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;text&quot;: {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*&lt;alarmName&gt; - ECSの必要なタスク数が足りていません*&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;fields&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*CloudWatch Alarm名:*\n&lt;alarmName&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*CloudWatch Alarmの詳細:*\n *&lt;https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/&lt;alarmName&gt;?|AWS Console URL&gt;*&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*AWSアカウント:*\n&lt;account&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*リージョン:*\n&lt;region&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*発生日時:*\n&lt;time&gt;&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;text&quot;: {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;アラートの内容に注意してください。\\n必要であれば *&lt;https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ&gt;* から「 &lt;alarmName&gt; 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *&lt;https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針&gt;* を参考に今回の対応記録も残しましょう。)&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant">}</span> <span class="synConstant"> EOF</span> <span class="synSpecial">}</span> } <span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_target&quot;</span> <span class="synConstant">&quot;demo_alert_ok&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">arn</span> = aws_cloudwatch_event_api_destination.demo_alert.arn <span class="synIdentifier">role_arn</span> = aws_iam_role.demo_alert.arn <span class="synIdentifier">rule</span> = aws_cloudwatch_event_rule.demo_alert_ok.id <span class="synType">input_transformer</span> <span class="synSpecial">{</span> <span class="synIdentifier">input_paths</span> = <span class="synSpecial">{</span> <span class="synIdentifier">account</span> = <span class="synConstant">&quot;$.account&quot;</span> <span class="synIdentifier">alarmName</span> = <span class="synConstant">&quot;$.detail.alarmName&quot;</span> <span class="synIdentifier">description</span> = <span class="synConstant">&quot;$.detail.configuration.description&quot;</span> <span class="synIdentifier">reason</span> = <span class="synConstant">&quot;$.state.reason&quot;</span> <span class="synIdentifier">region</span> = <span class="synConstant">&quot;$.region&quot;</span> <span class="synIdentifier">time</span> = <span class="synConstant">&quot;$.time&quot;</span> <span class="synSpecial">}</span> <span class="synIdentifier">input_template</span> = <span class="synConstant">&lt;&lt;EOF</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;attachments&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;color&quot;: &quot;#2DB57C&quot;,</span> <span class="synConstant"> &quot;blocks&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;text&quot;: {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*&lt;alarmName&gt; - ECSの必要なタスク数が足りていません*&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;fields&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*CloudWatch Alarm名:*\n&lt;alarmName&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*CloudWatch Alarmの詳細:*\n *&lt;https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/&lt;alarmName&gt;?|AWS Console URL&gt;*&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*AWSアカウント:*\n&lt;account&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*リージョン:*\n&lt;region&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*発生日時:*\n&lt;time&gt;&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;text&quot;: {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;引き続きアラートに注意してください&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant">}</span> <span class="synConstant"> EOF</span> <span class="synSpecial">}</span> } <span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_api_destination&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">connection_arn</span> = aws_cloudwatch_event_connection.demo_alert.arn <span class="synIdentifier">http_method</span> = <span class="synConstant">&quot;POST&quot;</span> <span class="synIdentifier">invocation_endpoint</span> = <span class="synConstant">&quot;xxxxxxxxxxxxxxxxxxxxxxxxxxxx&quot;</span> <span class="synComment"># webhook URL はここでは管理しない</span> <span class="synIdentifier">invocation_rate_limit_per_second</span> = <span class="synConstant">300</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synType">lifecycle</span> <span class="synSpecial">{</span> <span class="synIdentifier">ignore_changes</span> = <span class="synSpecial">[</span> invocation_endpoint <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_connection&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">authorization_type</span> = <span class="synConstant">&quot;API_KEY&quot;</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synType">auth_parameters</span> <span class="synSpecial">{</span> <span class="synType">api_key</span> <span class="synSpecial">{</span> <span class="synIdentifier">key</span> = <span class="synConstant">&quot;Authorization&quot;</span> <span class="synIdentifier">value</span> = <span class="synConstant">&quot;dummy&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synType">resource</span> <span class="synConstant">&quot;aws_iam_role&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">assume_role_policy</span> = <span class="synIdentifier">jsonencode</span>(<span class="synSpecial">{</span> <span class="synConstant">&quot;Statement&quot;</span> : <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synConstant">&quot;Action&quot;</span> : <span class="synConstant">&quot;sts:AssumeRole&quot;</span>, <span class="synConstant">&quot;Effect&quot;</span> : <span class="synConstant">&quot;Allow&quot;</span>, <span class="synConstant">&quot;Principal&quot;</span> : <span class="synSpecial">{</span> <span class="synConstant">&quot;Service&quot;</span> : <span class="synConstant">&quot;events.amazonaws.com&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, <span class="synConstant">&quot;Version&quot;</span> : <span class="synConstant">&quot;2012-10-17&quot;</span> <span class="synSpecial">}</span>) <span class="synIdentifier">managed_policy_arns</span> = <span class="synSpecial">[</span> aws_iam_policy.demo_alert.arn, <span class="synSpecial">]</span> <span class="synIdentifier">max_session_duration</span> = <span class="synConstant">3600</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synSpecial">}</span> <span class="synType">resource</span> <span class="synConstant">&quot;aws_iam_policy&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">name</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synIdentifier">policy</span> = <span class="synIdentifier">jsonencode</span>(<span class="synSpecial">{</span> <span class="synConstant">&quot;Statement&quot;</span> : <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synConstant">&quot;Action&quot;</span> : <span class="synSpecial">[</span> <span class="synConstant">&quot;events:InvokeApiDestination&quot;</span> <span class="synSpecial">]</span>, <span class="synConstant">&quot;Effect&quot;</span> : <span class="synConstant">&quot;Allow&quot;</span>, <span class="synConstant">&quot;Resource&quot;</span> : <span class="synSpecial">[</span> <span class="synConstant">&quot;arn:aws:events:ap-northeast-1:xxxxxxxxxxxx:api-destination/demo-alert/*&quot;</span> <span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span>, <span class="synConstant">&quot;Version&quot;</span> : <span class="synConstant">&quot;2012-10-17&quot;</span> <span class="synSpecial">}</span>) <span class="synSpecial">}</span> </pre> <h2 id="additional">additional</h2> <p>ここまでは基本的な実装方法をお伝えしましたが、対象の CloudWatch Alarm リソースが増える度に定義が増えてしまう冗長な設計となっていました。応用として下記のように local values に CloudWatch Alarm ごとの固有の設定に関する定義を切り出し、loop させて DRY に実装する方法もおすすめです。ここでは詳しくは解説しませんが、一部のサンプルコードをご紹介しておきます。</p> <pre class="code lang-terraform" data-lang="terraform" data-unlink><span class="synType">locals</span> <span class="synSpecial">{</span> <span class="synIdentifier">input_paths</span> = <span class="synSpecial">{</span> <span class="synIdentifier">account</span> = <span class="synConstant">&quot;$.account&quot;</span> <span class="synIdentifier">alarmName</span> = <span class="synConstant">&quot;$.detail.alarmName&quot;</span> <span class="synIdentifier">description</span> = <span class="synConstant">&quot;$.detail.configuration.description&quot;</span> <span class="synIdentifier">reason</span> = <span class="synConstant">&quot;$.state.reason&quot;</span> <span class="synIdentifier">region</span> = <span class="synConstant">&quot;$.region&quot;</span> <span class="synIdentifier">time</span> = <span class="synConstant">&quot;$.time&quot;</span> <span class="synSpecial">}</span> <span class="synIdentifier">config</span> = <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synIdentifier">target_arn</span> = aws_cloudwatch_event_api_destination.demo_alert.arn <span class="synIdentifier">name_prefix</span> = <span class="synConstant">&quot;demo-alert&quot;</span> <span class="synIdentifier">prefix_pattern</span> = <span class="synConstant">&quot;arn:aws:cloudwatch:ap-northeast-1:xxxxxxxxxxxx:alarm:required-tasks-dev&quot;</span> <span class="synIdentifier">alert</span> = <span class="synSpecial">{</span> <span class="synIdentifier">alarm</span> = <span class="synSpecial">{</span> <span class="synIdentifier">main_message</span> = <span class="synConstant">&quot;*&lt;alarmName&gt; - ECSの必要なタスク数が足りていません*&quot;</span> <span class="synIdentifier">sub_message</span> = <span class="synConstant">&quot;アラートの内容に注意してください。\\n必要であれば *&lt;https://www.notion.so/xxxxxxxxxxxxxxx|システムアラート管理表 対応ログ&gt;* から「 &lt;alarmName&gt; 」をアラーム名のプロパティでフィルターし、過去の記録を参考に対応してください。\\n( *&lt;https://www.notion.so/xxxxxxxxxxxxxxx|システムアラートの対応記録方針&gt;* を参考に今回の対応記録も残しましょう。)&quot;</span> <span class="synIdentifier">color</span> = <span class="synConstant">&quot;#E01D5A&quot;</span> <span class="synIdentifier">state_type</span> = <span class="synConstant">&quot;ALARM&quot;</span> <span class="synSpecial">}</span> <span class="synIdentifier">ok</span> = <span class="synSpecial">{</span> <span class="synIdentifier">main_message</span> = <span class="synConstant">&quot;*&lt;alarmName&gt; - ECSの必要なタスク数が足りていません*&quot;</span> <span class="synIdentifier">sub_message</span> = <span class="synConstant">&quot;引き続きアラートに注意してください&quot;</span> <span class="synIdentifier">color</span> = <span class="synConstant">&quot;#2DB57C&quot;</span> <span class="synIdentifier">state_type</span> = <span class="synConstant">&quot;OK&quot;</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">]</span> } <span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_rule&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">for_each</span> = <span class="synSpecial">{</span> <span class="synStatement">for</span> config <span class="synStatement">in</span> local.config : config.name_prefix =&gt; <span class="synSpecial">{</span> <span class="synIdentifier">name_prefix</span> = config.name_prefix <span class="synIdentifier">prefix_pattern</span> = config.prefix_pattern <span class="synIdentifier">alert</span> = config.alert <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synIdentifier">name</span> = <span class="synIdentifier">format</span>(<span class="synConstant">&quot;%s-%s&quot;</span>, each.value.name_prefix, <span class="synConstant">&quot;alarm&quot;</span>) <span class="synIdentifier">event_pattern</span> = <span class="synIdentifier">jsonencode</span>( <span class="synSpecial">{</span> <span class="synIdentifier">source</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;aws.cloudwatch&quot;</span> <span class="synSpecial">]</span>, <span class="synIdentifier">detail-type</span> = <span class="synSpecial">[</span> <span class="synConstant">&quot;CloudWatch Alarm State Change&quot;</span> <span class="synSpecial">]</span>, <span class="synIdentifier">resources</span> = <span class="synSpecial">[</span> <span class="synSpecial">{</span> <span class="synIdentifier">prefix</span> = each.value.prefix_pattern <span class="synSpecial">}</span> <span class="synSpecial">]</span>, <span class="synIdentifier">detail</span> = <span class="synSpecial">{</span> <span class="synIdentifier">state</span> = <span class="synSpecial">{</span> <span class="synIdentifier">value</span> = <span class="synSpecial">[</span>each.value.alert.alarm.state_type<span class="synSpecial">]</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synSpecial">}</span> ) } <span class="synType">resource</span> <span class="synConstant">&quot;aws_cloudwatch_event_target&quot;</span> <span class="synConstant">&quot;demo_alert&quot;</span> <span class="synSpecial">{</span> <span class="synIdentifier">for_each</span> = <span class="synSpecial">{</span> <span class="synStatement">for</span> config <span class="synStatement">in</span> local.config : config.name_prefix =&gt; <span class="synSpecial">{</span> <span class="synIdentifier">name_prefix</span> = config.name_prefix <span class="synIdentifier">prefix_pattern</span> = config.prefix_pattern <span class="synIdentifier">alert</span> = config.alert <span class="synIdentifier">target_arn</span> = config.target_arn <span class="synSpecial">}</span> <span class="synSpecial">}</span> <span class="synIdentifier">arn</span> = each.value.target_arn <span class="synIdentifier">role_arn</span> = aws_iam_role.demo_alert.arn <span class="synIdentifier">rule</span> = aws_cloudwatch_event_rule.alarm<span class="synSpecial">[</span>each.value.name_prefix<span class="synSpecial">]</span>.id <span class="synType">input_transformer</span> <span class="synSpecial">{</span> <span class="synIdentifier">input_paths</span> = local.input_paths <span class="synIdentifier">input_template</span> = <span class="synConstant">&lt;&lt;EOF</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;attachments&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;color&quot;: &quot;$</span><span class="synSpecial">{</span>each.value.alert.alarm.color<span class="synSpecial">}</span><span class="synConstant">&quot;,</span> <span class="synConstant"> &quot;blocks&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;text&quot;: {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;$</span><span class="synSpecial">{</span>each.value.alert.alarm.main_message<span class="synSpecial">}</span><span class="synConstant">&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;fields&quot;: [</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*CloudWatch Alarm名:*\n&lt;alarmName&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*CloudWatch Alarmの詳細:*\n *&lt;https://ap-northeast-1.console.aws.amazon.com/cloudwatch/xxxxxxxxxxxxxxx/&lt;alarmName&gt;?|AWS Console URL&gt;*&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*AWSアカウント:*\n&lt;account&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*リージョン:*\n&lt;region&gt;&quot;</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;*発生日時:*\n&lt;time&gt;&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant"> },</span> <span class="synConstant"> {</span> <span class="synConstant"> &quot;type&quot;: &quot;section&quot;,</span> <span class="synConstant"> &quot;text&quot;: {</span> <span class="synConstant"> &quot;type&quot;: &quot;mrkdwn&quot;,</span> <span class="synConstant"> &quot;text&quot;: &quot;$</span><span class="synSpecial">{</span>each.value.alert.alarm.sub_message<span class="synSpecial">}</span><span class="synConstant">&quot;</span> <span class="synConstant"> }</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant"> }</span> <span class="synConstant"> ]</span> <span class="synConstant">}</span> <span class="synConstant"> EOF</span> <span class="synSpecial">}</span> } </pre> <h2 id="おわりに">おわりに</h2> <p>今回は、EventBridge で CloudWatch Alarm のメッセージを加工し、Slack へ post する方法をご紹介しました。Lambda などを使わずとも柔軟にメッセージを加工できるのは個人的に発見でした。皆さんも機会があればぜひ試してみてください!</p> sasashuuu Go言語による並行処理の輪読会で行った、輪読会でワイワイするための工夫 hatenablog://entry/820878482969020407 2023-09-20T16:22:36+09:00 2023-09-20T16:22:36+09:00 こんにちは!@TOC です! 最近は「ゾン100〜ゾンビになるまでにしたい100のこと〜」というアニメにハマっており、見るたびに「やりたいことをやるんだ!」という気持ちになります🙌 キャラも魅力的なのでこれからが楽しみですね! さて、今回は社内で行った「Go言語による並行処理」本の輪読会が最近完走したので、その様子をご紹介しながら、輪読会をより良いものにするために行った工夫点についてご紹介します。 目次 輪読会で読む本の選定 どういう形式でやったのか 輪読会を進める上での工夫 1. 写経当番を決めて、実際に手を動かす 2. 学んだことを実践的にアウトプットしてみる勉強会でワイワイ 最後に 輪読… <p>こんにちは!<a href="https://twitter.com/toc_toc05">@TOC</a> です! 最近は「ゾン100〜ゾンビになるまでにしたい100のこと〜」というアニメにハマっており、見るたびに「やりたいことをやるんだ!」という気持ちになります🙌 キャラも魅力的なのでこれからが楽しみですね!</p> <p>さて、今回は社内で行った「Go言語による並行処理」本の輪読会が最近完走したので、その様子をご紹介しながら、輪読会をより良いものにするために行った工夫点についてご紹介します。</p> <hr /> <p><span style="font-size: 130%">目次</span></p> <ul class="table-of-contents"> <li><a href="#輪読会で読む本の選定">輪読会で読む本の選定</a></li> <li><a href="#どういう形式でやったのか">どういう形式でやったのか</a></li> <li><a href="#輪読会を進める上での工夫">輪読会を進める上での工夫</a><ul> <li><a href="#1-写経当番を決めて実際に手を動かす">1. 写経当番を決めて、実際に手を動かす</a></li> <li><a href="#2-学んだことを実践的にアウトプットしてみる勉強会でワイワイ">2. 学んだことを実践的にアウトプットしてみる勉強会でワイワイ</a></li> </ul> </li> <li><a href="#最後に">最後に</a></li> </ul> <hr /> <h2 id="輪読会で読む本の選定">輪読会で読む本の選定</h2> <p>今回はオライリーから出版されている「Go言語による並行処理」という本を読む本として選定しました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.oreilly.co.jp%2Fbooks%2F9784873118468%2F" title="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://www.oreilly.co.jp/books/9784873118468/">www.oreilly.co.jp</a></cite></p> <p>Goに関する本は数多く出版されていますが、この本はGo言語における並行処理に特化した本で、Goの並行処理を学ぼうと思った人は一度目にしたことがあるのではないでしょうか。</p> <p>数あるGoの本の中からこの本を選定した理由は2つあります。</p> <ol> <li>本の内容として難易度が高く、一人で読むと心が折れそうだから</li> <li>Goの強みでもある並行処理を業務で活かせるレベルに引き上げたいから</li> </ol> <p>一つ目は少し弱気な理由にはなりますが、今回輪読会をやった感想としてはかなり大事なポイントかなと思っております。「Go言語による並行処理」本はかなり丁寧に解説されてるので、読んだらなんとなくわかった気になりますが、理解を深める・自分の中で昇華するまで一人で行うにはかなり骨が折れます。</p> <p>以前、GoのLT会でこの本が紹介された時も「一人で読んで心が折れた」と言った意見が多く見られました。</p> <p>その点、輪読会で一緒に読む仲間がいると進めようという気持ちも高まりますし、議論することで理解が深まりやすいです。なので、この本に限らず、<strong>興味あるけどなかなか読み進められない本は一緒に読む仲間を見つける</strong>のは一つ良い方法かなと思います。</p> <p>二つ目に関しては、現在コネヒトではテックビジョンとしてLet's Goというものを掲げております。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech-vision.connehito.com%2Fstrategy%2Flets-go.html" title="Let&#39;s Go · Connehito Tech Vision" 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://tech-vision.connehito.com/strategy/lets-go.html">tech-vision.connehito.com</a></cite></p> <p>新たな武器としてGoを選定したわけですが、せっかくならGoの強みである並行処理についてちゃんと理解したい、業務で使えるようになりたい、という思いもあり、この本を選定しました。</p> <h2 id="どういう形式でやったのか">どういう形式でやったのか</h2> <p>頻度としては隔週くらいで行いました。事前に読む章を決めて読んでおき、学びになったこと、疑問に思ったことを付箋に書いて各付箋について議論していきます。 <figure class="figure-image figure-image-fotolife" title="輪読会の様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230920/20230920094257.png" width="1200" height="707" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>輪読会の様子</figcaption></figure> わからない箇所について議論をしたり、後述するようにコードを実際に動かしてみて理解を深めたりしました。</p> <p>こうやって議論をすることで一人だけでやるよりも理解を深められるのが輪読会のいいところですね!</p> <h2 id="輪読会を進める上での工夫">輪読会を進める上での工夫</h2> <p>今回輪読した本は丁寧に書いてありつつも、理解に至るには難易度も高く進めるのも一苦労でした。そのため輪読会を進める上で工夫した点を紹介できればと思います。</p> <h3 id="1-写経当番を決めて実際に手を動かす">1. 写経当番を決めて、実際に手を動かす</h3> <p>「Go言語による並行処理」は3章くらいからコード表記が多くなり、実践的な内容になってきます。並行処理は実際に動かしてみたり、コードを変えてみたりして挙動を確かめないと全然動きが想像できなかったりするので、3章以降では各パートで写経当番を決めて、各自写経してくることにしました。</p> <p>写経したコードをコミットできるリポジトリを用意し、自分の担当した分のコードをコミットしていきます。</p> <p>これは実際にコードを動かしながら学べるし、負担を分担できるため今回のケースとしてはかなり有用だったかなと思います。みんなやってるんだから自分もやらなきゃって気持ちになりますしね!</p> <p>やはりある程度イメージしづらい内容だとコードで語る方が理解しやすくなる側面があるかと思うので、難易度が高かったりイメージしづらい内容の輪読会の場合、この方法はおすすめです。</p> <h3 id="2-学んだことを実践的にアウトプットしてみる勉強会でワイワイ">2. 学んだことを実践的にアウトプットしてみる勉強会でワイワイ</h3> <p>輪読会を完走し、実際に写経しながら進めていたとはいえ、やはり <code>知っている</code> と <code>使える</code> には大きなギャップがあります。なので、テーマはなんでもいいから、実際に並行処理を書いてみてみんなでワイワイする勉強会を行おうという話になりました。</p> <p>せっかくならガッツリやって、最後みんなで飲みにでも行こうという話になったので、有志で日曜日に会社に集まって、各々がテーマを決めて並行処理を書いてみる会を行いました。</p> <p>テーマは以下のようなものがありました</p> <ul> <li>PHPのEnumからGoのEnumに変換するConverterを並行処理を使って作成してみる</li> <li>並行処理を使って、複数のAPIレスポンスをまとめる処理を書いてみる</li> <li>並列処理のアルゴリズムを自分で作ってみる</li> <li>プッシュ通知処理をゴルーチンを使って実装してみる</li> </ul> <p>それぞれテーマが興味深く、かつ具体性もあったので取り掛かりやすかったのかなとも思います!</p> <p>まずはみんなでワイワイランチ! <figure class="figure-image figure-image-fotolife" title="ランチの様子"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/toc8/20230920/20230920094406.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ランチの様子</figcaption></figure> お腹を満たしたら、それぞれもくもく作業!</p> <p>2時間やったら中間進捗発表して、さらに2時間やって最終発表といった感じでメリハリをつけてもくもくすることができました。</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/toc8/20230920/20230920094439.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>勉強会も終わってパシャリ</figcaption></figure> ちょっとしたプチ開発合宿みたいな感じで楽しかったです!</p> <p>最近はオフラインの場も増えてきたので、こういった機会をたまに設けると学びが加速しそうですね!</p> <h2 id="最後に">最後に</h2> <p>今回は「Go言語による並行処理」本の輪読会を有意義にするために行ったことを紹介させていただきました。</p> <p>この輪読会は途中で日程が合わずに継続が危ぶまれる時もありましたが、なんとか完走することができました(実際開始から終了までのリードタイムとしては10ヶ月ほどかかっています…笑)。</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/t/toc8/20230920/20230920094536.jpg" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>みんなで打ち上げ!</figcaption></figure> お疲れ様でした!!</p> toc8 DroidKaigi 2023 Day3 参加レポート hatenablog://entry/820878482968004068 2023-09-19T15:34:52+09:00 2023-09-19T15:34:52+09:00 こんにちは!Androidエンジニアの関根です。 2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。 3日目は、これまでと趣向を変えてコミュニケーションを中心にしたコンテンツが行われました。 一覧にすると以下の通りです。 Codelabs / コードラボ - Career Panel Discussion / キャリア・パネルディスカッション - Career Advice Sessions / キャリ… <p>こんにちは!Androidエンジニアの関根です。</p> <p>2023/09/14から3日間、<a href="https://2023.droidkaigi.jp/">DroidKaigi 2023</a>が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230914/20230914182306.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>3日目は、これまでと趣向を変えてコミュニケーションを中心にしたコンテンツが行われました。 一覧にすると以下の通りです。</p> <ul> <li>Codelabs / コードラボ -</li> <li>Career Panel Discussion / キャリア・パネルディスカッション -</li> <li>Career Advice Sessions / キャリア相談会 -</li> <li>Meetups on different topics / 特定のトピックについてのミートアップ -</li> </ul> <p>わたしは、コードラボとキャリア・パネルディスカッションに参加しましたので、2つのコンテンツを中心に感想を書かせていただきます。 補足ですが、バリスタの方が作るカフェラテの提供があり、リラックスして参加できました。</p> <p>※Day1、Day2の様子と、バリスタの方のお店のWebサイトをAppendixに載せているので、そちらもお読みください。</p> <h2 id="キャリアパネルディスカッション">キャリアパネルディスカッション</h2> <p>さまざまなバックボーンを持つパネリスト4名の、パネルディスカッションです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2023.droidkaigi.jp%2Ftimetable%2F374625f0-b7e2-4921-8c92-cb93fc15be1d%2F" title="キャリア・パネルトーク | DroidKaigi 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://2023.droidkaigi.jp/timetable/374625f0-b7e2-4921-8c92-cb93fc15be1d/">2023.droidkaigi.jp</a></cite></p> <ul> <li>キャリアプランを立てるかどうか</li> <li>キャリア形成をする上での行動や心構え</li> <li>プライベートとキャリア</li> </ul> <p>など、日常的には聞けないテーマが多く取り上げられました。<a href="https://www.slido.com/">slido</a>を使った質疑応答もあり、技術とは異なる観点で、コミュニティを支えるコンテンツだったと感じています。内容に共感しながら、業界を支えている方々の姿勢に触れ、背筋が伸びる思いがしました。</p> <h2 id="Codelab">Codelab</h2> <p><a href="https://developer.android.com/get-started/codelabs">Codelabs</a>の中から選定された、3つのコースが用意されており、完了するとプレゼントがもらえるというコンテンツです<a href="#f-2da5220c" name="fn-2da5220c" title="完了しましたが、プレゼントをもらい忘れてしまいました・・・">*1</a>。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2023.droidkaigi.jp%2Ftimetable%2Fe2d76d8b-f875-4919-810c-62a9b84b82df%2F" title="コードラボ / バリスタコーヒー | DroidKaigi 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://2023.droidkaigi.jp/timetable/e2d76d8b-f875-4919-810c-62a9b84b82df/">2023.droidkaigi.jp</a></cite></p> <p>わたしは「Jetpack Composeの基本」というコースを選んで取り組みました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fdeveloper.android.com%2Fcodelabs%2Fjetpack-compose-basics%3Fhl%3Dja%230" title="Jetpack Compose の基本  |  Android Developers" 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://developer.android.com/codelabs/jetpack-compose-basics?hl=ja#0">developer.android.com</a></cite></p> <p>じっくりとJetpackComposeに触れる機会を作れていなかったので、純粋に良い機会になりましたし、ランチタイムから同席になった方々と、お互いの作業内容の共有をしながら取り組めました。JetpackComposeやマルチモジュールの導入状況など、業務上での裏話を情報交換をできたので、貴重な時間になりました。</p> <p>参加したコンテンツ以外にも、魅力的なコンテンツがありましたので、紹介しておきます。</p> <h3 id="Meetup">Meetup</h3> <p>いくつかのテーブルに分かれ、特定のテーマを元に交流するコンテンツです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2023.droidkaigi.jp%2Ftimetable%2F45af393d-da44-4a66-a727-8d9d45184498%2F" title="特定のテーマについて話せるMeetup (おやつタイム) | DroidKaigi 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://2023.droidkaigi.jp/timetable/45af393d-da44-4a66-a727-8d9d45184498/">2023.droidkaigi.jp</a></cite></p> <p>わたしはCodelabsに集中していたため、時間切れとなり、参加できませんでしたが、終始人が集まり交流が行われていました。オンライン主流の日常では、得難い機会であり、オフラインならではのコンテンツだと感じました。</p> <h3 id="キャリア相談会">キャリア相談会</h3> <p>パネルディスカッションのパネリストの方々に、少人数で、キャリア相談ができるコンテンツです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2023.droidkaigi.jp%2Ftimetable%2F223cb36f-6d4c-40f1-8520-c2c1acda105b%2F" title="キャリア相談会 | DroidKaigi 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://2023.droidkaigi.jp/timetable/223cb36f-6d4c-40f1-8520-c2c1acda105b/">2023.droidkaigi.jp</a></cite></p> <p>キャリアの悩みは、所属会社だけでは解決できないケースもあると思うので、コミュニティで相談できることは心強いと感じました。満席のアナウンスもあり、盛況だったようです。</p> <h3 id="オフィスツアー">オフィスツアー</h3> <p>スカラーシッププログラムの参加者向けのコンテンツで、DroidKaigiに協賛している企業のオフィスを見学するツアーです。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fmedium.com%2Fdroidkaigi%2Fdroidkaigi-2023-scholarship-535e4c086f5a" title="DroidKaigi 2023スカラーシッププログラムのお知らせ" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://medium.com/droidkaigi/droidkaigi-2023-scholarship-535e4c086f5a">medium.com</a></cite></p> <p>コミュニティの一員として、学生を支援していることに、強く意義を感じます。オフィスツアーから戻った学生の方々を、拍手で迎えることもAndroidコミュニティの温かさを実感しました。</p> <h2 id="まとめ">まとめ</h2> <p>スピーカーの皆様、スタッフの皆様、そして参加された皆様、3日間お疲れ様でした!</p> <p>1日目、2日目では、技術的な見識を深く得られましたし、企業ブースではさまざまな業種の方々から、貴重な開発事例をお聞きできました。3日目には、Androidコミュニティの方々と、情報交換やもくもくコーディングできたので、有意義に過ごさせていただきました。</p> <p>スポンサー一覧を見ると、モバイルアプリ以外への広がりを感じますし、キャリアについて相談したり、スカラーシップでの学生支援など、Androidコミュニティを支えるイベントに、より一層進化していることを実感しております。</p> <p>DroidKaigiを支えてくれている皆様に、心から感謝です!また来年お会いしましょう!</p> <h2 id="Appendix">Appendix</h2> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F09%2F14%2F193300" title="DroidKaigi 2023 Day1 参加レポート - コネヒト開発者ブログ" 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.connehito.com/entry/2023/09/14/193300">tech.connehito.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F09%2F15%2F193138" title="DroidKaigi 2023 Day2 参加レポート - コネヒト開発者ブログ" 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.connehito.com/entry/2023/09/15/193138">tech.connehito.com</a></cite></p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Falphabetticafe.com%2F" title="Main Page - 鎌倉でラテアート。鎌倉市浄明寺のPOPなエスプレッソカフェ | Alpha Betti Cafe : アルファベッティカフェ" 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://alphabetticafe.com/">alphabetticafe.com</a></cite></p> <div class="footnote"> <p class="footnote"><a href="#fn-2da5220c" name="f-2da5220c" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">完了しましたが、プレゼントをもらい忘れてしまいました・・・</span></p> </div> katsutomu0124 DroidKaigi 2023 Day2 参加レポート hatenablog://entry/820878482967739065 2023-09-15T19:31:38+09:00 2023-09-21T23:54:30+09:00 こんにちは!Androidエンジニアの関根です。 2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。 本日は、2日目にわたしが聴講したセッションを紹介します。後日アーカイブ動画公開後に更新していく予定です。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。 ビジネス向けアプリを開発するときに知っておくべきAndroid Enterpriseの世界 最初は、Yusaku Tanakaさんによるモバイルデバイス管理(MDM)を利用した開発についてのセッショ… <p>こんにちは!Androidエンジニアの関根です。</p> <p>2023/09/14から3日間、<a href="https://2023.droidkaigi.jp/">DroidKaigi 2023</a>が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、僭越ながらレポートをします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230914/20230914182306.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>本日は、2日目にわたしが聴講したセッションを紹介します。後日アーカイブ動画公開後に更新していく予定です。 少しでもAndroid開発の盛り上がりに貢献できたら嬉しく思います。</p> <h2 id="ビジネス向けアプリを開発するときに知っておくべきAndroid-Enterpriseの世界">ビジネス向けアプリを開発するときに知っておくべきAndroid Enterpriseの世界</h2> <p>最初は、Yusaku Tanakaさんによるモバイルデバイス管理(MDM)を利用した開発についてのセッションです。</p> <p>MDMに関して、システム構成や配布方法、実装方法など始まりから終わりまでを理解できる内容になっていました。 相対的に事例を多く聞ける事例ではないので、初めての知見ばかりで、新鮮に聞かせていただきました。 すぐにでもMDMを利用して社内ツールの提供をすることもできそうに感じています。</p> <p><iframe id="talk_frame_1077849" class="speakerdeck-iframe" src="//speakerdeck.com/player/b04ff3c79173495c9a7c70a7ddb2038f" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/imsaku/introduce-android-enterprise">speakerdeck.com</a></cite></p> <p>ビジネス向けアプリの開発を予定している方、社内用のアプリを作りたい方におすすめしたいです。</p> <h2 id="Flutterにおけるアプリ内課金実装--AndroidiOS-完全なる統一-">Flutterにおけるアプリ内課金実装 -Android/iOS 完全なる統一-</h2> <p>弊社の中島さんのFlutterを用いたアプリ内課金の定期購入についてのセッションです。 Androidの関数と対比しながら解説して頂き、Flutter未経験の方にも理解しやすい内容でした。 終始落ち着いて発表されていて、改めて一緒に働けることを心強く思いました。中島さん登壇お疲れ様でした!</p> <p><iframe id="talk_frame_1077087" class="speakerdeck-iframe" src="//speakerdeck.com/player/8b2d15b7276f471a88f40ae34d32b706" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/nacatl/ioswan-quan-narutong">speakerdeck.com</a></cite></p> <p>Flutterで定期購入機能の導入を検討中、あるいはアプリ内課金機能があるアプリのFlutterへのリプレイス を検討中の方におすすめしたいです。</p> <h2 id="Androidアプリの良いユニットテストを考える">Androidアプリの良いユニットテストを考える</h2> <p>Nozomi Takumaさんによる、Androidアプリ開発でのユニットテストに関するセッションです。</p> <p>良いユニットテストの定義を提示して頂いた上で、テストの種類ごとにおける実行速度の対比や、 テストダブル利用時の考慮すべきトレードオフ、テスト実装時の手法など、網羅的でわかりやすい内容でした。テストコードはプロダクトコードとは違う知識や思考が必要だと思うので、解説いただいた内容をもとにチームでの議論もしやすくなると感じました。</p> <p><iframe id="talk_frame_1077864" class="speakerdeck-iframe" src="//speakerdeck.com/player/a780def0d1964c4ab3e875904bf8bb66" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/tkmnzm/thinking-about-good-unit-tests-for-android-apps">speakerdeck.com</a></cite></p> <p>ユニットテストを書いていない、もしくは書いているがもう一歩踏み込んで取り組みたい方におすすめです。</p> <h2 id="レイヤードアーキテクチャーでの例外との向き合い方">レイヤードアーキテクチャーでの例外との向き合い方</h2> <p>Yukihiro MoriさんによるMVVMをベースにした例外の扱いに関する発表です</p> <p>レイヤーごとの例外の対処方法や、ユーザーへの例外の伝え方のパターンと実装方法など、開発上の関心を総合的に解説した内容です。 実装方法とアーキテクチャ図を合わせて紹介していただき、理解しやすい内容で、実装内容を具体度高く聞けたので、弊社にも取り入れていきたいと思います。</p> <p><iframe id="talk_frame_1077794" class="speakerdeck-iframe" src="//speakerdeck.com/player/8a75ff30d480446ab8fe23406715d733" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/yukihiromori/reiyadoakitekutiyaniokeru-li-wai-tonoxiang-kihe-ifang">speakerdeck.com</a></cite></p> <p>なんとなく例外を扱っている、例外の種類が多くハンドリングに困っているという方にお勧めしたいです</p> <h2 id="できるアクセシビリティ向上">できる!アクセシビリティ向上</h2> <p>Ogura Yuriさんによるアプリのアクセシビリティに関するセッションです。</p> <p>そもそものアクセシビリティの定義や、TalkBackとスイッチアクセスの機能を、実際のデバイスで操作するデモから始まり、知識がない状態でも安心して聞けました。 デモの後は施策立案〜リリースまでのプロセスと、明日にでもできる容易な改善手法を解説いただいたので、導入時に参考にできることを多く知れたと思います。</p> <p><iframe id="talk_frame_1077394" class="speakerdeck-iframe" src="//speakerdeck.com/player/545f053591f64815ae54149881033655" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/nikkei_engineer_recruiting/droidkaigi2023-day2">speakerdeck.com</a></cite></p> <p>アクセシビリティに対して見識がない方にお勧めしたいです。</p> <h2 id="まとめ">まとめ</h2> <p>スピーカーの皆様、スタッフの皆様、2日目もお疲れ様でした!</p> <p>今日は企業ブースもいくつか回らせていただき、久しぶりにオフラインでの情報交換ができ、楽しませてもらいました。 今回は、NISSANさんの企業ブースでモビリティアプリを触らせていただくなど、スマートフォン以外を扱う企業参加も増えている印象で、Androidコミュニティのさまざまな広がりを感じました。 明日もよろしくお願いします!</p> <p><blockquote data-conversation="none" class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">日産自動車株式会社さん <a href="https://twitter.com/NissanJP?ref_src=twsrc%5Etfw">@NissanJP</a> のブース紹介です!<br><br>内製で開発されているNissanConnectアプリでChipmunkに展示してある車両を操作できる展示をされています!<br><br>ぜひお立ち寄りください!<a href="https://twitter.com/hashtag/DroidKaigi?src=hash&amp;ref_src=twsrc%5Etfw">#DroidKaigi</a> <a href="https://t.co/ri1ivyuejc">pic.twitter.com/ri1ivyuejc</a></p>&mdash; DroidKaigi (@DroidKaigi) <a href="https://twitter.com/DroidKaigi/status/1702583976098189535?ref_src=twsrc%5Etfw">2023年9月15日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> </p> <p>最後にコネヒトでは一緒に働く仲間を募集しています! 興味持っていただけた方は気軽にご連絡ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Fconnehito%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/connehito/projects">www.wantedly.com</a></cite></p> katsutomu0124 CakePHP4.3から非推奨になったFixtureのテーブル定義の問題を解決しました hatenablog://entry/820878482967383521 2023-09-15T10:42:16+09:00 2023-09-15T10:42:16+09:00 こんにちは。 今回はCakePHPのバージョン4.3から非推奨機能に追加されたTestFixture の対応をしたのでバックエンドエンジニアの共同制作でブログに書いてみました。 冒頭〜導入手順5までは高橋で、手順6以降は西中が書いております。 以前PHP8.1にアップデートした際にCakePHPのバージョンも4.2から4.3にアップデートしました。 その時のブログはこちら↓ https://tech.connehito.com/entry/2023/03/31/195819 この時からテストを実行する度に以下のような警告が出るようになりました。 Deprecated Error: You ar… <p>こんにちは。</p> <p>今回はCakePHPのバージョン4.3から非推奨機能に追加された<code>TestFixture</code> の対応をしたのでバックエンドエンジニアの共同制作でブログに書いてみました。</p> <p>冒頭〜導入手順5までは高橋で、手順6以降は西中が書いております。</p> <p>以前PHP8.1にアップデートした際にCakePHPのバージョンも4.2から4.3にアップデートしました。</p> <p>その時のブログはこちら↓</p> <p><a href="https://tech.connehito.com/entry/2023/03/31/195819">https://tech.connehito.com/entry/2023/03/31/195819</a></p> <p>この時からテストを実行する度に以下のような警告が出るようになりました。</p> <pre class="code lang-php" data-lang="php" data-unlink>Deprecated Error: You are using the listener based PHPUnit integration. This fixture system is deprecated, and we recommend you upgrade to the extension based PHPUnit integration. See https://book.cakephp.org/4/en/appendices/fixture-upgrade.html /var/www/html/server/vendor/cakephp/cakephp/src/TestSuite/Fixture/FixtureInjector.php, line: 79 </pre> <p>対象のリポジトリは元々CakePHP3系で作られていたので、テストに使うテーブル定義はFixtureクラスの中に定義されていました。 ですが、CakePHPのマイグレーションを利用しておらず、いわゆるマスタとなるようなテーブル定義は別のリポジトリで管理されています。</p> <p>そのため、テーブル定義が二つの場所にある二重管理状態になっていました。</p> <p>CakePHPの公式ドキュメントではCakePHPのマイグレーションを使った方法については詳細に記述されているのですが、DDLファイルを使った方法は簡潔に記述しかありません。ネットで探してもあまり見つからなかったので、同じように躓いた人の役に立てれたら嬉しいなと思っています。</p> <hr /> <h1 id="目次">目次</h1> <ul class="table-of-contents"> <li><a href="#目次">目次</a></li> <li><a href="#前提">前提</a></li> <li><a href="#導入手順">導入手順</a><ul> <li><a href="#1-アプリケーションリポジトリにDDLファイルをconfigschema配下に置く">1. アプリケーションリポジトリにDDLファイルを/config/schema配下に置く</a></li> <li><a href="#2-phpunitxmlを変更する">2. phpunit.xmlを変更する</a></li> <li><a href="#3-testsboostrapphpに追記する">3. tests/boostrap.phpに追記する</a></li> <li><a href="#4-Fixtureクラスからテーブル定義を削除する">4. Fixtureクラスからテーブル定義を削除する</a></li> <li><a href="#5-ユニットテストを回してみる">5. ユニットテストを回してみる</a></li> <li><a href="#6-自動化">6. 自動化</a><ul> <li><a href="#61-DDLファイルをS3のバケットにアップロードする">6.1. DDLファイルをS3のバケットにアップロードする</a></li> <li><a href="#62-S3のバケットからDDLファイルをダウンロードする">6.2. S3のバケットからDDLファイルをダウンロードする</a><ul> <li><a href="#テスト実行時にDDLファイルをダウンロードする">テスト実行時にDDLファイルをダウンロードする</a></li> <li><a href="#DockerビルドのタイミングでDDLファイルをダウンロードする">DockerビルドのタイミングでDDLファイルをダウンロードする</a></li> <li><a href="#Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする">Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする</a></li> </ul> </li> </ul> </li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> </ul> <hr /> <h1 id="前提">前提</h1> <p>対応方法をご紹介する前に前提条件をお伝えします。</p> <ul> <li>PHP: 8.1 <ul> <li>CakePHP: 4.3</li> <li>PHPUnit: 9.6.5</li> </ul> </li> <li>AWS <ul> <li>ECS</li> <li>Amazon Aurora MySQL v2(MySQL5.7)</li> </ul> </li> <li>DBのスキーマ管理はアプリケーションコードとは別リポジトリ <ul> <li>マイグレーションツールは <a href="https://github.com/ridgepole/ridgepole">Ridgepole</a></li> </ul> </li> <li>GitHub Actions</li> </ul> <hr /> <h1 id="導入手順">導入手順</h1> <p>mysqldumpによって取得したDDLファイルからテストのテーブル定義を使うように変更しました。</p> <h2 id="1-アプリケーションリポジトリにDDLファイルをconfigschema配下に置く">1. アプリケーションリポジトリにDDLファイルを<code>/config/schema</code>配下に置く</h2> <p>DDLファイルは以下のSQLコマンドで生成しました。</p> <pre class="code lang-sql" data-lang="sql" data-unlink>mysqldump -u[ユーザー] -h[ホスト] -P[ポート] -p[パスワード] <span class="synComment">--no-data --skip-column-statistics --no-create-db [データベース名] &gt; base_test.sql</span> </pre> <p>FixtureやFactoryでレコードを追加する際に、AUTO_INCREMENTの値が<code>1</code>ではない場合主キーが重複してしまうため、awkコマンドを使って初期値を変換しました。</p> <pre class="code lang-php" data-lang="php" data-unlink>awk '{ gsub(/AUTO_INCREMENT=[0-9]+/, &quot;AUTO_INCREMENT=1&quot;); print }' &quot;base_test.sql&quot; <span class="synError">&gt;</span> &quot;test.sql&quot; </pre> <h2 id="2-phpunitxmlを変更する">2. <code>phpunit.xml</code>を変更する</h2> <p><code>phpunit.xml</code> から <code>&lt;listeners&gt;</code> ブロックを削除し、以下の内容を <code>phpunit.xml</code> に追加しました。</p> <p>ref: <a href="https://book.cakephp.org/4/en/appendices/fixture-upgrade.html">https://book.cakephp.org/4/en/appendices/fixture-upgrade.html</a></p> <pre class="code lang-php" data-lang="php" data-unlink><span class="synIdentifier">&lt;</span>extensions<span class="synIdentifier">&gt;</span> <span class="synIdentifier">&lt;</span>extension<span class="synIdentifier"> </span><span class="synType">class</span><span class="synIdentifier">=</span><span class="synConstant">&quot;\Cake**\T**estSuite\Fixture\PHPUnitExtension&quot;</span><span class="synIdentifier"> /&gt;</span> <span class="synIdentifier">&lt;/</span>extensions<span class="synIdentifier">&gt;</span> </pre> <h2 id="3-testsboostrapphpに追記する">3. <code>tests/boostrap.php</code>に追記する</h2> <p>1で置いたDDLファイルを使用してテーブル定義を取得します。</p> <p>ref: <a href="https://book.cakephp.org/4/ja/development/testing.html#creating-test-database-schema">https://book.cakephp.org/4/ja/development/testing.html#creating-test-database-schema</a></p> <pre class="code lang-php" data-lang="php" data-unlink>$testSqlFile = dirname(__DIR__) . '/config/schema/test.sql'; (new SchemaLoader())-<span class="synError">&gt;</span>loadSqlFiles($testSqlFile, 'test'); </pre> <h2 id="4-Fixtureクラスからテーブル定義を削除する">4. Fixtureクラスからテーブル定義を削除する</h2> <p>実施したリポジトリは元々CakePHP3系で作られていたので、テストに使うテーブル定義はFixtureの中に定義されていました。 段階的なアップグレードを行っていたため、Fixtuer内にまだテーブル定義は残っている状態です。 そのため、まずは各Fixtureクラスからごっそりテーブル定義ブロックを削除していきます。</p> <pre class="code lang-diff" data-lang="diff" data-unlink><span class="synSpecial">&lt;?php</span> namespace App\Test\Fixture; use Cake\TestSuite\Fixture\TestFixture; /** * ArticlesFixture * */ class ArticlesFixture extends TestFixture { /** * Table name * * @var string */ public $table = 'articles'; public $connection = 'test_hoge'; <span class="synSpecial">- /**</span> <span class="synSpecial">- * Fields</span> <span class="synSpecial">- *</span> <span class="synSpecial">- * @var array</span> <span class="synSpecial">- */</span> <span class="synSpecial">- // @codingStandardsIgnoreStart</span> <span class="synSpecial">- public $fields = [</span> <span class="synSpecial">- 'id' =&gt; ['type' =&gt; 'integer', 'length' =&gt; 11, 'unsigned' =&gt; true, 'null' =&gt; false, 'default' =&gt; null, 'comment' =&gt; '', 'autoIncrement' =&gt; true, 'precision' =&gt; null],</span> <span class="synSpecial">- 'title' =&gt; ['type' =&gt; 'string', 'length' =&gt; 255, 'null' =&gt; false, 'default' =&gt; '', 'collate' =&gt; 'utf8mb4_general_ci', 'comment' =&gt; '', 'precision' =&gt; null, 'fixed' =&gt; null],</span> <span class="synSpecial">- 'description' =&gt; ['type' =&gt; 'string', 'length' =&gt; 255, 'null' =&gt; false, 'default' =&gt; '', 'collate' =&gt; 'utf8mb4_general_ci', 'comment' =&gt; '', 'precision' =&gt; null, 'fixed' =&gt; null],</span> <span class="synSpecial">- 'status' =&gt; ['type' =&gt; 'integer', 'length' =&gt; 3, 'unsigned' =&gt; false, 'null' =&gt; false, 'default' =&gt; '0', 'comment' =&gt; '', 'precision' =&gt; null, 'autoIncrement' =&gt; null],</span> <span class="synSpecial">- 'created' =&gt; ['type' =&gt; 'datetime', 'length' =&gt; null, 'null' =&gt; true, 'default' =&gt; null, 'comment' =&gt; '', 'precision' =&gt; null],</span> <span class="synSpecial">- 'modified' =&gt; ['type' =&gt; 'datetime', 'length' =&gt; null, 'null' =&gt; true, 'default' =&gt; null, 'comment' =&gt; '', 'precision' =&gt; null],</span> <span class="synSpecial">- '_indexes' =&gt; [</span> <span class="synSpecial">- 'index_status' =&gt; ['type' =&gt; 'index', 'columns' =&gt; ['status'], 'length' =&gt; []],</span> <span class="synSpecial">- ],</span> <span class="synSpecial">- '_constraints' =&gt; [</span> <span class="synSpecial">- 'primary' =&gt; ['type' =&gt; 'primary', 'columns' =&gt; ['id'], 'length' =&gt; []],</span> <span class="synSpecial">- ],</span> <span class="synSpecial">- '_options' =&gt; [</span> <span class="synSpecial">- 'engine' =&gt; 'InnoDB',</span> <span class="synSpecial">- 'collation' =&gt; 'utf8_general_ci',</span> <span class="synSpecial">- ],</span> <span class="synSpecial">- ];</span> /** * Records * * @var array */ public $records = [ ︙ ]; } </pre> <p>また、$recordsの中に配列がある場合、今まで$fieldsのtype指定していたのでこのように独自でjson形式にしなければなりません。</p> <pre class="code lang-php" data-lang="php" data-unlink>$records = [ [ 'id' =<span class="synError">&gt;</span> 1, 'urls' =<span class="synError">&gt;</span> [ 0 =<span class="synError">&gt;</span> [ 'type' =<span class="synError">&gt;</span> 1, 'url' =<span class="synError">&gt;</span> 'https://www.example1.co.jp', ], 1 =<span class="synError">&gt;</span> [ 'type' =<span class="synError">&gt;</span> 2, 'url' =<span class="synError">&gt;</span> 'https://www.example2.co.jp', ], ], ], [ 'id' =<span class="synError">&gt;</span> 2, 'urls' =<span class="synError">&gt;</span> [ 0 =<span class="synError">&gt;</span> [ 'type' =<span class="synError">&gt;</span> 1, 'url' =<span class="synError">&gt;</span> 'https://www.example1.co.jp', ], 1 =<span class="synError">&gt;</span> [ 'type' =<span class="synError">&gt;</span> 2, 'url' =<span class="synError">&gt;</span> 'https://www.example2.co.jp', ], ], ], ]; // テストで使用するのにjson形式に変換する foreach ($records as $seq =<span class="synError">&gt;</span> $record) { if (is_array($record['urls'])) { $records[$seq]['urls'] = json_encode($record['urls']); } } $this-<span class="synError">&gt;</span>records = $records; </pre> <p>以下のように直接json形式にするでも大丈夫です。</p> <pre class="code lang-php" data-lang="php" data-unlink>$records = [ [ 'id' =<span class="synError">&gt;</span> 1, 'urls' =<span class="synError">&gt;</span> '[{&quot;type&quot;: 1,&quot;url&quot;: &quot;https://www.example1.co.jp&quot;},{&quot;type&quot;: 2,&quot;url&quot;: &quot;https://www.example2.co.jp&quot;}', ] [ 'id' =<span class="synError">&gt;</span> 2, 'urls' =<span class="synError">&gt;</span> '[{&quot;type&quot;: 1,&quot;url&quot;: &quot;https://www.example1.co.jp&quot;},{&quot;type&quot;: 2,&quot;url&quot;: &quot;https://www.example2.co.jp&quot;}', ] ]; </pre> <h2 id="5-ユニットテストを回してみる">5. ユニットテストを回してみる</h2> <p>今回実施したリポジトリではDockerコンテナを利用しているので、コンテナの中に入ってPHPUnitを回すようにしていました。</p> <p>元々、composer.json 内でPHPUnitを定義しているため、composer から呼び出せるようになっています。</p> <pre class="code lang-json" data-lang="json" data-unlink> &quot;<span class="synStatement">scripts</span>&quot;: <span class="synSpecial">{</span> &quot;<span class="synStatement">post-install-cmd</span>&quot;: <span class="synSpecial">[</span> &quot;<span class="synConstant">App</span><span class="synSpecial">\\</span><span class="synConstant">Console</span><span class="synSpecial">\\</span><span class="synConstant">Installer::postInstall</span>&quot; <span class="synSpecial">]</span>, &quot;<span class="synStatement">post-create-project-cmd</span>&quot;: &quot;<span class="synConstant">App</span><span class="synSpecial">\\</span><span class="synConstant">Console</span><span class="synSpecial">\\</span><span class="synConstant">Installer::postInstall</span>&quot;, ︙ &quot;<span class="synStatement">test</span>&quot;: &quot;<span class="synConstant">phpunit --colors=always</span>&quot; <span class="synSpecial">}</span>, </pre> <p>テストコードの件数が少ない場合は気にしなくて良いのですが、テスト件数が多くテスト実行後に結果を見ようとするとターミナル上で見切れてしまうことが多々あったので実行結果はテキストファイルに書き出すようにしていました。 その時、composerのタイムアウトが発生してしまうことがあるので予め環境変数を上書きしてタイムアウトが起きないように設定した上でテストを実行し、出力したファイルを見比べながらテストコードを実情にあった形で直していきます。</p> <pre class="code shell" data-lang="shell" data-unlink>$ export COMPOSER_PROCESS_TIMEOUT=0 $ composer test &gt; text.log</pre> <p>CakePHPのFixtureはどうやら主キーやユニークキーが重複してしまうFixtureのレコードも許してしまい、レコード重複エラーが発生してしまうという事象が頻発してしまいました。</p> <p>このリポジトリではFixture Factoriesを利用しているので、重複が出ないようにテストケース内でFactoryクラスを使ってレコードを作成するように書き換えることで既存のテストに影響が出ないように修正を行いました。</p> <p>ref: <a href="https://tech.connehito.com/entry/2022/07/22/100000">https://tech.connehito.com/entry/2022/07/22/100000</a></p> <p>これで一応Fixtureの警告は消えましたが、これだとテーブル定義を変更した場合にアプリケーションリポジトリのDDLファイルを手動で更新しなくてはなりません。</p> <p>テーブル定義を管理しているリポジトリに変更があった場合に自動でAWSのS3にDDLファイルをアップロードし、アプリケーションリポジトリでそのDDLファイルをダウンロードしてくるという仕組みを作ることになりました。</p> <h2 id="6-自動化">6. 自動化</h2> <h3 id="61-DDLファイルをS3のバケットにアップロードする">6.1. DDLファイルをS3のバケットにアップロードする</h3> <p>テーブル定義変更の度にリポジトリ内のDDLファイルの変更を行わないといけないだけでなく、弊社の場合、複数のリポジトリで同じDBを参照しているということが多々あるため、スキーマ定義の変更の度に各リポジトリ内のDDLファイルを書き換えるのは手間だという問題もありました。</p> <p>今回の本題であるスキーマ定義のDDLファイルをダウンロードする仕組みは、正にこの問題を解決する手段として考えたものでした。</p> <p>弊社では前述の通り、 Ridgepole を使ってスキーマ定義を管理しているのでスキーマ定義の変更が発生したタイミングでDDLファイルを作成し、S3のバケットにアップロードする仕組みを作成しました。</p> <p>簡単に流れを説明するとこのようになっています。</p> <ol> <li>開発者がテーブル定義を変更し、スキーマ定義管理用のリポジトリに push します。</li> <li>スキーマ定義管理用のリポジトリに設定されているGitHub ActionsがDDL反映処理の実行のためのECSを起動します。 この一連の処理については過去にブログで投稿しているので、そちらを参考にしてください。 refs: <a href="https://tech.connehito.com/entry/2019/10/08/165500">https://tech.connehito.com/entry/2019/10/08/165500</a></li> <li>前述のDDL反映を行います。</li> <li>ECS内でmysqldumpコマンドを実行します。</li> <li>各スキーマのDDLファイルを取得し、ローカルに保存します。 この記事の冒頭で紹介させていただいたawkコマンドを使ってDDLファイルの一部を書き換える処理も行っています。</li> <li>S3にファイルをアップロードします。</li> </ol> <p><figure class="figure-image figure-image-fotolife" title="S3へのアップロードフロー"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/N/NahomiTakahashi/20230914/20230914153355.png" width="988" height="439" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>S3へのアップロードフロー</figcaption></figure></p> <h3 id="62-S3のバケットからDDLファイルをダウンロードする">6.2. S3のバケットからDDLファイルをダウンロードする</h3> <p>「S3のバケットからDDLファイルをダウンロードする」ということは決まったのですが、「じゃあどのタイミングでDDLファイルをダウンロードするのが適切なんだろう?」という課題が出てきました。</p> <p>そこで考えたのがこの3つのタイミングでした。</p> <ul> <li>テスト実行時にDDLファイルをダウンロードする</li> <li>DockerビルドのタイミングでDDLファイルをダウンロードする</li> <li>Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする</li> </ul> <h4 id="テスト実行時にDDLファイルをダウンロードする">テスト実行時にDDLファイルをダウンロードする</h4> <p>最初に思いついたのはこの方法です。 毎回テストの度に最新のDDLファイルをダウンロードすれば最新のスキーマ定義でテストが実行できるというメリットはあります。</p> <p>ですが、</p> <ul> <li>通信エラーでS3のバケットからDDLファイルをダウンロードできなかった場合にテストが動かなくなってしまう</li> <li>スキーマ定義の変更は頻繁に行われるわけではないので、毎回DDLファイルをダウンロードするのは無駄</li> </ul> <p>という問題があったため、却下となりました。</p> <h4 id="DockerビルドのタイミングでDDLファイルをダウンロードする">DockerビルドのタイミングでDDLファイルをダウンロードする</h4> <p>Dockerコンテナをビルドするのはそんなに頻繁ではないので一見良さそうに思えます。</p> <p>ですが、</p> <ul> <li>テストコードは本番環境には不要なため、不要なファイルがDockerイメージに含まれてしまう</li> <li>DDLファイルを含める分、Dockerイメージが大きくなってしまう</li> <li>本番環境には不要なファイルをダウンロードするために、DockerイメージをビルドするためのCIの設定にAWSのキーを設定しないといけない</li> </ul> <p>という問題があったため、こちらも却下となりました。</p> <h4 id="Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする">Dockerコンテナを立ち上げたタイミングでDDLファイルをダウンロードする</h4> <p>ローカルでDockerコンテナを立ち上げる際に必ず実行するシェルファイルがあるため、そちらにDDLファイルをダウンロードする処理を追加しました。</p> <p>ローカルのDockerコンテナは何かライブラリの更新・変更のような大きな変更があったらビルドし直すということもあり、いまの開発スタイルではこのタイミングが適切だろうということになり、このタイミングでDDLファイルをダウンロードすることになりました。</p> <p>また、「なんかスキーマ定義合ってないかも?」となった時でも「まず最初にDockerコンテナを立ち上げ直してみよう」という手段が取れるので、問題解決の第一歩の負担が重くないことも良さそうだねという話になりました。</p> <p>実際のシェルの処理とは異なりますが、以下のように aws-cli の <code>aws s3 cp</code> コマンドでS3のバケットからダウンロードし、 <code>tests/bootstrap.php</code> で読み込めるようにしました。</p> <pre class="code bash" data-lang="bash" data-unlink># copy table schema files from s3 aws s3 cp s3://[DDLファイルのあるバケット]/メインで使うDB.sql /tmp/ &amp;&amp; \ aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB1.sql /tmp/ &amp;&amp; \ aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB2.sql /tmp/ &amp;&amp; \ aws s3 cp s3://[DDLファイルのあるバケット]/連携して使うDB3.sql /tmp/ cp /tmp/*.sql /var/www/html/config/schema/</pre> <p>弊社のリポジトリはローカルのディレクトリを <code>/var/www/html</code> ディレクトリにマウントする形にしているので、マウントの対象外の <code>/tmp</code> ディレクトリにDDLファイルをダウンロードし、実際に読み込むファイルはマウント後のディレクトリにコピーしたものを利用する形にしています。</p> <pre class="code lang-php" data-lang="php" data-unlink>// Load one or more SQL files. $ddlFiles = [ 'test' =<span class="synError">&gt;</span> 'メインで使うDB.sql', 'test_sub1' =<span class="synError">&gt;</span> '連携して使うDB1.sql', 'test_sub2' =<span class="synError">&gt;</span> '連携して使うDB2.sql', 'test_sub3' =<span class="synError">&gt;</span> '連携して使うDB3.sql', ]; $schemaLoader = new \Cake\TestSuite\Fixture\SchemaLoader(); foreach ($ddlFiles as $schema =<span class="synError">&gt;</span> $file) { $fullPath = dirname(__DIR__) . '/config/schema/' . $file; if (!file_exists($fullPath)) { // ファイルが存在しない場合は /tmp ディレクトリからコピーしてくる copy('/tmp/' . $file, $fullPath); } $schemaLoader-<span class="synError">&gt;</span>loadSqlFiles($fullPath, $schema); } </pre> <hr /> <h1 id="まとめ">まとめ</h1> <p>スキーマ定義をダウンロードできる仕組みを入れ、単体テストで利用できるようにしました。</p> <p>これにより、最新のスキーマ定義でテストを実行できるようになり、テストコードとデータベース構造の整合性を維持できるようになりました。また、スキーマ定義の変更に柔軟に対応できるため、開発のスピードアップにもつながっていくことが期待できます。</p> <p>他のリポジトリでも同じDBを参照しているため、横展開していくことで開発効率・開発者体験の向上が見込めそうです!</p> <p>コネヒトでは一緒に働く仲間を募集しています!</p> <p>そして興味持っていただけた方は気軽にご連絡ください!</p> <p><a href="https://www.wantedly.com/companies/connehito/projects">https://www.wantedly.com/companies/connehito/projects</a></p> NahomiTakahashi DroidKaigi 2023 Day1 参加レポート hatenablog://entry/820878482967422313 2023-09-14T19:33:00+09:00 2023-09-21T23:52:17+09:00 こんにちは!Androidエンジニアの関根です。 2023/09/14から3日間、DroidKaigi 2023が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、末筆ながらレポートをします。 1日目に、わたしが聴講したセッションを紹介し、後日アーカイブ動画が公開されたら、リンクを貼りたいと考えています。 少しでも、Android開発の盛り上がりに貢献できたら嬉しく思います。 これで安心! Compose時代のDon't keep activities対応 最初はuponさんによるComposeの実装と絡めたActivity破棄に対処方法をまとめた発表です。 A… <p>こんにちは!Androidエンジニアの関根です。</p> <p>2023/09/14から3日間、<a href="https://2023.droidkaigi.jp/">DroidKaigi 2023</a>が開催されています。 弊社でもスポンサーをさせていただきオフライン参加しているので、末筆ながらレポートをします。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230914/20230914182306.png" width="1200" height="900" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>1日目に、わたしが聴講したセッションを紹介し、後日アーカイブ動画が公開されたら、リンクを貼りたいと考えています。 少しでも、Android開発の盛り上がりに貢献できたら嬉しく思います。</p> <h2 id="これで安心-Compose時代のDont-keep-activities対応">これで安心! Compose時代のDon't keep activities対応</h2> <p>最初はuponさんによるComposeの実装と絡めたActivity破棄に対処方法をまとめた発表です。</p> <p>Activity破棄が起こる要因や対処方法が、わかりやすくまとめられおり、サービスの特性によってどこまで対応するかなど、判断軸も話されていて、あまり社外の事例は知れないため、参考になりました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2023.droidkaigi.jp%2Ftimetable%2F494325%2F" title="これで安心! Compose 時代のDon’t keep activities対応 | DroidKaigi 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://2023.droidkaigi.jp/timetable/494325/">2023.droidkaigi.jp</a></cite></p> <p>Compose導入にあたりActivity破棄の対処方法を見直したい方に、おすすめしたいと思います。</p> <h2 id="Unleashing-the-Power-of-Android-Studio">Unleashing the Power of Android Studio</h2> <p>mhidakaさんによる、直近のAndroid Studioの追加機能をまとめた発表です。</p> <p>直近数回のAndroid StudioのアップデートをUI Tool、Build System、Inspectの区切りで解説いただきました。 アップデートをまとめていただいたことで、現状と今後の方向性がよくわかる内容になっています。 特に、Network Inspectorで通信内容をインタセプトし改変できるようになったのは革命だと思いました。</p> <p><iframe id="talk_frame_1077335" class="speakerdeck-iframe" src="//speakerdeck.com/player/2215c728fce44089b3197e28b6cd6284" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/mhidaka/droidkaigi-2023-unleashing-the-power-of-android-studio">speakerdeck.com</a></cite></p> <p>最近Android Studioのアップデートを追えてなかったなという方におすすめです。</p> <h2 id="iOSとAndroidで定期購入の意図しない解約を防ぐ">iOSとAndroidで定期購入の意図しない解約を防ぐ</h2> <p>Ryo YamazakiさんとYuta Satoさんによる定期購読の解約に関する発表です。 定期購読中のクレジットカードの決済エラーなどによる、非自発的解約をAndroidとiOSそれぞれの機能と実装を詳細に解説した内容です。 弊社のママリアプリでも定期購読の機能を提供しているため、改めて見直したいと感じました。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2023.droidkaigi.jp%2Ftimetable%2F493521%2F" title="iOSとAndroidで定期購入の意図しない解約を防ぐ | DroidKaigi 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://2023.droidkaigi.jp/timetable/493521/">2023.droidkaigi.jp</a></cite></p> <p>定期購読を導入している方、これから導入を検討している方におすすめしたいと思います。</p> <h2 id="YouTubeのLive配信をリリースするまで">YouTubeのLive配信をリリースするまで</h2> <p>yurihondaさんによるYouTubeのLive配信についての発表です。</p> <p>事前準備からリリースまでを網羅的に解説されいて、サンプルアプリのコードを元にした解説が、わかりやすかったです。 躓きやすいところも共有していただき、実装にこの発表を見ておくと安心して取り組めそうです。</p> <p><iframe id="talk_frame_1076719" class="speakerdeck-iframe" src="//speakerdeck.com/player/db3a01262f404444aa94628d3b7147d5" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/yurihondo/youtubehenoraibupei-xin-ji-neng-woririsusurumade">speakerdeck.com</a></cite></p> <p>Live配信は、昨今新しいコンテンツやビジネスとして盛り上がっているため、 実装機会も増えてくると思うので、現時点で予定がない方に、ぜひおすすめしたいです。</p> <h2 id="Master-of-Nested-Scroll">Master of Nested Scroll</h2> <p>すいみーさんによるネストスクロールに関する発表です。</p> <p>JetpackComposeとAndroidViewでのそれぞれの解説に加え、併用する際の実装方法も解説いただきました。 ネストスクロール問題は、AndroidViewから悩みの種でCompose時代の解決策を知れたので、これから導入する場合の考慮すべきことが明確になりました。</p> <p><iframe id="talk_frame_1077131" class="speakerdeck-iframe" src="//speakerdeck.com/player/cc5c5ab470f1446a8210590de3d55a6c" width="710" height="399" style="aspect-ratio:710/399; border:0; padding:0; margin:0; background:transparent;" frameborder="0" allowtransparency="true" allowfullscreen="allowfullscreen"></iframe> <cite class="hatena-citation"><a href="https://speakerdeck.com/reoandroider/master-of-nestedscroll">speakerdeck.com</a></cite></p> <p>Composeへの移行検討中で、ネストスクロール問題を整理したい方お勧めしたいと思います。</p> <h2 id="まとめ">まとめ</h2> <p>スピーカーの皆様、スタッフの皆様、1日目お疲れ様でした!</p> <p>今回参加してJetpackComposeを当たり前に利用している事例が増えていることを実感しました。 弊社でも、そろそろJetpackCompose導入に向けて、計画を立て始めたところなので、よりモチベーションが高まりました。 1日目は、発表を聞く機会が多かったので、2日目は企業ブースにもお邪魔したいと思います。</p> <p>明日は7月に弊社に参画した中島さんも発表されるので、応援も兼ねて2倍楽しみたいと思います。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2F2023.droidkaigi.jp%2Ftimetable%2F495123%2F" title="Flutterにおけるアプリ内課金実装 -Android/iOS 完全なる統一- | DroidKaigi 2023" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://2023.droidkaigi.jp/timetable/495123/">2023.droidkaigi.jp</a></cite></p> <p>最後にコネヒトでは一緒に働く仲間を募集しています! 興味持っていただけた方は気軽にご連絡ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Fconnehito%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/connehito/projects">www.wantedly.com</a></cite></p> katsutomu0124 第2回 リーン開発の現場輪読会 プロセス改善や WIP についてワイワイ編 hatenablog://entry/820878482964396810 2023-09-12T10:36:23+09:00 2023-09-12T10:36:23+09:00 こんにちは!コネヒトのプラットフォームグループでインフラエンジニアをしている @sasashuuu です。最近は VIVANT というドラマにハマっており、クラマックスに向けて目が離せず、日曜が待ち遠しい今日この頃です。 本日は、社内で実施中のリーン開発の現場という本の輪読会の様子についての記事の第2段です!(前回記事: 第1回 リーン開発の現場輪読会 技術課題についてワイワイ編) 中盤から終盤へ差し掛かり、輪読会もヒートアップしてきましたので得られたことを本記事でお伝えしていければと思っております。 ちなみに前回の記事でも説明しましたが、現在リーン開発の現場という本を輪読しています。アジャイ… <p>こんにちは!コネヒトのプラットフォームグループでインフラエンジニアをしている <strong><a href="https://twitter.com/sasashuuu">@sasashuuu</a> </strong> です。最近は VIVANT というドラマにハマっており、クラマックスに向けて目が離せず、日曜が待ち遠しい今日この頃です。</p> <p>本日は、社内で実施中のリーン開発の現場という本の輪読会の様子についての記事の第2段です!(前回記事: <strong><a href="https://tech.connehito.com/draft/entry/6ao24VpqnwkXDApagES7uFpWRLc">第1回 リーン開発の現場輪読会 技術課題についてワイワイ編</a></strong>)</p> <p>中盤から終盤へ差し掛かり、輪読会もヒートアップしてきましたので得られたことを本記事でお伝えしていければと思っております。</p> <p>ちなみに前回の記事でも説明しましたが、現在リーン開発の現場という本を輪読しています。アジャイルソフトウェア開発手法のひとつであるリーンソフトウェア開発手法を、スウェーデンの警察機関のシステム開発で事例をもとに解説した本で、カンバンシステムを中心に書かれています。</p> <p><strong><a href="https://www.amazon.co.jp//dp/427406932X">&#x30EA;&#x30FC;&#x30F3;&#x958B;&#x767A;&#x306E;&#x73FE;&#x5834; &#x30AB;&#x30F3;&#x30D0;&#x30F3;&#x306B;&#x3088;&#x308B;&#x5927;&#x898F;&#x6A21;&#x30D7;&#x30ED;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x306E;&#x904B;&#x55B6; | Henrik Kniberg, &#x89D2;&#x8C37; &#x4FE1;&#x592A;&#x90CE;, &#x5E02;&#x8C37; &#x8061;&#x5553;, &#x85E4;&#x539F; &#x5927; |&#x672C; | &#x901A;&#x8CA9; | Amazon</a></strong></p> <p>今回の輪読会では、9 ~ 16章までが対象でした。進め方は前回同様に参加者は事前に各章の内容を読み、「思ったこと」「あるある」「やってみたい」の3つの観点で付箋を miro に書いてくるようにし、当日はそれらをもとにワイワイ話し合うというワークを実施しました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903183458.png" width="1200" height="895" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></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/s/sasashuuu/20230903/20230903183536.png" width="1200" height="762" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>今回は以下のようなラインナップのテーマがフォーカスされました。</p> <h3 id="外部ファシリテータの話">外部ファシリテータの話</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903183640.png" width="702" height="300" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>これは「第10章 継続的プロセス改善」で触れられている内容の、チームのでふりかえりの際(スプリント等)に、外部からファシリテーターを連れてきてファシリテートをしてもらうというやり方に着目した際の議論です。 チームにとっても外部ファシリテーターにとっても新たな気づきがあるという点や、チームリーダーがファシリテートを行わないことでじっくりと参加することができるメリットなどが本書では解説されていました。</p> <p>以下、議論をした際のコメントを抜粋しておきます。</p> <ul> <li>チーム外のファシリをすることでお互いのチームに良さそう</li> <li>自分のチームはファシリテーターは毎回違うが同じようなやり方になっているので、他のチームメンバーがファリシテーターをすることで他の課題の解決の仕方の視点が取り込めそう</li> <li>チームのフェーズも関係しそう。 成熟しているチームは、やり方が固まっているところを見つめ直せるかもしれない。外部ファシリテーターでコンサル的な効果が見込めそうに思う</li> <li>純粋に他のチームのやり方を知りたい</li> <li>他のチームの雰囲気を知るのは良さそうだ</li> </ul> <h3 id="WIPとバッファを区別する話">WIPとバッファを区別する話</h3> <p>これは「第11章 WIP をマネジメントする」で触れられている内容の、WIP(仕掛かり作業)とバッファを区別するというものに着目した際の議論です。本書ではカンバンボード上のバッファは待ち状態を意味するため、無駄だと解説されており(ただし、開発とテストの間のバッファなど、必要である小さなバッファは例外)、それらを区別しておくことが重要だとされていました。</p> <p>以下、議論をした際のコメントを抜粋しておきます。</p> <ul> <li>WIPとバッファなど時間の使い方や仕事の積み方を意識して使えると今後にとって良くなりそう</li> <li>職種が分かれていることで、WIPが増えるという状況はあると思う。(ネイティブとバックエンドの開発)</li> <li>一旦スタブを作って、ネイティブアプリとの連携部分を先に進めるということはやっている(ネイティブとバックエンドの開発)</li> </ul> <h3 id="サイクルタイム計測の話">サイクルタイム計測の話</h3> <p>これは「第12章 プロセスメトリクス」で触れられている内容の、ある作業が完了するまでにかかった時間を計測するサイクルタイムに着目した際の議論です。本書では、ほとんどの人は機能の開発にどれくらいの時間がかかって気がついておらず、実際に開発に費やした時間を知ると恐怖に近い感覚を覚えると書かれていました。また、サイクルタイムを可視化することで WIP 制限のテクニックを行うことで、サイクルタイムの短縮が見込めると解説されていました。</p> <p>以下、議論をした際のコメントを抜粋しておきます。</p> <ul> <li>定めたポイントをもとに開発タスクの消化量を計測することはやっている</li> <li>ベロシティの計算はやっている</li> <li>サイクルタイムはフロー効率を可視化する意図がありそうに思った 。優先順位が高いものをリリースできているかどうかということの指標になりそう</li> <li>WIPの制限とセットでこの指標を取ると良さそう</li> <li>作業日数/経過日数で出せると良さそう。実際サイクルタイムはどう取ると良いのだろう?</li> <li>本書では着手日と終了日を付箋に記入していたようだ、アナログでやっても良いかも</li> <li>Notionだとプロパティで計算できそうだが、みんな使っていない?</li> <li>Zenhubにはreport機能でそういうものがあったかもしれない</li> </ul> <h3 id="テスターからのFBの話">テスターからのFBの話</h3> <p>これは「第9章 バグをさばく」で触れられている内容の、バグの対応の際に機能開発チームに存在するテスターと開発者が毎日一緒に働いている様子に着目した際の議論です。</p> <p>以下、議論をした際のコメントを抜粋しておきます。</p> <ul> <li>QAエンジニアの人がいると良さそうに思っている。仕様にFBをもらえたりQAツールに 詳しかったり、テストの行程からFBをもらえると良さそうだ</li> <li>QAエンジニアの採用は計画していないが、最近はMagicPodの導入は検討している</li> </ul> <h2 id="記入された付箋の数々">記入された付箋の数々</h2> <p>議論にはあがらなかったものの、それぞれの章で挙がった付箋も紹介しておきます。</p> <h2 id="第9章-バグをさばく">第9章 バグをさばく</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903195818.png" width="592" height="668" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="第10章-継続的プロセス改善">第10章 継続的プロセス改善</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903195832.png" width="650" height="602" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="第11章-WIP-をマネジメントする">第11章 WIP をマネジメントする</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903195842.png" width="502" height="570" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="第12章-プロセスメトリクス">第12章 プロセスメトリクス</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903195853.png" width="598" height="716" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="第13章-スプリントとリリースの計画">第13章 スプリントとリリースの計画</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903195906.png" width="410" height="460" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="第14章-バージョン管理の方法">第14章 バージョン管理の方法</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903195920.png" width="382" height="276" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="第15章-アナログなカンバンボードを使う理由">第15章 アナログなカンバンボードを使う理由</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903195930.png" width="414" height="636" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="第16章-僕たちが学んだこと">第16章 僕たちが学んだこと</h2> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903195949.png" width="286" height="314" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="輪読会実施後の組織の変化">輪読会実施後の組織の変化</h2> <p>先述した外部ファシリテーターの件は、社内の MTG でも取り入れようというムーブが...!?</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/s/sasashuuu/20230903/20230903201406.png" width="1200" height="530" 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/sasashuuu/20230903/20230903203559.png" width="1200" height="138" 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/sasashuuu/20230906/20230906100722.png" width="1200" height="513" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="最後に">最後に</h2> <p>今回は、第2部 9~ 16章の内容を輪読会の様子を発信しました!次回はラストパートの第3部 17~ 21章です!お楽しみに!</p> <p>本輪読会の本が気になった方はぜひ手に取ってみてください。</p> <p><strong><a href="https://www.amazon.co.jp//dp/427406932X">&#x30EA;&#x30FC;&#x30F3;&#x958B;&#x767A;&#x306E;&#x73FE;&#x5834; &#x30AB;&#x30F3;&#x30D0;&#x30F3;&#x306B;&#x3088;&#x308B;&#x5927;&#x898F;&#x6A21;&#x30D7;&#x30ED;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x306E;&#x904B;&#x55B6; | Henrik Kniberg, &#x89D2;&#x8C37; &#x4FE1;&#x592A;&#x90CE;, &#x5E02;&#x8C37; &#x8061;&#x5553;, &#x85E4;&#x539F; &#x5927; |&#x672C; | &#x901A;&#x8CA9; | Amazon</a></strong></p> sasashuuu LLMを用いた機能を "見せること" にこだわり実装する中で工夫したこと hatenablog://entry/820878482959190004 2023-09-08T12:07:00+09:00 2023-09-08T12:07:00+09:00 みなさんこんにちは。 MLエンジニアのたかぱいです。 本日は、LLMを用いた機能をリリースするにあたり、どのように実装を進めていったのかをお話したいと思います。 完璧でなくとも実際に動くものを見せることで「触ってもらう→フィードバックをもらう→改善する」といったループを素早く回しながら実装していったよ、という内容になっています。 プロジェクト推進の話に関しては先日公開したブログに書いているので、興味がある方はこちらも見てみてください! tech.connehito.com 目次 今回実装した機能 ML プロンプト周りの検証 LangChainを用いた実装 iOS 最後に 今回実装した機能 一言… <p>みなさんこんにちは。 MLエンジニアの<a href="https://twitter.com/takapy0210">たかぱい</a>です。</p> <p>本日は、LLMを用いた機能をリリースするにあたり、どのように実装を進めていったのかをお話したいと思います。 <br/> 完璧でなくとも実際に動くものを見せることで「触ってもらう→フィードバックをもらう→改善する」といったループを素早く回しながら実装していったよ、という内容になっています。</p> <p>プロジェクト推進の話に関しては先日公開したブログに書いているので、興味がある方はこちらも見てみてください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F08%2F14%2F185903" title="エンジニア発信でLLMを用いた機能のリリースまでの工夫とユーザーの反応 - コネヒト開発者ブログ" 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.connehito.com/entry/2023/08/14/185903">tech.connehito.com</a></cite></p> <hr /> <p><span style="font-size: 120%">目次<ul class="table-of-contents"> <li><a href="#今回実装した機能">今回実装した機能</a></li> <li><a href="#ML">ML</a><ul> <li><a href="#プロンプト周りの検証">プロンプト周りの検証</a></li> <li><a href="#LangChainを用いた実装">LangChainを用いた実装</a></li> </ul> </li> <li><a href="#iOS">iOS</a></li> <li><a href="#最後に">最後に</a></li> </ul> <hr /> <h2 id="今回実装した機能">今回実装した機能</h2> <p>一言で言うと「自動で文章の要約を作成してタイトルとして挿入できる」という機能です(以降、タイトル生成と呼びます)</p> <p><a href="https://tech.connehito.com/entry/2023/08/14/185903">先日のブログ記事</a>でも紹介しましたが、実際にユーザーさんからも反応があり、今後も改善しながらより良いものを作っていきたいと思っています💪</p> <blockquote><p>自動で要約を作成して挿入できる、って… ママリ機能すごい✨ たしかにめちゃくちゃ長文で、途中で読むの断念することよくあるから 助かる😂</p> <p>要約機能が面白くて遊んでしまう!</p> <p>ママリの要約を挿入するを使ってみました!</p> <p>AI搭載なんかな??今すごいよね。</p></blockquote> <p>以降、バックエンド(ML側の処理)とアプリケーション(iOS側の処理)に分けて、実装プロセスの裏側を紹介していこうと思います。</p> <h2 id="ML">ML</h2> <p>タイトル生成の裏側の処理には、Open AI APIとLangChainを利用しました。</p> <p><figure class="figure-image figure-image-fotolife" title="概要図"><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/taxa_program/20230817125048" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/taxa_program/20230817/20230817125048.png" width="1200" height="448" loading="lazy" title="" class="hatena-fotolife" style="width:700px" itemprop="image"></a></span><figcaption>概要図</figcaption></figure></p> <h3 id="プロンプト周りの検証">プロンプト周りの検証</h3> <p>今回はどのようなプロンプトを作成するか?が肝になってきます。</p> <p>そこで、まずは過去ママリに投稿されている文章を使い、様々なプロンプトを検証していきました。</p> <p>ある程度良いプロンプトができたタイミングで、検証用アプリを社内メンバーに配布して、実際にスマホから触ってもらいフィードバックをもらいつつ、プロンプトの微修正を行っていきました。</p> <p><figure class="figure-image figure-image-fotolife" title=" "><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/taxa_program/20230817125126" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/taxa_program/20230817/20230817125126.png" width="1200" height="551" loading="lazy" title="" class="hatena-fotolife" style="width:800px" itemprop="image"></a></span><figcaption>社内メンバーに協力を仰いでいる図</figcaption></figure></p> <p>今回はiOS端末限定で検証していたこともあり、検証用アプリがインストールできない人も何人かいたため、Redash上で同じ機能が触れるようにすることで、なるべく多くの人に使ってもらう工夫もしました。</p> <p>以下のようなPythonコードをRedashで記述することにより、特定のエンドポイントからのリクエストを取得し、Redashで扱える形式で表示することができます。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> json <span class="synPreProc">import</span> requests url = <span class="synConstant">'URL'</span> payload = { <span class="synConstant">'user_id'</span>: <span class="synConstant">9999999</span>, <span class="synConstant">'content'</span>: <span class="synConstant">&quot;{{content}}&quot;</span> } headers = { <span class="synConstant">'Content-Type'</span>: <span class="synConstant">'application/json'</span>, <span class="synConstant">'User-Agent'</span>: <span class="synConstant">'sample-UA'</span> } response = requests.post(url, json=payload, headers=headers, timeout=(<span class="synConstant">10.0</span>, <span class="synConstant">17.5</span>)) title = json.loads(response.text)[<span class="synConstant">'title'</span>] result = {} add_result_row(result, {<span class="synConstant">'title'</span>: title, <span class="synConstant">'content'</span>: <span class="synConstant">&quot;{{content}}&quot;</span>}) add_result_column(result, <span class="synConstant">'title'</span>, <span class="synConstant">''</span>, <span class="synConstant">'string'</span>) add_result_column(result, <span class="synConstant">'content'</span>, <span class="synConstant">''</span>, <span class="synConstant">'string'</span>) </pre> <p><figure class="figure-image figure-image-fotolife" title="redashで検証した時の図"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/taxa_program/20230817/20230817125157.png" width="1200" height="297" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>redashで検証した時の図</figcaption></figure></p> <h3 id="LangChainを用いた実装">LangChainを用いた実装</h3> <p><a href="https://github.com/hwchase17/langchain">LangChain</a> とはGPT-3のようなLLMを利用したサービス開発を支援してくれるライブラリです。</p> <p>複数のモジュールを組み合わせることで、複雑なLLMアプリケーションを作成するプロセスを簡易化してくれます。</p> <p>今回は以下のような要約クラスを作成し、ユーザーがママリ上で入力したテキストを要約しています。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> openai <span class="synPreProc">from</span> dotenv <span class="synPreProc">import</span> load_dotenv <span class="synPreProc">from</span> langchain.chains <span class="synPreProc">import</span> LLMChain <span class="synPreProc">from</span> langchain.chat_models <span class="synPreProc">import</span> ChatOpenAI <span class="synPreProc">from</span> langchain.prompts.chat <span class="synPreProc">import</span> ( ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate, ) load_dotenv() openai.api_key = os.environ[<span class="synConstant">&quot;OPENAI_API_KEY&quot;</span>] <span class="synStatement">class</span> <span class="synIdentifier">OpenAISummarizer</span>: <span class="synStatement">def</span> <span class="synIdentifier">__init__</span>(self, human_message: <span class="synIdentifier">str</span>) -&gt; <span class="synIdentifier">None</span>: self.llm = ChatOpenAI(model_name=<span class="synConstant">&quot;gpt-3.5-turbo&quot;</span>, temperature=<span class="synConstant">0.2</span>) self.delimiter = <span class="synConstant">&quot;####&quot;</span> self.system_template = <span class="synConstant">&quot;&quot;&quot;</span> <span class="synConstant"> ここにプロンプトを挿入</span> <span class="synConstant"> 以下のように記述することで、どこからどこまでがユーザー入力文(質問文)かを明確にしている</span> <span class="synConstant"> The questions are separated by {delimiter} characters.</span> <span class="synConstant"> &quot;&quot;&quot;</span> self.human_message = human_message <span class="synStatement">def</span> <span class="synIdentifier">_create_prompt_template</span>(self) -&gt; ChatPromptTemplate: <span class="synComment"># systemメッセージプロンプトテンプレートの準備</span> system_message_prompt = SystemMessagePromptTemplate.from_template(self.system_template) <span class="synComment"># humanメッセージプロンプトテンプレートの準備</span> human_template = <span class="synConstant">&quot;&quot;&quot;{delimiter}\{human_message}\{delimiter}&quot;&quot;&quot;</span> human_message_prompt = HumanMessagePromptTemplate.from_template(human_template) <span class="synComment"># chatプロンプトテンプレートの準備</span> prompt = ChatPromptTemplate.from_messages([system_message_prompt, human_message_prompt]) <span class="synStatement">return</span> prompt <span class="synStatement">def</span> <span class="synIdentifier">run</span>(self): <span class="synComment"># プロンプトの定義</span> chat_prompt = self._create_prompt_template() <span class="synComment"># LLMチェーンの準備</span> summary_chain = LLMChain(llm=self.llm, prompt=chat_prompt) result = summary_chain.run(delimiter=self.delimiter, human_message=self.human_message) <span class="synStatement">return</span> result <span class="synComment"># 〜リクエストを受け取る部分は省略〜</span> <span class="synComment"># 要約の実行</span> summarizer = OpenAISummarizer(human_message=text) response = summarizer.run() </pre> <p>LangChainの具体的な使い方などは以下のブログにも書いているので、興味のある方は見てみてください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Ftech.connehito.com%2Fentry%2F2023%2F05%2F24%2F182929" title="LangChainとOpenAI APIを組み合わせて、文脈を考慮して会話できるSlack Botを作った話 - コネヒト開発者ブログ" 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.connehito.com/entry/2023/05/24/182929">tech.connehito.com</a></cite></p> <p>以降のiOSパートはiOSエンジニアのゆりこにバトンタッチしてご紹介していきます。</p> <h2 id="iOS">iOS</h2> <p>タイトル生成機能のiOSアプリ実装を担当した<a href="https://tech.connehito.com/entry/2023/07/07/192756">ゆりこ</a>です。</p> <p>実装の進め方で工夫した点を挙げたいと思います。</p> <p><figure class="figure-image figure-image-fotolife" title=" "><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/yoshitaka465/20230817141953" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20230817/20230817141953.png" width="1200" height="591" loading="lazy" title="" class="hatena-fotolife" style="width:800px" itemprop="image"></a></span><figcaption>全体スケジュール</figcaption></figure></p> <p>施策具体化された6月1週目の打ち合わせでPdMより3週間後の4週目のリリースを目標にすることが伝えられました。この段階では仕様・デザインのFixがまだできていない状態。Miroでワイヤーを作りながらディスカッションを行なっていたので大まかな仕様とざっくりデザインはありました。</p> <p><figure class="figure-image figure-image-fotolife" title=" "><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/y/yoshitaka465/20230817/20230817143255.png" width="1200" height="811" loading="lazy" title="" class="hatena-fotolife" style="width:800px" itemprop="image"></span><figcaption>Miroに書かれた大まかな仕様とざっくりデザイン</figcaption></figure></p> <p>アプリ実装担当として、まずは仮実装のものが開発環境で動かせる状態を早く作りたいと思いました。</p> <p>理由は以下の通りです。</p> <ul> <li>3週間後にリリースするイメージをチームで共有したい</li> <li>仕様・デザインのFixで時間がかからないようにしたい</li> </ul> <p>( ※ コネヒトではFirebaseのAppDistributionを使いリリース前のバージョンを検証端末として登録しているデバイスに配布することができるようになっています)</p> <p>実際のところ、3週間後にMiroの大まかな仕様とざっくりとしたデザインが形になりユーザーが使っている状態をチームとしてはまだ想像しきれていなかったと思います。</p> <p>また、デザイナーもこれまで打ち合わせには参加していないため、デザイン相談するにしてもキャッチアップに時間がかかりそうでした。</p> <p>このあたりの課題を早い段階に仮実装アプリを配布することで一部解決できるのではと考えました。</p> <p>当然、機能として動かすにはML側のAPIとの接続も必要でしたが、仮実装ではスピード重視でモックを使いAPI接続なしで動かしました。</p> <p><figure class="figure-image figure-image-fotolife" title=" "><span itemscope itemtype="http://schema.org/Photograph"><a href="http://f.hatena.ne.jp/taxa_program/20230817125417" class="hatena-fotolife" itemprop="url"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/t/taxa_program/20230817/20230817125417.png" width="1200" height="773" loading="lazy" title="" class="hatena-fotolife" style="width:500px" itemprop="image"></a></span><figcaption> </figcaption></figure></p> <p>最初に配布した仮実装は粗々なものでしたが、PdM・デザイナーに手元のデバイスで動かしてもらったことにより、仕様やデザイン観点ですぐにフィードバックがもらえ、最終的な「良さそう!」がもらえるまでスピード感を持ちながら進めることができました。</p> <p>結果的に目標通りの期日に機能リリースできた点を踏まえると、粗々なものでも早い段階で仮実装を配布する動き方は施策をスピード感持って進める上で効果的であったと思います。</p> <p>また開発環境への仮実装配布はチーム外の人も触れる状態になるので、この施策が閉じた環境で動いてないことをアピールする形にもなったと感じます。</p> <p>この経験を他の施策にも活かしてアプリの機能改善をスピード感持ちながら進めていきたいです!</p> <h2 id="最後に">最後に</h2> <p>コネヒトでは一緒に働く仲間を募集しています! そして興味持っていただけた方は気軽にご連絡ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Fconnehito%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/connehito/projects">www.wantedly.com</a></cite></p> taxa_program 第1回 リーン開発の現場輪読会 技術課題についてワイワイ編 hatenablog://entry/820878482963696104 2023-09-06T11:00:00+09:00 2023-09-06T11:00:03+09:00 こんにちは。Androidエンジニアのkatsutomuです。 前回に引き続き、狩野英孝さんのマイクラ配信にハマっています。 #12の家の地下が怪しいぞ!の巻のスポナーのくだりには驚愕しました! さて本日は、社内で実施している輪読会の様子を、紹介したいと思います。 リーン開発の現場を3回に分けて輪読会を実施していく予定です。 コネヒトでは輪読会をはじめとして、共に学び合うカルチャーがあるので、少しでも社内の雰囲気を感じて貰えば嬉しいです。 輪読会の進め方 今回はリーン開発の現場という本を輪読しています。アジャイルソフトウェア開発手法のひとつであるリーンソフトウェア開発手法を、スウェーデンの警察… <p>こんにちは。Androidエンジニアのkatsutomuです。 前回に引き続き、狩野英孝さんのマイクラ配信にハマっています。 #12の家の地下が怪しいぞ!の巻のスポナーのくだりには驚愕しました!</p> <p>さて本日は、社内で実施している輪読会の様子を、紹介したいと思います。 リーン開発の現場を3回に分けて輪読会を実施していく予定です。</p> <p>コネヒトでは輪読会をはじめとして、共に学び合うカルチャーがあるので、少しでも社内の雰囲気を感じて貰えば嬉しいです。</p> <h2 id="輪読会の進め方">輪読会の進め方</h2> <p>今回はリーン開発の現場という本を輪読しています。アジャイルソフトウェア開発手法のひとつであるリーンソフトウェア開発手法を、スウェーデンの警察機関のシステム開発で事例をもとに解説した本で、カンバンシステムを中心に書かれています。</p> <p><a href="https://www.amazon.co.jp//dp/427406932X">&#x30EA;&#x30FC;&#x30F3;&#x958B;&#x767A;&#x306E;&#x73FE;&#x5834; &#x30AB;&#x30F3;&#x30D0;&#x30F3;&#x306B;&#x3088;&#x308B;&#x5927;&#x898F;&#x6A21;&#x30D7;&#x30ED;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x306E;&#x904B;&#x55B6; | Henrik Kniberg, &#x89D2;&#x8C37; &#x4FE1;&#x592A;&#x90CE;, &#x5E02;&#x8C37; &#x8061;&#x5553;, &#x85E4;&#x539F; &#x5927; |&#x672C; | &#x901A;&#x8CA9; | Amazon</a></p> <p>コネヒトでもGithub ProjectsやZenhubで、カンバンを活用して開発を進めているので、業務のヒントを見つけるべく、有志を募って輪読会の開催に至りました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901144221.png" width="1200" height="546" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>そこそこボリュームのある本なのでそれぞれが個別で読み、事前に付箋を書く方法ですすめています。初回は第1章から第8章までが対象として、「思ったこと」「あるある」「やってみたい」の3つの観点で事前に付箋を書いてくるようにしました。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901144249.png" width="1195" height="836" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="参加メンバーのお声">参加メンバーのお声</h2> <p>最初に、参加メンバーから感想を紹介します。</p> <p><strong>ささしゅう</strong></p> <p>技術課題の話で盛り上がる場面があったが、自身が所属しているプラットフォームグループのミッションとしても知りたい内容だったのでよかった。また、技術課題とは別の内容に比重をおいて本書を読み解くメンバーもいて、色々な視点を養えてよかった。</p> <p><strong>ほみちゃん</strong></p> <p>YuyaAboの話を聞いて捉え方が変わった。技術課題を解決することで、事業にどのような影響があるかは考えられていなかったので、いいきっかけになった。</p> <p><strong>otukutun</strong></p> <p>編成が違うプロジェクトのチームの事例が色々知れたので、今後の引き出しになると思った。 今のチームにどれぐらい当てはめられるかは、状況も違うのでしっかりと考えたい。</p> <p><strong>YuyaAbo</strong></p> <p>ボトルネックが可視化される一つの物理看板がすごい良いと思って読んでいた。 技術課題もボトルネックを解消するための一つの要素として扱うなど新鮮だった。今後も楽しみ。</p> <p>それぞれ、何かしら学びを得られたと思いますし、今後の期待についてのコメントもあり、嬉しい限りです。この感想だけで満足せず、開発フローの改善もトライしていきたいと思います。</p> <h2 id="輪読会でワイワイと話したこと">輪読会でワイワイと話したこと</h2> <p>当日は上げられた付箋から、いくつか「特に話したいこと」をピックアップし、社内の課題と合わせて1時間程度ワイワイと話しました。</p> <p>今回は「第8章 技術課題をさばく」の内容が多くふれられ、</p> <ul> <li>技術課題をどう可視化するか</li> <li>チームや組織で、どう管理するか</li> <li>そもそも、どのように技術課題は捉えているか</li> </ul> <p>というようなことが主題になりました。</p> <p>この本には、技術課題の取捨選択のエピソードとして、開発フローのボトルネックになっている課題や、管理不能なクラスのリファクタリングの必要性を示すために、クラス全体を紙にプリントアウトした長さを元に、技術課題として対処すべきことを決める様子が書かれています。</p> <p>それらエピソードから、開発のリードタイムを下げるなど事業的な価値と関連づけることが重要なのではないかという話に落ち着きました。</p> <h3 id="記入された付箋の数々">記入された付箋の数々</h3> <p>せっかくなので、今回の輪読会で上がった付箋を紹介します。</p> <h3 id="第1章-プロジェクトについて">第1章 プロジェクトについて</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901144505.png" width="974" height="1054" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第2章-チーム編成">第2章 チーム編成</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901144702.png" width="944" height="1030" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第3章-デイリーカクテルパーティに参加しよう">第3章 デイリーカクテルパーティに参加しよう</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901145017.png" width="556" height="752" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第4章-プロジェクトボード">第4章 プロジェクトボード</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901145029.png" width="724" height="748" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第5章-看板ボードをスケールさせる">第5章 看板ボードをスケールさせる</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901145112.png" width="654" height="716" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第6章-プロジェクトのゴールを追え">第6章 プロジェクトのゴールを追え!</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901145122.png" width="628" height="780" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第7章-準備OKを定義する">第7章 準備OKを定義する</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901145045.png" width="542" height="828" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h3 id="第8章-技術課題を捌く">第8章 技術課題を捌く</h3> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901145135.png" width="616" height="898" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>個人的な推し付箋は以下の2つです。</p> <div class="images-row mceNonEditable"> <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/k/katsutomu0124/20230901/20230901145228.png" width="1047" height="1200" 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/k/katsutomu0124/20230901/20230901145259.png" width="1047" height="1200" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span> </div> <p>それぞれ4章プロジェクトボードと、8章技術課題を捌くで上げられた付箋です。 この2つを推している理由は、、今回、筆者が輪読会を開催した理由の一つに、開発フローのブロッキング要素可視化や技術課題の扱い方の事例を学び、チーム改善の種にしたいというモチベーションがあり、それが叶ったと感じた為です。</p> <h2 id="最後に">最後に</h2> <p>今回は、開発フローの改善のきっかけとして輪読会を実施した様子をお伝えしました。 今後は輪読会をきっかけに、どんな変化が起きたかもお伝えする機会を作れればと考えています。まずは、第2部 9~ 16章の内容を輪読会する予定です!</p> <p>カンバンの運用に課題感を感じている方がいたら、ぜひ一度お手に取っていただくと良いかと思います。</p> <p><a href="https://www.amazon.co.jp//dp/427406932X">&#x30EA;&#x30FC;&#x30F3;&#x958B;&#x767A;&#x306E;&#x73FE;&#x5834; &#x30AB;&#x30F3;&#x30D0;&#x30F3;&#x306B;&#x3088;&#x308B;&#x5927;&#x898F;&#x6A21;&#x30D7;&#x30ED;&#x30B8;&#x30A7;&#x30AF;&#x30C8;&#x306E;&#x904B;&#x55B6; | Henrik Kniberg, &#x89D2;&#x8C37; &#x4FE1;&#x592A;&#x90CE;, &#x5E02;&#x8C37; &#x8061;&#x5553;, &#x85E4;&#x539F; &#x5927; |&#x672C; | &#x901A;&#x8CA9; | Amazon</a></p> <p>そして、コネヒトでは一緒に働く仲間を募集しています! 興味持っていただけた方は気軽にご連絡ください!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fwww.wantedly.com%2Fcompanies%2Fconnehito%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/connehito/projects">www.wantedly.com</a></cite></p> katsutomu0124