Upgrade to Pro — share decks privately, control downloads, hide ads and more …

社内フレームワークとその依存性解決 / in-house framework and its ...

社内フレームワークとその依存性解決 / in-house framework and its dependency management

https://bitkey.connpass.com/event/338871/
Go Far. #2 〜ナレッジワーク×メルカリ×newmo×サイバーエージェント×ビットキー〜
登壇資料です
スライド中のリンクはPDFとしてダウンロードした後Chromeなどで開くと飛べることを確認しています

Masahiro Wakame

January 28, 2025
Tweet

More Decks by Masahiro Wakame

Other Decks in Programming

Transcript

  1. 5 社内フレームワークを作っている話 • メルペイではmonorailという社内フレームワークを作っています ◦ v1 → 大昔ある人物によって生み出された ◦ v2

    → メルコインのマイクロサービスでガッツリ使われている ◦ v3 → 現在3つのマイクロサービスで試用中→さらなる拡大の野望 • vvakameは去年の5月末くらいから開発に参加 ◦ おもしろそうなところの開発をサポートする気持ちだったが チームメンバーの離職もありTech Leadっぽい感じに… 🙀 歴史とかの話
  2. 6 社内フレームワークを作っている話 • マイクロサービスなので疎結合!それぞれが自由にできるぞ! ◦ 組織規模がでかくなるにつれ設計や運用の乖離がキツくなってきた ◦ チーム内やチーム間で知見が共有できたほうがいいじゃん ◦ 統一的な設計や運用への要求の高まり

    ◦ そんなこんなで社内フレームワークが求められるようになった ◦ 制約やベストプラクティスの集積や横展開など ◦ “Learn Once, Write Anywhere” という標語が生まれた ▪ 一回学べば色々なチームで働ける ▪ 他のチームのマイクロサービスを容易に移管できる 経緯
  3. 7 • Productチームのためのフレームワーク ◦ ゆるいLayered Architecture などProductチーム各所で育てられてき たベストプラクティスをゆるく集約 ◦ Productチームの要望を最重要視する

    ▪ forkが生まれるのが最悪パターンなので避けたい ◦ monorailの機能はどこかのProductチームで使われていたもの • ArchitectチームやPlatformチームに詳細を移管(できるといいな) ◦ ベストプラクティスはそれぞれ最適なチームで管理したい ◦ Productチームの工数をなるべく使わずに土台の改善を行いたい 社内フレームワークを作っている話 ねらい
  4. 8 社内フレームワークを作っている話 Updatable • テンプレートからforkした後のアップデートの追従にも課題が • Productチームから見て低コストにアプデに追従したい • MakefileやGitHub Actions

    Workflowやlintなどを共有したい • Kubernetes Manifestのベストプラクティスをパッケージ化したい • terraformの設定項目をパッケージ化したい • このあたりを解決し、持続的に発展・共有可能な仕組みがほしかった • 一部出来てるけど難しい箇所も多い ◦ Goコードのアップデートは難しい ◦ すでにガッツリできてるインフラにアドオンで何かするのが難しい
  5. 9 社内フレームワークを作っている話 それ以外にも … • Architecture Decision Recordを導入しました ◦ 技術書典

    Unleash Mercari Tech! Vol.5 に収録 • monorail-todo というテンプレMSがあり、みんなでこれを改善する ◦ go get でmonorailを更新するとGitHub Actions上で `monorail update` が実行されファイル更新PRが自動作成される • などの工夫が無数にあり、それぞれ面白いと思うけど社外公開は… ◦ 社内インフラにべったり依存しているので公開しても誰も使えない • 興味がある人がいれば懇親会で深堀りしにきてください
  6. 11 依存性解決で困った話 • monorailはServerという単位でプロセス内に複数の実装を持てる • Spanner(DB)のClientなど、Server間でシェアしたい要素がある • シェアしたくないものもある • 各パーツは別のものに依存している

    ◦ Spanner → OpenCensus → Datadog Agent → 環境変数 ◦ など… • DIといえばgoogle/wireだが… ◦ フレームワーク内で実装するので静的に解決するのが難しい • 必要なものの生成を手動で解決していた ◦ 当然だけどメンテが大変 当初の実装
  7. 14 depmanというパッケージを作った話 • github.com/vvakame/depman ◦ 年末年始に実装しなおした私家版 ◦ 社内版とは実装の詳細が異なります(私家版のほうが多分便利) • DEPendency

    MANager 略して depman ◦ ということにしてありますが… • 発想元はJavaScriptのSymbol ◦ symbolの公開範囲はデータの公開範囲制御と等価にできる 作ったもの
  8. 15 depmanというパッケージを作った話 • 作りたい実装に対応する ResourceSpec を実装する ◦ CreateResource(ctx context.Context) (T,

    CloseFn, error) を実装 ◦ 依存関係が何もなければ、単にそのリソースを作って返す ◦ 終了処理が必要ならそのための関数も返せる • CreateResource の実行中に別のResourceSpecを使ってもよい ◦ それぞれが欲しいものを要求すれば最終的に依存性解決できる ◦ 同じものは2つ作りたくない キャッシュにあればそれを返す • depman本体はリソース作成待ちとかをいい感じに調整する ◦ 単にロック取ってリソースできたらGoするだけ ◦ 循環参照は検出してエラーにする 基本仕様
  9. 16 depmanというパッケージを作った話 • 共有範囲を制御しよう ◦ 同じものって何? ▪ 同じ型でも接続先が違うClientなら別物だよね とか ◦

    キャッシュ(map)のkeyが同じなら同じ、違うなら別 とする • ざっくりした比較の仕様 ◦ リテラル系は比較可能 ◦ struct同士の比較は、全フィールドが同じなら同じ ◦ pointer同士の比較は、同じ変数を指してたら同じ • mapのkeyに使う型は比較可能である必要がある ◦ 比較可能であればstructでもpointerでもよい Goの比較演算子の 仕様とかの話
  10. 17 depmanというパッケージを作った話 Example (go.dev/play/p/RyLTWfpVqYV) type StructType struct { Bool bool

    } func Test_goComparisonSpec(t *testing.T) { s1 := StructType{Bool: true} s2 := StructType{Bool: true} s3 := StructType{Bool: false} if s1 != s2 { t.Error("unexpected. s1 != s2") } if s1 == s3 { t.Error("unexpected. s1 == s3") } if &s1 == &s2 { t.Error("unexpected. &s1 == &s2") } }
  11. 18 depmanというパッケージを作った話 Example (go.dev/play/p/RyLTWfpVqYV) m := map[any]any{} m[s1] = "s1"

    m[s2] = "s2" // s1 == s2. overwrite m[s3] = "s3" m[&s1] = "&s1" m[&s2] = "&s2" m[&s3] = "&s3" specs := []struct { key any value string }{ {key: s1, value: "s2"}, // s2. not s1 {key: s2, value: "s2"}, {key: s3, value: "s3"}, {key: &s1, value: "&s1"}, {key: &s2, value: "&s2"}, {key: &s3, value: "&s3"}, }
  12. 19 depmanというパッケージを作った話 • 次のような柔軟性を得られる ◦ 設定値が同じだったら同じキャッシュを使うよね ▪ PublicResourceSpec { Endpoint

    string } ▪ ↑このResourceSpecを使うと接続先が同じなら同じ値が取れる ◦ 誰とも共有したくないからkeyを隠すわ ▪ &privateResourceSpec{} ▪ ↑パッケージ外からはこのリソースにはアクセスできない • 問題 ◦ Goの仕様に依存しているため、一定の熟練度が必要になってしまう ResourceSpecをmapのkeyとする
  13. 21 depmanというパッケージを作った話 • monorailの起動プロセスをdepman管理に置き換えてみた ◦ Pros ▪ 何が何に依存しているかを記述できるようになった ▪ Serverが必要としている値のみを生成できるようになった

    ▪ Productの実装側からは見えないように隠蔽できた ◦ Cons ▪ 仕組みが複雑でメンテナンスにやや属人性を感じている ▪ Product側へどの程度カスタマイズ性を提供するか線引が難 • ないよりはあったほうが断然よいがこの設計で走り続けるかは注視が必要 社内で使ってみた話
  14. 23 技術書典とかで使ってみた話 • 技術書典APIはGoで書かれています ◦ 技術書典API アーキテクチャ という本に書きました • google/wire

    を使っているけど課題があった ◦ 循環参照が解決できない ▪ ProductInfoRepo → ProductVariantRepo → … ◦ 中間成果物が取り出せない ▪ REST API, GraphQL Endpoint, IdP Endpoint etc… ▪ 共有したいものは多いがほしい形が違う 使ってみた
  15. 24 技術書典とかで使ってみた話 • wireのProviderとしてdepmanのResourceSpecを簡素にラップしたものを 登録 ◦ 既存Providerをどんどん置き換えていくとwireが生成するコードが減って いく ◦ やりきると、最終成果物が一発で出てくる

    ▪ そうなったらwireを外してよい ◦ wireはいらなくなったか? ▪ 結局GraphQLの巨大のstructのfieldに必要な値をセットするコード を手で書くのが面倒で残すことにした ▪ wireはフィールドと型と値のパズルの解決をするだけ wireからの移行
  16. 25 depmanというパッケージを作った話 実際の利用例 func CreateSearchService(ctx context.Context) (*SearchService, error) { return

    depman.RequestResource(ctx, SearchServiceSpec{}) } type SearchServiceSpec struct{} func (spec SearchServiceSpec) CreateResource(ctx context.Context) (*SearchService, depman.CloseFn, error) { productInfoRepo, err := depman.RequestResource(ctx, ProductInfoRepositorySpec{}) if err != nil { return nil, nil, err } service, err := NewSearchService(productInfoRepo) return service, nil, err }
  17. 26 技術書典とかで使ってみた話 • REST API, GraphQL Endpoint etc で値をそれぞれ生成するとき、 depmanのManagerを共有しておけば出てくるインスタンスも同じなので各所

    で値を共有できる • 循環参照を解決できるようになった ◦ 生成経路がコントロールできていないインスタンス生成が撲滅された • package間の循環参照は相変わらず厳しい ◦ 気合で解決可能だけど美しくはない • デバッガを使ったインスタンス生成の追跡はwireと変わらず可能 ◦ さすがにコード生成してno reflectionのwireには劣るが苦ではない 使ってみた