YarnをYarn 2(berry)にアップグレードした話
stand.fm でエンジニアをしている三堀です。
弊社では、stand.fm という 音声配信プラットフォームを開発しているのですが、JavaScriptのパッケージマネージャーとして利用しているYarnのバージョンを2にアップグレードしたので、その際の手順やハマった点、アップグレードによって何が改善されたかなどを紹介していこうと思います。これからアップグレードする方の参考になれば幸いです。
Yarnとは
Yarnとはfacebook発祥のJavaScriptのパッケージマネージャーです。(現在はコミュニティに委譲しているそうです)
弊社では、このYarnを利用してモノレポによるプロジェクト管理をしています。また、workspacesという機能を使って、プロジェクト全体における依存関係の管理を行っています。
stand.fmでのyarn workspacesを用いたモノレポ構成イメージ
standfm
|___projects
| |___api
| | |___package.json
| |___app
| |___web
| |___webServer
| |___admin
| |___adminServer
| |___infra
| |___...(計25個)
|
|___package.json
|___yarn.lock
アップグレードのモチベーション
解決したかった課題: yarn install に時間がかかる・CIが失敗する
アップグレードのきっかけとなったのは、CIの実行時間が遅いかつ頻繁に失敗するという問題でした。
弊社はCIにCircleCIを採用しています。Githubにpushした際に変更があったコードに対して、
依存パッケージのinstall > lint > Flow型チェック > test実行
という順序でジョブが実行されていくのですが、yarn install にかなりの時間を要していたり、testがすべて完了せず気づいたらプロセスがキルされていたりということがありました。
原因と解決案
これらの原因として考えられたのは、膨大な量の依存パッケージがキャッシュされていたことによるCircleCIのリソースの枯渇です。
この問題が発生する際の状況を観察してみると、多くのメンバーがコードをプッシュしているときに頻繁に発生していて、しばらくしてから再度ジョブを実行し直すと成功するという状況になっていました。
Yarn 1.xでは yarn install 時に node_modules を各プロジェクトごとに生成し、そこに依存パッケージが保存されるようになりますが、これらの node_modules をすべてキャッシュするようにしていました。それゆえに、キャッシュサイズが膨大でダウンロードと展開に時間がかかっていた点がCIが失敗する原因になったのではと考えました。
また、直接的な原因ではないかもしれませんが、弊社のプロジェクトはモノレポになっているので、複数プロジェクトにまたがる変更をまとめて修正できるなど良い面がある一方、CIの実行対象は複数プロジェクトがある分増えてしまい、実行時間が増えリソース枯渇が起きやすい原因の一つになっていたかもしれません。
このCIが失敗する問題に対して解決策として上がったのがYarn 2へのアップグレードでした。
アップグレードした結果
Yarn 2へのアップグレードをして約3週間ほど経過観察をしたのち、他の開発メンバーにもいくつかフィードバックをもらいました。結論から申し上げると、解決したかった「CIが落ちる」「installなどのコマンド実行に時間がかかる」問題は解決することができました。
CIが以前のような原因で落ちなくなった
CircleCIのジョブ実行で突然プロセスがキルされて落ちるといった問題やジョブが固まるなどの問題が発生しなくなりました。
以下がCircleCIのジョブの週ごとの実行時間推移になります。こちらはmasterブランチでの実行時のデータで、作業ブランチからマージされたときのジョブになります。グラフでいうとMarch7より後がYarn 2の移行後の数値で、移行前では失敗しているジョブがいくつかありますが、移行後は失敗率が0になっています。
CIの実行時間が早くなった
落ちなくなったことだけでもありがたいですが、ジョブの実行時間も分散がかなり小さくなり、安定性が増したと感じます。以下はmasterブランチでのジョブの実行時間の推移になりますが、Yarn 2移行以前(March7より前)は95パーセンタイルの数値が上振れている箇所があるのに対して、移行後は低い数値で安定して推移していることがわかると思います。
もともと時間がかかっていた要因としては、yarn install 時にパッケージを追加・削除した場合にかなりの時間がかかっていたことが主な原因と考えられます。
Yarn 2
Yarn 2にはZero-Installsという思想のもとにPlug'n'Playという機能が新たに追加されています。それ以外にもworkspacesでのパッケージ管理が厳密になったりなどYarn 1.xと比べて多くの変更点があります。
本記事では割愛しますが、Yarn 1.xとの違いについては以下の記事がとてもわかりやすく説明されているので、ご覧いただければと思います。
・https://qiita.com/dojineko/items/6f65fde3c47aed8b6318
・https://qiita.com/mizchi/items/db776f6f8e0a76e38575
Zero-Installs
Yarnが目指す「Installすることなくアプリケーションを動かすことができる」という思想・哲学のことを示しています。これを実現するための機能の一つとして後述するPlug'n'Playなどの機能があるという理解です。
公式ドキュメントはこちらです。
Plug'n'Play(pnp)
Yarn 2の最も特徴的な機能で、既存の node_modules による依存解決の問題点を解決しています。詳細は省略しますが node_modules のファイル群が膨大であったこと、インストールの効率が悪かったことなどが問題点としてあり、それらを解決しているそうです。対応しているライブラリもあれば、まだ対応していないものもある(Flow,ReactNativeなど)ので、既存の node_modules を生成するモードを使うことも可能です。
公式ドキュメントはこちらです。
アップグレードにおいてやったこと
ここではアップグレードにおいてやったことを5つ紹介していきます。説明のためかなり簡略化していますが、実際に対応しているときは試行錯誤の繰り返しでしたし、かなり労力が必要とされました。。
また、移行に際して実際に対応すべき内容はプロジェクトによって様々だと思うので、あくまで一例として紹介させていただければと思っております。
1. Yarnのバージョンを変更
まず最新のYarnをインストールし yarn set version berry というコマンドを実行します。berryとはYarn 2のコードネーム的なものです。
こうすると .yarn ディレクトリが生成され、Yarn 2の実行ファイルが .yarn ディレクトリに配置されます。また、プロジェクトルートに .yarnrc.yml というファイルが自動生成されます。このファイルはyarnコマンド実行時の様々な設定を記述するファイルで、例えば yarn install 時のログ出力をするかどうかや、先述のpnpを有効化するかなどの設定ができます。初期状態では以下のようにYarnの実行ファイルのパスが書かれた状態で出力されます。
yarnPath: .yarn/releases/yarn-2.4.1.cjs
2. .yarnrc.yml の記述
Yarnの実行時に必要な設定ファイルです。設定の一覧はこちらから確認することができます。
今回主に設定したのは、npmレジストリを一部プライベートな場所に置いているので、そこにアクセスする際に必要となる記述を追加しました。
npmScopes:
newn-team:
npmAlwaysAuth: true
npmAuthToken: "${GITHUB_TOKEN}"
npmRegistryServer: "https://npm.pkg.github.com"
また nodeLinker というオプションがあり、これはnodeのパッケージをインストールするときの方法を設定するもので pnp と node-modules の2つが利用可能です。
pnp を指定すると、Yarn 2から導入されたPlug'n'Playという機能を利用できます。 pnp を利用すると、効率的なインストールプロセスが適用され、インストール時間の削減が期待できます。ただし、ReactNativeやFlowはまだ pnp に対応していないので、弊社では pnp は利用せず node-modules を指定しています。 pnp で管理するプロジェクトと node-modules で管理するプロジェクトを分けることも検討しましたが、プロジェクト全体の管理のしやすさと動作の安全性を考えてすべて node-modules での管理に寄せたほうが良いという判断をしました。
nodeLinker: node-modules // or pnp
そして、 nmHoistingLimits というオプションがあります。これはyarn workspacesにて依存関係の吸い上げ(依存モジュールをworkspaceのどの階層に配置するか)に関して制限を指定できるものです。指定可能なものは、none, workspaces, dependencies の3つから選択可能です。それぞれの挙動に関しては、こちらをご覧ください。
Yarn 1.xでは、package.json に noHoist というオプションで指定していましたが、Yarn 2ではこの nmHoistingLimits で指定するか package.json に installConfig というオプションで指定するのが推奨となっています。
nmHoistingLimits: workspaces
他にもログ出力制御ができる logFilter オプションやパッケージのinstall時にタイムアウトを設定できる httpTimeout オプションなど様々な設定が可能なので詳しくは公式ドキュメントを参照いただければと思います。
3. yarn --production などの記述を書き換える
yarn install コマンドを叩くときに devDependencies で定義しているモジュール以外をインストールしたいときはYarn 1.xでは yarn install --production という形でオプションを付けて実行していました。
Yarn 2ではこちらのコマンドが使えなくなり、代わりにworkspace-toolsというプラグインを入れて、 yarn workspaces focus --all --production というコマンドを利用するようにしました。
$ yarn plugin import workspace-tools //プラグインのインストール
$ yarn workspaces focus --all --production
4. CIのキャッシュ対象を node_modules → .yarn/cache ディレクトリに変更
弊社ではプッシュ時のコードに対してlint、flow、testによるチェックをCircleCI上で行っているのですが yarn install 時の時間短縮のためにworkspace内のプロジェクト全ての node_modules をキャッシュしていました。これが合計4GBを超える量になってしまっていて、キャッシュされているデータをダウンロード及び展開する際にオーバヘッドが発生していました。
Yarn 2では、この依存パッケージの管理方法が変わり大元となるパッケージの情報は .yarn/cache というディレクトリにzipファイルで格納されます。この .yarn/cache ディレクトリ内のファイルは以前の node_modules で格納されているものより遥かに少ない(弊社のプロジェクトでは468MB)のでキャッシュの展開時のオーバーヘッドを削減することができました。
また、先述した .yarnrc.yml ファイルにて nodeLinker: node-modules を指定すれば yarn install 時に node_modules ディレクトリを生成をすることもできるので、Yarn 1.xでのパッケージ管理方法を踏襲しつつ yarn install におけるパフォーマンスの恩恵だけを享受することができました。
5. yarn.lock ファイルの更新
Yarn 2にして yarn install を実行すると yarn.lock ファイルがYarn 1.xの形式からYarn 2の形式に自動的に書き換わります。形式が全く変わってしまうので、アップデートの作業をしている途中に別ブランチでライブラリのバージョンアップなどが行われると yarn.lock ファイルのコンフリクト解消が困難になります。
そのため yarn.lock ファイルのコミットは最後までせずに、最後レビューしてもらうときにコミットをするようにしました。ただ、その最中もバージョンが書き換わったりする場合があったので、その際は yarn.lock ファイルをYarn 1.xのときのものにrevertした上で変更を取り込むようにして、コンフリクトを解消するようにしていました。
デバッグ時に役に立つErrorCode
これは余談ですが yarn install を実行するときのログ出力で、以下のように出力ごとにErrorCodeが表示されるようになりました。このErrorCodeはエラーの原因ごとに割り振られているので、デバッグする時に便利です。
ハマったところ&未解決問題
node_modules内のモジュールのプロジェクトをビルドしている箇所が失敗する
Yarn 2から package.json の dependencies に定義されていないパッケージは、実行するコードにおいては利用できないようになりました。そのため package.json に post install コマンドとして $ cd node_modules/hoge && yarn install のような依存モジュールをビルドするようなことができなくなりました。
テストが失敗する
Reactのコンポーネントのテストなどがなぜか失敗するようになりました。原因はいまのところ不明で、jestの実行時のプロセス数を制限したりなど試みましたが、まだ解決できていません。
yarn licenses generate-disclaimer が使えなくなった
Yarn 1.xでは yarn licenses generate-disclaimer というコマンドが用意されており、こちらを使うとアプリで利用しているOSSライセンス利用の条項文を出力できます。
こちらのコマンドがYarn 2ではデフォルトで利用できなくなりました。代わりとしてyarn-plugin-licensesというpluginを作っている人がいるようですが、エラーが出たのでまだ使える安定して使える状態ではなさそうです。
dependabotが動かなくなった
ライブラリの更新をチェックするためにdependabotを導入していたのですが、以下のissueでもあるようにまだYarn 2をサポートしていないようなので、一旦無効にしました。
まとめ
今回はCIを安定化、高速化したいという目的で、Yarn 2に移行した話を紹介させていただきました。
Yarn 2に移行したことによって以下のメリット・デメリットがありました。
メリット
・install時の効率が良くなり速度が向上した。
・キャッシュの仕組みが変わったことによって、CircleCIでキャッシュしていたデータを大幅に削減することができ安定性が向上した。
デメリット
・dependabotが動かなくなった。(対応待ち)
・テストが失敗するようになった(調査中)
・node_modules 内での yarn install ができなくなった
結果的には目的となるCIの安定化・高速化を実現することができたので、課題の解決はできたかなと思いますが犠牲にした部分もありました。それらについては今後ライブラリの対応状況をウォッチするなどして対応していきたいと思っています。
宣伝
最後に会社の宣伝なんですが、いまstand.fmではエンジニアを熱狂的に募集しているので、よかったら採⽤ページをご覧ください。
技術イベントも定期開催しています。ぜひご参加ください。
最後までご覧いただきありがとうございました。
参考にさせていただいたサイト
・https://yarnpkg.com/
・https://qiita.com/dojineko/items/6f65fde3c47aed8b6318
・https://qiita.com/mizchi/items/db776f6f8e0a76e38575
この記事が気に入ったらサポートをしてみませんか?