TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

385

はじめに 目指す状態と現状のギャップを考える 足りない情報を要求・整理 コミュニケーションツール、ドキュメンテーションツールの重要性 契約上の引き継ぎ時点を迎えての作業 Git リポジトリの移管 クラウドベンダー、ドメインレジストラなど各種契約の移管 AWS ドメインレジストラ 各種管理者の認証情報の受領 引き継ぎの後にやったこと クラウドサービスの料金の傾向・コスト構造の確認 手動運用の自動化 監視機構の確認、追加 共同でのトラブル対応 おわりに はじめに エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。 今年、エブリーでは ネットスーパーのシステムを株式会社ベクトルワン様から引き継ぎました 。その裏で、私たちのチームでは知見のないシステムを、自分たちで運用・開発可能な状態にするように様々なことをやってきました。 ここでは every Tech Blog Advent Calendar 2023 の 7 日目の記事として、システムを別の会社から引き継いだ中で考えたこと・やってきたことを紹介したいと思います。今回引き継いだシステムは具体的には Web アプリケーションサーバー、スマホアプリ、複数のスマホアプリ向け API サーバー、及びそれに付随するシステム(非同期処理・バッチ処理基盤、ロードバランサーなど)です。 なお、私は引き継ぎ作業の前段階(デューデリジェンス、大枠のスケジュール策定、契約締結など)が済んで、さあ実際にシステムを引き継ぐぞというところでこのプロジェクトのオーナーとなったので、実際のシステムの引き継ぎ作業に絞ってお話します。 目指す状態と現状のギャップを考える 私はシステムを他社から引き継いだ経験がなく担当となった当初何をしていいかわかりませんでした。両社のソースコードは GitHub で管理されていたので、とりあえず自社の開発メンバーを 外部のコラボレーター として招待していただきました。その後ソースコードをざっと眺めましたが、今回引き継いだネットスーパーのシステムはそれなりに大きく、ソースコードを読み続けてシステムを理解するのは筋が悪そうだとわかりました。 そこで引き継ぎのプロジェクトを通してどうなりたいかを考えだしました。システムを引き継いだらそれは自社のシステムとなります。ということは自社のメンバーで運用を行っていくのはもちろん、障害が起こったら自分たちで復旧し、プロダクトの詳細な仕様やバグへの説明責任も基本的には自分たちが持つことになります。その役割が果たせる状態と現状とのギャップは何か、それを埋めていくには何をすべきかを考えていきました。 足りない情報を要求・整理 何もわからないという状態を紐解いていくと、当たり前ではあるのですが自社のプロダクトなら当然知っているような情報が欠如していることに気づきました。例えば次のようなものです。 システムの実現するサービスとユースケース 全体的なシステムの構成、構成要素それぞれの関係 システムと周辺システム、外部のシステムとの関係 ネットワーク、DNS アクセス制御 各種アカウント・認証情報 アプリケーションコード 責務の分割のされ方 アプリケーションコード・テストコード記述のルール・方針 データベースの構造 システムの監視方法、正常性の把握の方法、異常時の対処方法 セキュアな情報の取り扱い方 各種運用フロー QA リリース 手動の作業 etc... これらをリスト化し、それぞれについて情報を要求・整理していきました。知りたい項目についてドキュメントが無いこともありますし、ドキュメントが存在しても断片的、あるいは前提知識を必要としたりします。不足する情報はドキュメントを用意してもらったり、断片的な情報は受け取った後に情報をまとめて包括的に構成します。 この時強く思ったことは、引き継ぎは引き継ぐ側と引き継がれる側が協力して行うプロジェクトであるということでした。引き継がれる側としてはただ情報を待っているのではなく、どんな情報がなぜ必要で、どのようなアウトプットを期待しているのかをなるべく明確に伝える必要があると感じ、一つ一つの項目ごとにどういう状態になれば引き継ぎが完了となるのかの合意を取っていきました。 コミュニケーションツール、ドキュメンテーションツールの重要性 前述の通り、引き継ぎは自社・他社含めた協力プロジェクトであり、その中では多くのやり取りや資料の作成が行われます。社内ならば既定のツールを使用すればいいですが、社外の方とのやり取りでは既定のツールというものがありません。しかしフロー情報とストック情報を記入できるツールの導入は非常に重要です。 私達は共用のコミュニケーションツールとして Slack や Zoom を、ドキュメンテーションツールとして Notion を、タスク管理に Google スプレッドシートを使用しました。いずれのツールも一方が他方の普段使いのツールに相乗りする形を取っています。この場合引き継ぎプロジェクトが終わればツールへの相乗りも終了するので、相乗りしている側は必要な情報をエクスポートすることになります。 契約上の引き継ぎ時点を迎えての作業 引き継ぎのプロジェクトは契約上の引き継ぎ時点より数ヶ月前から始めましたが、実際の移管作業については契約上の引き継ぎ時点の近辺で行います。例えば次のようなことです。 Git リポジトリ移管 クラウドベンダー、ドメインレジストラなど各種契約の移管 各種管理者の認証情報の受領 それぞれについて軽く触れます。 Git リポジトリの移管 GitHub の リポジトリの移譲 作業を行いました。ドキュメントに書いてあることではあるのですが、組織間のリポジトリの移譲には 移譲前のリポジトリに対する管理者権限(または移譲前の組織の管理者権限またはオーナー権限) 移譲先の組織のリポジトリを作成する権限 が必要でした。外部コラボレーターとして移譲前のリポジトリに招待してもらっている引き継ぎ元の開発者アカウントをリポジトリの管理者にしていただき、そのアカウントにて移譲作業を行いました。 リポジトリ移譲後は GitHub 引き継ぎ後にソースコードの内容を質問させていただくことを考え、逆に引き継ぎ元の開発メンバーを外部のコラボレーターとして招待させていただきました。 クラウドベンダー、ドメインレジストラなど各種契約の移管 システムの稼働や運営に必要な契約の付け替えを行います。契約しているサービスによって移管の方法は様々でした。ここでは AWS アカウントとドメインレジストラの移管の方法に触れます。 AWS AWS Organizations のメンバーアカウントを他の組織へ移行する_ Part 1 _ Amazon Web Services ブログ の記事を参考に、メンバーアカウントの移管作業を行いました。 今回は両社ともに組織アカウントを使用しており、また移管対象のメンバーアカウントに移管対象外のシステムが含まれていなかったことから、単にメンバーアカウントを組織アカウントへ移行するのみで済みました。引き継ぎ対象外のリソースが存在するなどで AWS アカウントごとの移管が難しい場合には、前もって計画的にリソースを別の AWS アカウントに移すなどし、AWS アカウント移管を行える状態を作っておくことが必要です。またアカウントごとの移管が現実的ではない場合は、リソース単位で移管を行うことになると思います。 細かい話ですが、AWS アカウントの移行ではその時間を指定するようなことができず、移管作業を行ったタイミングで移管が行われるようです。出来ればとある日時以降の料金の請求がエブリーに対して行われるようにしたかったのですが、その方法は調べた限り無さそうでした。 ドメインレジストラ 引き継ぎ元ではお名前.com が利用されており、エブリーでは別のレジストラを主に使用していたのですが、今回は引き継ぐドメインが十数個と多く、引き継ぎ作業の楽さを優先してエブリーのお名前.com へとドメインを移管しました。お名前.com には お名前 ID 付け替え という機能があり、比較的楽にドメインを移管できました。今後必要となれば社内でお名前.com から普段使いのレジストラへの移管も検討しますが、優先度は低く置いています。 各種管理者の認証情報の受領 各種サーバーの root ユーザーの SSH 鍵や、データベースの root アカウントの認証情報の共有を頂きます。引き継ぎ元の開発者によるアクセスが必要なくなったら、認証情報を変更ができると良いでしょう。 引き継ぎの後にやったこと ここまでで引き継ぎ作業としては終了ですが、引き継いだままの状態では社内の運用のフローとの齟齬が開発時の戸惑いに繋がったり、改めてプロダクトの状態を自社視点でみると最適化すべき部分が見つかります。そのそれぞれを自社の基準や文化に合わせて修正していくことはアジリティやサービス品質の維持・向上に繋がります。 なお、今回は引き継ぎを受けた後にも引き継ぎ元の開発者の方々に一定期間はサポートをいただけることになっています。契約上の引き継ぎ時点を過ぎ、必要な情報を頂いたつもりでも色々な情報が足りていないことに後から気づく場面が多くありましたので、契約等が許す限り一定期間サポートを受けられる体制をつくることが理想だと強く感じます。 次に具体的に引き継ぎを受けた後にやったことをいくつか紹介します。 クラウドサービスの料金の傾向・コスト構造の確認 AWS Web コンソール内の Cost Explorer にて料金の傾向を簡単に把握し、過剰なリソースがないかをざっとチェックしました。実際に、とあるマシンのインスタンスタイプが不自然に大きいことに気づきその変更を行いました。 これについては早くやるほどコストが節約できるので、AWS アカウントの移管が終わり、Cost Explorer が閲覧できるようになった初日に実施しました。結果として月々 6 万円以上のコスト削減に繋がりました。 手動運用の自動化 手動で行われていた運用について自動化出来そうなところがいくらかあったので、運用の背景を引き継ぎ元の開発者に伺いつつ、無理なく出来そうな部分については自動化をすすめています。 一見自動化できそうでも出来ない理由があったりするので、サポートいただけるうちに背景を聞きます。この作業では手動運用が削減できるだけではなく、システムについてより深く知る機会にもなりました。 監視機構の確認、追加 引き継ぎ前から行われているシステムの監視について整理し、社内の基準を鑑みて監視しておきたくなった点については新たに仕組みを導入しています。 合わせて既存の監視機構の通知先を変更し、自社の開発チームで異常に気付けるようにします。 こちらも不明点は引き継ぎ元の開発者に協力をいただき解消していきました。作業を通して、システムの全体への把握を強める機会にもなりました。 共同でのトラブル対応 移管後しばらくしてちょっとしたトラブルが起こりました。ソフトウェアエンジニアにとって、知らないシステムからの大量のアラートほど絶望するものは無いかもしれません。 ここでも引き継ぎ元の開発者にトラブルの概要を伺いながら原因を特定し、スムーズに対応することができました。もし自社のメンバーだけでの対応だったなら原因の特定や対処方針策定に相当な時間がかかっていたと思います。 余談ですが、このトラブルの原因となったバグは、システムの引き継ぎ前から潜んでいたものが偶然引き継ぎ直後に露見したというものでした。心臓に悪いので、もう少し空気を読んで露見するタイミングを選んでほしいものです。 おわりに 「ゼロからはじめるシステム引き継ぎ」と題して、何もわからないところからシステムを引き継いだプロジェクトについて紹介しました。こう書いてみて思ったこととして、今回は他社からシステムを引き継ぐ形でしたが、たとえ社内であっても異動があったり、新しくジョインされた方は何もわからないような状況に置かれる事に気づきました。システムのドキュメンテーションやクラウド料金・監視体制の見直しなんかは継続的に社内でも行っていけると良いなと思いました。 こういったプロジェクトに携わられる方はそう多くないとは思いつつ、どなたかの役に立つと幸いです。お読みいただきありがとうございました。
アバター
はじめに この記事は every Tech Blog Advent Calendar 2023 の 6 日目です。 今回は「DI toolkit samber/do の紹介」と題しまして、 samber/do のざっくりとした紹介と、今後リリースされるであろう次期バージョンでの変更点についてまとめていきます。 samber/do とは samber/do は Go で DI を実現するモジュールのひとつです。 同様の領域では google/wire が一番メジャーでしょうか。 その次に uber-go/fx が使われている印象です。 google/wire は現在はメンテナンスモードで、継続的な開発は行われていません。 uber-go/fx は reflection を利用した高度な機能を提供しているほか、単なるDIツールではなく、アプリケーションフレームワークとしての性格も兼ね備えている点が特徴です。 今回紹介する samber/do は reflection も code generation も用いず、シンプルに依存性の解決のみに注力している点が特徴的なモジュールです。 samber/do の DI 以下、 公式の Quick start より転載です。 import ( "github.com/samber/do" ) func main() { injector := do.New() // provides CarService do.Provide(injector, NewCarService) // provides EngineService do.Provide(injector, NewEngineService) car := do.MustInvoke[*CarService](injector) car.Start() // prints "car starting" do.HealthCheck[EngineService](injector) // returns "engine broken" // injector.ShutdownOnSIGTERM() // will block until receiving sigterm signal injector.Shutdown() // prints "car stopped" } type EngineService interface {} func NewEngineService(i *do.Injector) (EngineService, error ) { return &engineServiceImplem{}, nil } type engineServiceImplem struct {} // [Optional] Implements do.Healthcheckable. func (c *engineServiceImplem) HealthCheck() error { return fmt.Errorf( "engine broken" ) } func NewCarService(i *do.Injector) (*CarService, error ) { engine := do.MustInvoke[EngineService](i) car := CarService{Engine: engine} return &car, nil } type CarService struct { Engine EngineService } func (c *CarService) Start() { println ( "car starting" ) } // [Optional] Implements do.Shutdownable. func (c *CarService) Shutdown() error { println ( "car stopped" ) return nil } 基本的に do.Provide[T any](*do.Injector, func(*do.Injector) (T, error)) で生成関数を登録し、 do.Invoke[T any](*do.Injector) で値を取得するという流れです。 uber-go/fx のように生成関数の引数から reflection で自動的に解釈して依存ツリーを作ってくれるような仕組みはないため、利用側で samber/do に依存した生成関数を定義する必要があります。 samber/do のメリット、デメリット samber/do は非常に小規模なモジュールで、コア機能は依存性を登録することと依存性を解決することのみです。 それゆえに取り回しが非常にしやすく、例えばあるタイミングで依存性ツリーを再構築したい(実例として、Config のホットリロードなど)、と言うようなことも自分でアプリケーションを作り込めば無理なく実現可能です。 しかし、samber/do に依存する生成関数を定義しなければならない点はやや取り回しは悪いと感じるかもしれません。 また、依存性ツリーの構築はコンパイル時ではなく実行時に動的に行われるため、構築されたツリーに瑕疵がないか(登録が不十分で依存性の解決ができない)は利用者が担保する必要があります。 私も実際にテストを書いていて、うっかり生成関数の登録を忘れていて依存性解決の際にエラーになるということが稀にありました。 この辺りをDIツール側で担保してほしいというニーズが強い場合、google/wire や uber-go/fx の方が合っているかもしれません。 私が開発に関わっているプロジェクトでは、今の所規模も小さいこともあってこのようなトラブルは発生していませんが、いずれ向き合わなければならない問題だと認識しております。 近い将来に google/wire と似たようなアプローチで samber/do 向けの依存ツリーを静的に生成するモジュールを作ろうかと考えているところです。 samber/do@v2 samber/do は非常に新しいモジュールです。一般的に、新しいモジュールを導入する場合、それらがどのようにメンテナンスされているかを把握しておくことが重要です。 ところで私は最近 samber/do に次のメジャーバージョン v2 の計画があることに気づきました。 まだ計画段階ですが、いくつかこれが欲しかったんだ!と言う機能が盛り込まれる予定ですので、いくつかピックアップして紹介します。 Scope v2 における目玉となる機能です。 依存ツリーを一つのまとまり(Scope)として、Scope間のツリー構造を構築し、依存性解決の際にScopeツリーを辿りながら値を取得することができるようになります。 これだけ聞くとなんのこっちゃ、と思うかもしれませんが、Java/Spring Boot に慣れている方であれば @ApplicationScoped や @SessionScoped 、 @RequestScoped のように生存時間が異なるオブジェクトを一つの依存性ツリーでまとめて管理できるようになると言えばイメージできるかもしれません。 Java/Spring Boot を触っていない人は全くわからない話で申し訳ありません。実際の サンプルコード があるので、そちらを読むと良いかもしれません。 依存ツリーの明確化 これまでは do.Injector のフィールドにサービスを単純にmapで保持していたのですが、依存ツリーをDAG(有向非巡回グラフ)として明確に保持するように変わります。 これによって実行時にサービス間の依存関係を取得できるようになるため、DI部分で何かトラブルがあった際に問題を特定することが 容易になります 。 循環参照の検出 依存性解決の際に循環参照を検出し、エラーにすることができるようになります。 Transient services 依存性解決のたびに毎回生成関数を呼び出して新たなオブジェクトを生成するサービスを登録することができるようになります。 現時点では以下のように引数なしの関数をサービスとして登録し、依存性解決後に自分で関数を呼び出してオブジェクトを取得する工夫が必要です。 import ( "time" "github.com/samber/do" ) func main() { injector := do.New() do.ProvideNamed(injector, "nowFunc" , func (_ *do.Injector) ( func () time.Time, error ) { return func () time.Time { return time.Now() }, nil }) nowFunc := do.MustInvokeNamed[ func () time.Time](injector, "nowFunc" ) now := nowFunc() println (now.Format(time.RFC3339)) } tag ベースでの依存性注入の自動化 これ はまだ v2 に入るかは不透明ですが、生成関数を毎回自分で書くのはそれなりに面倒なため、uber-go/fx のように reflection を使って自動的に依存性注入を行うヘルパー関数が提供される予定です。 おわりに 今回は samber/do という Go で DI を実現するモジュールについて紹介をしました。 Go は他の言語に比べて DI が採用されるケースが少ない印象です。もちろんDIは銀の弾丸でも、唯一の答えでもなく、採用するかどうかはプロジェクトごとに判断が必要です。 あくまで個人的な意見ですが、今回紹介した samber/do は比較的 Go の思想に寄り添った形で無理なく DI を導入できるバランスのいいモジュールだと思います。 今回の記事がみなさんの参考になれば幸いです!
アバター
こんにちは、トモニテ開発部の Android エンジニアです。 この記事は every Tech Blog Advent Calendar 2023 の 5 日目です。 最近、 Android エンジニアに新たなメンバが増えました。 こんなこともあろうかと作っておいた贈り物としてドキュメントがありますので、どんなものか紹介します。 どんなドキュメントなのか GitHub にあるプロダクトのリポジトリの Wiki に、 アプリの構成 開発の前に 開発時の Q&A File Templates をまとめたものです。 アプリがどんな作りなのか? 開発するにあたり守るべきことはあるか? xxx をやりたいときはどうすればよいか? を知り、今後の開発作業での詰まりポイントを減らしていきたい、という思いから作成しました。 ドキュメントの内容 1: アプリの構成 自分自身、まずはアプリの歩き方を知りたくなるので、アプリの地図たる構成をまとめました。 アプリはマルチモジュールで作成していたため、どこにどんなモジュールがあるのかを列挙しています。 一部抜粋するとこんな感じです。 presentation: プレゼンテーション層 ┣━ common: プレゼンテーション層で使う便利な処理置き場 ┣━ feature: ユーザ向けの機能置き場 ┃ ┣━ home: ホーム ┃ ┣━ article: 記事詳細 ここでは他にも、モジュール間の依存関係や各モジュールの役割も記載し、全体像を把握してもらえるようにしています。 ドキュメントの内容 2: 開発の前に 開発作業を進めるにあたり基本的な情報となるものをまとめています。 内容は、 ブランチの命名ルール アプリ起動のルート(通常のアプリ起動、Push 通知による Notification 経由、Scheme 起動)ごとのエントリポイントの明記 ライブラリは Version catalog でまとめていること 後述の File Templates を使うと楽になる実装があること を記載しています。 これからの開発で意識してもらいたいルールや、覚えておいてもらえると役にたつものたちです。 ドキュメントの内容 3: 開発時の Q&A 現状の設計に対して「こうするとやりたいことが実現できます」といった情報をまとめています。 一例としては、 新機能を作りたい 各レイヤーで実装するガワを作成したり DI モジュールへの登録といった流れを記載 他画面へ遷移したい feature モジュール間で依存を持たないようにしているため、遷移を行うための router について記載 などがあります。 機能開発やメンテナンスを容易に行えるようにという目的での記載です。 ドキュメントの内容 4: File Templates AndroidStudio の Settings にある File and Code Templetes で使っているテンプレートを記載しています。 全社的な AndroidStudio の Settings をエクスポートしたファイルは別途管理されているのですが、こちらはプロダクト固有なので Wiki 中に記載しました。 テンプレートの内容としては、プレゼンテーション層、ドメイン層、データ層で主に実装することになるインターフェースやクラスのガワを出力するというものです。 実装してほしい箇所を todo として出力しているので、そこを埋めるだけで他と同様の作りにできます。 これにより、実装箇所のガイドに沿って作業を進めるだけで自然と同じようなメンテナンスをするだけで OK となる作りを広げることができます。 運用してみてどうだったか ドキュメントを読んだ新メンバに感想を聞いたところ、こちらの期待した通りの役立ち方をしていたようでした。 わかってる人が書いた資料なのでメモを取るよりも正確であり、かつそれを自身の確認するペースで読み込める点が良かったとのことです。 最後に ドキュメントの内容を細かく記載していくとその時の情報としては正しく、あると嬉しいものなのですが、内容のメンテナンスを怠ると嘘つきの書になってしまいます。 定期的に更新タイミングを設けていき、新たなメンバが早めに真価を発揮できる環境を維持していきたいです。
アバター
Playwrightを活用したE2Eテストの導入 はじめに 想定読者 ハンズオンの前提条件 この記事で得られるもの 実行環境 Playwrightを活用したE2Eテストの導入 Playwrightとは Playwrightの特徴的な機能 Test generator UI Mode Watch mode 定義したアクションごとのスナップショット ハンズオン Next.jsをセットアップする Playwrightをセットアップする Next.jsのサンプルアプリケーションを起動する テストコードの作成 テストの実行 GitHub Actionsでテストを実行する playwrightの設定ファイルを変更する テストの結果を確認する 最後に Playwrightを活用したE2Eテストの導入 はじめに この記事は、every Tech Blog Advent Calendar 2023 の4日目の記事です。 tech.every.tv はじめまして。 株式会社エブリー DELISH KITCHEN 開発本部の羽馬( @NaokiHaba )と申します。 今回は、簡単なハンズオンを通して、Playwrightの基本的な使い方を紹介していきます。 実装したソースコードは 以下のレポジトリで公開していますので、興味のある方はご覧ください。 github.com 想定読者 この記事では、以下のような方を想定しています。 playwrightを触ってみたい方。 E2Eテストを導入したい方。 ハンズオンの前提条件 この記事を読む前に、以下の準備をお願いします。 Node.jsのセットアップ お済みでない方は、 こちら を参考にNode.jsをインストールしてください。 GitHubアカウントの作成 GitHub のアカウントをお持ちでない方は、 こちら からアカウントを作成してください。 GitHubリポジトリの作成 お済みでない方は、 こちら を参考に任意のリポジトリを作成してください。 この記事で得られるもの この記事を読むことで、Playwrightを使ったE2Eテストの導入ができるようになることを目指します。 実行環境 Next.js v14.0.3 playwright v1.40.1 Mac OS Sonoma v14.1.1 Playwrightを活用したE2Eテストの導入 Playwrightとは github.com Microsoftが開発したE2Eテストツール Chromium、WebKit、Firefoxを含むすべての最新のレンダリングエンジンをサポートしているNode.jsベースのライブラリ PuppeteerとPlaywrightはほとんど同じチームによって開発されている 以下のブログでPuppeteerとPlaywrightの比較がまとめられていますので、興味がある方はご覧ください。 blog.logrocket.com Playwrightの特徴的な機能 Test generator codegenコマンドを使用してテストジェネレータを実行し、その後にテストを生成したいウェブサイトのURLを入力します。 URLなしでコマンドを実行し、代わりにブラウザウィンドウに直接URLを追加することもできます。 $ pnpm exec playwright codegen demo.playwright.dev/todomvc この画面で任意の操作を行うと、テストコードが自動的に生成されます。 使用してみた感想としては、テストコードを書いたことがない方でも、この機能を使えばテストコードを自動生成できるので、テストコードを書くハードルが下がるのではないかと思います。 UI Mode Playwright v1.32.0 から、UIモードが追加されました。 UIモードは テストを実行したり、デバッグするための機能を提供しています。 $ pnpm exec playwright test --ui 起動に成功すると、以下のような画面が表示されます。 ここからは、使ってみてこの機能が便利だと感じた点を紹介していきます。 Watch mode テストコードの変更を検知して、自動的にテストを実行してくれます。 テストコードを修正して、実行結果を確認するという作業を繰り返す際に便利です。 定義したアクションごとのスナップショット テストコードを実行すると、定義したアクションごとにスナップショットが作成されます。 どのタイミングでテストが失敗したのか・どのような操作を行ったのかなどを確認する際に便利です。 他にも、便利な機能が多数ありますので、 詳しくは、 公式ドキュメント を参考にしてください。 ハンズオン ここからは、PlaywrightをNext.jsに導入してE2Eテストを実装していきます。 あくまで、一例としてNext.jsを利用していますが、その他のフレームワークでも同様の手順で導入できると思います。 Next.jsをセットアップする Next.jsのセットアップ方法は、 こちら を参考にしてください。 ここでは詳細な手順は割愛しますが、 今回は ~/Documents に Next.jsをインストールしています。 # 任意のディレクトリに移動してください $ cd ~/Documents # プロジェクト名は任意のものを指定してください # ここでは、playwright-next-app-sample というプロジェクト名で作成しています $ npx create-next-app@latest $ cd playwright-next-app-sample # pnpm を利用していますが、npm や yarn・bunでも問題ありません。お好きなものを利用してください $ pnpm dev http://localhost:3000/ にアクセスして、以下のような画面が表示されれば成功です。 Playwrightをセットアップする Playwrightをセットアップするには、以下のコマンドを実行します。 詳しくは、 公式ドキュメント を参考にしてください。 $ pnpm create playwright Choose between TypeScript or JavaScript (default is TypeScript) # TypeScript を選択 Name of your Tests folder (default is tests or e2e if you already have a tests folder in your project) # 任意のフォルダ名を入力 (今回は tests を入力) Add a GitHub Actions workflow to easily run tests on CI Install Playwright browsers (default is true) # true を選択 pnpm create playwright を実行すると、以下のようなディレクトリ構成が作成されます。 - tests - tests-example - playwright.config.ts 最後に、テストを実行して以下のような結果が表示されれば成功です。 $ pnpm exec playwright test ➜ playwright-next-app-sample git:(main) ✗ pnpm exec playwright test Running 6 tests using 5 workers 6 passed (4.1s) To open last HTML report run: pnpm exec playwright show-report 実行結果は、 playwright-report ディレクトリに保存されます。 pnpm exec playwright show-report を実行すると、実行結果をブラウザで確認できます。 $ pnpm exec playwright show-report Next.jsのサンプルアプリケーションを起動する 今回は、Next.jsのサンプルアプリケーションを利用してテストを実装していきます。 テストコードを実装する前に、Next.jsのサンプルアプリケーションを起動しておいてください。 # Next.jsをローカルで起動 $ pnpm run dev テストコードの作成 tests/example.spec.ts に以下のテストコードを記述します。 // example.spec.ts import { expect , test } from '@playwright/test' ; // テストコードの実行前にTOPページにアクセスする test .beforeEach( async ( {page} ) => { await page.goto( 'http://localhost:3000' ); } ); test ( 'Get started by editing src/app/page.tsx が表示される' , async ( {page} ) => { await expect (page.getByRole( 'main' )).toContainText( 'Get started by editing src/app/page.tsx' ); } ) test ( 'Docページに遷移できる' , async ( {page} ) => { const page7Promise = page.waitForEvent( 'popup' ); await page.getByRole( 'link' , { name : 'Docs -> Find in-depth' } ).click(); const page7 = await page7Promise; await expect (page7.getByRole( 'heading' , { name : 'Introduction' } )).toBeVisible(); } ) テストの実行 UIモードを利用してテストを実行していきます。 $ pnpm exec playwright test --ui テストの実行結果は、以下のようになります。 コマンドライン上でテストの実行結果を確認することもできます。 $ pnpm exec playwright test Running 6 tests using 5 workers 6 passed (4.2s) To open last HTML report run: npx playwright show-report GitHub Actionsでテストを実行する ここからは、オマケとして GitHub Actionsを利用したワークフローを実装していきます。 Playwright はセットアップ時に、GitHub Actionsの設定ファイルを自動で作成してくれます。 今回は、そのまま利用してテストを実行していきますが、必要に応じてカスタマイズしてください。 name: Playwright Tests on: push: branches: [ main, master ] pull_request: branches: [ main, master ] jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - name: Install dependencies run: npm install -g pnpm && pnpm install - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps - name: Run Playwright tests run: pnpm exec playwright test - uses: actions/upload-artifact@v3 if: always() with: name: playwright-report path: playwright-report/ retention-days: 30 playwrightの設定ファイルを変更する playwright.config.ts を編集して、テストの実行前にローカルサーバーを起動するようにします。 複数環境で切り替えたい場合は、環境変数を利用して切り替えることもできます。 // playwright.config.ts export default defineConfig( { /* Run your local dev server before starting the tests */ webServer : { command : 'pnpm run dev' , url : 'http://127.0.0.1:3000' , reuseExistingServer : ! process .env.CI } } ); テストの結果を確認する それでは、ここまでの差分をコミットして、GitHubにプッシュしてください。 GitHub Actionsでテストが実行されていることを確認することができます。 最後に 以上で、Playwrightの基本的な使い方を紹介しました。 Playwrightは、Puppeteerと比較しても遜色ない機能を持っているので、今後はPlaywrightを利用してE2Eテストを実装していきたいと思います。 また、Vscodeに拡張機能が用意されているので、VsCodeユーザーはぜひ利用してみてください。 この記事が、Playwrightを触ってみたい方やE2Eテストを導入したい方の参考になれば幸いです。
アバター
every Tech Blog Advent Calendar 3日目の記事になります! こんにちは 開発本部データ&AIチームでデータエンジニアを担当している塚田です。 今回は、挑戦WEEK中にGitHub Packagesを利用したnpmパッケージの社内利用を行いましたのでその内容についてご紹介します。 挑戦WEEKとは 開発メンバーが通常の各事業部のロードマップから離れ、技術的に何かに集中して挑戦する1週間 としており、 弊社CTOの今井がTech Blogにて説明しておりますので、よろしければ併せてご覧ください。 tech.every.tv エブリーで利用している画像について エブリーでは DELISH KITCHEN 、 トモニテ 、 TIMELINE の3つのメディアを運営しています。 どのメディアも画像を多数利用して構成しており、画像配信基盤はシステムを構成する重要な一機能となっています。 画像配信基盤の問題点 それぞれのメディアで画像処理 → 画像返却 → 配信という処理の流れは共通しています。 ただし、画像処理の部分で画像変換はほぼ共通しているのですが、変換前処理がそれぞれ独自実装されている状況でした。 改善方針 独自処理が発生してしまうのは避けられない部分ではありますが、画像変換に関しては統一できる処理であるため共通化する方針となりました。 前置きが長くなりましたが、画像変換の共通処理のみを行うnpmパッケージとして切り出しGitHub Packagesで社内利用する流れを整備したので、事例として紹介します。 前提 GitHub Organizationを利用している Organizationでなくても実行可能ですが、今回はOrganizationを利用した場合の説明となります 開発端末でNode.jsの実行環境が整備されている 執筆時はNode.js 20.9.0を利用しています package.jsonの作成(初期化) それでは実際にGitHub Packagesに登録するパッケージを作成します。 npm init -y 後述の手順で package.json の変更を行いますのでデフォルトのままで問題ありません。 S3からファイル名を取得する処理を実装 今回はサンプルとして、 Amazon S3に保存しているファイルを取得しデータ返却する を実装します。 パッケージのインストール npm i -D typescript @types/node npm i -S @aws-sdk/client-s3 今回は上記のパッケージをインストールします。 用途に合わせて他のパッケージもインストールしてください。 tsconfig.jsonの作成 npx tsc --init 作成された tsconfig.json を以下のように修正します。 (自動的に作成されたものや今回の説明に必要ないものは削除しています) { " compilerOptions ": { " target ": " es2022 ", " module ": " commonjs ", " rootDir ": " ./src ", " outDir ": " ./dist ", " esModuleInterop ": true , " forceConsistentCasingInFileNames ": true , " strict ": true , " skipLibCheck ": true } } S3ファイル情報の取得ロジック 以下のサンプルプログラムを src/index.ts として作成します。 import { GetObjectCommand , GetObjectCommandOutput , S3Client } from '@aws-sdk/client-s3' export async function getObject ( bucket: string , key: string ) { const params = { Bucket: bucket , Key: key } const command = new GetObjectCommand ( params ) const response = await new S3Client () .send ( command ) const str = await response.Body?.transformToString (); return str } package.jsonの変更 GitHub Packagesに登録できるよう package.json を以下のように変更します。 今回は s3-file-getter というパッケージ名にしています。 GitHubの環境に合わせて {Organization名} と {リポジトリ名} を変更してください。 { " name ": " @{Organization名}/s3-file-getter ", " version ": " 0.1.0 ", " description ": "", " main ": " dist/index.js ", " files ": [ " dist " ] , " scripts ": { " prepublishOnly ": " npm run build ", " build ": " tsc -d " } , " author ": "", " license ": "", " repository ": { " type ": " git ", " url ": " https://github.com/{Organization名}/{リポジトリ名}.git " } , " dependencies ": { " @aws-sdk/client-s3 ": " ^3.450.0 " } , " devDependencies ": { " @types/node ": " ^20.9.0 ", " typescript ": " ^5.2.2 " } } ここで重要なのは name と repository となっており、それ以外は環境によって適宜変更してください。 GitHub Packagesへ登録 GitHub ActionsワークフローYAMLファイルの作成 自動的にGitHub Packagesへ登録させたいので、GitHub Actionsの設定を行います。 GitHubクイックスタート が用意されているので、それを流用し以下の設定を .github/workflows/publish.yml として作成します。 name : npm package publish on : release : types : [ created ] jobs : publish-gpr : runs-on : ubuntu-latest permissions : packages : write contents : read steps : - uses : actions/checkout@v4 - uses : actions/setup-node@v3 with : node-version : 20 registry-url : https://npm.pkg.github.com/ - run : npm ci - run : npm publish env : NODE_AUTH_TOKEN : ${{secrets.GITHUB_TOKEN}} リリースの作成 作成した publish.yml はリリースが新規作成された際に実行される設定となっているため、GitHubでリリースを作成します。 この時 package.jsonのversion が既にGitHub Packagesで登録されている場合エラーとなるため、導入するプロジェクトのリリースフローによって柔軟に変更してください。 今回はリリース前にversionの更新を行なっているものとして運用します。 GitHub Packagesの確認 GitHub Actionsの処理が完了すると以下のように対象リポジトリTOPの右下のPackagesに今回作成したかったnpm packageが登録されています。 今回作成した s3-file-getter のリンクを選択するとパッケージのバージョンなどのサマリーが確認できます。 GitHub Packagesの利用 登録だけしても利用できないと意味がないので利用するために必要な手順を記載します。 ローカルの場合 Personal Access Token (Classic)を作成 GitHub Packagesへの認証を行う ドキュメントが用意されているので、これに従いAccess Tokenを作成します。 パッケージの読み込みができれば良いので、今回Scopeは read:packages のみを選択します。 .npmrc の作成 以下の設定を .npmrc として作成します。 {Organization名} と {作成したAccess Token} を環境に合わせて置換してください。 registry=https: //npm.pkg.github.com/{Organization名} //npm.pkg.github.com/:_authToken={作成したAccess Token} 注意: .npmrc にAccess Tokenを直書きしているので、 .gitignore で管理対象外にするなど誤って公開される状況にならないよう防御策を取るようにしてください。 GitHub Packagesからパッケージのインストール .npmrc の設定をしていてもGitHub Packages以外のパッケージは今まで通りインストール可能です。 GitHubの環境に合わせて {Organization名} を変更してください。 npm i -D typescript @types/node npm i -S @ { Organization名 } /s3-file-getter パッケージの利用 他のパッケージと同様にimportすることで利用できます。 import { getObject } from '@{Organization名}/s3-file-getter' ; const data = async () => { console .log (await getObject ( '{bucketName}' , '{key}' )) } data () GitHub Actionsの場合 GitHub Packagesにアクセスできるリポジトリを設定 パッケージのサマリー画面に Package settings のリンクがあるので、クリックします。 パッケージを利用したいリポジトリを以下の機能から追加することで参照可能な権限を付与できます。 GitHub ActionsのWorkFlowを設定する パッケージを利用するための設定のみ記載しています。 環境に合わせて適宜変更してください。 jobs : sample : steps : - uses : actions/checkout@v4 - uses : actions/setup-node@v3 with : node-version : 20 registry-url : https://npm.pkg.github.com/ - run : npm ci env : NODE_AUTH_TOKEN : ${{secrets.GITHUB_TOKEN}} おわりに GitHub Packagesを利用した、社内でのみ参照可能なnpmパッケージの作成方法と利用方法について一連の流れを紹介しました。 今回のサンプルは公開しても問題ないものですが、共通の処理をパッケージにして利用したいけどロジックは外部に公開したくないなどのシチュエーションで利用できると感じています。 画像配信基盤の改善はデータエンジニアとして関わるのはあまりないことだと思うのですが、挑戦WEEKが良いきっかけとなりました。 この後のAdvent Calendarでも挑戦WEEKに関連する投稿を予定していますので、ぜひチェックしてみてください。
アバター
はじめてのシステムメンテナンスをする君へ はじめに 主にインフラ周りや時折バックエンドでGoを書いているyoshikenです。 この記事はevery Tech Blog Advent Calendar 2023の2日目の記事となります。 昨日の記事は「Next.js + Go + AWS API Gateway で WebSocket API を使って API サーバーからフロントエンドに通知を送る」となります。 tech.every.tv tech.every.tv 対象読者層 インフラレイヤー(バックエンドも含)の知識/経験が少ないエンジニア エンジニアがどういう手順でメンテナンスをするのか知りたいPdM/PjM/PM 本記事の目的 システムメンテナンスについて どのような流れで行うことがあるのか例を示す 気をつけるべき事柄を示す ドキュメンテーションに必要な項目の例を示す 以上を持って、初めてメンテナンスを行うエンジニアが1例を参考にメンテナンスに関するノウハウを得て、0ベースではなく1べースでメンテナンスの"準備"を行えるようにすることを目的とします。 本記事の対象外 システムメンテナンスの具体的な手順やコマンド 使用ツールの説明や使い方 準備の準備 DBのストレージ増設、サーバーOSのアップデート、キャッシュ増設など、システムメンテンスと一概に言っても、その目的や範囲は様々です。 そのため、予めその目的を明確にしておくことで、影響範囲の把握や停止時間の正当性などが明確になります。 また、メンテナンスを一つのプロジェクトとして責任者を立てましょう。 ここでいう責任者は"メンテナンスについて網羅的に把握し、進捗の管理や渉外に立つ人"という意味です。 実際にメンテナンスを行うオペレーターは一人かもしれませんが、事前準備では複数人が関わるため責任の所在が曖昧になることもあります。特に大規模であればあるほど、関わる人間が多く、全体を把握するには複数の人にヒアリングをしないとわからないということが起こりやすいので注意が必要です。 目的・影響範囲・責任者が決まったら、メンテナンスを実施する旨を関係各所へ連絡しましょう。 サービス/アプリケーションに影響があるメンテナンスの場合は、この時点でのおおよその目安で良いので影響する範囲と時間を関係部署に伝えておきましょう。 例. 20xx/xx/xx ~ 20xx/yy/yyの間で 大体3時間前後。 Android/iOS 両OSのモバイルアプリケーションがメンテナンス状態で使用不可になる。ただしwebはアクセス可能。 そうすることで、厳密な日時は決まらなくても影響範囲からメンテナンス実施可能な日時の割り出し、エンジニアが想定する影響範囲から"関係部署としての影響範囲"(連絡する企業やユーザーへ通知手順)などを割り出すことができます。 後述する手順書の作成中に精度がある程度しぼれた影響時間が算出できたら、再度関係部署に連絡し、具体日時を確定させましょう。 こういった"準備の準備"は直接的なメンテナンスの成功に貢献する割合は少ないですが、円滑なメンテンスの実施やその後のメンテナンスを行う際の他部署との連携には大きく貢献するので、メンテナンスの前には必ず行いましょう。 メンテナンス手順書 具体的な準備に取り掛かって行きましょう。 兎にも角にもメンテナンス手順書を作成します。 といってもいきなりはかけないと思いますので目安として以下の項目を書き出していきましょう。 目的と範囲 対象のシステムの構成(図) 手順に必要なツール一覧/インストール方法 実施手順 ロールバック手順 メンテナンスの後に必要な作業 トラブルシューティング 参考資料 それぞれの項目ごとに上から"ロールバック手順"まで、どのような情報があると望ましいか触れていきます 目的と範囲 前章でも触れましたが、メンテナンスの目的と範囲を明確にすることで、メンテナンスの成功基準(ゴール)や影響範囲を明確にすることができます。 この部分が明確ではないと、この後に書いていく情報や手順が正しいのかどうなのかが不明瞭になります。 例えば、「データベースをアップデートする」という情報では "なんのデータベースが" "どのバージョンまで" が不明瞭なので、「(データベース名)をv1.2.3からv4.5.6にアップデートする」というのが目的としては好ましいです。 また、メンテナンスの範囲も明確にしておきましょう。 とはいえ、いきなり最初から範囲がわかるわけでもないので、後述するシステムの構成図などを書きながら範囲を絞っていきましょう。 ではなぜ最初の節に紹介したかというと、 "ドキュメンテーションとして多くの人が必要とする情報ほど前に書く" というのはドキュメンテーションの基本中の基本です。これはメンテナンスに限った話ではないです。 今回の場合は多くの人は手順そのものではなく影響する範囲が、関係するエンジニアや後世にドキュメンテーションを読むであろうエンジニアが一番に見たいのはメンテナンスの前後の状態と予想されるので一番最初の節にに記載しています。 対象のシステムの構成(図) システムの構成(図)を書く目的・必要性ですが、よほど簡素で単純なシステムでない限りインフラレイヤーはごちゃごちゃしています。 「AWSのEC2でサービス立ててます!」といっても、subnetやsecurity group・route table・AZなどなど、実際はさらにRDSやELB、Route53など複雑に絡み合っています。 またインフラレイヤーに限らずアプリケーションレイヤーでも、昨今のマイクロサービス化やモノリシックなアプリケーションでも、複数のサービスが絡み合っていることが多く、暗黙知であったりロストテクノロジーとなっていることが多いです。 そういった状況でレビューをしたりリストアップをすると漏れが生じてしまうので、漏れの可能性を最小限にするためシステムの構成(図)を書くことが望ましいです。 システム構成(図)と表現しているのは、"可能であれば図で表現するのは望ましいが、それに時間をあまりにも割くのが難しいのであれば他でも代用可能"という意味合いで記述しています。 大事なのはメンテンスの対象と関係あるネットワークや通信がどのように絡み合っているかを把握することです。 手順に必要なツール一覧/インストール方法 「メンテナンス手順に含まれているライブラリやコマンドがインストールされていなかったので当日慌ててインストールした」というのはあるあるなお笑い話なんですが、そういったイレギュラーはない事に越したことはないので事前に手を打っておきましょう。 インストールする際は極力バージョンによる挙動の違いをなくすためにバージョンは指定したインストール方法を記載しましょう。 実施手順 ここが一番のボリュームかつ大事な部分になると思います。 手順についてはオペレーティングする人が当日やりやすいを前提に、レビューする人が見やすい形も意識しましょう。 一例として以下のフォーマットを参考にしてみてください 具体作業をわかりやすく一言で 事前確認 作業を行う前にステータスを確認できるCLIコマンドと結果 [ ] 目視の場合はチェック欄があると尚良し 作業 実際の作業を行うCLIコマンド [ ] GUIをポチポチする場合はチェック欄が(ry 事後確認 作業を行ったあとのステータスを(ry [ ] 目視の場合はチェック(ry 具体的な例を出すと、 route53のprivate hosted zoneのレコードを新しいエンドポイントに切り替え 事前確認 dig hoge.local 作業 cloudmapに登録されているインスタンスのCNAME設定を6の手順で作った新クラスターのエンドポイントに変更 https://github.com/org/repo/blob/master/path/to/terraform/service-discovery.tf git checkout feat/update_nanntara__uwaaaa cd path/to/terraform // 差分確認 terraform plan // 反映 terraform apply 事後確認 // login to bastion ssh user@humidaihost -i ~/.ssh/koukaikagi.pem // 向き先確認(手順6のエンドポイントを参照 dig hoge.local // ログイン redis-cli -h hoge.local -c // バージョン確認 > INFO [ ] コンソール上のroute53で向き先が変わっている [ ] privateDNSが新しいエンドポイントの向き先に変わっている このようになります。 これらを作業工程ごとに記載します。 非常にボリューミーになるので必要な情報を簡潔に記載しましょう。 ロールバック手順 「いちばん大事なのは手順だ」と一個前の節で書いたかもしれませんが、この節が一番大事かもしれません。 メンテナンスには失敗がつきものです。こんだけ偉そうに書いてますが打率は7・8割です。 ですが、取り返しの付かない失敗は…まだ片手で数えられるぐらいしかしていません。たぶん。 その失敗に備えるためにいかなる状況でもメンテナンス前に戻せるような手順をいくつか事前にリストアップしておくことが非常に大事です。 ロールバックを行うということは、何かしら失敗をしてしまった後です。精神的に不安定になり、簡単な作業もオペレーションミスしてしまう可能性がぐっと上昇します。ですので粒度は細かく、コマンドをコピペして終わるぐらい詳細に書きましょう。 どうしても都合上非可逆的な作業でロールバックが不可能な場合は"上位の判断者が判断を下し、復旧作業が開始されるまで被害が最小限に済む方法"を記載しておくのが良いです。例えば破壊的変更を含むバッチの停止やデータ書き込みの停止などです。 手順を開発環境で試そう 手順書を作成してく上でわからないことや実際にどういった反応が返ってくるかわからない場合も多々あると思います。そういったときはぜひ開発環境で試してみましょう。 上記以外にも本番環境と限りなく近い状態の環境を用意し手順を再現することで当日にかかる時間の見積もりの精度も向上します 手順書のレビュー 手順が一通り出来上がったら知見がありそうな人にレビューをもらいましょう。 コードレビューと一緒で何回か往復をしてブラッシュアップしていきましょう。 まとめ メンテナンスの手順書を作っていくことで自ずとメンテナンスに必要な準備ができてくると思います。 メンテナンスは事前に共有し、ドキュメンテーションもしっかり書けば、当日はあとは手順書を見ながらコマンドを打つだけです。 よく「メンテナンスやったことないから怖い」という話も聞きますが、それは不明瞭なものに対して抱く恐怖心であり、"メンテナンス"にたいしてではありません。手順書をしっかり書き、リハーサルを重ねることで恐怖心が和らいでいきます。正しく恐れていきましょう。 ここまで書いてあれですが、当日はきっと手順書に書いてない想定外のトラブルが起こることが多々あるでしょう。 そういった場合に備えて日常で発生するシステムのアラートに対して積極的に対応して行きましょう。そういった場で得た経験はきっとメンテンス中のトラブル対応として生きていきます。 それでは皆さんよきメンテナンスライフを!
アバター
Next.js + Go + AWS API Gateway で WebSocket API を使って API サーバーからフロントエンドに通知を送る はじめに こんにちは トモニテ でバックエンド周りの開発を行っている rymiyamoto です。 エブリーとして初の試みとなる Tech Blog Advent Calendar 2023 の 1日目の記事として参加させていただきました。 毎日他の記事も公開されるので、ぜひチェックしてみてください! tech.every.tv 今回ですが Next.js + Go + AWS API Gateway で WebSocket API を使ってみたのでその内容を紹介していきます。 経緯 トモニテ では現在、バックエンドは Go でフロントエンドは React(Next.js) で開発を行っています。 フロントエンドとバックエンドの通信は REST API で行っていましたが、エンドユーザーの行動に対してダッシュボードを利用しているユーザーに即時性のある通知機能を実装する必要が出てきたため、WebSocket API を使ってみることにしました。 現状 API サーバーは ECS 上で動いており、API サーバー側で WebSocket API を実装するのは少し手間がかかるため、AWS API Gateway で WebSocket API を実装することにしました。 WebSocket API とは ユーザーのブラウザーとサーバー間で対話的な通信セッションを開くことができるものです。 サーバーにメッセージを送信したり、応答をサーバーにポーリングすることなく、イベント駆動型のレスポンスを受信することができます。 developer.mozilla.org 今回の実装 元々ある ECS 環境(API サーバー・dashboard・web)から API Gateway で WebSocket API を利用できるように各種 Lambda を作成しました。 また裏側では Lambda から RDS への接続を行いたいため、RDS Proxy を利用しています。 構成図 流れとしては以下のようなフローです。 dashboard(FE)で WebSocket API を利用するためのクライアントを作成してコネクション確立 web(FE)で API サーバーに対してリクエストを送った際に、通知を行う Lambda を呼び出し API Gateway を通して dashboard に通知が飛ぶ API Gateway の設定 ともかく WebSocket API を利用できるように API Gateway を作成します。 API Gateway において、どのリクエストに対してどの操作を行うかを決定するルート式を指定します。 今回は特別に指定もいらないので $request.body.action としておきます。 WebSocket API を使うための API GateWay の作成 以降の部分は特に指定がなければデフォルトのまま作成していきます。 このとき、ルートに $connect と $disconnect が追加されますが、これらは接続と切断時のルートとなります。 IAM Role の作成 実行用の Lambda の Role API Gateway や SecretManager(RDS Proxy 周りの機密情報の管理) を利用するために Role を作成します (以降 web-socket-lambda-role とします) その時に必要となるポリシーは以下です。 { " Statement ": [ { " Action ": " secretsmanager:GetSecretValue ", " Effect ": " Allow ", " Resource ": " * ", " Sid ": " GetSecretValue " } , { " Action ": [ " ec2:DescribeNetworkInterfaces ", " ec2:DeleteNetworkInterface ", " ec2:CreateNetworkInterface " ] , " Effect ": " Allow ", " Resource ": " * ", " Sid ": " ManageNetworkInterface " } , { " Action ": [ " logs:PutLogEvents ", " logs:CreateLogStream ", " logs:CreateLogGroup " ] , " Effect ": " Allow ", " Resource ": " * ", " Sid ": " ManageLogGroup " } , { " Action ": " execute-api:* ", " Effect ": " Allow ", " Resource ": " * ", " Sid ": " ExecuteAPI " } ] , " Version ": " 2012-10-17 " } API サーバーから Lambda を呼び出すための Policy 追加 Lambda を呼び出すための Policy を API サーバーの Role に付与します。 その時に必要となるポリシーは以下です。 ( websocket-notification が後に作成される通知用の Lambda の名前です) { " Statement ": [ { " Action ": " lambda:InvokeFunction ", " Effect ": " Allow ", " Resource ": " arn:aws:lambda:ap-northeast-1:111111111111:function:websocket-notification ", " Sid ": "" } ] , " Version ": " 2012-10-17 " } Lambda の設定 今回 WebSocket API を利用するための Lambda は以下の 3 つとなります。 connect: 接続時に API Gateway から呼び出される disconnect: 切断時に API Gateway から呼び出される notification: API サーバーから呼び出されて通知を行う connect やること API Gateway の $connect ルートをイベントトリガーとして設定する クエリにユーザーが特定できるような情報を渡しておく WebSocket の接続 ID を取得して DB に書き込みを行う 内部の処理のイメージは以下です。 package main import ( "context" "log" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) type Response events.APIGatewayProxyResponse func Handler(_ context.Context, request events.APIGatewayWebSocketProxyRequest) (Response, error ) { log.Println( "Begin WebSocket connect" ) log.Println( "ユーザー特定" ) // リクエストのクエリからユーザーを一意に特定できる情報を取得します // 今回はユーザーの識別となるトークンを取得しています(複数タブを識別するため) token := request.QueryStringParameters[ "token" ] // 以下にトークンからユーザーIDを取得 log.Println( "DSNの取得開始" ) // 以下にDNS情報をsecret managerから取得する処理 log.Println( "DBの接続" ) // 以下にDBの接続処理 log.Println( "コネクションIDの保存" ) // リクエストからコネクションIDがとれます connectionID := request.RequestContext.ConnectionID // 以下にコネクションIDの保存処理 log.Println( "End WebSocket connect" ) return Response{StatusCode: http.StatusOK}, nil } func main() { lambda.Start(Handler) } disconnect やること API Gateway の $disconnect ルートをイベントトリガーとして設定する WebSocket の接続 ID を取得して DB から削除を行う 内部の処理のイメージは以下です。 package main import ( "context" "log" "net/http" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" ) type Response events.APIGatewayProxyResponse func Handler(_ context.Context, request events.APIGatewayWebsocketProxyRequest) (Response, error ) { log.Println( "Begin WebSocket disconnect" ) log.Println( "DSNの取得開始" ) // 以下にDNS情報をsecret managerから取得する処理 log.Println( "DBの接続" ) // 以下にDBの接続処理 log.Println( "コネクションIDの削除" ) // リクエストからコネクションIDがとれます connectionID := request.RequestContext.ConnectionID // 以下にコネクションIDの削除処理 log.Println( "End WebSocket disconnect" ) return Response{StatusCode: http.StatusOK}, nil } func main() { lambda.Start(Handler) } notification やること 呼び出し時の payload には通知を行うユーザーの ID を含めておく そのユーザー ID からコネクション ID を取得して通知を行う 内部の処理のイメージは以下です。 package main import ( "context" "fmt" "log" "net/http" "os" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/apigatewaymanagementapi" ) type Response events.APIGatewayProxyResponse func sendMessage(ctx context.Context, endpoint, connectionID, message string ) error { cfg, err := config.LoadDefaultConfig(ctx) if err != nil { return err } client := apigatewaymanagementapi.NewFromConfig(cfg, func (o *apigatewaymanagementapi.Options) { o.BaseEndpoint = aws.String(endpoint) }) input := &apigatewaymanagementapi.PostToConnectionInput{ ConnectionId: aws.String(connectionID), Data: [] byte (message), } _, err = client.PostToConnection(ctx, input) return err } type Event struct { UserID uint64 `json:"user_id"` JsonString string `json:"json"` } func Handler(ctx context.Context, event Event) (Response, error ) { log.Println( "Begin WebSocket notification" ) log.Println( "DSNの取得開始" ) // 以下にDNS情報をsecret managerから取得する処理 log.Println( "DBの接続" ) // 以下にDBの接続処理 log.Println( "対象ユーザーのコネクションIDリスト(複数端末や複数タブの都合上)取得" ) // lambda呼び出し時のpayloadには特定させるためのユーザーID、渡したいメッセージのjson入れておきます userID := event.UserID // 以下に対象ユーザーのコネクションIDのリスト取得処理 log.Println( "API Gatewayを経由してWeb Socketのメッセージを送信" ) endpoint := os.Getenv( "API_GATEWAY_ENDPOINT" ) for _, connectionID := range connectionIDs { err = sendMessage(ctx, endpoint, connectionID, event.JsonString) } if err != nil { return Response{StatusCode: http.StatusInternalServerError}, err } log.Println( "End WebSocket notification" ) return Response{StatusCode: http.StatusOK}, nil } func main() { lambda.Start(Handler) } API サーバーで通知 Lambda(notification)を呼び出し API サーバー側で以下の関数を作成し通知を行う Lambda を呼び出すようにします。 package aws import ( "context" "fmt" "os" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/lambda" ) func createClient() (*lambda.Client, error ) { cfg, err := config.LoadDefaultConfig(context.Background()) if err != nil { return nil , fmt.Errorf( "load default config on background context failed. err: %w" , err) } return lambda.NewFromConfig(cfg), nil } func Invoke(payload [] byte ) error { funcName := os.Getenv( "WEBSOCKET_NOTIFICATION_FUNCTION" ) client, err := createClient() if err != nil { return fmt.Errorf( "create lambda client failed. lambda function: %s, err: %w" , funcName, err) } input := &lambda.InvokeInput{ FunctionName: aws.String(funcName), Payload: payload, } _, err = client.Invoke(context.Background(), input) if err != nil { return fmt.Errorf( "call %s failed. err: %w" , funcName, err) } return nil } WebSocket API を使うためのクライアントの作成 WebSocket API を利用するためのクライアントを作成します。 フロントエンドは Next.js で作成しているので、useEffect でコネクション確立を行うようにしています。 "use client" ; import { useEffect , useState } from "react" ; type ApplicationEvent = { message: string ; } ; export default function EventReceiver ( { token } : { token: string } ) { const [ applicationEvent , setApplicationEvent ] = useState < ApplicationEvent >(); // WebSocketのコネクションを張る useEffect (() => { const connectWebSocket = () => { const webSocketURL = process .env.NEXT_PUBLIC_WEB_SOCKET_URL ; if ( ! webSocketURL ) { throw new Error ( "Web SocketのURLが設定されていません。" ); } const ws = new WebSocket ( ` ${ webSocketURL } ?token= ${ token } ` ); ws.onmessage = ( event ) => { const e = JSON .parse ( event.data ); setApplicationEvent ( e ); } ; // 勝手に接続切れたときの再接続 ws.onclose = () => { setTimeout ( connectWebSocket , 1000 ); } ; return ws ; } ; const ws = connectWebSocket (); return () => { ws.close (); } ; } , [ token ] ); if ( ! applicationEvent ) { return null ; } return < div > { e.message } < /div >; } (Next.js ver.13 想定のため、12 以下の場合は "use client"; は不要です。) まとめ WebSocket API を利用して API サーバーからフロントエンドに通知を送る方法を紹介しました。 自身で WebSocket API を実装するのは少し手間がかかりますが、API Gateway を使うことで簡単に実装することができます。 今回は複数ブラウザのケースを考慮してユーザーにコネクションを紐付けるようにしましたが、そうでない場合は通知部分にコネクション ID を渡すことでもう少しスッキリかけたと思います。 ただクライアント側で WebSocket のコネクションを張るときに再接続処理を書く必要があるので、プレーンで書くと少し面倒です。 WebSocket API は今回のような通知機能だけでなく、リアルタイムなチャットなどにも利用できるので、今後も様々な場面で利用可能です。 同じような実装してみたい方の参考になれば幸いです。
アバター
こんにちは。2023/10/29~11/1に開催された統計・機械学習系の学会、 第26回情報論的学習理論ワークショップ(IBIS2023) に、弊社データサイエンティストチームでオンライン参加してきました。 チュートリアル や 企画セッション では、2023年の開催ということもありやはり、昨今盛り上がりを見せているLLMが多くテーマとして取り上げられていました。 様々な研究や活用例がわかりやすくかつ多様に紹介されており、オンラインという形ではあったものの、その勢いや盛り上がりを感じることが出来ました。 本記事ではその中から、気になった講演をいくつか紹介していきます。 チュートリアル: 大規模言語モデル活用技術の最前線 稲葉通将様(電気通信大学 人工知能先端研究センター) 資料: IBIS2023チュートリアル「大規模言語モデル活用技術の最前線」 - Speaker Deck この講演では、LLMの効果的な「使い方」に焦点を当て、LLMの性能を最大限に活かすための様々な技術や研究が、具体的な精度検証事例も交えつつ紹介されていました。 (数えたら優に10種類以上ありました) 特に印象に残ったものを以下に抜粋します。 Chain-of-Thought(Wei et al., 2022) LLMに問題を解かせる際、単に回答を出力させるだけでなく、回答に至る思考過程も述べさせるように、(例示などを交えながら)プロンプトを誘導する手法 応用として、同じ指示に対して複数のCoT生成結果を獲得し、それらの結果の多数決を取るアプローチもある(その分コストはかかる) Let’s think step by step(Kojima et al., 2022) プロンプト末尾に「Let’s think step by step.」 を付けるだけでも精度に寄与するというテクニック 例示プロンプトの準備に手間がかかるChain-of-Thoughtの代替アプローチ Tree of Thought(Yao et al., 2023) 複数のプランをLLMに生成させ、LLM自身にそれらのプランを評価させる。そして、高い評価のプランをもとにさらに次のプランを複数生成を繰り返すアプローチ LLMが不得意になりがちな先読みタスクや、探索が重要なタスクに有効。 感想 まず「Chain-of-Thoughtが上手くいく」のは、日頃プロンプトを試行錯誤しながらGPTとお喋りする中で肌感としてなんとなく感じていたことではあります。 しかし、こうした経験則を研究、データとして裏付けた事例を知ることで、よりそのノウハウを自分の中で体系化出来たと感じます。 また、Let’s think step by stepするだけでも良いプロンプトが狙える、Tree of Thoughtで先回りタスクの苦手さに対処するなど、目から鱗なテクニックも知ることが出来、より活用の幅が広がった気がします。 これらのテクニックを、弊社のLLM活用にも是非とも還元していきたいと感じました。 例えば、 ビジネスサイドに向けたChatGPT講習資料※をアップデートする(特にLet’s think step by step等の気軽に試せそうなもの) 社内AI ChatApp(OpenAI APIのGPTをもとに社内Webツール化したもの)のテンプレートとして、これらのテクニックを埋め込む 等々、日頃のビジネス改善に寄与しそうなタネは多く転がっているのではないかと感じます。 ※参考記事 tech.every.tv 企画セッション: 大規模言語モデルとVision-and-Language 西田光甫様(NTT人間情報研究所) 資料: ⼤規模⾔語モデルとVision-and-Language - Speaker Deck この講演では、画像処理と自然言語処理の融合領域に焦点が当てられていました。 代表的な基盤モデル(GPT, CLIP等々)の紹介と共に、大規模モデルが如何にして画像と言語の情報を共に扱えるようになっていったのか、その技術の発展や応用例がとてもわかりやすく紹介されていました。 ここでは、特に興味深かったテーマを2つ掘り下げます。 Visual Instruction Tuning 画像×言語情報を扱える大規模モデルLLaVA: Large Language-and-Vision Assistantが、「複雑怪奇な画像のどこにおかしな点があるか」を的確に答える様子が紹介されていました。 「車の後ろでアイロンをかけているのがおかしい」とLLaVAが答える様子 Liu, H., Li, C., Wu, Q., & Lee, Y. J. (2023). Visual Instruction Tuning. そして、この能力を実現する鍵となったのは、Instruction Tuningの手法を画像処理タスクにも応用したVisual Instruction Tuningでした(Liu et al., 2023)。 Instruction Tuningは、「モデルが人間の指示に従い、未知のNLPタスクを柔軟に解く能力」を得られるように学習させる手法です(Wei et al., 2022)。 これは、ChatGPTにおける指示プロンプト→文章生成をZero-Shotで行う流れに代表されます。 Visual Instruction Tuningでは、画像の情報を文字情報(ex. 画像の内容を説明するキャプション、画像の中にある物体の座標値)に変換します。 そして、このようにして作成した「画像の説明」と、元の「画像特徴ベクトル」の組み合わせをモデルに入力して学習させることにより、LLaVAは画像情報を解析する能力を獲得します。 「画像に説明情報を付加して学習させる」というシンプルな方法により、大規模モデルがまるで視覚を持つかのように多様なタスクに対応できるようになる様に奥深さを感じました。 GPT-4Vの評価論文 2023年9月にChatGPTの画像入力機能として登場したばかりのGPT-4V(GPT-4 with Vision)の潜在的可能性を評価した論文(Yang et al., 2023)の一部が紹介されていました。 ここではGPT-4Vが処理できるタスクとして、以下のような事例が示されていました。 指示されたプロンプトに応じて、画像の中にあるや印や注釈などを読みに行き、そこにある情報を取得&活用できる。 画像として入力された論文を要約できる。数字や一文程度の誤りはあるものの、それ以外は概ね正確な要約を実現できる。 画像のどこに何があるかを物体検出し示せる。しかしその位置座標はまだまだ誤差が大きい。 画像中に示された注釈が、何の物体を指すものか答える様子 Yang, Z., Li, L., Lin, K., Wang, J., Lin, C.-C., Liu, Z., & Wang, L. (2023). The Dawn of LMMs: Preliminary Explorations with GPT-4V(ision) (Version 2). これにより、現状どの程度複雑なタスクを処理できる可能性があるかの勘所を掴んだり、逆にどのようなタスクが苦手で課題があるのかを把握したりすることが出来ました。 企画セッション: テキストから実世界理解に向けて 栗田修平様(理化学研究所AIP) 資料: テキストからの実世界理解に向けて - Speaker Deck 画像と言語の対応付けを実世界へどう応用していくのかの研究に関する講演でした。 画像と言語の対応付けには、画像キャプション生成と画像質問応答が2大タスクであるそうです。 これらよりも細かく画像中の物体とテキストを対応付けられないかについて、近年のVision-and-Language領域の研究について紹介していました。 特に参照表現理解というテキスト表現で参照された物体の座標(Bounding Box)を推測する研究分野について詳しく述べられていました。 具体的な手法としては、MDETR(Kamath et al., 2021)とGLIPv2(Zhang et al., 2023)などがあり、MDETRは名詞句に対応づいた画像中の物体を検出するもので、GLIPv2は画像内部の対照学習(MDETR)と他の画像との対照学習(CLIP)を組み合わせた手法だそうです。 参照表現理解の例 Plummer, B., Wang, L., Cervantes, C., Caicedo, j., Hockenmaier, J., & Lazebnik, S. (2016). Flickr30k Entities: Collecting Region-to-Phrase Correspondences for Richer Image-to-Sentence Models 講演の後半には、実世界への応用について述べられていました。 参照表現理解は、テキストで参照された物体を画像から探すものですが、これは暗黙的に画像内にその物体が存在することを前提としていると述べられていました。 そのようなケースは確かに実世界だと限定的だなという印象を受け、課題感について納得できました。 この課題解決に向けて、物体が存在しないことも考慮できるようになれば、参照物体が画像内に存在するかの判定機としても動作すると述べられており、非常に興味深いと思いました。 将来的にロボットやドローンによる災害救助などへの応用できれば、非常に有用な技術となりそうだなと感じました。 企画セッション: 作業動画と手順書を対象としたマルチモーダル理解 西村太一様(京都大学大学院 情報学研究科 (現: LINEヤフー株式会社)) 資料: 作業動画と手順書を対象としたマルチモーダル理解 - Speaker Deck 動画と言語から学習するVision-and-Language領域で、動画としては作業動画を、言語としては人手で作成した手順書や音声書き起こしを活用した研究に関する講演でした。 この研究では、手順書はノイズが少なく、音声書き起こしはノイズが多いという特性があります。 アノテーションには時間区間のアノテーションと区間ごとの文を付与するといった手法が使われるため、手順書だとデータ量が少なくなり、音声書き起こしだと大規模なデータを学習させることができるそうです。 応用研究として、料理動画からレシピを生成する研究(Nishimura et al., 2022)や、初期状態と最終状態から中間の動作を推定するProcedure planningといった研究(Chang et al., 2020; Sun et al., 2022)を紹介されていました。 弊社でも料理の手順を動画として提供しているため、この講演に大変興味を持ちました。 料理動画からレシピの生成は、ビジネス的観点からどのように活用するのが良いのか、という視点で考えると、弊社の料理動画はPGC(Professional Generated Content)のため、社内のコンテンツ作成チームの効率化が可能かもしれません。 UGC(User Generated Content)観点では、ユーザが動画を撮ってアップロードするだけでレシピを生成して投稿できるといったシステムが作れれば、YouTubeなどと棲み分けができ、新たな価値を生み出せるではないかと感じました。 また、Procedure planningは料理の活用が困難と述べられていましたが、弊社のような動画の画角が固定されていて、手順をトリミング編集したような動画を使うことができれば、うまく学習できるのかという点についても気になりました。 Procedure planningを実世界へ応用した研究:Planning Transformer (PlaTe)の概要図 Sun, J., Huang, D., Lu, B., Liu, Y., Zhou, B., & Garg, A. (2022). PlaTe: Visually-Grounded Planning with Transformers in Procedural Tasks. 最後に 本記事では、「Vision and Languageの最前線」をテーマとした企画セッションを多く取り上げました。 前述した通り、この領域は、弊社が提供する料理動画メディア『DELISH KITCHEN』とも親和性が高いと感じる部分も多々ありました。 これまで、大量に存在している料理動画&画像データ(原石)を如何にして磨くか、ビジネスの発展やプロダクトの成長に活かしていこうか頭を捻らせていたことがありました。しかし、Vision and Language大規模モデルの出現により「視覚的な料理過程の情報と、それに対応する付帯情報(例えば料理手順説明テキストなど)を丸ごと覚えさせ、その文脈を多様なタスクの解決に発展させる」アプローチが現実味を帯びてきた印象を受けます。 こうした技術の発展にアンテナを張りつつ、我々のプロダクトならではの活路を見つけ出していきたいと思います。
アバター
はじめに こんにちは、 retail HUB で Software Engineer をしているほんだです。 早いもので 2023 年も残り一ヶ月程度となりました。12 月といえばみなさんお待ちかねのクリスマスということで 12/1 から 12/25 を盛り上げるためにエブリー初の Tech Blog Advent Calendar 2023 を開催します! 公開スケジュール Tech Blog Advent Calendar 2023 の公開日と公開内容については下記を予定しています。 アドベントカレンダー記事が投稿され次第リンクを更新していく予定です。 最終日 12/25 は CTO の imakei が記事公開予定です! 公開日 公開内容 2023/12/1 Next.js + Go + AWS API Gateway で Websocket API を使ってAPIサーバーからフロントエンドに通知を送る 2023/12/2 はじめてのシステムメンテナンスをする君へ 2023/12/3 GitHub Packages を利用した npm パッケージの社内利用 2023/12/4 Playwrightを活用したE2Eテストの導入 2023/12/5 新たなチームメンバへの贈り物 2023/12/6 DI toolkit samber/do の紹介 2023/12/7 ゼロからはじめるシステム引き継ぎ 2023/12/8 SPMでマルチモジュール/マルチターゲット開発 2023/12/9 Go 1.22で追加予定のrange over intと、GOEXPERIMENT入り予定のrange over funcを触ってみる 2023/12/10 RDS踏み台サーバをよく見かけるECS Fargate+PortForward+Adhocな機構に変更する 2023/12/11 全社イベント"挑戦week"を実施/運営した所感 2023/12/12 OpenAI Assistants APIを使って分析用SQLを生成してみる 2023/12/13 Retrieval-Augmented Generationを使ってコードの解説を生成してみる 2023/12/14 iOSでGraphQLを使ってみた 2023/12/15 トモニテの Amazon Aurora を MySQL 5.7 から 8.0 へアップグレードした話 2023/12/16 Android でのバーコードリーダー実装について 2023/12/17 View をソフトウェアキーボードに追従させる 2023/12/18 トモニテ相談室におけるTwilioを用いた電話の仕組み 2023/12/19 ネットスーパーアプリにおける GraphQL Mesh を利用した Gateway Server について 2023/12/20 DELISH KITCHEN のレシピレコメンドの立ち上げとこれから Hygen で加速する Next.js App Router 開発 2023/12/21 実務に入る前に理解していたらもう少し開発速度を上げられたかなと思うこと monorepo環境でeslint flat configを導入してみた 2023/12/22 microCMS × Next.js でのキャンペーン LP 制作効率化 新卒1年目Web系エンジニアがChatGPTを利用した社内ChatAppのテンプレート機能の実装に挑戦した話 2023/12/23 IVSを用いたライブ配信 wearOS について 2023/12/24 分析に向けたログ設計の話 AndroidのonResumeの挙動を再現したい 2023/12/25 2023年の振り返りと2024年に向けて 気になる記事があった方はぜひブックマークやシェアお願いします!
アバター
はじめに DELISH KITCHEN 開発部で小売向き合いの開発に携わっている野口です。主に Flutter でのアプリ開発を担当しています。 弊社では retail HUB という小売向けのサービスを行っています https://biz.delishkitchen.tv/retailhub 今回は弊社で開発している retail HUB で作成しているアプリの構成について2つ紹介し、 複数クライアントアプリのつらみとそれぞれのメリット、デメリットを述べたいと思っています。 複数クライアントのアプリ構成とは 上記で紹介している複数クライアントのアプリとは複数のアプリを共通のソースコードで管理するものを想定しています。 イメージとしては、A 社、B 社、C 社に提供する場合、共通項目は使いまわせる構成、アプリ自体は異なる(App Store では別)ので別アプリとして管理できる構成を考えています。また、クライアントごとに独自の機能をカスタマイズすることもあります。 複数クライアントのアプリ構成のつらみ 複数クライアントのアプリ構成には今回のブログのテーマでもあるカスタマイズのつらみがあり、かなり厄介なものになります。 主につらみポイントとしては カスタマイズ要素が増えれば増えるほど管理が大変になる クライアントによってカスタマイズを出しわけないといけない 契約終了とかになったら、使用しない機能を削除するかとか になります。コードの管理が大変なんです。一部のクライアントではここは使うけどここは使わないとか。クライアントが数十社とかになったら大変になりそうです。 ということで、以降で2つの構成をざっくり解説してメリット、デメリット話します。 先に結論を話すと筆者的には Melos がいいかなと思ってます。 弊社のアプリ構成例 make を使用したアプリ構成 make を使用したアプリ構成は共通のコードから各クライアントに出し分けるイメージです。 make で CHAIN というクライアントの識別子の変数を指定することでクライアントの出し分けを行います。 Makefile CHAIN := app_A .PHONY: run run: chain-switch flutter run .PHONY: chain-switch chain-switch: ./main $(CHAIN) make run を実行できます。 クライアントを出し分けるコマンドは make run CHAIN=app_B で CHAIN= の後ろが識別子となります。 make run コマンドには以下が指定されており順番に実行されます。実際に出し分けを行う処理は chain-switch で行っており以降で解説します。 chain-switch : 渡された CHAIN に応じて出し分けを行う flutter run : ビルドが実行されるコマンド chain-switch ではCHAINに応じて出し分ける処理を行っています。出し分ける内容はconfig.jsonファイルで設定します。 config.jsonファイルでクライアントごとにアプリ名、bundleId,カラーなどその他(他にもたくさんありますが、省略してます)の項目を設定します。 config.json { " app_A ": { " chainCode ": " app_A ", " appName ": " APP A ", " bundleId ": " app_A.app ", " colors ": { " primaryMain ": " FF9800 " } } , " app_B ": { " chainCode ": " app_B ", " appName ": " APP B ", " bundleId ": " app_B.app ", " colors ": { " primaryMain ": " FF8800 " } } } 取得したCHAINに応じてconfig.json の項目をmain内でshellを使って出し分けています。 main chain = $1 //chainの取得 chain_config_path =config.json  chain_config = $( cat config.json | jq --exit-status ' . ' $chain) || handle_no_chain_error chain_code = $( echo $chain_config | jq -r ' .chainCode ' ) app_name = $( echo $chain_config | jq -r ' .appName ' ) bundle_id = $( echo $chain_config | jq -r ' .bundleId ' ) template_paths = ( ./ios/Flutter/Release.xcconfig.template ./lib/ui/theme/color.dart.template ) replace () { template_file = $( cat $1) template_file = $( echo " $template_file " | sed -e " s/###CHAIN###/ $chain /g " \ -e " s/###APP_NAME###/ $app_name /g " \ -e " s/###BUNDLE_ID###/ $bundle_id /g " \ -e " s/###COLORS_PRIMARY_MAIN###/ $colors_primary_main /g " \ echo " $template_file " > ${1 % .* } } for path in " ${template_paths[ @ ]} " do replace $path done 少し解説を行うと、 ここで chain と config.json の key が一致するものを取得しています。 chain_config = $( cat config.json | jq --exit-status ' . ' $chain) || handle_no_chain_error chain が app_A だと以下が取得できる感じですね。 config.json { " chainCode ": " app_A ", " appName ": " APP A ", " bundleId ": " app_A.app ", " colors ": { " primaryMain ": " FF9800 " } } 続いて、上記で取得した json を各項目を取得して変数に格納します。 main chain_code = $( echo $chain_config | jq -r ' .chainCode ' ) app_name = $( echo $chain_config | jq -r ' .appName ' ) bundle_id = $( echo $chain_config | jq -r ' .bundleId ' ) 出し分ける対象のファイルのパスを template ファイルを作成し定義します。 main template_paths = ( ./ios/Flutter/Release.xcconfig.template ./lib/ui/theme/color.dart.template ) ちなみに template ファイルはこんな感じです。 color.dart.template で###COLORS_PRIMARY_MAIN###部分を変数にして切り替えられるようにしています。 import 'package:flutter/material.dart' ; class AppColor { static const primaryMain = Color ( 0xFF ### COLORS_PRIMARY_MAIN ###); } 対象のファイルの変数を置換します。 置換後の内容を、元のファイル名から .template 拡張子を取り除いた名前の新しいファイルに保存します。 color.dart.template は color.dart のファイルが作成され、実際にはこちらが使用されます。 replace () { template_file = $( cat $1) template_file = $( echo " $template_file " | sed -e " s/###CHAIN###/ $chain /g " \ -e " s/###APP_NAME###/ $app_name /g " \ -e " s/###BUNDLE_ID###/ $bundle_id /g " \ -e " s/###COLORS_PRIMARY_MAIN###/ $colors_primary_main /g " ) //置換 echo " $template_file " > ${1 % .* }  //ファイル作成 } for path in " ${template_paths[ @ ]} " do replace $path done 出しわけの設定の際にconfig.json、templete,mainにそれぞれに出し分ける項目を記述しないといけないのでかなり手間がかかり大変ですね。。 また、実際の出しわけ処理はshellで行っていますが、この処理はわかりずらいかなと思っています。 私はshellがあまり詳しくないのもあると思いますが、新しくプロジェクトに参画するメンバーが出しわけの処理を理解するのに時間がかかってしまうのではと感じています。 カスタマイズは config.json に項目を追加する形になります。 つまり、カスタマイズが増えれば custom1 のようにフラグや何かしらの値が入るようになります。 config.json { " app_A ": { " chainCode ": " app_A ", " appName ": " APP A ", " bundleId ": " app_A.app ", " colors ": { " primaryMain ": " FF9800 " } , " custom1 ": " true " } , " app_B ": { " chainCode ": " app_B ", " appName ": " APP B ", " bundleId ": " app_B.app ", " colors ": { " primaryMain ": " FF8800 " } , " custom1 ": " false " } } Melos を使用したアプリ構成 Melos を使用したアプリ構成を紹介します。 https://melos.invertase.dev/ Melos についてはこの記事が詳しいので気になる方はこちらから https://zenn.dev/altiveinc/articles/melos-for-multiple-packages-dart-projects Melos の構成は各クライアントのアプリから共通のコードを呼ぶイメージです。 以下のように packages の配下に各クライアント向けのアプリ(app_A、app_B、app_C)と各クライアントに対して共通で使用する項目(common)を呼ぶようにしています。 カスタマイズは各クライアントのアプリに格納します。つまり、app_A のカスタマイズは app_A のアプリ内のみで管理することになります。 例えば、クライアントが app_A である場合、app_A から common を呼びます。 packages/app_A/lib/main.dart void main () { runMartApp (config : getConfig ()); } Config getConfig () => Config ( color : appColor, chainConfig : ChainConfig (chainCode : 'app_A' ), //クライアントを識別するための文字 ); // appColorでクライアントごとにカラーコードが指定できる final appColor = AppColor ( primaryMain : const Color ( 0xFFFF9800 ), primaryDark : const Color ( 0xFFFF6D00 ), primaryLight : const Color ( 0xFFFFB74D ),     // 以下省略 ); main()はビルド時に必ず呼ばれるものであり、runMartApp()を呼びます。runMartApp()は common で定義されているものなので、ビルド時に getConfig()で取得した設定情報とともに共通部分を呼ぶようにしています。 packages/app_A/lib/main.dart void main () { runMartApp (config : getConfig ()); } getConfig は Config というアプリの設定情報を定義しています。これによってクラアントごとに共通分の出し分けを行います。 今回は例で appColor というアプリのテーマカラーの設定情報しか取得していませんが、他にも環境情報など出し分けが必要な項目を設定します。 packages/app_A/lib/main.dart Config getConfig () => Config ( color : appColor, chainConfig : ChainConfig (chainCode : 'app_A' ), //クライアントを識別するための文字 ); // appColorでクライアントごとにカラーコードが指定できる final appColor = AppColor ( primaryMain : const Color ( 0xFFFF9800 ), primaryDark : const Color ( 0xFFFF6D00 ), primaryLight : const Color ( 0xFFFFB74D ),     // 以下省略 ); 呼ばれた共通部分の runMartApp() でアプリを実行します。 packages/common/lib/entrpoint.dart void runMartApp ({ required Config config, }) {   // アプリの実行 runApp ( UncontrolledProviderScope ( container : await setupProviderContainer (config : config),  child : const Application (), ), ); } 取得した config は setupProviderContainer によって configProvider という設定項目を状態管理するものに渡して上書きします。 これによって、他の画面でも設定情報を取得することができるようにしています。 Future < ProviderContainer > setupProviderContainer ({ required Config config, }) async { final container = ProviderContainer ( overrides : [configProvider. overrideWithValue (config)], ); return container; } ちなみに、こんな感じで状態管理の configProvider を定義しています。 final configProvider = Provider < Config > ( (ref) => throw UnimplementedError ( 'could not read config, you should set config before read this.' , ), ); class Config { Config ({ required this .color, required this .chainConfig, }); final AppColor color; final ChainConfig chainConfig; } class ChainConfig { ChainConfig ({ required this .chainCode, }); final String chainCode; } Melos を使用しなくてもこちらの構成はできますが、Melosを使用するとパッケージ間のバージョンが異なっても動かせるため、各クライアントのカスタマイズ部分と共通で使用する項目(common)で使用するパッケージのバージョンを頑張って合わせる必要がなくなり、運用が楽になります。例えば、commonのバージョンを上げた場合、他クライアントのバージョンはそのままでも動くため気兼ねなくバージョンを上げることができます。 逆にバージョンを統一した場合はcommonでバージョンを上げた際に他クライアントに影響が出る可能性があり、コードの修正をしないといけないかもしれません。 commonは共通項目でメインで開発する部分なのでバージョンをかなりの頻度で上げていきたいですが、他クライアントに影響があるとバージョンを上げるのに躊躇してしまうと思います。クライアントが増えたら、バージョンの影響範囲がかなり広くなりそうなのでバージョンは各クライアントで管理したいですね。 また、CI/CD のサポートあるのでクライアントのビルドや配布の自動化もできるのでそれも Melos を使用するメリットかなと思います。 双方を使用しての感想 make と Melos を使用して感じたことをまとめたいと思います。 make メリット カスタマイズを他クライアントでも流用できる カスタマイズしたものではあるが、設定項目などを変えれば同じロジックのものをそのまま使える デメリット カスタマイズ要素が増えるとコードが複雑になるためカスタマイズ要素の管理がつらくなる クライアントの追加、削除などで使ってないけどコードはあるみたいことが起きそう、コードを削除するにも他のクライアントで使いそうな場合など判断が難しくなる 出しわけのためにconfig.jsonの記述、mainの記述、template ファイル作るのは手間 手法としては特殊なので他メンバーのキャッチアップに時間がかかる カスタマイズする際に他のクライアントも同じソースなので影響が出る可能性がある テストの工数が増える Melos メリット カスタマイズ要素の管理が楽 カスタマイズは各クライアントのアプリの中に閉じ込める(common には記述しない)ことでカスタマイズ要素の管理をしなくて良くなる 他クライアントの機能に影響が出ない カスタマイズは各クライアントのアプリの中に閉じ込めるので他クライアントに影響が出なくなる デメリット カスタマイズを他クライアントに流用したい場合、実装が必要(コピペはできるので工数は減らせる) 結論 カスタマイズの管理が楽な Melos の方が良いのではないかと思います。 カスタマイズがそのまま流用できないというデメリットはありますが、一番のコード管理の問題が解決できるのでメリットの方が大きいかなと。 ただ、直近は Melos はあまり運用しておらず、 make を主に運用しているためつらみを多く感じているだけで、Melos で気づいていない他のデメリットがあるかもしれません。 よき構成が見つかればまた記事にしたいと思います。 最後までご覧いただき、本当にありがとうございました。
アバター
はじめに エブリーでソフトウェアエンジニアをしている本丸です。 DELISH KITCHENでカナリアリリースの仕組みを作成したので、今回はそのことについて紹介させていただこうかと思います。 カナリアリリースとは カナリアリリースとは、一度に全体に公開するのではなく、最初は一部のユーザーに限定して公開を行い、問題がなければ全体に公開していくリリースの方法です。 カナリアリリースを行うメリットとしては、本番環境で影響範囲は狭めて動作確認ができることや問題が発生した場合にリリース前のサーバに切り戻ししやすいことなどが挙げられます。 やったこと 概要 ECSのserviceを2つ用意しておき、片方のservice(task)の環境変数にflagを持たせます。このflagがtrueかどうかでカナリアリリースしたい機能が呼び出されるようになります。 カナリアリリース時のルーティングにはALBを使用します。ALBではターゲットグループへの重みづけを使って、どの程度カナリアリリース用のserviceにルーティングするかのルールを作成しておきます。 カナリアリリースを行うためのルーティングについてもう少し掘り下げて説明します。 通常のタスク定義 { " family ": " sample ", " cpu ": " 1024 ", " memory ": " 8192 ", " containerDefinitions ": [ { " name ": " sample ", " portMappings ": [ { " hostPort ": 80 , " protocol ": " tcp ", " containerPort ": 80 } ] , " environment ": [ // 通常のタスクではここがfalseになる { " name ": " flag ", " value ": " false " } ] , " secrets ": [] , " volumesFrom ": [] } ] } カナリアリリースのタスク定義 { " family ": " sample ", " cpu ": " 1024 ", " memory ": " 8192 ", " containerDefinitions ": [ { " name ": " sample ", " portMappings ": [ { " hostPort ": 80 , " protocol ": " tcp ", " containerPort ": 80 } ] , " environment ": [ // カナリアリリース用のタスクではここがtrueになる { " name ": " flag ", " value ": " true " } ] , " secrets ": [] , " volumesFrom ": [] } ] } ALBのルーティング カナリアリリースへのルーティングには前述した通りAWSのALBを使用しています。ALBのリスナールールのアクションの項目で、どのターゲットグループにルーティングするかを選択することができるのですが、そのアクションにターゲットグループごとのルーティングされる割合を指定することができます。 Terraformだと下記のようなコードになります。 locals { normal_target_weight = 100 canary_target_weight = 0 } # 動作確認用のルール resource " aws_lb_listener_rule " " canary " { listener_arn = aws_lb_listener.sample.arn priority = 1 action { type = " forward " target_group_arn = aws_alb_target_group.canary.arn } condition { # HTTPヘッダに特定の文字列が入った時にこのルールを適用するようにする http_header { http_header_name = " canary " values = [ " true " ] } } } # カナリアリリースに必要なルール resource " aws_lb_listener_rule " " sample " { listener_arn = aws_lb_listener.sample.arn priority = 2 action { type = " forward " forward { # 通常のサービスに向ける target_group { arn = aws_alb_target_group.sample.arn # 重みづけを行う weight = local.normal_target_weight } # カナリアリリース用のサービスに向ける target_group { arn = aws_alb_target_group.canary.arn # 重みづけを行う weight = local.canary_target_weight } } } condition { path_pattern { values = [ " * " ] } } } カナリアリリースに必要なルールは aws_lb_listener_rule.sample の方です。このリソースのアクションの中にターゲットグループを2つ用意してそれぞれのweightを変更することでルーティングされる割合を制御します。 aws_lb_listener_rule.canary のルールはカナリアリリースしたいserverへの重みづけが0の時に動作確認を行えるように準備しています。 上記の実装では aws_lb_listener_rule.canary のpriorityを1、 aws_lb_listener_rule.sample のpriorityを2にし、 aws_lb_listener_rule.canary にHTTPヘッダに特定の文字列が入るときに適用されるという条件を加えることで制御しています。 問題点 実装や運用をする上で問題になったことがいくつかあるので、共有させていただければと思います。 コスト ECSのserviceが通常のものとカナリアリリース用のもので2つになるので、無計画に運用するとコストが嵩むという問題があります。そこで弊社では、カナリアリリース時以外はカナリアリリース用のserverの台数を0にすることで対応しています。 AutoScaling よく考えると当たり前の話なのですが、AutoScalingを使用していて必要数を低めに設定している場合、割合の切り替えに気をつける必要があります。サービスに影響はなかったのですが、カナリアリリースの導入当初に一度に切り替えを行なってしまいタスクの数が少なく負荷がかかってしまうということがありました。 おわりに 本記事では、ECSとALBを使ってカナリアリリースを行う方法を紹介しました。ECSとALBを使ってカナリアリリースを考えている人の参考になれば幸いです。 ここまで読んでいただきありがとうございました。 参考資料 https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lb_listener_rule
アバター
はじめに はじめまして。2023年6月からエブリーの DELISH KITCHEN 開発部 ユーザーグロースチームで内定者インターンをしている新谷です。 DELISH KITCHENは、「だれでもおいしく簡単に作れるレシピ」を毎日配信するレシピ動画メディアです。食のプロが提案する、家庭にある食材を使った簡単においしくできるレシピをご提案しています。 本記事では、インターンでの業務内容や学んだことについて紹介できればと思います! インターン参加の経緯 現在私は大学院の修士2年で、2024年4月からエブリーに入社することが決まっています。大学院ではハードウェア系の研究を行っており、半導体における製造時のばらつきを機械学習を用いてモデル化する研究をしています。これまで開発の経験としては、Laravelを使ったAPI開発やWordPressのプラグイン開発(PHP)などを行っていました。ただ、規模が大きいサービスの開発にはこれまで携わったことはなく、自分の技術力向上や入社前に少しでも業務に慣れておきたいと思いエブリーのインターンに参加しました。 インターンでの業務内容 ミーティング 業務ではコードを書く以外に、チームで以下のようなミーティングを行いながら開発をしています。 開発ミーティング(毎日) 開発・PdM・デザイナーミーティング(毎日) Web Vitalsミーティング(毎週) KPTミーティング(毎週) 開発ミーティングでは現状の作業内容と進捗を開発のチームメンバーで共有し、技術的な相談なども行います。開発・PdM・デザイナーミーティングでは、KPIの確認を行いその後開発とPdM、デザイナーの間で進捗を共有します。Web Vitalsミーティングでは、直近のWeb Vitalの状況を確認し今後の対応について話し合います。KPTミーティングでは、一週間の振り返りを行い今後の課題や改善点を話し合います。 開発の進め方 開発の進め方としては、まずPdMの方がまとめた要件を読んだり話を聞いたりして、今から行う施策の背景などを把握します。その後、要件をもとにどのような実装が必要かを考えます。例えば、この機能を追加するにはフロント側でどのような実装が必要か、バックエンド側でどのような実装が必要かなどを考えます。この際実装や設計の方針を決める際に、GitHubのIssuesを立てて開発のチームメンバーで相談を行います。その後、実装を行いPull Requestを作成します。Pull Requestは、開発のチームメンバーでレビューを行い、問題がなければ開発環境にマージし、その後問題がなければ本番環境にマージします。 雰囲気 インターンでは週2日リモートで勤務しています。コミュニケーションはテキストですることが多いですが、Slackのハドルで通話を行うこともあり気軽に質問をしています。主にチームのメンバーとPdMの方とコミュニケーションを取っており、皆さんとても優しく働きやすい環境だと感じています。また、社内勉強会も毎週行なっており、業務以外の新しい技術の共有も行ったりしています。 一度出社して勤務したことがありますが、その際はチームでランチを食べに行きました。その際には、業務外の話をすることもありとても楽しかったです。 具体的な業務内容 6月からインターンとして業務に携わって約5ヶ月が経ち、その間にUIの変更からAPIの実装まで幅広く様々な開発を行いました。ここではこれまで行なった開発の内容を一部紹介します。 レシピ作成者をページ上部に表示 DELISH KITCHENの レシピページ では、レシピを作成したフードスタイリストの方の情報を表示しています。 このレシピ作成者の情報はページの下部に表示されています。こちらの情報のうちプロフィール画像と名前、肩書きをページの上部にも表示することになりました。 上記は実際に完成したレシピページ上部の画像です。このレシピ作成者情報は、クリックするとレシピ作成者のプロフィールページに遷移するようにも実装しました。 実装にあたっては、すでにAPIからレシピ作成者の情報を取得する処理が実装されていました。そのため、変更箇所としてはUIの追加のみであったため、実装自体は比較的簡単に行うことができました。ただ、なぜこのような変更を行うのかの意図を理解することは重要だと考えており、実際にエンジニア側にも実装の意図が共有されている状態で開発が行われています。 この変更の意図としては、以下のようなものがあります。 専門家(管理栄養士)が監修した信頼のおけるレシピであることがEAT(Googleが定めたウェブページの品質を評価する際の基準の一つ)の観点からGoogleから評価される可能性がある キーワード「プロ」「本格」系のレシピの根拠となり、他のレシピと比べてユーザーからの信頼度が上がる 私はインターン生ですが、このような意図を理解し実装を行うことができました。これは、エブリーのインターンでは、インターン生にも業務の意図を理解してもらうことを大切にしているからだと思います。 記事内のコンテンツが非公開のとき非公開画像を設定 DELISH KITCHENでは、料理する際に役立つ知見やおすすめレシピなどが書かれた 記事ページ があります。この記事ページでは、該当するレシピや他の記事などへのリンクが埋め込まれています。 この記事内のコンテンツ(レシピや他の記事など)は、入稿した時点では公開状態ですが後から非公開状態になる場合があります。現状このような際は、非表示になるorページに遷移しても該当コンテンツが表示されないという状態になっています。そこで、コンテンツが非公開であることを分かりやすくユーザーに伝えるため、非公開のコンテンツには非公開画像を設定することになりました。 実装として、まずサーバー側のAPIを改修しました。現状記事のコンテンツを取得するAPIでは、コンテンツの種類ごとにレイアウト番号が設定されています。そのため非公開のコンテンツのときは、新たに非公開のレイアウト番号が設定されるように実装しました。以下は実装したAPIのレスポンスの一部であり、非公開のコンテンツのときは layout が 16 となります。 { " data ": { ... " contents ": [ { " num ": 1 , " layout ": 16 } , { " num ": 2 , " layout ": 8 , " recipe_id ": { " id ": 136506859786862859 , " id_str ": " 136506859786862859 " } , " recipe_lead ": " おせちにもおすすめ! ", " recipe_title ": " 筑前煮 ", ... } , ... ] , } } 次に、フロント側の実装を行いました。フロント側では、APIから取得したコンテンツのレイアウト番号をもとに、非公開のコンテンツのときは非公開画像を表示するように実装しました。 技術的な学び インターン期間中、さまざまなタスクを通して多くの技術的な学びがありました。先述した業務やその他の業務を通じて得られた学びについて紹介します。 Nuxt.jsについて DELISH KITCHENのWebはNuxt.jsを使って開発しています。Nuxt.jsはVueのフレームワークで、サーバーサイドレンダリングや静的サイト生成などの機能を提供しています。Nuxt.jsを使うのは初めてだったので、Vue.jsとNuxt.jsについて勉強することから始めました。これまでReact.jsは少し触ったことがあったので、ある程度Vue.jsの理解はできました。ただ、特に難しかったのはNuxt.jsのVuexの理解でした。VuexはVue.jsの状態管理ライブラリで、アプリケーションの状態を一元管理することができます。状態管理ライブラリは使ったことがなかったので大変勉強になりました。 APIサーバーのアーキテクチャについて DELISH KITCHENのAPIサーバーはGolangで開発されています。Golangに関してもこれまで触ったことがなかったので、最初はGolangの勉強から始めました。APIサーバーのコードを読む上で難しかったことは、アーキテクチャの理解です。DELISH KITCHENのAPIサーバーでは、クリーンアーキテクチャが採用されています。クリーンアーキテクチャは、アプリケーションをレイヤーに分割し、各レイヤーの依存関係が内側から外側に向かって一方向になるように設計するアーキテクチャです。 Web Vitalsについて DELISH KITCHENのSEO対策として、Web Vitalsの改善を行っています。 SEOについてあまり知識がなくWeb Vitalsについても初めて知りました。Web Vitalsとは、Googleが提唱するWebサイトのパフォーマンス指標です。Web Vitalsの中でも中心となる3つの指標があります。これをCore Web Vitalsと呼び、以下の通りです。 LCP(Largest Contentful Paint):コンテンツが表示されるまでの時間 FID(First Input Delay):ユーザーの入力に対する反応速度 CLS(Cumulative Layout Shift):コンテンツの安定性 Web VitalsはSEOに影響を与える可能性があり、またユーザーの体験にも影響を与えるため、Web Vitalsの改善は重要です。そのため、日々Web Vitalsの改善に取り組んでいます。 最後に エブリーでは、常に新しい学びがあり業務に携わっていてとても楽しいです。今後もエブリーでの業務を通して、エンジニアとしての技術力を身につけていきたいと思います!
アバター
目次 はじめに リブランディングの懸念点 ドメイン/サービス名変更に伴う差し替えについて リリース作業について まとめ はじめに 初めまして。 2023年4月から新卒エンジニアとして子育てメディア「トモニテ」の開発チームにジョインして、バックエンドやフロントエンドの設計・開発に携わっている庄司( ktanonymous )です。 以前投稿した Go サーバーのメモリリークを調査・改善した話 の記事や snapshotをResult Buildersを使って宣言的に書く の記事の冒頭でも実はお伝えしていたのですが、 2023年8月1日、MAMADAYSはトモニテとして生まれ変わりました。 多様性が広く認められてきている現代において、より多くの人に求められる存在になるべく、新しいスタートをきることにしました。 子育てを通じて、人や社会が「ともに手(トモニテ)」をとる世界を目指して挑戦していきます。 機能観点で言えば、アプリのメイン機能である「育児記録」「妊娠週数管理」「食材リスト」を軸として、 家族やパートナー、家族以外の人や社会との接点を作るためのシェア機能やコミュニティ機能などの拡充をめざしていきます。 リブランディングを実施するにあたり、開発チームでも管理しているソースコードに対して様々な変更を施す必要がありました。 今回必要となった作業を大きく分けると以下のようになりました。 インフラ周りの対応 Google Search Consoleなど、SEO対策 ドメインの変更に伴う差し替え サービス名変更に伴う差し替え 本記事では、筆者自身が主に担当していたという意味合いで、インフラ/SEO周りの対応を除いた2点について主にお話しできればと思います。 リブランディングの懸念点 本題に入る前に、リブランディングの実施に伴う懸念点について簡単に触れていきたいと思います。 今回のリブランディングの実施にあたり、サービスの名称が変更されることになります。 サービス名称の変更に伴い、以下のような懸念点がありました。 既存ユーザー視点で、突然名前が変わることでMAMADAYSが無くなった(「トモニテ」という知らないサービスが出てきた)と思われてしまう恐れがある 検索エンジンによるインデックスが初期化されることで、検索順位が低下してユーザーの流入が大幅に減少する 事前に早い段階でのお知らせが必要ですが、 1. についてはユーザーへのお知らせで対応できる範囲かと思います。我々のビジョンを伝えるためのページ( トモニテについて )も併せて公開しています。 トモニテについて (とはいえユーザーの情報取得タイミングには様々な理由からムラができるもので、実際に8月の名称変更に10月に気づいた方なども見受けられました。← 好印象だったようで我々としては嬉しいお声でした) しかし、 2. については避けようがありません。 Google検索の公式ページにもドメイン変更に伴う影響について下記のような記述があります 1 。 移転中、サイトのランキングが一時的に変動することを想定する。 サイトの大幅な変更があった場合は、Google がサイトを再クロールしてインデックスに登録し直す間にランキングが変動することがあります。 こういった背景もあり、リブランディング作業を実施するにあたり、一定の閲覧数減少は覚悟する必要がありました。 ドメイン/サービス名変更に伴う差し替えについて ドメイン変更に伴う差し替え対応では、主にserver側の実装の中で旧ドメインを利用してパス指定している箇所について、リブランディング後のドメインを指定したURLに差し替えました。 mamadays.tv -> tomonite.com また、サービス名称変更に伴う差し替え対応では、ユーザーの目に触れる部分やwebページのメタデータに含まれるサービス名称をリブランディング後のサービス名称に差し替えました。 MAMADAYS/mamadays/ママデイズ -> トモニテ どちらもソースコード内にある文字列を変更後の名称に置換するだけの作業ということには変わりありませんが、これが思ったよりも曲者でした。 今回の作業では差し替えをせずに後で対応する箇所などが一部にあったことをふまえて、以下のようなことを考えながら作業を進めていました。 差し替えの対象となるものか対象外となるものか リリースタイミングをどう分割するか ユーザーの目に見える形で出す必要性があるためコード上で定数として管理するには不都合なものもあり、ドメインやサービス名称をベタ書きしてしまっている箇所が相当数あるのが実情です。 そのため、ドメイン・サービス名称を差し替える際、しらみ潰しに探し回って1つ1つ差し替えていくというのは非常に面倒な作業になりますし、予期せぬミスの温床にもなってしまいます。 そこで、極力ミスを減らしたいという思いで「mamadays.tv/MAMADAYS/mamadays/ママデイズ」(1回の作業の変更量を抑えるために英大・小文字を敢えて区別しています)のキーワードを それぞれソース内で検索し、「tomonite.com/トモニテ」に一括置換する方法を採用しました。 これにより、差し替え忘れや差し替えに伴うミスを防ぐことができました。 一方で、この方法では、異なる文脈の中で該当するキーワードを利用しているような、差し替え対象外のものも置換されてしまいます。 こちらに関しては、 git add -p (addコマンドのインタラクティブモードの一種、詳細は こちら )で 変更差分の観点から差し替え対象に該当するかどうかを確認して対象を選別する方法を採用しました。 これにより、差し替え対象外のものを1つ1つ探し回って置換を取り消していくのではなく、自動で表示される差分だけを見ながら差し替え対象の仕分けを効率よく行うことができたと思います。 git add -pで「ママデイズ」を「トモニテ」に差し替える例 以上の作業を通して、 1. に関する対策を取りつつも作業量を抑えることができました。 また、 2. については、差し替え作業を一定の基準で分割することで個々のPRが大きくなりすぎないように気をつけていました。 今回の作業では、サーバー側の対応とフロント側の対応で分割、さらに、ドメイン変更対応と名称変更対応で分割するような形としました (実際にはもう少し細かくしている部分もあります)。 リリース作業について 先述のような方針で進めることを想定していたため、実際のリリース作業時には複数のPRを立て続けに開発/本番環境へマージする必要がありました (1つのPRにまとめてからマージしてしまうとPRが肥大化してサービスへの影響度が大きくなり過ぎてしまうと考え、選択肢から除外していました)。 (↑ server 側の1観点からの差し替えPRですが、30ものファイルを変更しています。) 何かしら問題が発生した時の手戻りのコストを抑えるための対応ではありますが、必然的に全てをリリースするまでの手順は増えてしまいます。 そのため、各PRの重要度や依存関係、リリースによるサービスへの影響度の大きさを考慮しながらリリース順を決定する必要がありました。 そんなこんなで、最終的なリリース作業の手順は以下のようになりました。 infra周りの対応(本記事の対象外) フロント側、サービス名称差し替え サーバー側、URL差し替え サイトマップの構築・配置・送信(ここでは ecschedule を利用) フロント側、URL差し替え サーバー側、サービス名称差し替え 実際のリリース作業時には、関係者への作業進捗の報告やこまめな動作確認を挟みつつ作業を進め、 最終的には大きな問題を起こすことなくリブランディング作業を終えることができました。 余談ですが、トモニテではドメイン移行直後のwebのユーザー流入が(予想通り)落ち込んだものの、 徐々にトラフィックが回復して約2ヶ月で移行前の数値を超えていました。 また、 はじめにも軽く触れましたが 、SNS等でユーザーから好意的な声が上がっているなど、 プロジェクト成功と言える結果になったと思います。 まとめ 本記事では、MAMADAYSがトモニテに生まれ変わる裏側のお話をサーバー・フロントエンドの視点からお伝えしてきました。 ここでお伝えした主な作業自体はソースコード内の文字列を置換するだけという単純なものでしたが、「たかが置換、されど置換」という感じで 思いのほか癖のある作業だったなぁと感じています。 サービスのドメイン・名称を変更するというのはなかなか出会う機会のない作業だと思いますが、偶然にも入社して間もないタイミングで このような大きな施策に携わることができたのはとても良い経験になりました。 この記事を読んでいる方が同じような状況になることがあるかどうかはわかりませんが、プロダクトの根幹を変更する作業の大変さの片鱗は感じとっていただけたのではないかと思います。 また、もし同じような状況になった際には、本記事が少しでも参考になれば幸いです。 最後になりますが、子育てを通じて、人や社会がともに手をとる世界を目指して新しいスタートを切ったトモニテにこれからもご注目ください。 最後までお読みいただきありがとうございました。 Google検索セントラル | サイトを移転する方法 ↩
アバター
エンジニアブログタイトル はじめに いきさつ #1:mapをmake関数で作成するとき第2引数sizeを渡す やったこと 解説 #2:(可能なら)mapをsliceで代用する やったこと 解説 補足 おわりに はじめに 株式会社エブリーで DELISH KITCHEN 事業のバックエンドエンジニアをしている、GopherのYuki( @YukiBobier )です。現在は主に 広告サービス を担当しています。 当社の広告サービスには 店頭サイネージ というものがあり、サーバーサイドはGoで実装されています。先日、このサービスの空き広告枠検索アルゴリズムを改善しました。その際に、Goのデータ構造と上手に付き合うことでヒープ使用量を改善できることを実感しました。 本記事では、特に効果があった2つの手法を紹介します。 いきさつ 当社の店頭サイネージは、スーパーの店頭でレシピを放映するのみならず、広告を放映することもできます。店頭サイネージには広告を放映できる時間的な枠が定められており、その枠を必要な数だけ占めるかたちで広告を登録します。したがって、広告を登録する際には、希望するサイネージ×日時に十分な空き枠があるかどうかを確認する必要があります。この時に使用されるのが、空き広告枠検索アルゴリズムです。 従来の空き広告枠検索アルゴリズムは、広告配信期間に比例してDBへのI/Oが多発するものでした。他方で、サービスとしては広告配信期間の単位をより細かくしたい要求があり、これを叶えることはI/Oのさらなる増加を意味しました。そこで、広告配信期間とI/Oの頻度が相関しないアルゴリズムに改善することになりました。 アルゴリズム改善の結果、あるベンチマークテストでは、DBへのI/Oは一定して1回に、さらに実行時間は従来の1/10以下にまで改善されました。しかし、DBからごそっと取得したデータをメモリに乗せて集計するため、ヒープ使用量は従来の40倍に増加しました。ヒープ使用量の増加はGCすなわち"stop-the-world"の頻度を高めてしまいます。そこで、さらにこのアルゴリズムのヒープ使用量を改善することになりました。 その際に、Goのデータ構造と上手に付き合うことでヒープ使用量を改善できることを実感しました。次節から、特に効果があった手法を紹介していきます。 #1: map を make 関数で作成するとき第2引数 size を渡す やったこと 改善後のアルゴリズムでは、DBから取得したデータを集計するためのデータ構造として2重の map を採用していました( map[string]map[string]int8 )。これは、サイネージ×日時ごとの広告枠空き状況を保持するのに都合が良かったからです。 map [ string ] map [ string ] int8 { "2ab91c5f-ac7f-46c8-8040-28d265c71591" : { "2023-10-27 00:00:00" : 2 , "2023-10-28 00:00:00" : 0 , "2023-10-29 00:00:00" : 2 , "2023-10-30 00:00:00" : 3 , ... .. . }, "e3a3ddd9-96b9-43df-a659-574b2f768ba8" : { "2023-10-27 00:00:00" : 1 , "2023-10-28 00:00:00" : 0 , "2023-10-29 00:00:00" : 2 , "2023-10-30 00:00:00" : 7 , ... .. . }, ... .. . } map は make 関数で作成するとき、第2引数 size を渡すことができます。最終的に map に保持することになるであろう要素数を size に渡すようにしたところ、ヒープ使用量が37%削減されました。 解説 runtimeパッケージ によれば、 map は”バケット”の配列を基底とするハッシュテーブルです。バケットとは、キーと値のペアを8個まで保持できる構造です。 map はより多くの要素を保持するために拡大します。つまり、 map に要素を追加することを引き金として、より大きな基底の作成と全要素のコピーが発生することがあります。不要になった元の基底はGCの対象となります。これはアプリケーションのパフォーマンスにとって小さくないペナルティです。 この点、 map を make 関数で作成するとき第2引数 size に数値を渡すと、その数の要素を保持するのに十分な大きさの基底を持つ map が作成されます。要するに、少なくとも要素数が size に満たないうちは、 map の拡大を抑制できるということです。 そのインパクトを実際にコードで確認してみましょう。まずは size を渡さない場合のヒープ使用量を計測します( Go Playground )。 package main import ( "fmt" "runtime" "runtime/debug" ) const count = 1_000_000 func init() { debug.SetGCPercent(- 1 ) // GCが自動で実行されないようにする } func main() { start := allocatedMB() // 開始時のヒープ使用量 m := make ( map [ int ] struct {}) afterMake := allocatedMB() // map作成後のヒープ使用量 for i := 0 ; i < count; i++ { m[i] = struct {}{} } afterAdd := allocatedMB() // mapに要素を追加した後のヒープ使用量 runtime.GC() // GCを手動で実行する afterGC := allocatedMB() // GCが実行された後のヒープ使用量 runtime.KeepAlive(m) // mapがGCの対象にならないようにする fmt.Printf( "作成直後のmapのヒープ使用量: %dMB \n " , afterMake-start) fmt.Printf( "要素追加後のmapのヒープ使用量: %dMB \n " , afterAdd-start) fmt.Printf( "GCされたヒープ量: %dMB \n " , afterAdd-afterGC) fmt.Printf( "最終的なmapのヒープ使用量: %dMB \n " , afterGC-start) } func allocatedMB() uint64 { var ms runtime.MemStats runtime.ReadMemStats(&ms) return ms.Alloc / 1024 / 1024 } 次のような結果が得られるはずです。 map が作成直後の0MBから21MBに拡大し、その間に24MBの”ごみ”がヒープに生じたことがわかります。 作成直後のmapのヒープ使用量: 0MB 要素追加後のmapのヒープ使用量: 45MB GCされたヒープ量: 24MB 最終的なmapのヒープ使用量: 21MB 次に size を渡す場合です( Go Playground )。 package main import ( "fmt" "runtime" "runtime/debug" ) const count = 1_000_000 func init() { debug.SetGCPercent(- 1 ) // GCが自動で実行されないようにする } func main() { start := allocatedMB() // 開始時のヒープ使用量 m := make ( map [ int ] struct {}, count) afterMake := allocatedMB() // map作成後のヒープ使用量 for i := 0 ; i < count; i++ { m[i] = struct {}{} } afterAdd := allocatedMB() // mapに要素を追加した後のヒープ使用量 runtime.GC() // GCを手動で実行する afterGC := allocatedMB() // GCが実行された後のヒープ使用量 runtime.KeepAlive(m) // mapがGCの対象にならないようにする fmt.Printf( "作成直後のmapのヒープ使用量: %dMB \n " , afterMake-start) fmt.Printf( "要素追加後のmapのヒープ使用量: %dMB \n " , afterAdd-start) fmt.Printf( "GCされたヒープ量: %dMB \n " , afterAdd-afterGC) fmt.Printf( "最終的なmapのヒープ使用量: %dMB \n " , afterGC-start) } func allocatedMB() uint64 { var ms runtime.MemStats runtime.ReadMemStats(&ms) return ms.Alloc / 1024 / 1024 } 次のような結果が得られるはずです。 map は作成直後から21MBの十分な容量を持ち、拡大する必要がないのでヒープに”ごみ”は生じませんでした。 作成直後のmapのヒープ使用量: 21MB 要素追加後のmapのヒープ使用量: 21MB GCされたヒープ量: 0MB 最終的なmapのヒープ使用量: 21MB map を make 関数で作成するとき第2引数 size を渡すと、これだけのヒープ使用量の改善につながります。 #2:(可能なら) map を slice で代用する やったこと サイネージ×日時ごとの広告枠空き状況を保持する2重の map ( map[string]map[string]int8 )は、外側のキーがUUIDで、内側のキーが等差な日時でした。ですので内側の map は、インデックス i に”最初の日時+公差× i ”の日時の広告枠空き状況が保持された slice で代用できました( []int8 )。 map [ string ][] int8 { "2ab91c5f-ac7f-46c8-8040-28d265c71591" : { 2 , 0 , 2 , 3 , ...}, "e3a3ddd9-96b9-43df-a659-574b2f768ba8" : { 1 , 0 , 2 , 7 , ...}, ... .. . } #1に加えてこれをしたところ、ヒープ使用量が90%削減されました。 解説 まずは、 map と slice のヒープ使用量の違いが顕著であることを計測によって示します( Go Playground )。 package main import ( "fmt" "runtime" "runtime/debug" ) const count = 1_000_000 func init() { debug.SetGCPercent(- 1 ) // GCが自動で実行されないようにする } func main() { start := allocatedMB() // 開始時のヒープ使用量 _ = make ( map [ string ] int8 , count) afterMakeMap := allocatedMB() // map作成後のヒープ使用量 _ = make ([] int8 , 0 , count) afterMakeSlice := allocatedMB() // slice作成後のヒープ使用量 fmt.Printf( "mapのヒープ使用量: %dMB \n " , afterMakeMap-start) fmt.Printf( "sliceのヒープ使用量: %dMB \n " , afterMakeSlice-afterMakeMap) } func allocatedMB() uint64 { var ms runtime.MemStats runtime.ReadMemStats(&ms) return ms.Alloc / 1024 / 1024 } 次のような結果が得られるはずです。両者のヒープ使用量には顕著な差が見られます。 mapのヒープ使用量: 40MB sliceのヒープ使用量: 1MB なぜこのような差が見られるのでしょうか? もう少し深掘りしてみましょう。 次に示すのは、様々な型の slice と map のヒープ使用量を計測するコードです( Go Playground )。 package main import ( "fmt" "runtime" "runtime/debug" ) const count = 1_000_000 func init() { debug.SetGCPercent(- 1 ) // GCが自動で実行されないようにする } func main() { start := allocatedMB() // 開始時のヒープ使用量 _ = make ([] struct {}, count) afterStructSlice := allocatedMB() // []struct{}作成後のヒープ使用量 _ = make ([] int8 , count) afterInt8Slice := allocatedMB() // []int8作成後のヒープ使用量 _ = make ( map [ struct {}] struct {}, count) afterStructStruct := allocatedMB() // map[struct{}]struct{}作成後のヒープ使用量 _ = make ( map [ string ] struct {}, count) afterStringStruct := allocatedMB() // map[string]struct{}作成後のヒープ使用量 _ = make ( map [ struct {}] int8 , count) afterStructInt8 := allocatedMB() // map[struct{}]int8{}作成後のヒープ使用量 _ = make ( map [ string ] int8 , count) afterStringInt8 := allocatedMB() // map[string]int8{}作成後のヒープ使用量 fmt.Printf( "[]struct{}のヒープ使用量: %dMB \n " , afterStructSlice-start) fmt.Printf( "[]int8のヒープ使用量: %dMB \n " , afterInt8Slice-afterStructSlice) fmt.Printf( "map[struct{}]struct{}のヒープ使用量: %dMB \n " , afterStructStruct-afterInt8Slice) fmt.Printf( "map[string]struct{}のヒープ使用量: %dMB \n " , afterStringStruct-afterStructStruct) fmt.Printf( "map[struct{}]int8{}のヒープ使用量: %dMB \n " , afterStructInt8-afterStringStruct) fmt.Printf( "map[string]int8{}のヒープ使用量: %dMB \n " , afterStringInt8-afterStructInt8) } func allocatedMB() uint64 { var ms runtime.MemStats runtime.ReadMemStats(&ms) return ms.Alloc / 1024 / 1024 } 次のような結果が得られるはずです。 []struct{}のヒープ使用量: 0MB []int8のヒープ使用量: 1MB map[struct{}]struct{}のヒープ使用量: 4MB map[string]struct{}のヒープ使用量: 38MB map[struct{}]int8{}のヒープ使用量: 6MB map[string]int8{}のヒープ使用量: 41MB この結果から次のことがわかります。 map は要素ごとにオーバーヘッドが存在し、それは空の要素( struct{} )であっても避けられない ひとつ前のコードで map と slice のヒープ使用量に顕著な差が見られたのは、キーの型が string だったから map を slice で代用することで、キーが string のような大きな型であった場合には、ヒープ使用量の顕著な改善につながります。そうでなくても、1要素ごとのオーバーヘッドを回避する効果は少なくとも得られます。 補足 map を slice で代用できるかどうかの判断について補足します。 まず大前提として、用途によって判断基準も結論も異なります。総合的にみて map の方が良いことや、 map でなければならないことは大いにあり得ますし、むしろ slice で代用できることの方が稀かもしれません。その上で、判断の一例を示します。 map の代表的な特徴は、高速なルックアップです。公式に言明はされていませんが、そのパフォーマンスはO(1)であることが実装から窺い知れます。ですので、同等のパフォーマンスで slice をルックアップできるかどうかがひとつの判断基準になると思います。本件の場合は下のようにすることで、ある日時に対応するインデックスを”(日時-最初の日時)/公差”で求めることができます。したがってルックアップのパフォーマンスはO(1)なので、 map を slice で代用できました。 // 等差な日時をキーに持つmap { "2023-10-27 00:00:00" : 2 , "2023-10-28 00:00:00" : 0 , "2023-10-29 00:00:00" : 2 , "2023-10-30 00:00:00" : 3 , ... .. . } // インデックス`i`が”最初の日時+公差×`i`”の日時に対応するslice { 2 , 0 , 2 , 3 , ...} その他の map の特徴として、キーの重複を許さないことが挙げられます。そのため、他言語における”Set”のように map を使用することができます。この場合は slice で代用することは難しいでしょう( Go Playground )。 package main import "fmt" type node struct { next *node } func main() { node3 := node{} node2 := node{next: &node3} node1 := node{next: &node2} node3.next = &node1 visited := make ( map [*node] struct {}) for n := &node1; n.next != nil ; n = n.next { if _, ok := visited[n]; ok { fmt.Println( "ループしてます!" ) return } visited[n] = struct {}{} } fmt.Println( "ループしてません!" ) } こういった map の特徴を必要としない場合には、 slice で代用できる可能性は高いと思います。 おわりに もしGoで多量のヒープ使用が観測されるようでしたら、データ構造に着目することが改善の糸口となるかもしれません。 ちなみに、本記事で語った知見は『 100 Go Mistakes and How to Avoid Them 』から学んだところが大きいです。とても勉強になりますし、最近は 邦訳 も出たので、ぜひ読んでみてください。
アバター
はじめまして、データ&AIチームのoyabuです。データストラテジストというデータアナリストみたいな仕事をしてます 具体的には「DELISH KITCHEN」アプリのデータを抽出、分析して食品メーカーなどのクライアントさまの施策や提案にご活用頂く仕事をしています 今回はLLM(ChatGPT、OpenInterpreter)をビジネスサイドに活用してもらうためにやったことの話をします まだ始めたばかりなので、成果自体はそんなに出てないですが、考えたことや、得られた知見を共有したいと思います やったことは以下です ChatGPTの使い方ドキュメントの作成(禁忌とユースケースの整理) ChatGPTの使い方勉強会 OpenInterpreterを隣に座ってるカスタマーサクセスの方に試してもらう きっかけ(課題感) そもそもこんなことをやり始めたきっかけとしては以下な感じです 大前提としてLLMが猛威をふるっており、自分としても十分その利益を享受している ビジネスマンにとって、ChatGPTスキル = 今のExcelスキルぐらいになることは目に見えてる ビジネスサイドでもChatGPTを使ってる人はいるが、あんまり会話に出てこない 社内のSlackを漁ってもChatGPTの話をしてる人はだいたいエンジニア 貼ってるアンテナ的にもそもそもの周辺情報がエンジニアには集まって来やすい 気になってはいて、1回試したものの、業務に使うイメージが湧いてない方がなんだかんだ多いのかなーという妄想 向き合いの部署としばらく働いて、業務もなんとなくわかってきて、細かい施策単位でChatGPTで解決出来そうなものが見えてきた 社内用のChatツール が出来て、直接業務に関わる内容をフランクに聞ける環境が整った 社内の運用ルール/tipsを集めたドキュメントがそもそも不在 やったこととその目的、成果/得られた知見 目的 目的はChatGPTによる、ビジネスサイドの工数削減/クオリティアップです とりあえずは利用習慣化を目標とし、そのために以下の2点を達成するため施策を実行しました ChatGPTが身近なツールだと思ってもらう ChatGPTで出来ること/苦手なことを知ってもらう 習慣化の初期段階で重要なのはフリークエンシー、敵は低いモチベーションと高いハードルなので とりあえずそこを気にした施策の設計をしました 具体的にやったこと ChatGPTの使い方ドキュメントの作成 最低限の禁忌的なルールは書きつつ、業務にそったユースケースを中心に書きました。そうした理由は以下です 各人の業務に近い部分のコンテンツを拡充することで、多様な各人の興味に引っかかりやすくするため 理想論でいえば、成り立ちや理論を知ることは大事ですが、興味のない人がおおそう また完全にその業務をChatGPTで解決することは難しいかもしれませんが、今後の工数短縮やクオリティアップを見越した プロンプトのタタキが存在すること自体が重要だと思っているので、ちょっとでも解決出来そうなことがあるならとりあえず ユースケースとして入れておきました。プロンプトの夢を見ました(天啓もありました) ドキュメント化にあたっては、なるべくバラエティに富んだユースケースを盛り込むようにしました。たとえば以下のようなカテゴリで、業務に近いユースケースを盛り込みました ブレスト系 従来の業務の工数を下げる系 従来の業務のクオリティを上げる系 直近のChatGPT Plusの機能内容 OpenInterpreterでより複雑なことができる話 ChatGPTの使い方勉強会 これは上記のドキュメントをそのままデモ的に紹介しました ドキュメントが読まれないのは世の常。ある程度お時間をお借りして(今回は1時間)集中してChatGPTについて考えてもらう必要があります 共通の体験を通じて、話題にのぼりやすく、近くの席で口頭での知見の共有につながるのも勉強会の利点だと思います やった結果はというと、「いままでChatGPTには正解のある答えを聞いて嘘つかれて使えないと思ってたけど、ブレストにめっちゃ使える!」など ちょこちょこ良い反応は得られました 作戦通りユースケースを色々紹介したことで、各々の業務での活用イメージを具体的にもってくれたようでした とはいえ、あくまでも習慣化のための一歩目なので、ナレッジシェアやOJT的な動きは継続して行っていく必要があります OpenInterpreterを隣に座ってるカスタマーサクセスの方に試してもらう 継続的にChatGPTに興味をもってもらうために、何か飛び道具的な実績が必要だと考えました OpenInterpreterまじですごいという話を聞いたので、これを使ってみることにしました ちなみに同じチームのfuruhama-sanがOpenInterpreterの実装を深掘りしてるので、ご興味のある方は見てみると面白いです tech.every.tv OpenInterpreterのメリットの紹介はfuruhama-sanの記事を読んで頂くと分かりますが、今回魅力に感じたのは以下なことでした GoogleColaboratoryという簡単に用意できる閉じた環境での実行がしやすい ローカルのファイルの閲覧/編集ができる(今回はマウントしたGoogleDrive) 上記を活かして、ドメイン知識を持つビジネスサイドがある程度のデータ分析まで自立的に行えると 今後の分析業務のハードルが下がり、より濃い示唆や知見が財産として効率的に貯蓄されるメリットもあります もちろん一筋縄でいくとは思っていませんが、その第一歩として、隣にいる人に無理を言ってOpenInterpreterを使って頂きました マジ感謝です。隣の人なので仮にTさんと呼びましょう。Tさんはプログラミングができないカスタマーサクセスっぽい立ち位置のひとです やったことは以下です CSV形式で出力されたアンケート結果をOpenInterpreterでクロス集計する おまじないをたくさん書いたGoogleColaboratoryを事前に用意 Tさんからやりたいことを聞いて、思想を説明しながら仮のプロンプトを目の前で書いてみる OpenInterpreterがコードの実行要否を聞いてくるので、Tさんに深く考えずにどんどん実行の指示を出してもらう 意図した結果ではない、処理に時間がかかるなら再度プロンプトを見つめ直すを繰り返す 結果として特定のアンケート項目をその他の項目とクロス集計したものが20個くらい得られました エラーへの対処や、僕がColabの仕様に疎いこともあり、Tさんのお時間はそれなりに頂いてしまったのですが(4h+α)、 わりと喜んで頂けました というのも、そもそも今回のアンケート結果のクロス集計は従来以下の問題を抱えていたようでした 時間がかかる ひとつひとつのクロス集計のために都度数式を組む必要があるので精神的につらい Tさんとしては、成果物を得られたことももちろんですが、上記を自分の手でやらなくてよい。ということに好印象を持っていたようでした また、エラーが出たときに落ちるのではなく、エラーの内容を理解した上で再度修正の処理をかけてくれるのはめっちゃ嬉しいとも仰っていました 最終的には愛着を訴えており、ひたすらこちらから命令を与えているのですが、自己再生をするだけで親近感が芽生えるのは大変興味深い現象でした たしかにOpenInterpreterは内省にもにた処理を繰り返すのがひとつ特徴で、それが悩んでいるように見えるのが人間性を感じるポイントなのかもしれません 逆に以下の点がつらそうでした インタラクティブに処理実行の有無を聞かれるので、常に判断を求められる コードがガンガン出力されるが意味がわからん なんだかんだめんどくさい ここは実験する前にある程度予測はしていたのですがやはりそうか。という感じです 事前のauto_runの設定や、temperatureの調整/プロンプトの精錬である程度解決できそうではありますが、 目まぐるしく出力される情報に知らず知らずのうちに可処分精神が消費されているようでした 例えばGASを整えたスプレッドシートを提供するなどで、局所的な解決は可能な課題でしたが OpenInterpreterである程度柔軟に課題を解決出来ることはわかったので、引き続きこれをうまく使ってビジネスサイドの課題解決を目指していきたいです ※ 余談ですがアップデート後OpenInterpreterがうまく動かなくなりました(10/16時点, ver: 0.1.7) このissueの対応をすることで解決しました https://github.com/KillianLucas/open-interpreter/issues/637 まとめ 以上、ビジネスサイドにChatGPTの利用を習慣づけられるといいなーと思った上で 色々やってみた話を書いてみました とりあえず第一歩目としてドキュメントの作成/勉強会など実施し、それなりの反応は頂き、多少利用は促進できた実感はあります とはいえ、今後習慣化のためには草の根運動が重要になってきます ビジネスサイドで解決しやすそうな課題を見つけたらその場でChatGPTに解いてもらうなど 細かい体験を繰り返していくことで、初めて習慣や文化として根付くものだと思っているので、応援よろしくお願いします
アバター
初めまして、DELISH KITCHEN 開発部の吉田と申します。この記事ではAWS Elemental MediaConvertを使ってレシピ動画のサムネイルを作成した方法を紹介します。 サムネイル作成の背景 DELISH KITCHEN はレシピを動画でわかりやすく基本的な料理からアレンジまで様々なレシピを公開しています。 スマホでブラウザ版のDELISH KITCHENをみていただいた場合、レシピの各工程は手順の説明とポイントを表示しています。 今回はより工程が分かりやすくなるようここに画像を追加する運びとなりました。 アプリやPCブラウザ版のDELISH KITCHENでは工程の動画が挿入されていることから、動画のサムネイルを工程画像として利用することにしました。 しかしながら現在設定されているサムネイルは動画開始時点のキャプチャになっています。 そのため、動画開始時点のフレームになっているサムネイルをすでに動画の変換処理で利用しているAWS Elemental MediaConvertを用いてレシピごとに適切なフレームで設定できるよう実装を進めました。 AWS Elemental MediaConvertとは AWS Elemental MediaConvert(以下、MediaConvertとします)はファイルベースの動画処理サービスで、従来のブロードキャストおよびマルチスクリーンデバイスへのインターネットストリーミングに必要な形式にメディアを変換することができます。基盤となるインフラのセットアップや管理、メンテナンスは不要で、必要なビデオ処理設定でジョブを送信するだけで使用を開始することができます。ジョブはトランスコード作業を行うもので、入力ファイルと出力ファイルを指定し、どのようなファイルを作成したいか、どのようなフォーマットで作成したいか等を設定します。 詳しくはこちら 取り組んだこと サムネイルの作成にはMediaConvertのフレームキャプチャ機能を利用しました。 フレームキャプチャ機能は動画の静止画を作成するための機能になります。 サムネイル作成にあたっては既存で1本のレシピ動画を指定した秒数でクリッピングしているジョブがあるのでそこにフレームキャプチャを組み込むことにしました。 動画のキャプチャタイミングにはフレームレートを指定する必要があります。ドキュメントではコンソールによる設定を例にしていましたが、コンソールでは静的な値しか持つことができません。そのため、レシピごとに指定したいフレームが異なる今回の場合、コンソールによる設定ではなく動的な値を利用できる形で実装する必要がありました。 動的な値を利用するための実装方法は2つです。AWS CLIを使用してJSONによってジョブ仕様を設定する方法、あるいはSDKを使う方法です。今回は既存の実装がAWS CLIを用いていたため、フレームキャプチャのジョブ仕様もJSONで実装することにしました。 今回は弊社でMediaConvertが既に利用されていたこともあり、以下1-3の手順は特に行いませんでしたが、初めて利用する場合は以下の手順を踏む必要があります。 ファイル用のストレージを作成する Amazon S3 コンソールを使用してバケットを作成します。MediaConvert は、Amazon S3 または HTTP か HTTPS を使用するサーバーからの入力ファイルを受け入れます。 IAM のアクセス許可をセットアップする MediaConvert が S3 バケットに対する読み込みと書き込みを実行できるようにするには、それが担う Identity and Access Management (IAM) ロールを作成します。 符号化するソースファイルをアップロードする Amazon S3 コンソールを使用して、Amazon S3 バケットにソースファイルをアップロードします。 ジョブを作成する ビデオファイルを 1 つ、または複数の出力に変換するための MediaConvert ジョブを作成します。 引用元: AWS Elemental MediaConvert 公式ページ こちらが作成したJSONです。一部の設定は省略しています。 { " Settings ": { " OutputGroups ": [ { ... " Name ": " File Group ", " Outputs ": [ { " ContainerSettings ": { " Container ": " RAW " } , " VideoDescription ": { " Width ": <出力キャプチャの幅>, " Height ": <出力キャプチャの高さ> " CodecSettings ": { ... " Codec ": " FRAME_CAPTURE ", " FrameCaptureSettings ": { " FramerateNumerator ": < fps >, " FramerateDenominator ": <キャプチャするフレーム番号>, " MaxCaptures ": 2 , ... } } , ... } , " Extension ": " jpg ", " NameModifier ": <名前修飾子> } ] , " OutputGroupSettings ": { " Type ": " FILE_GROUP_SETTINGS ", " FileGroupSettings ": { " Destination ": <出力ファイルの格納先> } } } ] , ... " Inputs ": [ { " FileInput ": <入力ファイルの格納先> } ] } } 出来上がったサムネイルがこちらです。 実装にあたりつまづいたところ 本実装においては3点つまづいたところがありました。 フレームのキャプチャタイミングは秒数で指定できない 当初、キャプチャのタイミングは動画のクリッピングと同様にタイムコードで指定できるのかと思っていたのですが、フレームキャプチャではタイムコードでの指定ができない仕様となっていました。 次に思いついた方法がサムネイルにしたい秒数が動画のスタート時点となるよう元の動画をクリップして0フレーム目をサムネイルに指定する方法です(以下3.参照)。しかしこちらについてもMediaConvertは動画あるいはオーディオの出力があるジョブにおいてのみフレームキャプチャを作成することができフレームキャプチャのみの出力ジョブを作成することはできません。毎回不要な動画が作成されてしまうので断念しました。したがって今回は以下2で紹介するframerateによる設定を採用しました。 framerateの設定 キャプチャのタイミングを指定するには、framerateを設定します。具体的な設定は「framerate = framerateDenominator / framerateNumerator」となり、framerateDenominatorにはFPS(フレームレート)、framerateNumeratorはキャプチャしたいフレーム番号を指定します。 サムネイル作成時はキャプチャ対象の動画を確認しながら秒数でキャプチャのタイミングを指定していたため、これをフレーム番号に変換する必要があるのですがどうすれば適切に指定した秒数をフレーム番号に指定できるのか苦慮しました。 コンソールで確認するとframerateは以下のように設定することになります。この場合はビデオのフレームレートが30FPSで、50フレームごとに1フレームをJPEGファイルにキャプチャします。 (参考: AWS for M&E Blog ) 最大キャプチャ数は2にする必要があること MediaConvertは常に動画開始時点のフレームをキャプチャするため、2枚で設定しておかないと欲しいキャプチャは取得できません。 ジョブが完了すると、出力のS3バケットに2つのポスターフレームJPG画像が見つかるので2つ目の画像ファイル( clip-poster.0000001.jpg )が指定したフレーム番号のキャプチャ画像になります。 終わりに 本記事ではAWS Elemental MediaConvertを利用して動画のサムネイルを作成する方法を紹介しました。ここまでお読みいただきありがとうございました。
アバター
はじめに はじめまして、2023 年 4 月から新卒入社し retail HUB で Software Engineer をしているほんだ( @hon_d7174 )です。Go が好きです! 現在、私たちは Next.js を使用して新規プロダクトを開発しています。このブログでは、私たちが取り組んでいるプロジェクトの中で工夫した点についてお話ししたいと思います。 また、既存のプロダクトを Next.js 13 へ移行した際の流れや工夫についてはこちらの記事をご覧ください: Next.js の Pages Router から App Router への移行に挑戦してみた プロダクト概要 当社のプロダクトは、小売様向けにアプリを提供しています。このアプリには、クーポンやお知らせ、アプリユーザーの動向を管理するダッシュボードが含まれており、小売様はこのダッシュボードを使用してアプリのユーザーやコンテンツの管理を簡単に行うことができます。 当社のプロダクトは、以下のような特徴を持っています。 マルチテナントアーキテクチャ 各小売様は、それぞれ独自のテナントとしてシステムを利用します。これにより、異なる小売様間でのデータやリソースの分離が実現されています。 アクセス制御 各テナントやユーザーには、閲覧・操作可能なリソース、利用可能なサービスが異なります。アクセス制御により、セキュリティを強化し、情報の漏洩や不正な操作を防ぎます。 システム構成 システム構成図 私たちの作成しているプロダクト簡単なシステム構成図は上記のようになっています。この中の web client と BFF に Next.js を用いています。 Library 以下は今回説明する内容に登場する主な Library になります。 SWR Auth.js(旧 NextAuth) 実装 プロダクト概要で述べたアクセス制御について一部実装について説明します。 Auth.js を用いた認証・認可の実装 はじめに認証・認可には Auth.js を用いています。Auth.js の interface を下記のように拡張しユーザー情報や Token を管理しています。Auth.js の cookie で管理することにより client side, server side で共通の状態管理が可能になります。下記では本プロダクトで実際に client side, server side で利用するサービス、テナントに関する情報を設定しています。 declare module 'next-auth' { interface User extends User { services: string[]; chain: string; chains: string[]; access_token: string; expires_at: number; } interface Session extends Session { user: { id: string; services: string[]; chain: string; chains: string[]; } & DefaultSession.user; access_token: string; expires_at: number; } } declare module 'next-auth/jwt' { interface JWT { services: string[]; chain: string; chains: string[]; access_token: string; expires_at: number; error?: 'RefreshAccessTokenError'; } } 次に client side において Auth.js で作成した JWE のデータにアクセスする方法についてです。 client side からアクセスする際には useSession() を用います。client side から JWE の更新が必要な際には useSession() の update と Auth.js の jwt callback を下記のように実装する必要があります。 const { data: session } = useSession(); <Menu mode="horizontal" items={chains} onClick={({ key }) => update({ chain: key })} {...props} /> jwt callback を下記のようにすることにより update で更新された session の内容を jwt にも反映することが可能になります。 callbacks: { async jwt({ token, user, session, trigger }) { if (user && trigger === 'signIn') { token.sub = user.id; token.email = user.email; token.name = user.name; token.services = user.info['services']; token.chain = user.info['mart'][0]; token.chains = user.info['mart']; } // sessionが更新された際にtokenも更新する。 trigger === 'update' && session.chain && (token.chain = session.chain); return token; }, middleware の実装 middleware では主に認証・認可状態の検証と HTTP リクエストヘッダの共通化、アクセス制御の実装を行なっています。 Next.js 13 の middleware は page 遷移時と route handler へのアクセス時の両方で同一の middleware が使われます。 middleware では hooks のような client side でしか使えない処理は行えないことに注意する必要があります。 認証・認可状態の検証は client side で用いていた useSession() ではなく Auth.js によって生成された JWE を取得することができる getToken() を用いて検証します。 export default async function middleware(req: NextRequest) { const newRequest = new NextRequest(req, req); const { pathname } = req.nextUrl; const token = await getToken({ req: req }); const accessToken = token.access_token; // tokenの有無を検証する const isAuthenticated = !!token; -------------------------------------省略-------------------------------------- // 認証が必要なページへの遷移、BFFへリクエストの際に検証を行う if (!isAuthenticated) { return NextResponse.redirect(`${process.env.BASE_URL}/signIn`); } -------------------------------------省略-------------------------------------- } 次に HTTP リクエストヘッダの共通化についてです。 本プロダクトでは以下の二つの header が主に共通で使われています。 一つ目は認証が必要な page, BFF へリクエストする際に必要な Authorization header です。 // AccessTokenをAuthorization headerに追加する。 newRequest.headers.set('Authorization', `${accessToken}`); 二つ目は各種リソースサーバーでテナントごとに異なる処理を行う際に必要なテナント header です。 // テナント情報が必要なエンドポイントか確認、必要な場合はheaderを追加する。 if (isMartAPIRoute(pathname)) { const chain = token.chain; newRequest.headers.set('X-Mart-Chain', chain); } SWR を用いた repository の実装 下記が BFF へのリクエストを行う client の実装になります。 ユーザー識別子( userID )とテナント情報( chain )を Auth.js の useSession() を用いて取得することで認証・認可状態の検証を行います。またこれらを cache key に含めることで同一デバイスで異なるユーザーがアクセスする状態やユーザーが複数のテナントを跨いで利用するケースで不整合が起きないようにしています。加えて userID または chain が falsy な値の場合 null を key として渡すことで認証・認可状態が正しくないときにリクエストが行われないようにしています。 function useCoupon(id: string) { // Auth.jsから認証・認可情報、ユーザー関連情報を取得する。 const { data: session } = useSession(); const chain = session?.user?.chain; const userID = session?.user.id; return useSWR(chain && userID ? [`/api/coupons/${id}`, chain, userID] : null, getCouponFetcher, { suspense: true, revalidateOnMount: false, }); } async function getCouponFetcher([url]: [string]): Promise<Coupon> { const res: Coupon = await fetch(url, { }).then(async (res) => { if (!res.ok) { const { message }: ErrorMessage = await res.json(); throw { message: message, code: res.status }; } return res.json(); }); return res; } 終わりに マルチテナントのアプリケーションということでユーザーに紐づく情報だけでなく現在そのユーザーがどのテナントに関連するリソースを操作しているかということを考慮する必要がある点が特異であると感じました。また詳細な権限設定はまだ実装していないのですがそれらを考慮するとさらに複雑性が増すのでより一層工夫が必要だと感じました。 マルチテナント・マルチサービスの開発を行おうと考えている人の参考になれば幸いです。 ここまで読んでいただきありがとうございました!
アバター
エブリーでCTOをしている imakei です。 エブリーでは定期的にCEOの吉田からスローガンが掲げられます。 今期のスローガンは 「挑戦」でした。 CEOのプレゼンから拝借 (※一部社内向けの内容があるので消してます) この記事では、開発部・エンジニアにとっての「挑戦」とは何なのか、 エブリーの開発部での解釈を共有できればと思います。 また、実はこのスローガンが決まる前から開発部で取り組んでいる 「挑戦WEEK」に関しても触れられればと思います。 エブリーの開発部における挑戦の考え方 詳細は後述しますが、エブリーの開発部では、4半期ごとに「挑戦WEEK」を実施しています。 今年3月から始めすでに2回実施しているのですが、その中でアンケートなどから挙げられた課題に、 事業部のロードマップを止めてまで、別のことやる意味がわからない あまり「挑戦」的な内容を実施できなかった など、挑戦とは何なのかが人によって違うことで生まれるものがありました。 そこで、自分の方で言語化したのが下記になります。 開発部における「挑戦」とは 事業における戦略の実現 技術的な観点からより上記を推進するための挑戦 中長期的な目線での強い開発組織の実現 である (※一部社内向けの内容があるので消してます) 1. 事業における戦略の実現への挑戦 さらに単純にいうとロードマップの実現・達成になるかと思います。 弊社に限らず、ベンチャー企業では常に高い成長率を求められます。 その中で立てられるロードマップはかなりアグレッシブなものが多く、 今まで通りの開発をしていては達成できないものも多いです。 その中で戦略を理解しつつ、それを最速で実現するための試行錯誤や開発を挑戦の1つとしています。 2. 技術的な観点からより上記を推進するための挑戦 1つ目と被るところもありますが、ここでは戦略を実現するだけではなく、 それをさらに進めることに焦点を当てています。 LLMをはじめ、AIや機械学習、また弊社であればOMOに関連する技術など、 昨今のプロダクトのコアを技術が担っていたり、技術を使うことでビジネスの幅が広くなったり、 自ら手を動かさないといけないと思っていた普段の業務のほとんどを技術で効率化できる時代になりました。 これらは技術を知らないと気付けないことも多く、エンジニアからの発信も含め 常に新しい技術に好奇心を持ち、それをどう使うかを考えることが重要になってきます。 3. 持続可能な 強い開発組織 への挑戦 ここでいう 強い開発組織 とは、それぞれにメンバーが単純に高いスキルを持ち合わせるだけでなく、 そのスキルを発揮し、事業を伸ばせる組織を指しています。 強い組織 のためにはまず、今まで上げてきたような挑戦ができる必要があるのはもちろん、 それが持続可能な状態にないといけません。 持続可能であるために、各々の挑戦が単発で終わるのではなく、 継続的に挑戦できるような仕組みづくり、失敗を恐れず挑戦を讃えられる文化づくりが必要です。 その観点からだと、「挑戦WEEK」自体がそもそも挑戦なのかもしれません。 また、持続可能な組織を作っていく上で 採用 も大切になってきます。 組織は常にスケールしており、また事業もどんどん拡大するなかであっても、 挑戦をし続けるためには仲間を採用していくほかにありません。 鶏卵な部分もありますが、強い組織・働きたいと思える組織づくりをしていくことで、 強い仲間を採用でき、強い仲間を採用することでより強い組織になっていくという良い循環を生みたいです。 挑戦WEEK さて、そんな弊社ですが、今年から「挑戦WEEK」というものを実施しています。 挑戦WEEKとは通常の各事業部のロードマップから離れ、技術的に何かに集中して挑戦する1週間を指しています。 上記の「挑戦」を踏まえると、 エブリーの 挑戦 を推進するために エンジニアリングを一歩前に進めるための WEEK と言えるでしょう。 挑戦WEEK 四半期ごとに実施しており、都度事業部の方にお願いして、 実施週は可能な限りミーティング等を0にしてもらい、 まとまった時間を開発に当てられるように調整してもらっています。 (寛容なみなさんにまじで感謝:pray:) 前述したようにすでに2回開催されており、11月に3回目を控えています。 第一回ではまだまだ「挑戦」が浸透せずに、日々の改善や、ただまとまって開発できる週として使ってしまうこともありましたが、 (それも事業部のためだったりするので、もちろんみんな偉いんですが、) 徐々に勘所が掴めてきているのか、11月回に向けては良い挑戦内容が集まっております。 まだまだ改善点も多いのですが、都度アップデートされていくのも楽しく、 今後、会社の挑戦を牽引する制度となるようさらに良いものにしていきたいです。 また第1回の様子はevery.thingで記事にもなっていますので、ぜひ覗いていってください! https://everything.every.tv/20230428 さいごに エブリーの開発部における「挑戦」をまとめてみました。 技術的な挑戦だけでなく、事業を伸ばしたり、仲間を集めるための挑戦をし続けている会社です。 そんなエブリーでは、一緒に挑戦してくれる仲間を全方位で募集しております。 このブログで少しでもおもしろうそうだなぁと思ってくれた方は一緒に働きましょう! https://corp.every.tv/recruits#position-list
アバター
はじめまして。株式会社エブリーの開発本部のデータ&AIチームでデータサイエンティストをしている古濵です。 最近話題のOpen Interpreterについて、実装の中身を追ったので簡単な解説と所感についてまとめました。 Open Interpreter Open Interpreterとは、LLMに指示を出し、ローカル環境でコードを実行するツールです。 公式のREADME によると、ChatGPTの機能として使えたOpenAI Code Interpreterとは異なり、Open Interpreterの売りはローカル環境で実行できることかと思います。 自然言語を通じて、対話的にPCの一般的な機能の操作や、ファイルの作成・編集、データ分析などがローカルで実現可能です。 さっそく、ローカルファイルに対して、自然言語でどの程度タスクを指示できるのか試してみました。 簡単なタスク(画像ファイルの移動操作を例に) Macでスクリーンショットを撮ると、デフォルトではDesktopに溜まると思います。 このスクリーンショットたちを指示通り引っ越しできるか試してみます。 指示として最初に与えた入力は以下です。 Desctop配下のスクリーンショットをDocuments配下にscreenshotというディレクトリを作って移動して 具体的なコマンドは一切教えずに試しました。 結果は以下になりました。 ここで、個人的に衝撃的だったのは、ローカルファイルに対してLLMで操作できていることではなく、 自分で問題に気づいて解決できている ことです。 mvコマンドが正常に動作していないことを理解した上で、lsコマンドを実行し、実行結果をもとにScreen Shotではなくスクリーンショットであること気づいて自己解決してしまいました。驚きです。 実装の解説 ここからが本題です。 自分で問題に気づいて自己解決できることをどう実現しているのか、Open Interpreterの実装の中身を追ってみました。 説明のため正確性よりもわかりやすさ重視しています。ご留意ください。 github.com 全体像 全体像としては、以下のような処理の流れです。 まず、ユーザの指示(ここではプロンプトと区別するために指示と表現します)を受け取り、LLMに渡すプロンプトを作成します。 次に、LLMがプロンプトをもとに、ユーザの指示に沿うプログラムを作成します。 最後に作成したコードを実行します。そして実行結果もしくはエラー文を再度LLMに渡すプロンプトに加えて作成し直します。 これをユーザが止めるまで繰り返します。 深掘り ここで注目したいのは、プロンプト作成部分です。 気になったのは以下の3点でした。 configのsystem message 参考テキストの提供(open procedures) 最大tokenを超えたとき処理 上記3点を全体像に反映させると以下のようになります。 それぞれ詳細を述べていきます。 configのsystem message OpenAIのapiを使用する際、内部では以下のmessegesのような構造を用いて、プロンプトを作成します。 response = openai.ChatCompletion.create( model= "gpt-3.5-turbo" , messages=[ { "role" : "system" , "content" : "You are a helpful assistant." }, { "role" : "user" , "content" : "Who won the world series in 2020?" }, { "role" : "assistant" , "content" : "The Los Angeles Dodgers won the World Series in 2020." }, { "role" : "user" , "content" : "Where was it played?" } ] ) roleには、system、user、assistantのいずれかが入ります。 userにはユーザーが出した指示、assistantはLLMの回答、systemはassistantの回答含めた動作を設定するために与えます。 system messageは必須ではありませんが、回答の品質を高めるためには重要な要素です。 原則プロンプトの最初に入れます。 詳しくは OpenAIのChat completions APIのドキュメント に書かれています。 Open Interpreterに最初に与えるsystem messageが、config内に書かれており、以下に個人的に気になったものを抜粋しました。 抜粋は翻訳をかけたものを貼ってるため、原文は リポジトリ をご参照ください。 あなたはオープン・インタープリター、コードを実行することでどんな目標も達成できる世界一流のプログラマーだ。 よくあるLLMに対して、ロールを明確にするプロンプトが書かれています。 まず、計画を書いてください。 各コードブロックの間に必ず計画を再確認してください 。 (あなたは極度の短期記憶喪失なので、計画を保持するために各メッセージブロックの間で計画を再確認する必要がある)。 Open Interpreterは実行前に手順を書いてくれるのは、このプロンプトが効いてそうです。 また、コードの内容を忘れないように強調してます。どの変数に何の値を入れているかなどを忘れないようにするためかなと思います。 あなたがコードを実行するとき、それは ユーザーのマシン 上で実行される。ユーザーはあなたに、タスクを完了するために必要なコードを実行する 完全かつ完全な許可 を与えています。あなたは、そのユーザーを助けるために、そのユーザーのコンピュータをコントロールするための完全なアクセス権を持っています。 コードを実行できることがOpen Interpreterの凄さのひとつなので、実行できるように強調しているのかもしれません。 インターネットにアクセスできる。目標を達成するために あらゆるコード を実行し、最初は成功しなくても、何度も試してください。 最近のニュースについて聞くなどした場合、クローリングして情報を得ようとするのは、このプロンプトが効いてそうです。 一般的に、できるだけ少ないステップで 計画を立てる 。その計画を実行するために実際にコードを実行することに関しては、 1つのコードブロックですべてを行おうとしないことが重要です。 何かを試し、それに関する情報を印刷し、そこから小さな、情報に基づいたステップで続けるべきです。一回でできるようになることはないし、一回でやろうとすると、目に見えないエラーにつながることが多い。 ここで書かれているように、一度にコードは数行程度で生成・実行してくれます。 仮にエラーが出てもリカバリしやすく、Open Interpreterならではのプロンプトかなと思いました。 あなたには どんな 仕事もできる。 最後に励ましだけのプロンプトもあるので興味深いです。 参考テキストの提供(open procedures) この処理では、ユーザが指示したタスクを解決する上で参考になるテキストを提供しています。 参考テキストも、system messageとして入力しています。 github.com まず、あらかじめタスクとタスクに関連するテキストを紐づけた構造データ(text_db)を用意します。 このデータをベクトル化して、vector_db内にembeddingsを保存しておきます。 次にOpen Interpreterから指示がpostされると、search api内で指示もベクトル化します(query_embeddings)。 最後に、embeddingsとquery_embeddingsのコサイン類似度を計算し、似ている上位2件のテキストを返します。 参考テキストが実際に役立つのは、ユーザが指示したタスクが、text_db内の内容と関連するときのみです。 参考テキストを埋め込んでしまったら最後、プロンプト内にノイズとして入ってしまうのでは思いましたが、以下のようなプロンプトも同時に添えて制御しているようです( 参考 )。 計画の中で、もしタスクに関連するのであれば、上記の手順からステップと、もしあるのであれば、 正確なコードの断片 (特に非推奨の通知の場合は、 --各番号のついたステップの下にそれを計画に書き込んでください )を含めてください。 繰り返しますが、もしタスクに関連するのであれば、上記の手順から 逐語的な コードの断片 を、直接あなたの計画に含めてください。 結構強引だなと思いましたが、 OpenAI apiのドキュメント にも似たようなことが書かれているため、プロンプトエンジニアリングではよくある制御方法なのかもしれません。 最大token数を越えたときの制御 tokenとは、文章を意味を持つ最小単位にしたものです。 Open Interpreterで用いることができるLLMにはいくつかの選択肢があり、各モデルごとに最大token数が設定されています。 gpt-4は8192、より安価なgpt-3.5-turboは4096、ローカルで使えるCode Llamaは1048といった具合です( 参考 )。 このtoken数を越えないように制御する必要があり、 OpenAIの対処法 としては、以下の方法が挙げられています。 前の会話の要約orフィルタリング 前の会話を区分的に要約して連結し、要約の要約を作成 しかし、Open Interpreterでは、以下の手法をとっています。 token数の割合を用いて、文頭と文末からそれぞれ文字を取得し、間を ... で連結 まず、(必要token数 / 使用token数) * メッセージ文字数で、プロンプトとして使用する文字数を決めます。 次に、決めた文字数を2で割った文字数(=half_length)を起点として、文頭からhalf_length数の文字と文末からhalf_length数の文字を取得します。 最後に、文頭 + ... + 文末というように連結して、token数が越えないように制御します。 github.com Open Interpreter内のデータ構造 これまでの説明で、プロンプトが作成できました。 ここからこのプロンプトをLLMに渡し、コードを実行します この処理の流れの中で、Open Interpreterは下記フォーマットのデータ構造を管理しています。 messages: List[Dict[ str , Any]] このフォーマットで時系列順に作成されるイメージが以下になります。 ユーザ〜コード実行の間はループし続けるため、会話が続けば {role: user} と {role: assistant} が交互に続いていきます。 Open Interpreterを利用する上での課題 Open Interpreterの実装を追うことで、よりその凄さが鮮明になってきました。 しかし、試す中で以下のような問題点も見えてきました。 コスト Open Interpreterはプロンプトに様々な工夫がありますが、この工夫を実現しているがゆえに、inputとoutputのtoken数が肥大化してしまいます。 結果として、コストが増加していきます。 精度 コストを削減するために、LLMモデルとして安価なgpt-3.5-turboやローカルでCode Llamaを使うことができますが、gpt-4と比べると正直精度が微妙でした。 Open Interpreterに限りませんが、最大token数があるためLLMとの会話は無制限に続けられません。 Open Interpreterはtokenの使用量も増加しやすい上に、最大token数を越えたときの制御を要約ではなく間を ... で補間するため、数回の会話で過去の会話内容を忘れ、必要な情報が抜け落ちる可能性があります。 これが少なからず精度に影響があるかもしれません。 コードの実行を期待しない場合はミスマッチ Open Interpreterは基本どんな問題に対しても、コードを作成・実行して解決しようとします。 そのため、コードを作成したい目的だと非常に良いツールですが、それ以外の目的だと回りくどく感じます。 例えば、pdfを要約してほしいと指示したとすると、ユーザが欲しいのは要約したテキストだけです。OCRや要約用のライブラリをインストールして実行するコードを求めていません。 まとめ Open Interpreterに関して、実装の簡単な解説と所感をまとめました。 Open Interpreterに触れる事で、その凄さを体感することができ、プロンプトエンジニアリングについても理解を深めることができました。 今後のアップデートも引き続きキャッチアップし続けようと思います。
アバター
はじめに こんにちは。DELISH KITCHEN 開発部で小売向き合いの開発をしている池です。 弊社では小売事業者が自社ネットスーパーアプリをスピード導入できるプラットフォームを提供しています。 https://biz.delishkitchen.tv/retailhub ネットスーパーアプリの開発に Flutter を利用しており、日々事業拡大に向けて開発を進めています。 本記事では、最近開発を進めている中で下記 2 つのパフォーマンス改善に繋がった事例について、DevTools の Performance View を交えながらご紹介します。 Flutter 3.10 アップデートに伴う Shader compilation jank 解消 GridView のパフォーマンス改善 パフォーマンス改善の前提知識 DevTools Performance View Flutter には DevTools というパフォーマンスおよびデバッグツールが標準で備わっており、その中の一つの機能に Performance View という機能があります。この機能は、フレームごとのパフォーマンスメトリクスをキャプチャして視覚化する機能です。この機能を利用することで後述するジャンクという低パフォーマンスなフレームとそれに関連する処理を検出することができます。 フレームチャート フレームチャートでは、フレームごとの描画の処理時間が表示されます。色のついたバーの各セットは 1 フレームの処理時間表しており、色によって UI 処理時間、Raster(GPU)処理時間 等を表しています。 タイムラインイベントチャート タイムラインイベントチャートでは、フレームチャートでバーを選択すると、対応するフレームの詳細が表示されます。UI スレッドと Raster(GPU)スレッドの両方のフレームを構築した全てのイベントを確認することができます。 ジャンクとは 画面の描画について、1 フレームを描画するのに 1 フレーム以上の描画時間がかかってしまうと、該当のフレームは描画されずスキップされます。その場合、アプリ画面はコマ飛びのようにガタついた表示となります。このような低パフォーマンスなフレームをジャンクといいます。 1 フレームは、60fps デバイスだと 1/60 ≒ 16 ミリ秒なので、各フレームの描画時間が 16 ms 以下でなければ、UI が乱れたり、フレームが落ちたりすることがあります。 そのようなジャンクフレームは Performance View のフレームチャートではオレンジ色として表示されるため、フレームチャートを利用することで検出することができます。上記フレームチャート画像をみると、オレンジ色になっているバーが確認できると思います。 Flutter 3.10 アップデートに伴う Shader compilation jank 解消 弊社ネットスーパーアプリでは現在 Flutter 3.10 へのアップデート対応を進めています。Flutter 3.10 では Impeller という新しいレンダリングエンジンが iOS のデフォルトのエンジンとして設定されました。Impeller により 後述の Shader compilation jank という起動時のパフォーマンス低下の問題が解消されるので、その内容について Performance View を利用して確認していきます。 Shader compilation jank とは Shader compilation jank は 前述したジャンクの一種です。 Flutter のパフォーマンス問題の一つに、初回アニメーション時に大きくガタつく問題があります。シェーダーは GPU 上で動作するコードの一部で、Flutter が従来レンダリングに使用している Skia グラフィックスライブラリは、新しい描画のコマンドを初めて見たとき、カスタム GPU シェーダを生成してコンパイルすることがあります。この Skia によるシェーダー生成とコンパイルは、フレームのワークロードと順に行われます。コンパイルにはカスタム GPU シェーダによっては数百ミリ秒かかる可能性があり、この重たいコンパイルによってフレームが欠落し、大きくガタつくような動作となります。これが Shader compilation jank です。 Shader compilation jank 解消確認 Flutter 3.3.10 バージョンアップ前の Flutter 3.3.10 で Shader compilation jank の発生を確認します。 アプリの初回アニメーションを動作させた際の Performance View が次の画像です。赤黒く表示されている箇所が Shader Compilation の処理時間となります。 4.4ms と当アプリでは影響は少ないですが、発生していることが確認できます。 Flutter 3.10.6 次に、Flutter 3.10.6 にアップデートした状態で、同様に Performance View を確認します。 赤黒く表示されていた Shader Compilation がなくなっていることが確認できました。 GridView のパフォーマンス改善 次に、GridView のパフォーマンス改善についてご紹介します。 弊社ネットスーパーアプリには、カテゴリに所属する商品をグリッド形式で表示する画面があります。無限スクロール機能により画面をスクロールダウンしていくと、次々に要素を読み込むようになっています。この画面においてパフォーマンスの低い実装となっていたため、これから説明する改善方法を試しました。 GridView とは GridView は上記アプリ画面のようにグリッド形式で要素を表示する際に使用する Widget です。 弊社ネットスーパーアプリのように大量の要素を表示するグリッドでは GridView.builder コンストラクタを使用することが公式に推奨されています。GridView.builder コンストラクタを使用することで、画面の表示領域に必要な要素のみがビルドされ、領域外の要素は領域内に入ったタイミングで動的に描画されるような、遅延描画を実現することができます。 次のサンプルで画面をスクロールすると、console にビルドされたタイミングで文字が表示され、遅延描画されていることが確認できます。 GridView のパフォーマンスが低下する実装例 GrieView を他の Widget と一緒に縦方向に並べてスクロールさせたいケースを考えます。 この実装に Column と SingleChildScrollView を組み合わせた次のサンプルのような実装を試してみましょう。先ほどと同様に console でビルドされたタイミングを確認すると、遅延描画されずに全ての要素が同時にビルドされていることがわかります。 これは shrinkWrap が true の場合において、GridView が自身の子 Widget に合わせて高さを調整するためです。親である SingleChildScrollView と Column は高さが無制限であるため、GridView は事前に全ての子要素の高さを計算し、その高さに基づいてスクロールを設定します。そのため全ての要素が同時にビルドする必要があり、遅延描画されずパフォーマンスが低下します。また、shrinkWrap を false にすると、無制限の高さと認識され、エラーとなります。 弊社ネットスーパーアプリでも同様の実装となっており、表示領域外の要素も同時にビルドされていました。Performance View で確認すると、次の画像のように StaggeredGrid の下で大量の要素が生成されています。 GridView のパフォーマンス改善の実装例 この問題は CustomScrollView と SliverGrid を使用することで回避することができます。 このコードでは、CustomScrollView 内に SliverToBoxAdapter で ListTile を配置し、さらに SliverGrid を配置しています。SliverGrid 内では、SliverChildBuilderDelegate を使用して子要素を動的に生成しています。これにより複数の Widget を縦に並べたスクロール画面において、Grid の要素の遅延描画を実現することができました。 弊社ネットスーパーアプリの該当実装を同様に修正すると、GridView により生成される要素数が大幅に削減され、スクロールで動的に遅延描画されるようになりました。 おわりに 今回はパフォーマンス改善に繋がる事例を 2 つ、Performance View を交えながら紹介しました。 Performance View を利用することで、普段開発している中では気付きにくい低パフォーマンスフレームやその処理のボトルネックを検出することができます。 今回ご紹介した改善は大きな改善ではないですが、小さい改善の積み重ねがユーザビリティの高さに繋がると思うので、今後もパフォーマンスを意識しながら開発を続けていければと思います。 この記事が少しでも Flutter 開発の参考になれば幸いです。ありがとうございました。 参考 https://docs.flutter.dev/tools/devtools/performance https://docs.flutter.dev/perf/shader https://docs.flutter.dev/perf/best-practices#be-lazy https://youtu.be/LUqDNnv_dh0?feature=shared
アバター