しばやん雑記

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

複数の App Service / Azure Functions へのデプロイを GitHub Actions を使って並列に実行する

少し前に書いたエントリの続きに近い内容ですが、実際に GitHub Actions を使って複数の App Service / Azure Functions へのデプロイを並列実行するという話です。

ビルドに関しては並列実行しても効果が薄いケースが多いですが、デプロイは前回書いたようにネットワーク I/O やウォームアップの待ち時間が多く、依存関係がほぼないため並列実行に向いています。

ただし効率良く並列デプロイを行うには事前の準備が必要になってきます。

具体的にはビルド時にデプロイ用パッケージとなる zip ファイルを生成しておくことと、それぞれの Job では必要な Artifact のみをダウンロードするという点です。

発行時に zip ファイルを自動生成

ぶっちゃけ今回の肝となるのはこの dotnet publish のタイミングで zip ファイルを生成しておくことです。Azure Pipelines では zip にするオプションがありましたが、GitHub Actions や .NET Core CLI にはそんな機能はありません。

だからと言って手動で 7zip などを使って圧縮するのもアレなので、今回は MSBuild の ZipDirectory Task を使って行います。詳細は坂本さんが既に書いてくれているので、そっちを参照してください。

今回は複数のアプリケーションに対して行う必要があったので、それぞれの csproj に書くのではなく以下のような内容の Directory.Build.targets を用意して、一括で処理を追加するようにしました。

このファイルはソリューションと同じ位置に置いておけば良いです。

<Project>

  <PropertyGroup>
    <_MakeZipTarget Condition="$(_IsFunctionsSdkBuild) == 'true'">Publish</_MakeZipTarget>
    <_MakeZipTarget Condition="$(_IsFunctionsSdkBuild) != 'true'">AfterPublish</_MakeZipTarget>
  </PropertyGroup>

  <Target Name="MakeZipPackage" AfterTargets="$(_MakeZipTarget)">
    <ZipDirectory SourceDirectory="$(PublishDir)" DestinationFile="$(ProjectDir)$(AssemblyName).zip" Overwrite="true" />
  </Target>

</Project>

Azure Functions 向けだと AfterTargetsPublish にしないと正しい構造にならなかったため変更しています。そして Linux 上でビルドする場合には publishUrl を使うとディレクトリ名の case 問題に引っ掛かるので、同時に PublishDir を使うように変更しました。

この状態でローカル環境で dotnet publish を実行すると zip が同時に生成されます。

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

ASP.NET Core と Azure Functions のプロジェクトを用意していたので、それぞれの zip には以下のような構造でファイルが圧縮されています。

特に Azure Functions は SDK で後処理が行われるので、正しいタイミングで zip にしないと狂います。

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

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

これで 1 回の dotnet publish 実行で全てのアプリケーションに対して zip が作成されるようになりました。Azure Pipelines でもソリューション単位で行うことでビルド時間の短縮は図れると思います。

生成した zip ファイルをアップロード

アプリケーション毎に zip が作成できたので、次はその zip を Artifact としてアップロードします。

本来ならアップロードも step を分けずに 1 度に行いたいところですが、現在の Action では対応していないので、個別にアップロードする処理を書いています。

build:
  runs-on: ubuntu-latest
  steps:
  - uses: actions/checkout@v2

  - name: Use .NET Core ${{ env.DOTNET_VERSION }}
    uses: actions/setup-dotnet@v1
    with:
      dotnet-version: ${{ env.DOTNET_VERSION }}

  - name: Publish projects
    run: dotnet publish -c Release

  - name: Upload artifact (webapp)
    uses: actions/upload-artifact@v2
    with:
      name: webapp-test-1
      path: WebApplication1/*.zip

  - name: Upload artifact (function)
    uses: actions/upload-artifact@v2
    with:
      name: function-test-2
      path: FunctionApp1/*.zip

アップロード時に name で実際の App Service 名を指定するようにしました。App Service 名で Artifact を分けることで、後続のデプロイ処理では必要なパッケージだけを簡単にダウンロードできるわけです。

ここまでの Workflow を実際に実行してみると、以下のように 1 つずつ zip がアップロードされます。

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

GitHub の Artifact は自動で圧縮されるのではなくファイル毎にそのままアップロードされるようなので、予め zip に圧縮してまとめておくことでアップロードの時間が短縮されます。

App Service / Azure Functions へのデプロイも Run From Package で行うので zip は都合が良いです。

strategy / matrix を使ってデプロイを並列実行

Artifact としてアップロードしてしまえば、後は Workflow に strategymatrix を追加して App Service / Azure Functions 単位でデプロイ Job を実行するだけです。

デプロイ用に azure/webapps-deployazure/functions-action が用意されていますが、この Action を使うと Job を App Service 用と Azure Functions 用に分ける必要が出てくるので、今回は Azure CLI にある zip デプロイを行うコマンドを使いました。

deploy-all:
  runs-on: ubuntu-latest
  needs: build
  strategy:
    matrix:
      appName: [webapp-test-1, function-test-2]
  steps:
  - name: Download artifact
    uses: actions/download-artifact@v2
    with:
      name: ${{ matrix.appName }}

  - name: Login to Azure
    uses: azure/login@v1
    with:
      creds: ${{ secrets.AZURE_CREDENTIALS }}

  - name: Deploy to Dev
    run: az webapp deployment source config-zip -g ${{ env.resourceGroup }} -n ${{ matrix.appName }} --src $(find -name *.zip)

matrix には App Service 名を配列で指定しているので、App Service 単位で Job が並列実行されます。

その後は Artifact から必要なパッケージだけをダウンロードし、Azure CLI で zip をデプロイしているだけなので簡単です。zip ファイルのパスだけは指定しないといけないので、適当にコマンドを叩いて拾っています。

これで Workflow は完成したので実行してみると、Artifact が作成されてデプロイ用の Job が App Service 単位で実行されていることが確認できます。この例のように 2 つだと差は小さいですが、アプリケーションが増えるとデプロイにかかる時間が増えるので効果が大きくなります。

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

Workflow はシンプルに保ちつつ、全体としての実行時間を短縮できたのでかなり満足しています。

今回はサンプルを用意して試しましたが、既に実際のアプリケーション向けに組み込んでいて 3 アプリケーションで実行時間が 20% ほど削減できました。アプリケーションは増えるので更に差が広がるはずです。

並列実行するかどうかは別として dotnet publish を使って同時に zip ファイルを生成する方法を確立できたのが、個人的には大きな収穫でした。