マルチアプリプロジェクト

この記事は英語の原文を日本語に翻訳したものです。原文: https://adoptingerlang.org/docs/development/umbrella_projects/

翻訳元: adoptingerlang/adoptingerlang 2025-12-18(コミット 899008f

複数アプリケーションからなるプロジェクトは、俗にアンブレラプロジェクトとも呼ばれ、単一のリポジトリの中で複数のOTPアプリケーションを維持しやすいという理由から、業務環境でのプロジェクト構成として最もよく採られる形です。この章では、その構造、どのような場面で有効あるいは不向きになるか、この構造ならではの細かな注意点、そして最後にモノリシックなライブラリを複数のOTPアプリケーションへ分割するためのコツを扱います。

マルチアプリプロジェクトの構成

アンブレラプロジェクトの一例として、以前から使ってきたservice_discoveryリポジトリが挙げられます。それがアンブレラだとひと目でわかる手がかりは、ディレクトリ一覧そのものに表れています。

$ ls
apps  cloudbuild.yaml  deployment          Dockerfile  README.md     rebar.lock  Tiltfile
ci    config           docker-compose.yml  LICENSE     rebar.config  test

マルチアプリプロジェクトでは、トップレベルの各アプリケーションのソースコードをまとめて置くapps/ディレクトリが必要です(libs/ディレクトリでも構いません)。rebar.configファイルとともにapps/libs/が見えたら、まずアンブレラプロジェクトだと考えて間違いありません。

そのディレクトリの中を見れば、プロジェクトの全体像がかなりつかめます。

$ ls apps
service_discovery       service_discovery_http      service_discovery_storage
service_discovery_grpc  service_discovery_postgres

プロジェクト内のOTPアプリケーションの命名に厳格なルールはありませんが、経験上うまくいくと分かっているのは、何らかの名前空間を必ず使うことです。VMにはその種の仕組みが備わっていないためです。先ほどの一覧では、service_discoveryというアプリが1つと、service_discovery_<何か>という形のアプリが複数あることがわかります。

これは、これらのアプリケーションがすべて関連していることを示しています。中心となるのはおそらくservice_discoveryで、他はそれを補うものです。_grpc_httpのアプリはおそらくフロントエンドやクライアントライブラリで(実際その通りです)、_storageは明らかにストレージを扱い、_postgresもおそらく何らかのストレージを扱います。

コードを掘り下げると、_storageは汎用的なストレージAPI(その.app.srcファイルのdescriptionフィールドを見るとわかります)であり、_postgresは拡張性を意識して書かれた具体的な実装の1つだとわかります。

このディレクトリ内のアプリケーションの名前は、まったく異なっていても構いません。たとえば認証ライブラリを自作するかベンダリングするなら、authlibauthlib_httpのようなアプリケーションを置くこともできます。

こうしたパターンがあるからこそ、大規模プロジェクトでは名前空間が役に立ちます。プロジェクトは成長するにつれてAPIやエンドポイント、クライアント、それらとやり取りする手段がどんどん増えていきます。手作業での名前空間管理は書く際には面倒でも、必要な場面で明確な切り分けを可能にしてくれます。

こうしたマルチアプリ用ディレクトリの有無が、単一アプリプロジェクトとマルチアプリプロジェクトの最大の構造的な違いです。もう1つ小さな違いとして、rebar.configtestディレクトリを複数持てる点があります。service_discoveryの場合、トップレベルにrebar.configファイルとtest/ディレクトリがありますが、個々のアプリケーションの中を見るとさらに増えます。

$ ls apps/service_discovery*
apps/service_discovery:
src

apps/service_discovery_grpc:
proto  rebar.config  src

apps/service_discovery_http:
src

apps/service_discovery_postgres:
priv  src

apps/service_discovery_storage:
src

どのアプリも、OTPアプリケーションとして最低限必要なsrc/ディレクトリは持っていますが、それに加えて独自のテストディレクトリやpriv/ディレクトリ、その他必要なものや新しいrebar.configファイルを自由に追加できます。

service_discovery_grpcの設定を見てみましょう。

{grpc, [{protos, "proto"},
        {gpb_opts, [{descriptor, true},
                    {module_name_prefix, "sdg_"},
                    {module_name_suffix, "_pb"}]}]}.

この設定はトップレベルのrebar.configで宣言されているgrpcbox_plugin専用のものですが、このプラグインが実際に必要なOTPアプリケーションだけで実行されるようにしています。

つまり、アプリケーションの構築と構成には多層的な力学が働きます。

  • トップレベルにあるものはすべて、トップレベルの全アプリケーション(テストも含む)で共有されます。
  • 各アプリケーションは、ディレクトリやテストファイルのローカル版を作ることで、より個別の事情に対応できます。

これにはいくつか例外もあります。たとえば依存関係はプロジェクト全体で共有されます。各トップレベルアプリが独自の依存関係を指定できるとはいえ、ErlangとRebar3は(ライブコードアップグレードは別として)一度に1つのバージョンのライブラリしか読み込めません。つまり依存関係解決では、競合するバージョンの中から1つの勝者となるアプリケーションが選ばれるため、共有されるものとして扱うのが理にかなっています。逆に、priv/のようなディレクトリは1つのアプリケーション専用のファイルのためのものであり、誰でも中身を読めるとはいえ、同じディレクトリを複数のアプリケーションが所有することはできません。

もう1つの細かな注意点はフックに関するものです。単一のOTPアプリケーション向けとプロジェクト全体向けの両方で定義できるフックがあります。たとえばトップレベルで定義されたcompileフックは全アプリケーションのビルドの前後に実行されますが、apps/内の個々のアプリケーションで定義された同じフックは、そのアプリケーション1つのコンパイル前後にしか実行されません。

移行すべきか

数十もの単一アプリリポジトリと比べたときのアンブレラプロジェクトの主な利点は、開発の大部分を1か所に集約でき、多くのツールに共通の設定を一括で適用しやすくなることです。レビューや移行、履歴もすべて1つのリポジトリで追いやすくなります。これだけ聞くと単純な決断のように思えますが、実際にはそう簡単ではありません。

マルチアプリプロジェクトへの移行には大きな注意点が2つあります。1つ目は、Rebar3を使うErlangプロジェクトの依存関係は、少なくとも許可する新機能が追加されるまでは、単一アプリのリポジトリでなければならないという点です。複数のプロジェクトにまたがって使うライブラリを書くつもりなら、開発もすべてそのマルチアプリプロジェクトへ移さない限り、マルチアプリプロジェクトの中で書いてもうまくいきません。

2つ目の注意点は、開発のすべてを1つの大きなマルチアプリプロジェクトへ移すこと自体もそう単純ではないという点です。多くのツールは、1つのプロジェクトにつきリリースが1つ(あっても数個)であることを前提としており、トップレベルのアプリケーションすべてに対してコード解析や再ビルドを一括で実行することをためらいません。

つまり、そこそこの規模のリポジトリがいくつかある代わりに巨大なリポジトリが1つある状態になると、ありふれたコマンドの実行にずっと時間がかかるようになることがあります。単に、巨大なプロジェクトの方が珍しいという前提でツールが作られているからです。

Rebar3(や他のツール)がモノレポに追いつくまでは、次のように構成するとよいでしょう。

  • サービスやマイクロサービスなど、より大きなプロジェクトごとに1つのマルチアプリリポジトリを作る
  • より大きなプロジェクトをまたいで共有される共通ライブラリは個別に保守・公開し、必要な各プロジェクトに取り込む
  • サービスやWeb API、CI設定などのレイアウトと仕様を自動化するために、汎用的なプラグインに格納したテンプレートをチームで共有し、各自がグローバルにインストールする

もしマルチアプリプロジェクトの中のあるライブラリが、組織内の他の利用者にとっても役立つとわかったら、それを独立したリポジトリへ切り出して公開し、依存関係として再度取り込むのは簡単です。同様に、孤立したライブラリやフォークされたライブラリは、それぞれのプロジェクト内でローカルに保守できます。

この構造は、組織がオープンソースコードにパッチを当てたり開発したりしてから公開しようとするときにも役立ち、利用者全員を一斉に同期させる必要なく、ライブラリに変更を加えるコストを多少下げてくれます。

各プロジェクトのデプロイやCI向けのスクリプトを開発する際にも、ある程度は単純さを保てます。オープンソースのツールはおおむね問題なく動き続けます。ただし、すでにモノレポとそのためのツール群を持っている組織では、コストがより高くつきます。もう1つよくある障害は、あるプロジェクトのCIやビルドサーバーが、依存先のCIやビルドサーバーへの読み取りアクセス権を必要とすることで、これはすべての組織で当たり前に整っているとは限りません。

アプリを切り分ける

単一アプリ、マルチアプリ、モノレポのどの方針を好むにせよ、コードを扱いやすい単位にどう切り分けるかは考えなければなりません。

何を書くときであれ、これは昔から難しい問題です。関数の理想的なサイズや、モジュールやクラスがどれだけの内容を含み公開すべきか、マイクロサービスはどれくらいの大きさが適切かについて無数のブログ記事や論考があるのと同じように、OTPアプリケーションの最適なサイズについても唯一の規範となる答えはありません。

厳密なルールを示すのではなく、何がよい切り分けだと感じられるかという一種の勘に従って構成する傾向があります。これは経験の結果であって教えるのが難しいものですが、判断を単純にするために好んで問いかけているいくつかの質問を紹介します。

  • その特定の機能は、いずれ他のプロジェクトでも必要になりそうか。そうであれば、独立したOTPアプリケーションにしておくと切り出して共有しやすくなります
  • そのプロジェクト固有の関心事にとても特化したコードか。service discoveryで言えば、それはサービスの追跡に関わるものか、それとも「データの保存」のような一般的なものか。特化しているほど中心となるアプリに近づけ、一般的であるほどOTPアプリケーションとして独立させやすくなります
  • 特有の設定値やドメイン知識を必要とするか。そうであれば、その呼び出しをまとめて1つのOTPアプリケーション内の限られたモジュール群に閉じ込めておくと、他の人がよい抽象化として使いやすくなります(認証や特定のプロトコルの処理などが好例です)
  • 特定の文脈やビルドでのみ起動する様子が想像できるか。そうであれば、独立したアプリケーションにしておくと後々楽になります。よい例はヘルスチェックや監視のエンドポイントで、メインアプリに依存はしていても、そのテストや特定のビルドでは不要な場合があります

これらの質問はいずれも、業務ロジック(たいていトップレベルのリポジトリとアプリに置かれます)と、いま書いているそれ以外のコードとの間にどれだけ結合があるかを見極めやすくするための手がかりです。

こうした線引きを試すことは、構成の仕方を考えるうえでたいてい有用な作業になります。

service_discoveryを例にとると、service_discovery_storageをメインのservice_discoveryアプリケーションから独立させる強い技術的な必要性は、実のところありません。しかし、メインアプリケーションがストレージ層の詳細、それが汎用的かディスクに書き込むのか、別サービスなのか、メモリ上なのかを気にしなくてよい方が理にかなうと考えました。メインアプリはそれを気にする必要がありません。そのすべての複雑さと間接性は、狭く定義された1つのアプリケーション(service_discovery_storage)の中で扱い、特定のプラグイン(service_discovery_postgresライブラリなど)で構成できるようにしてあります。

この分離によって、メインアプリが汎用的な抽象化として呼び出す以外のところでストレージ固有の悩みに巻き込まれないことをより明確に保証できると考えました。切り替え可能なバックエンドという複雑さ自体は残りますが、コード中の1か所にはっきりとラベル付けして隔離してあるため、保守がより簡単になることを期待しています。

結局のところ、コードとリポジトリを構成し整理する最善の方法は、いまの組織の文脈においてチームが最も効果的だと感じる方法を選ぶことです。私たち著者が好むやり方は、この10年ほどの間にいくつもの組織で働いてきた中で見出した、妥当な落としどころの1つですが、いま与えられているものを全面的に受け入れるやり方ほど効果的とは限りません。

1つずつ問題を解決していきましょう。チームが初めてErlangを学んでいるところなら、その組織が既に持っているデプロイやビルドの仕組みに沿って、最も抵抗の少ない道からすべてを始めるのが理にかなうこともあります。あとでもっと快適な形に作り直すことになるとわかっていても構いません。すべての問題を一度に、しかも最初から完璧に解決しようとするのは、使えるエネルギーや時間、リソースが限られているときにはかえって非生産的になりえます。