Yarnを使うとAzure FunctionsのGitHub ActionsからのZipデプロイが檄遅になるのを回避する

3 min read読了の目安(約2700字

npmとYarnでファイルのタイムスタンプの扱いが違うとかとんでもない罠だ。

問題

Azure FunctionsにJavaScript(TypeScript)のコードをGitHub ActionsでAzure/functions-actionアクションを使ってZipデプロイを行なっていました。
プロジェクトは func init --typescript で生成した構成ほぼそのまま。
パッケージマネージャーをnpmからYarnに変えたところ、数分で済んでいたデプロイが数十分かかるようになってしまいました。

原因

Azure/functions-actionアクションを使ってデプロイする場合、node_modulesディレクトリごとZipで固めてデプロイ先の環境に展開しなおします。
ですが、毎回全ファイルをコピーしているわけではなく、タイムスタンプが更新されていなければスキップするようになっています。

Efficient file copy: Files will only be copied if their timestamps don't match what is already deployed. Generating a zip using a build process that caches outputs can result in faster deployments.

https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url#comparison-with-zip-api

ここで、npmとYarnの動作の違いがネックになってきます。

npm

npmはnode_modulesの下に展開されるファイルのタイムスタンプを全て 1985-10-26T08:15:00.000Z に固定しています。
これは、タイムスタンプの違いによってファイルのハッシュ値が異なってしまい発生する問題を回避するためだそうです。

https://github.com/npm/npm/commit/58d2aa58d5f9c4db49f57a5f33952b3106778669

Yarn

一方、Yarnではnode_modulesの下のファイルのタイムスタンプは、単純にインストールを行なった日時、またはローカルキャッシュされた日時になります。
つまり、GitHub Actions(またはその他のCI)でYarnのキャッシュを持ち回すようにしていない場合、常にCIが走った日時のタイムスタンプでZipデプロイが行われることになります。

npmでも一番最初のデプロイでは全ファイルのコピーが行われていましたが、当時は依存パッケージも少なく気にならなかったので気付いていなかっただけのようです。

解決策

npmに戻してしまうのも手段の一つです。
しかし、やんごとなき理由でパッケージマネージャーをYarnにせざるを得ないこともあるので、Yarnを使う前提の回避方法が必要です。[1]

キャッシュによって問題を回避する方法と、抜本的にAzure Functionsの使い方を変えてしまう方法の2種類があります。

キャッシュする

Yarnではnode_modulesの下のファイルのタイムスタンプはキャッシュに依存するので、素直にGitHub Actions上でYarnのキャッシュを保持するようにしましょう。

公式のcacheアクションのドキュメントにYarnの場合のサンプルが載っているので参考にします。

https://github.com/actions/cache/blob/main/examples.md#node---yarn

GitHub Actionsの設定ファイルは、例えば次のような形になるはずです。

.github/workflows/deploy.yml の一部
# 関係するstepsだけ抜粋
- name: Get yarn cache directory path
  id: yarn-cache-dir-path
  run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
  id: yarn-cache
  with:
    path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
    key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
    restore-keys: |
      ${{ runner.os }}-yarn-
- run: yarn install --frozen-lockfile
- run: yarn run build
- run: yarn install --frozen-lockfile --production
- name: Deploy
  uses: Azure/functions-action@v1
  id: fa-foobar
  with:
    app-name: func-foobar
    publish-profile: ${{ secrets.FOOBAR }}

Run From Package

Zipパッケージを環境上に展開するためファイルコピーに関連する問題が発生してしまっています。
そこで、Zipパッケージをそのまま実行ファイルにする方法が用意されています。

https://docs.microsoft.com/ja-jp/azure/azure-functions/run-functions-from-deployment-package

むしろ今Azure Functionsを使うならRun From Packageを使うのが安牌なのですが、手が追いついてませんでした😓
既にZipデプロイをしているなら、よほど特殊な事をしていない限りはRun From Packageへの移行はすんなりいきます。

脚注
  1. 🍏はnpmでいいじゃん派です。 ↩︎