😃

yarnをv1からv2(Berry)へ移行する

2021/04/18に公開

yarnのv2(Berry)が登場してしばらく経ちましたが、依然v1を使い続けている方が多いかと思います。最近になってやっと問題なく移行できると判断できるようになってきたので、この度移行してみました。

yarn v1の問題点

散々各所で語られていると思いますが、簡単にyarnのv1の問題点をまとめてみます。

1. node_modulesのサイズが肥大化する

中規模程度のリポジトリでも 2GB を前後になるのは当たり前です。
そのためとにかく重いのと、Node.jsを使う複数のリポジトリで開発をしているとこの容量だけでマシンのディスク残量を消費するので
マシンにも優しくありません。

2. yarn add や yarn removeを繰り返すと頻繁に壊れる

yarn v1の一番の問題は恐らくこれです。
壊れるというのは具体的には依存関係の参照が整合性が取れなくなって、追加したコマンドの実行エラーが起きるという現象が起きます。

例えば、私は普段gRPCを使っているのでのですが、Node.jsから protoc を簡単に利用できるモジュールとして grpc-tools というモジュールがあります。
これは要するに protoc をわざわざダウンロードしなくても、このモジュールを追加することで
protoc同等の grpc_tools_node_protoc というコマンドが使えるようになるというものです。
ただ、これが他の依存関係の yarn add や yarn remove を行うと、モジュール間の参照がおかしくなって実行エラーが発生するようになって壊れます。

これは node_modules を消して、再度 yarn install を行うと直るのでそれほど大きな問題ではないのですが、
前述のとおり node_modules はリポジトリによっては2GBを超えるケースもあり、そのサイズを都度消して再インストールするのは割と時間を取られてしまいます。

yarn v2(Berry)への移行

v1の問題点をおさらいしたところで、v2(Berry)へ移行してみたいと思います。

1. v2(Berry)の利用開始設定

v2を利用したいリポジトリ配下(package.jsonのあるディレクトリ)で、以下のコマンドを実行して v2(Berry) の利用開始設定を行います。

yarn set version berry

このコマンドを実行すると、同一ディレクトリ配下に .yarn ディレクトリと .yarnrc.yml ファイルが作成されます。
.gitignore に以下の設定を追加しておきます。

# yarn (using Zero-Installs)
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

yarn v2では依存関係のキャッシュをリポジトリ内に一緒にコミット可能にする Zero-Installs という手法が提案されています。
キャッシュは小さなZIPファイルが1ファイルごと1MBにも満たないくらい細分化されており、
合計しても数百MBのサイズしかないので、リポジトリへ一緒にコミットしても多くのケースでは問題にはならなくなっています。
但し、依存関係の規模によっては500MBを超えることもあり、LFSを使わないとリポジトリが肥大化して重くなるのでLFSの設定はした方が良さそうです。

また、これはあくまでオプションなので、数百MBくらいとはいえ大きなサイズなので気になるという方のために
Zero-Installsを利用しないことも選択出来ます。
その場合、 .gitignore は以下の通りに設定してください。

# yarn (not using Zero-Installs)
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*

詳しくは公式の方でも説明されているので参照してみてください。

2. node_modules を利用する設定を追加する

私がv2(Berry)についてまだ移行できないと思ってずっと移行していなかったのは、 node_modulesがデフォルトだと生成されない からでした。

Node.jsのモジュールの中にはこの node_modules ディレクトリがあることを前提に作られているものも多く、例えば絶対パスを使用する上でよく利用される tsconfig-pathsmodule-alias を使うケースでは特に問題になります。

nodeやts-nodeの -r オプションで上記モジュールを指定すると思いますが、
そのときに指定されたモジュールは node_modules ディレクトリを探しに行きます。
そのため、node_modules が存在しないと実行エラーになるのです。

この問題への対処としては、v1のときと同様に node_modules ディレクトリを利用するオプションを有効にする必要があります。
.yarnrc.yml ファイルを開き、以下の1行を追加してください。

nodeLinker: node-modules

※上記はアンダースコア(node_modules)ではなく ハイフン区切り(node-modules) で指定しないと動きません。私はこれでちょっとだけハマりました…

これで yarn install を行うとv1のときのように node_moduels ディレクトリが作成されるようになります。
ファイルサイズはv1よりはずっと軽くなってます。
.gitignoreによる除外設定は依然必要になりますので、そのままにしておいてください。

3. プラグインを追加する

v2(Berry)はいくつかの機能はプラグインとして分離されています。
例えばv1でいう yarn outdated のように、どの依存関係がアップデートされているかを確認するようなオプションがデフォルトだと利用できません。
そのため、v1のときに利用していた一部機能についてはプラグインで追加する必要があります。

interactive-tools

v2(Berry)では interactive-tools というプラグインを追加し、 yarn upgrade-interactive というコマンドを使えるようにすることで、 outdated でやりたかったアップデートされたモジュール検知およびアップデートが実現できるため、これを追加します。

yarn plugin import interactive-tools

これで yarn upgrade-interactive コマンドが利用可能になります。
importしたプラグインは .yarn/plugins ディレクトリへ追加されるため、一緒にコミットします。

workspace-tools

Dockerfileでマルチステージビルドを行う際に、サイズ削減のために dependencies にある依存関係のみをインストールして、devDependenciesはインストールしないようにするケースがあるかと思います。

yarnには npm でいう prune コマンドがないのですが、 v1 では以下のコマンドで同等のことが実現できました。

yarn --production --ignore-scripts --prefer-offline

ただし、v2にはこれに相当するコマンドがないため、
workspace-tools プラグインを追加する必要があります。
このプラグインは実際はその名の通り workspace 機能を実現するプラグインなのですが、
v1の --production 相当の機能も提供してくれます。

# プラグインの追加
yarn plugin import workspace-tools

# dependenciesのみのインストール
yarn workspaces focus --all --production

Dockerfileのビルド移行例

Dockefile のビルド移行例を紹介します。
Dockefileでv2(Berry)を利用する際は、以下のように利用します。
(Next.jsプロジェクトの例です)

# ------------------------------
# Builder stage
# ------------------------------
FROM node:15.14.0-stretch-slim as builder

WORKDIR /var/opt/app
COPY package.json yarn.lock ./
COPY . .
RUN yarn --immutable --immutable-cache \
  && yarn build \
  && yarn workspaces focus --all --production

# ------------------------------
# Execution image
# ------------------------------
FROM node:15.14.0-stretch-slim

WORKDIR /var/opt/app
RUN apt-get update \
  && apt-get install -y --no-install-recommends ca-certificates apt-transport-https curl rsync lsof procps \
  && apt-get clean \
  && rm -rf /var/lib/apt/lists/* \
  && rm -rf /usr/share/doc /usr/share/man /var/log/* /tmp/*

COPY --from=builder /var/opt/app/node_modules ./node_modules
COPY --from=builder /var/opt/app/package.json ./
COPY --from=builder /var/opt/app/.yarn ./.yarn
COPY --from=builder /var/opt/app/.yarnrc.yml ./
COPY --from=builder /var/opt/app/yarn.lock ./
COPY --from=builder /var/opt/app/next.config.js ./
COPY --from=builder /var/opt/app/.next ./.next
COPY --from=builder /var/opt/app/dist ./dist
COPY --from=builder /var/opt/app/public ./public

CMD ["./docker-entrypoint.sh"]

インストール部分について解説していきます。

v1のときは yarn.lock に差分が出るような意図しない変更があった際には、
yarn install時にエラーになるように以下のようなオプション指定をしていたと思います。

yarn --frozen-lockfile --check-files

v2(Berry)では以下のようになります。

# (Zero-Installsを利用する場合)
yarn --immutable --immutable-cache

# (Zero-Installsを利用しない場合)
yarn --immutable

そのため、まずこのコマンドの実行を行います。
Zero-Installs のときは yarn install は不要と思われる方がいるかもしれませんが、
node_modules ディレクトリを使う場合は話は別です。
yarn install コマンドを使うことで node_modules ディレクトリが生成されるので、v1のときと同様に実行する必要があります。
(キャッシュが既にある場合は実行は数秒で終了します)

次の yarn build は Next.js のビルドになります。
ビルド時は devDependencies も含めた全ての依存関係が必要になりますので、直前で yarn install をしています。

次の yarn workspaces focus --all --production コマンドは
devDependencies の依存関係を除くコマンドになります。
そのため、マルチステージビルドの際は、ビルド後にアプリケーション起動に必要最低限な依存関係のみを保持し、起動時には不要な依存関係を削除するようにしています。

v1と比較して実際に使えるかどうか

以上の手順でv2(Berry)を使えるようになりますが、v1と比較して実際に使えるかどうか(移行する必要性があるかどうか)という点について考えてみます。

インストール速度

インストール速度については、v2(Berry)はキャッシュなしの場合だと基本的に v1 より遅いです。
CIの実行時間にも影響するため、キャッシュを上手く活用できないケースだと速度については v1 を選んだ方が良いということになります。
Zero-Installsを使っているのであれば当然速いのですが、容量によってはLFSを使わなければいけないので、利用するかどうかの判断が難しいところです。
但し、例えばGitHub ActionsやCircleCIを使っているケースであれば node_modules を直接キャッシュするよりも、.yarn/cache ディレクトリをキャッシュした方がキャッシュ容量が小さいためトータルのCI実行時間としては速くなるケースがあります。
そのため、利用しているCI環境に応じて上手くキャッシュを活用できそうであれば、v2を選んでも良さそうです。

npm-scriptsの機能の違い

npm-scriptsの機能として、 タスク名の先頭に pre や post を付けると、その pre や post が付いていない名前のタスクを実行したときに、処理を事前or事後実行させることが出来ます。
但し、v2(Berry)ではこの機能が使えなくなっており、postinstall など一部の機能しかサポートされていません。
他にも、githubリポジトリを直接 dependencies へ指定してインストールしているケースだと、prepare などのスクリプト機能がインストール時に動作しないため、prepare を前提としているようなTypeScriptのリポジトリを直接指定してインストールしているケースでは問題になるケースがあります。
この辺りは現状代替手段がなく、置き換えの可能性があるため注意する必要があります。

まとめ

以上でv1からv2(Berry)への移行が出来ました。
v1とv2は利用できるコマンドが異なっているので、最初は戸惑うポイントが多く、またキャッシュなしのケースでは v1 に速度も劣るため正直無理に移行する必要はないかと思います。
ただ、v1の欠点が解消されていたり、Zero-Installsの仕組みも導入されていて、v1よりは使いやすくなっていますので、必要に応じて移行を検討してみてください。

Discussion