しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

NuGet のロックファイルと CI でのパッケージキャッシュ

基本的に NuGet に関しては CI でのパッケージのキャッシュがあまり効果的ではないのですが、推奨設定での使い方をちゃんと試しておこうと思ったので残します。

GitHub Actions や Azure Pipelines には NuGet 向けのサンプル定義が用意されています。中身はほぼ一緒なのでサンプル自体も非常に似通ったものになっています。

今回は GitHub Actions で試すので actions/cache@v2 の方のサンプルを持ってきましたが、見慣れない packages.lock.json というファイルのハッシュを使ってキャッシュキーを生成しています。

- uses: actions/cache@v2
  with:
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
    restore-keys: |
      ${{ runner.os }}-nuget-

ぶっちゃけ今回はこの packages.lock.json に関して試すのがメインです。ファイル名の通り NuGet パッケージの依存関係ロックファイルなのですが、使い方や説明を見た記憶があまりありません。

基本的には NuGet 公式ブログの以下の記事が実質的なリファレンスという感じです。

最近のパッケージマネージャではロックファイルで依存関係のバージョンを固定化して、CI などで利用するのが一般的になっているので、それを NuGet でも利用可能にしたという話です。

特に Floating Version を使っている場合には、ビルドのタイミングでバージョンが変わる可能性もあるので、ロックファイルを使って CI ではバージョンを固定しつつも、開発中には自動で最新バージョンを使うという流れは重要になります。

Docs にも一部だけロックファイルについての説明があるので、上の記事と併せて読みましょう。

メリットはブログにもいろいろと書いてありますが、全てのパッケージ依存関係が書き込まれているので、CI での利用を考えるとキャッシュキーとして最適という訳です。ちなみに csproj はパッケージ以外の情報も含まれるため、キャッシュキーとしては若干非効率です。

早速ロックファイルを有効化してみますが、いくつか方法がある内の csproj にプロパティを追加する方法を選びました。単純に RestorePackagesWithLockFile を追加するとパッケージの復元・更新時にロックファイルが生成されるようになります。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <AzureFunctionsVersion>v3</AzureFunctionsVersion>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Sdk.Functions" Version="3.0.11" />
  </ItemGroup>
  <ItemGroup>
    <None Update="host.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="local.settings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      <CopyToPublishDirectory>Never</CopyToPublishDirectory>
    </None>
  </ItemGroup>
</Project>

csproj に追加しておくと、これまでと同じようにパッケージの追加や更新の時にロックファイルも自動的に更新されるので、特に意識する必要が無く便利です。

最近の .NET CLI はビルド時にパッケージの復元も自動的に行うので、プロパティを追加してビルドすると以下のようにプロジェクトのルートに packages.lock.json が生成されます。

ロックファイルはちゃんとリポジトリに含めてコミットするようにしましょう。Visual Studio 向けの .gitignore には含まれていないので勝手に入るはずですが、忘れないようにします。

実際にロックファイルを使ってパッケージの復元を行うには dotnet restore--locked-mode スイッチを渡すのが簡単です。これは Node.js で言うところの npm ci に相当するコマンドになります。

# restore と build を分ける
dotnet restore --locked-mode
dotnet build --no-restore

# build + MSBuild パラメータを渡す
dotnet build -p:RestoreLockedMode=true

依存関係の再評価をロックファイルを使う場合は行う必要が無いので、復元のパフォーマンスが向上するのではと期待しましたが、自分が試した範囲では特に変化を感じませんでした。インストールされたパッケージの数にも寄るかもしれませんが、少し残念でした。

ロックファイルを有効にすると NuGet の Fallback Folder 周りでエラーが出ることもありますが、その場合は DisableImplicitNuGetFallbackFolder プロパティを追加して Fallback Folder を使わないようにします。

この辺りのコマンドを使い分けるのが面倒な場合は Directory.Build.props を以下のような内容で用意しておくと、csproj への設定も既存の Workflow への変更もなしにロックファイルを有効化できます。

<Project>

  <PropertyGroup>
    <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
    <RestoreLockedMode Condition="'$(CI)' == 'true'">true</RestoreLockedMode>
  </PropertyGroup>

</Project>

GitHub Actions 向けになっているので CI 環境変数を見るようにしていますが、TF_BUILD を見るように変えれば Azure Pipelines 向けにも出来ます。

実際に GitHub Actions で NuGet パッケージのキャッシュを試してみましたが、予想通りアプリケーションに左右される部分と Agent に依存する部分が多すぎるので、明確な差は出ませんでした。早い時もあれば遅い時もあるので、大体は外部要因でしょう。

もう少し規模の大きいプロジェクトの場合は差が出るかもしれませんが、当然キャッシュの保存と復元にもコストがかかるので、その辺りを上手く吸収できるかは怪しいです。ビルドのパフォーマンス改善という面ではあまり期待できないでしょう。

ただしデフォルトだと常に NuGet からパッケージをダウンロードしているので、NuGet に障害が発生すると CI が通らなくなりますが、パッケージのキャッシュを行うと構成を変えない限りは影響を受けません。

NuGet の障害対策で有効化するのは要件によってはアリだと思います。事前に検証はちゃんとしましょう。