【Azure Static Web Apps】Freeプラン × .NET 8 Isolatedで「APIが認識されない」&「100MB制限」にハマった

個人開発において、コストを抑えつつモダンな環境を構築する上で Azure Static Web Apps (Freeプラン) は非常に魅力的な選択肢です。しかし、そこに .NET 8 Isolated Functions をバックエンドとして統合しようとした際、ドキュメントの行間にある「仕様の壁」に何度も激突しました。

この記事では、GitHub Actionsでのデプロイは成功するのにAPIが404になる「サイレント障害」から始まり、立ちはだかる「100MBの壁」、そして PublishTrimmed の罠まで、解決に至るまでの試行錯誤と最終的なベストプラクティスを詳細に共有します。

環境と前提

  • フロントエンド: React (Vite)
  • バックエンド: Azure Functions (.NET 8 Isolated Worker Model)
  • ホスティング: Azure Static Web Apps (Free Plan)
  • デプロイ: GitHub Actions

第1の壁:デプロイ成功、しかしAPIは「不在」

開発当初、私はビルド時間を短縮し、プロセスを細かく制御するために、GitHub Actions側でAPIを事前ビルドし、そのアーティファクトをデプロイする構成をとっていました。

.github/workflows/azure-static-web-apps-deploy.yml の設定は以下の通りです:

skip_api_build: true
api_location: 'frontend/receiptfly-web/api' # 事前ビルドした成果物を指定

GitHub Actionsのログは「緑色(成功)」を表示しています。しかし、Azure Portalで確認すると衝撃の事実が判明しました。

現象

  • Azure Portalの「APIs」セクションを確認すると、Backend Resources: – と表示される。
  • ブラウザから /api/xxxx にアクセスしても 404 Not Found が返る。

原因:事前ビルドの罠とManaged Functions

当初、私はこれを「Freeプランでは自前ビルドが禁止されている(Bring your own API扱いになる)」と誤解していましたが、正確には以下のような挙動でした。

  1. Bring your own API (BYO API):これは既にデプロイ済みの 別のAzure Function App をStatic Web Appsにリンクする機能です。これは確かに Standardプラン以上 が必要です。
  2. Managed Functions (Freeプラン対応):Static Web Appsの一部として管理される関数です。通常はSWAがソースから自動ビルドしますが、事前ビルドした成果物をアップロードすることも技術的には可能です。

しかし、ここが落とし穴でした。

.NET Isolated Functions の場合、フォルダ構造や functions.metadata、依存ファイルの配置などが非常にシビアです。skip_api_build: true で自前ビルドした成果物を渡していましたが、SWA側がこれを「有効なManaged Functions」として正しく認識できていなかったのです。

結果として、デプロイエラーは出ないのに、APIだけが静かに無視される という状態に陥っていました。

解決策

Freeプランで確実に Managed Functions を動作させるための最も安全な方法は、Static Web Apps (Oryx) にビルドプロセスを委譲すること でした。

GitHub Actionsのワークフローを以下のように変更し、Azure側に「正しい形」でビルド・配置してもらいます。

skip_api_build: false
api_location: 'backend/Receiptfly.Functions' # ソースコードの場所を指定

これでAPIが認識されるようになるはずでしたが、それでもうまくいかない。

第2の壁:立ちはだかる「100MB制限」

設定を変更し、SWAに自動ビルドを任せたところ、今度はデプロイ自体が明確に失敗するようになりました。エラーログには以下のように記されていました。

The content server has rejected the request with: BadRequest
Reason: The size of the function content was too large. The limit for this Static Web App is 104857600 bytes.

誤解しやすい制限仕様

「Static Web AppsのFreeプランって上限250MBじゃなかったっけ?」

ここで多くの人が混乱するポイントがあります。

  • アプリ全体のサイズ制限: Freeプランは250MB、Standardは500MB。
  • API Functionsのサイズ制限: プランに関係なく一律 100MB (104,857,600 bytes)

私のAPIプロジェクトは、PDF処理ライブラリなどを含んでいたため、ビルド後のサイズがこの100MBを軽く超えていたのです。Standardプランに上げても解決しない(API制限は変わらない)という点が非常に厄介です。

試行錯誤:ビルドサイズのダイエット

ここから、100MB以内に収めるための作戦が始まりました。

試行1:.csproj での最適化(効果:小)

まず、Release ビルドの設定を極限まで絞りました。デバッグシンボルやXMLドキュメントなど、実行に不要なメタデータを削除します。

<PropertyGroup>
  <DebugType>None</DebugType>
  <DebugSymbols>false</DebugSymbols>
  <IncludeSymbols>false</IncludeSymbols>
  <IncludeSource>false</IncludeSource>
  <DebuggerSupport>false</DebuggerSupport>
</PropertyGroup>

多少サイズは減りましたが、まだ100MBの壁は超えられません。

試行2:PublishTrimmed の罠(失敗)

「未使用のコードを削除(トリミング)すれば劇的に減るはずだ」と考え、PublishTrimmed を有効にしました。

<PublishTrimmed>true</PublishTrimmed>

すると、ビルド時に新たなエラーが発生しました。

error NETSDK1102: Optimizing assemblies for size is not supported for the selected publish configuration.
Please ensure that you are publishing a self-contained app.

原因とジレンマ:

  • .NETのトリミング機能(PublishTrimmed)は、自己完結型(Self-Contained)デプロイでしか使えません。
  • しかし、Static Web AppsのManaged Functionsは、フレームワーク依存(Framework-dependent) が推奨されており、ランタイムはAzure側が管理するものを使います。

つまり、Managed Functionsを使う以上、最も効果的なサイズ削減手段であるトリミング機能が封じられている ということになります。

試行3:.funcignore の活用(効果:中)

Functionsのデプロイパッケージに含まれる不要なファイルを除外するために .funcignore ファイルを作成しました。

# .funcignore
*.pdb
*.xml
*.Tests.dll
bin/
obj/
.git/

これによりゴミファイルは減りましたが、根本的な解決(バイナリサイズの削減)には至りません。

最終解決:不要な依存パッケージの断捨離

結局のところ、魔法のような設定はなく、「本当にそのパッケージはAPIに必要なのか?」 という原点に立ち返る必要がありました。

私のプロジェクトでは、PDFを画像化するために PDFtoImage と、その依存関係である SkiaSharp を含んでいました。しかし、よく設計を見直すと、この重い処理はAPI(HttpTrigger)で同期的に行うものではなく、Queueを介した別のProcessing Function(非同期処理)で行うべきものでした。

そこで、API用プロジェクトから以下のパッケージを削除しました。

  1. PDFtoImage (非常に重い)
  2. SkiaSharp (ネイティブライブラリを含み巨大)
  3. Npgsql.EntityFrameworkCore.PostgreSQL (API層で直接DBを触らない設計に変更していたため不要)

結果、劇的なサイズダウンに成功。 無事に100MB制限をクリアし、デプロイが成功しました。

最終的な設定コード(コピペ用)

最終的に動作した設定をまとめます。

1. GitHub Actions (.yaml)

Managed Functionsとして正しくビルド・認識させるため、skip_api_buildfalse です。

jobs:
  build_and_deploy_job:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # (Node.jsのセットアップなどは省略)
      
      - name: Build And Deploy
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: 'upload'
          app_location: 'frontend/receiptfly-web'
          api_location: 'backend/Receiptfly.Functions' # ソースコードディレクトリを指定
          output_location: 'dist'
          skip_api_build: false # 重要:自動ビルドでManaged Functionsとして認識させる

2. .csproj の最適化設定

トリミングは使わず、デバッグ情報の削除のみを行います。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <AzureFunctionsVersion>V4</AzureFunctionsVersion>
    <OutputType>Exe</OutputType>
    
    <PublishSingleFile>false</PublishSingleFile>
    <SelfContained>false</SelfContained> <DebugType>None</DebugType>
    <DebugSymbols>false</DebugSymbols>
    <IncludeSymbols>false</IncludeSymbols>
    <IncludeSource>false</IncludeSource>
    
    <DebuggerSupport>false</DebuggerSupport>
    <EnableSGen>false</EnableSGen>
  </PropertyGroup>
  </Project>

学んだ教訓まとめ

今回のトラブルシューティングを通じて得られた教訓は以下の4点です。

  1. 自動ビルドが一番安全:skip_api_build: true で事前ビルドしたものを持ち込むことも理論上は可能ですが、FreeプランのManaged Functionsとして正しく認識させる難易度が高いです。素直に skip_api_build: false を使うのが近道です。
  2. 100MBの壁は絶対:プランを上げてもAPI Functionsのサイズ上限(100MB)は緩和されません。設計段階からライブラリのサイズを意識する必要があります。
  3. トリミング機能への過度な期待は禁物:Managed Functions環境ではフレームワーク依存デプロイとなるため、.NETの強力なトリミング機能(PublishTrimmed)は利用できません。
  4. 依存関係の整理が最強の最適化:ビルドオプションをいじるよりも、巨大なNugetパッケージを1つ削除する方が、はるかに効果的です。機能の責務を分離し、APIプロジェクトを軽量に保つ設計(Clean Architecture等)が、デプロイの成功にも直結します。

Azure Static Web Appsは非常に便利ですが、.NET Isolated Functionsとの組み合わせには特有のクセがあるなという感じです。