しばやん雑記

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

GitHub Actions を使って Windows Containers のビルドと Web App for Containers へのデプロイを自動化する

暫く触っていなかったのですが、若干 Windows Containers が必要になりそうな気配を感じ取ったので、最新の Windows Containers 事情と Azure / GitHub での扱いについて再度確認しました。

Windows Containers を使って動かしたいアプリケーションは Classic ASP か ASP.NET のどちらかだと思うので、今回は ASP.NET をベースに話を進めていきます。Web App for Containers への ASP.NET アプリケーション移行はいくつかドキュメントが存在しています。

App Service を使えば Classic ASP と ASP.NET アプリケーションを実行できますが、特殊なライブラリやカスタムフォントのインストールが必要な場合には Windows Containers が選択肢に上がってきます。

Docker Image のカスタマイズには今回触れずに、GitHub Actions を使ったビルドとデプロイに特化して確認しています。Web App for Containers は暫く触っていない間に割と進化していました。

ここから先は ASP.NET アプリケーションを Web App for Containers にデプロイするまでの流れとなります。

フォルダー用 Publish Profile を作成

ASP.NET アプリケーションの Docker Image をビルドするためには、MSBuild を使ってアプリケーションを Release 向けにビルドする必要がありますが、全てのパラメータをコマンドライン引数で渡すのは面倒なので、Visual Studio から Publish Profile を作成しておくと楽が出来ます。

Publish Profile を作成する際に Docker Container Registry がありますが、こちらは使わずにフォルダーを選択してファイルシステムにビルド結果を出力するようにします。

ASP.NET アプリケーションの Docker Image 向けビルド時には、プリコンパイルを更新不可能な形で実施することを強くお勧めしています。これだけで Web Forms や Razor を使ったページのコールドスタートが大幅に改善するのと、Docker Image はイミュータブルなので更新可能なプリコンパイルの必要がありません。

プリコンパイルの設定は Publish Profile を作成後に設定から有効化出来ます。

ビルド時間は多少伸びますが、その分コールドスタートがかなり改善するのでメリットは大きいです。

MSBuild を使って ASP.NET アプリケーションをビルド

Publish Profile を作成後は MSBuild を使って ASP.NET アプリケーションのビルドを行いますが、今回は Multi-stage build を使う予定なので、ビルド周りの処理全てを Dockerfile 内で完結させる必要があります。

ASP.NET Core の dotnet コマンドは NuGet パッケージの復元を自動で行ってくれますが、MSBuild では明示的に指定する必要があります。更に ASP.NET では依存する NuGet パッケージの保存に packages.config が使われているので -p:RestorePackagesConfig=true という追加のオプションが必要です。

あまり情報が出てこない部分でもあるので、公式ドキュメントを紹介しておきます。

実際には .NET Framework SDK の Docker Image には NuGet CLI がインストールされているので、これまで通り nuget restore で復元を実行できますが、稀にビルド時と一致した構成での復元が必要なケースがあるので覚えておいて損はありません。*1

NuGet パッケージの復元が完了すれば、後は DeployOnBuildPublishProfile を指定すれば MSBuild が Publish Profile の設定に従ってビルドを行ってくれます。

フォルダー向けの Publish Profile を利用するとファイルシステム上に書き出されるので、Dockerfile ではその出力されたパスのファイルを全てコピーするだけで完了です。

ビルド・実行用の Dockerfile を作成

ここまでの手順で Dockerfile で必要となる設定とコマンドが確認出来ているので、後は Multi-stage build 用の Dockerfile を作成していきます。.NET Framework 向けの Dockerfile は以下のリポジトリで公開されているので、必要に応じて参照してください。

今回は .NET Framework 4.8 向けのアプリケーションなので、以下の 2 つの Docker Image を利用します。

  • mcr.microsoft.com/dotnet/framework/aspnet:4.8
    • 実行用
  • mcr.microsoft.com/dotnet/framework/sdk:4.8
    • ビルド用

ASP.NET 向けの Docker Image では IIS と関連機能の有効化と Roslyn 周りの追加が行われているだけですが、SDK では .NET Framework アプリケーションのビルドに必要な各種コンポーネントがインストールされているので、Docker Image のサイズは大きめです。

OS バージョンは指定していませんが、省略した際には GitHub Actions の Windows Hosted runner の OS バージョンが使われるので Windows Server 2022 となります。利用するコンポーネントによって OS バージョンを指定する必要がある場合は、タグ名に 4.8-windowsservercore-ltsc2022 のように OS バージョンを追加して対応します。現状では ltsc2022 / ltsc2019 / ltsc2016 のどれかになると思います。

ASP.NET Core 向けで使われる Dockerfile をベースに ASP.NET 向けに修正したものが以下のようになります。Multi-stage build を使って必要なファイルのみ実行用の Docker Image に含めるようにしています。

FROM mcr.microsoft.com/dotnet/framework/aspnet:4.8 AS base

FROM mcr.microsoft.com/dotnet/framework/sdk:4.8 AS build
WORKDIR /src
COPY . .
RUN msbuild WebApplication1.sln /Restore /p:RestorePackagesConfig=true /p:Configuration=Release /verbosity:minimal

FROM build AS publish
RUN msbuild WebApplication1.sln /p:DeployOnBuild=true /p:PublishProfile=FolderProfile /verbosity:minimal

FROM base AS final
COPY --from=publish /src/bin/publish /inetpub/wwwroot

IIS なのでビルドの最終成果物は /inetpub/wwwroot にコピーして終わりになっています。実行用の Docker Image に修正が必要な場合は、最初の FROM の後に処理を追加すれば良いです。

GitHub Actions で Docker Image のビルドとデプロイを行う

アプリケーションと Dockerfile の準備が完了したので、最後は GitHub Actions で Docker Image のビルドと Web App for Containers へのデプロイを行えば完了です。

GitHub Actions を使って Docker Image のビルドを行う場合は Buildx を使うのが一般的だと思いますが、残念ながら Windows 環境では "buildx failed with: error: Error response from daemon: Windows does not support privileged mode" というエラーで怒られてしまうので、これまで通り docker build を利用します。

name: Build and Deploy

on:
  push:
    branches:
      - master
    paths:
      - 'app/**'
      - '.github/workflows/build.yml'

  workflow_dispatch:

jobs:
  build:
    runs-on: windows-latest
    steps:
    - uses: actions/checkout@v3

    - name: Log in to container registry
      uses: docker/login-action@v1
      with:
        registry: xxxx.azurecr.io
        username: ${{ secrets.REGISTRY_USERNAME }}
        password: ${{ secrets.REGISTRY_PASSWORD }}

    - name: Build and push container image to registry
      run: |
        docker build -t xxxx.azurecr.io/wincontainer-test:${{ github.sha }} -f ./app/Dockerfile ./app/
        docker push xxxx.azurecr.io/wincontainer-test:${{ github.sha }}

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
    - name: Deploy to Web App for Container
      uses: azure/webapps-deploy@v2
      with:
        app-name: xxxx
        slot-name: production
        publish-profile: ${{ secrets.AZURE_PUBLISH_PROFILE }}
        images: xxxx.azurecr.io/wincontainer-test:${{ github.sha }}

基本的な Workflow の流れは Container Apps と同じように実装しました。Buildx を使っていない点と Docker Image のビルド時のみに windows-latest を使っているのが特徴的な部分です。Container Apps の Workflow については以下のエントリを参照してください。

GitHub Actions では windows-latestubuntu-latest の 2 倍時間を消費するのと、スピンアップまでの時間がかかるのでデプロイ部分は ubuntu-latest を使うようにしています。azure/webapps-deploy@v2 を使ったデプロイは OS に依存しない処理なので問題ありません。

作成した Workflow を実行すると build と deploy が実行されて、両方とも成功していることが分かります。

それなりの頻度で GitHub Actions と Azure Pipelines を使った ASP.NET / ASP.NET Core 向けの Workflow 全てで windows-latest を使っているケースを見ますが、OS に依存しない処理の場合は必ず ubuntu-latest を利用するようにしましょう。

App Service にデプロイされた結果を確認

デプロイ先の Web App for Containers の URL をブラウザで開くと、懐かしい ASP.NET MVC のテンプレートサイトが表示されます。Windows Containers は Linux ベースの Docker Image に比べると格段にサイズが大きいので起動までにはそれなりに時間がかかっていたのですが、Windows Server 2022 へのアップデートが行われたからか思ったよりも素早く起動しました。

実際の本番利用時には Deployment Slot と組み合わせる必要はあると思いますが、それでもアプリケーションの起動が早かったので割と実用的だなと感じました。

本番利用時には気を付けておきたい設定として WEBSITE_MEMORY_LIMIT_MBWEBSITE_CPU_CORES_LIMIT の 2 つがあります。ぶっちゃけ名前から想像つくと思いますが、詳細はドキュメントにまとめられているので目を通しておくと良いです。

Web App for Container (Windows) は Premium V3 のどのサイズを選んでいても、デフォルトでは 1GB のメモリしかコンテナに割り当てられないようなので、処理の内容次第ではメモリ不足となる可能性があります。

1 つの App Service Plan に 1 つの Web App for Container だけ載せるような場合には、明示的に WEBSITE_MEMORY_LIMIT_MB を指定してサイズを大きくしておかないと無駄になります。

補足 : Web App for Containers (Windows) のアップデートについて

最後に Web App for Containers (Windows) で気になった点について少しだけ書きます。まずは Windows Server 2022 に恐らくサイレントアップデートされている件です。

Hyper-V ベースの Windows Containers は下位互換性があるので、プラットフォームが Windows Server 2022 にアップデートされても 2019 や 2016 の Docker Image は正常に動作しますし、サポートもされています。

このような事情もあるので App Service チームは結構アグレッシブにプラットフォーム側のアップデートが行えるのかもしれません。次は通常の App Service も Windows Server 2022 にアップデートして欲しいです。

後はこれまで稼働中のコンテナーへのアクセスは WinRM を使う必要がありましたが、気が付いたら Azure Portal や Kudu から簡単にコンソールでの接続が出来るようになっていました。

WinRM はクライアント側に追加の設定が必要でかなり面倒でしたが、他のプラットフォームと同様にブラウザから簡単に接続できるようになったのでトラブルシューティングが容易になっています。