🦔

yarn workspacesのモノレポをHerokuにデプロイする

2023/04/19に公開

yarn workspacesのモノレポで、Webアプリ・モバイルアプリ・APIを開発しています。このうちAPIをHerokuにデプロイしたので、調べたことや手順をまとめてみます。

記事の要約

  • Herokuにモノレポのサブディレクトリをデプロイする場合は、heroku-buildpack-monorepoを使う
  • yarn workspaceはロックファイルがプロジェクトルートにあるため、heroku-buildpack-monorepoを使うとパッケージのバージョンが固定できなくなる。そのため、普通のNode.jsのbuildpackを使う。
  • Node.jsのbuildpackはyarn installで全ての依存をインストールするため、インストールに時間がかかったり、ビルドサイズが大きくなる。yarn berryにアップグレードすると、yarn workspaces focusを使って一部の依存のみをインストールできる。

Herokuのbuildpackとは何か

buildpackは、アプリケーションコードをslug(ビルドの成果物)に変換するためのスクリプトです。buildpackでは、依存関係の取得やコンパイルなどを行います。

buildpack | Heroku Dev Center

Herokuへのモノレポのデプロイ方法

Railwayのmonorepoの分類が分かりやすかったので、こちらの定義を使用します。

Isolated Monorepo
A repository that contains components that are completely isolated to the directory they are contained in (eg. JS frontend and Python backend)

Shared Monorepo
A repository that contains components that share code or configuration from the root directory (eg. Yarn workspace or Lerna project)

Isolated Monorepoの場合

heroku-buildpack-monorepoを使います。このbuildpackは、環境変数で指定したディレクトリを持ち上げてビルド対象にします。

# 該当の処理
STAGE="$(mktemp -d)"
(
    mv "${BUILD_DIR}/${APP_BASE}" "${STAGE}" &&
    rm -rf "${BUILD_DIR}"/* &&
    mv "${STAGE}/$(basename "$APP_BASE")"/* "${BUILD_DIR}"
)

Shared Monorepoの場合

heroku-buildpack-multi-procfileを使います。このbuildpackは、指定したProcfile(とそのディレクトリにあるapp.json)をビルドディレクトリに移動します。

yarn workspacesを使う場合はこのパターンに該当します。しかし、単一のアプリをデプロイするのであればルートにProcfileを置くだけで問題ありません。そのため、今回はこちらのbuildpackは使いませんでした。

yarn workspacesのモノレポのデプロイ

それでは実際にデプロイしてみます。サンプルとして、client/server/があるアプリを使います。コードはこちらにあります。

https://github.com/tekihei2317/yarn-trpc-express-sample

デプロイの詳細は長くなるため省略しますが、次のような手順で進めました。

  • Review Appsを使うためにはパイプラインが必要なため、パイプラインを作成する
  • パイプラインにproduction用のアプリを作成する
  • パイプラインをGitHubに連携する
  • パイプラインのReview Appsを有効化する
  • app.jsonを書く。Node.jsのbuildpackの設定だけ書きました。
  • PRを作成し、それに対応するレビューアプリがデプロイされることを確認する
  • PRをマージして、productionのアプリのデプロイを実行する

ビルドサイズを小さくする

上記の方法で無事にデプロイすることができました。しかし、全てのアプリケーションの依存をインストールしているため、インストールに時間がかかったり、ビルドサイズが肥大化してしまいました。

対策として、デプロイするアプリに必要な依存のみをインストールすることが考えられます。yarn berry(yarn >= v2)では、yarn workspaces focusで、指定したワークスペースの依存のみをインストールできます。

yarn workspaces focus | Yarn - Package Manager

そのため、yarn berryにアップグレードしてやってみることにしました。yarn berryへのアップグレードは、次の記事を参考にしました。yarn berryへのアップグレードは、PnPを使わない場合はハマりどころは多くない印象です。

https://zenn.dev/mizchi/articles/yarn-v1-to-v3

yarn workspaces focusで必要なパッケージのみをインストールする

yarn workspaces focusは、次のように使います。

# @sample/serverの依存のみをインストールする
yarn workspaces focus @sample/server

# @sample/serverの依存(devDependenciesを除く)をインストールする
yarn workspaces focus --production @sample/server

yarn berryには、パッケージのインストール戦略が複数あります。現在のプロジェクトでは次のようにしています。

  • nodeLinker: node_modules(node_modulesを使う)
  • nmHoistingLimits: workspace(依存の巻き上げはワークスペースまで)
  • zero installは不使用(.yarn/cacheはgitにコミットしない)

Herokuがyarn workspaces focusに対応してない

heroku-buildpack-nodejsは、現時点ではyarn workspaces focusに対応していません[1]

該当のIssue: Yarn 2: support focusing on a workspace for monorepo

Issueを見た感じ、yarn workspaces focusを使うためにはフォークする必要がありました。既にフォークを作成している方がいたので、その方を参考にフォークを作りました。

https://github.com/tekihei2317/heroku-buildpack-nodejs

変更点は以下の3つです。

  • インストールするとき、指定したワークスペースに対してyarn workspaces focusを実行する
  • devDependenciesを削除するとき、yarn workspaces focus --productionを実行する
  • devDependenciesの削除で再度レジストリからのfetchが実行されないように修正(devDependenciesの削除の前に、レジストリのキャッシュがmvされるのが原因。cpに変更して対応。)[2]

app.jsonにフォークしたビルドパックを指定し、YARN2_FOCUS_WORKSPACE環境設定にフォーカス対象を指定するとデプロイできます。

{
  "buildpacks": [
    {
-      "url": "heroku/nodejs"
+      "url": "https://github.com/tekihei2317/heroku-buildpack-nodejs"
    }
  ]
}

現在取り組んでいるプロジェクトでは、この方法でビルドサイズを330MB→70MBに削減できました。

まとめ

yarn workspacesのモノレポのうち、APIをHerokuにデプロイしました。通常のNode.jsのビルドパックを使うと、他のワークスペースの依存をインストールしてしまうため、デプロイに時間がかかったりビルドサイズが大きくなってしまいました。

この問題をyarn workspaces focusで改善しました。Herokuのbuildpackが未対応だったため、フォークする必要がありました。

フォークをメンテナンスし続けるのは大変そうです。yarn berryには色々なパターンがあるため、その辺りを整理したりテストを書いてからPRを出してみようと思います。

参考

脚注
  1. 他のPaaSのサポート状況も気になるので、今度試してみようと思います ↩︎

  2. <package> can't be found in the cache and will be fetched from the remote registryと表示されるものの処理はすぐに終わっていたので、fetch自体は実行されていないかもしれません ↩︎

GitHubで編集を提案

Discussion