しばやん雑記

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

ASP.NET Core / Azure Functions アプリケーションの発行後サイズを削減する

Azure Storage を使った Run From Package で公開している Azure Functions 向けパッケージの転送量が、最近になって急激に増えたのでパッケージのサイズを削減して、Azure Functions で実行するための最適化を行ったのでメモとして残します。

何故マネージコード中心のアプリケーションでパッケージサイズが増えたのか調べると、使っている NuGet パッケージがネイティブ向けライブラリを含んでいるのが大きな原因でした。

.NET Core では単純に dotnet publish を実行すると、ポータブルな形でアプリケーションが発行されます。ここでいうポータブルというのは .NET Core がインストールされている場合、ファイルだけ持っていけば OS や CPU 関係なく動くという状態です。Framework Dependent モードでの発行と呼ばれます。

Framework Dependent で発行されたアプリケーションはどのプラットフォームで実行されるのか分からないため、出力には全てのプラットフォームで必要なライブラリが含まれています。

本筋とは関係ないですが、各プラットフォーム向けのライブラリを含んだ NuGet パッケージの作成方法はぼんぷろ師匠が昔に書いていたので、仕組みが気になる方はこっちを参照してください。

プラットフォーム依存のライブラリは bin\runtimes の中に RID 単位で入っています。例として Acmebot を発行して試すと以下のような構造です。このアプリケーションは Windows の Azure Functions 向けなので Linux や OS X 向けのライブラリは完全に不要です。

ましてや ARM 向けのライブラリは完全に発行後は無駄な容量になっています。

f:id:shiba-yan:20201218024840p:plain

.NET Core の開発が進むにつれ対応するプラットフォームが増えた結果、インストールする NuGet パッケージによっては無視できないサイズになりつつあります。

RID を指定して発行されたアプリケーションは、ビルド時にプラットフォームが限定出来るので必要最小限のライブラリのみが含まれますが、同時に Self Contained をオフにしないとランタイム自体が含まれてしまうので、明示的にオフにします。

# Windows x86 向け
dotnet publish -c Release -r win-x86 --self-contained false

# Windows x64 向け
dotnet publish -c Release -r win-x86 --self-contained false

今回の例では zip 圧縮後で 4.6MB から 2.9MB に削減出来たので、発行後アプリケーションの 40% 近くが不要なライブラリで占められていたことになります。毎回不要なものを一緒にデプロイするのはストレージやトラフィックの無駄でしかないです。

微妙に関係ない話題ですが dotnet publish 時に zip を同時に作成することも出来るので、複数アプリケーションをデプロイする場合には便利です。詳細は以下のエントリを参照してください。

GitHub Actions や Azure Pipelines を使ってビルドする場合には、上のコマンドを組み込めば発行後サイズを削減できます。ちなみに Ubuntu 上でビルドしたとしても win-x86win-x64 向けに発行出来ます。*1

RID の一覧は何回か紹介した気がしますが、以下に全て記載されています。ASP.NET Core や Azure Functions 向けで使う場合は win-x86 / win-x64 / linux-x64 といったところでしょう。

Azure Functions の Consumption Plan の場合はスペック的に 64bit で動かすメリットが無いので、基本的には win-x86 を使うのが正解です。なので固定してもデメリットがありません。

Acmebot には 12/10 ぐらいにこの対応を入れてリリースしたところ、転送量が大幅に削減できました。リクエスト数はほぼ変わらなかったので、純粋にパッケージサイズの削減が効いていることになります。

f:id:shiba-yan:20201222011643p:plain

今回は利用していたパッケージの数が少なかったので 1MB ちょいの削減になりましたが、もう少し規模の大きなアプリケーションでライブラリをたくさん使っていた場合にはかなりの差がつく可能性があります。

GitHub Actions や Azure Pipelines といった CI SaaS では Artifacts をアップロードする処理が大体挟まるので、パッケージサイズを小さく保てればビルド時間の短縮にもつながります。もちろん App Service にデプロイする時にも差は出ますし、実際に出ました。

Visual Studio を使ったデプロイ向け設定

ここまでは手動や CI を使う前提でコマンドベースで書いてきましたが、Visual Studio を使ったデプロイ時にもターゲットのプラットフォームを指定してビルド可能です。

Visual Studio で作成された App Service や Azure Functions のプロファイル設定を開くと、ビルドに関連する設定を選べるようになっています。重要なのは配置モードとターゲットランタイムです。

f:id:shiba-yan:20201222011914p:plain

デフォルトではそれぞれフレームワーク依存と移植可能即ちポータブルになっていますが、ターゲットランタイムを実際にデプロイする先のプラットフォームに合わせた RID を選択するだけです。

ARM64 向けの RID は選べませんが、App Service と Azure Functions では使わないので問題ないです。

f:id:shiba-yan:20201222012024p:plain

この設定を保存してデプロイを実行すると、指定した RID 向けのライブラリのみが含まれたパッケージが生成されて、App Service や Azure Functions にデプロイされます。

今は Visual Studio を使ったデプロイは開発の極々初期か趣味アプリぐらいでしか行わないと思いますが、検証向けとしては手軽なので一応残しておきます。

*1:ただし ReadyToRun はクロスコンパイル未対応なので無理