しばやん雑記

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

Run From Package と Managed Identity を使った App Service / Azure Functions のデプロイ戦略

App Service / Azure Functions のシークレットを安全に扱う際に利用する Key Vault Reference が User Assigned Managed Identity に対応しつつ、Linux 周りであった色々な制限が削除されたのと同時に、突然 Run From Package でも Managed Identity が使えるようになりました。

タイトルからは Run From Package の Managed Identity 対応は全く読み取れませんが、本文にはしれっと対応したと書かれています。

ここで久しぶりに Run From Package についておさらいしておくと、Run From Package には Zip ファイルを SCM に用意された API で直接デプロイする方法と、Zip の URL を指定して App Service / Azure Functions からプルしてもらう方法があります。

基本的には Zip ファイルを直接デプロイする方法がコールドスタートにおけるパフォーマンスが良いのですが、Azure Functions の Linux Consumption Plan ではビルド済みアプリケーションをデプロイする場合には、URL を指定した方法のみ利用可能です。

従って GitHub Actions から Linux Consumption Plan にデプロイする際には、内部でストレージアカウントにアップロードしてから SAS 付き URL を設定するというデプロイフローになっています。正直なところ SAS 付き URL を設定するのは期限の問題があり厄介です。

そこで今回 Managed Identity を使って、App Service / Azure Functions に対して直接 Blob に対してアクセス許可を割り当てると、その辺りの厄介な部分を全て解消出来ます。と言いたいところですが、肝心の Linux Consumption Plan で試したところ動作しなかったので、Windows Consumption Plan で試しました。

折角試すのなら外部 URL を使った Run From Package を活用したいと思ったので、以下のようなフローを組んで複数リージョンに対して GitHub Actions を使ってデプロイするようにしました。

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

Zip を直接デプロイする方法と比べると、外部 URL を指定する方法は Docker Image を Container Registry にプッシュして、その後アプリケーションに新しい Image Tag を設定する方法に近いです。ビルドされたパッケージ名をコミットハッシュと紐づければバージョン管理も行いやすいです。

現在デプロイされているバージョンも URL を見ればすぐに分かるのもメリットかなと思いました。ここからは実際に Managed Identity で試した例です。

Function App を作成して Managed Identity を有効化

適当に Function App を作成後、System Assigned Managed Identity を有効化しておきます。

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

アップデート内容には User Assigned Managed Identity が使えるとは書かれていなかったので、恐らく System Assigned のみ使えます。

Storage Account を作成して RBAC を設定

アプリケーションのパッケージを保存するための Storage Account を新しく作成しますが、全て Managed Identity で扱うために Blob public access と Storage account key access を無効化しておきます。

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

共有キーを使ったアクセスを無効化すると SAS も使えなくなるので、必然的に Azure AD を使った認証が必要となります。この辺りはドキュメントにも書かれているので参考にしてください。

RBAC で Function App に対して Blob への読み取り権限だけを付けておきたいので、Container の IAM 設定から Storage Blob Data Reader を割り当てておきます。

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

今回は 3 つのリージョンにそれぞれ Function App をデプロイしていたので、RBAC の設定は以下のようになりました。User Assigned Managed Identity が使えるようになるともう少しシンプルに管理できます。

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

ここまでの設定で Function App からアプリケーションのパッケージが保存された Storage Account へ Managed Identity でアクセスできるようになりました。

Function App にアプリケーションをデプロイ

アプリケーションを実行する準備は出来ているので、適当に Azure Function プロジェクトを作成して簡単な HttpTrigger を用意しました。バージョンとデプロイされたリージョンを返すだけの Function です。

public class Function1
{
    [FunctionName(nameof(Function1))]
    public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
    {
        var applicationVersion = typeof(Function1).Assembly
                                                  .GetCustomAttribute<AssemblyInformationalVersionAttribute>()
                                                  ?.InformationalVersion;

        var message = $"App Version = {applicationVersion}, Runtime Version = {RuntimeInformation.FrameworkDescription}, Region Name = {Environment.GetEnvironmentVariable("REGION_NAME")}";

        return new OkObjectResult(message);
    }
}

まずは手動で Zip ファイルを作成して Storage Account にアップロードしておきました。アップロードするためには自分自身にも RBAC で Storage 関連のロールを割り当てる必要があります。

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

URL にアクセスしてみるとエラーとなります。パブリックアクセスが出来ないことが確認できました。

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

その Storage への URL を Function App の App Settings から WEBSITE_RUN_FROM_PACKAGE という名前で設定すると、問題なく動作するようになりました。

当然ながら同一のパッケージを指定しているので、まったく同じバージョンと動作となります。

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

これで Managed Identity を使って Run From Package で設定されたパッケージが利用できることを確認出来ました。新しいバージョンに更新する際は WEBSITE_RUN_FROM_PACKAGE の URL を変更するだけで OK です。

GitHub Actions を使った自動デプロイと動作確認

最後はデプロイ周りを GitHub Actions を使って自動化していきます。特に難しいことはなく、先に Storage Account へアップロードしてから App Settings を新しい URL に更新しているだけです。

ビルド結果を Storage Account にアップロードしてしまうので、Artifacts を使う必要もありません。

jobs:
  publish:
    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: Setup Version
      id: setup_version
      run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}+${GITHUB_SHA::8}

    - name: Publish Functions
      run: dotnet publish -c Release -o ./dist -p:Version=${{ steps.setup_version.outputs.VERSION }}

    - name: Zip Functions
      run: 7z a -mx=9 package.zip ./dist/*

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

    - name: Upload to Blob
      run: az storage blob upload --auth-mode login --account-name *** -c packages -n ${{ github.sha }}.zip -f package.zip

  deploy:
    runs-on: ubuntu-latest
    needs: publish
    strategy:
      matrix:
        functionapp:
          - func-zipdeploy-japaneast
          - func-zipdeploy-centralus
          - func-zipdeploy-northeurope
    steps:
    - name: Login with Azure
      uses: azure/login@v1
      with:
        creds: ${{ secrets.AZURE_CREDENTIALS }}

    - name: Deploy to ${{ matrix.functionapp }}
      run: az functionapp config appsettings set -g *** -n ${{ matrix.functionapp }} --settings WEBSITE_RUN_FROM_PACKAGE=https://***.blob.core.windows.net/packages/${{ github.sha }}.zip

デプロイ用パッケージの作成と実際のデプロイを別の Job にしておくと、今回の例のように Matrix を使った並列実行や Environments の利用が行えるのでお勧めです。

実際にデプロイを実行させると以下のように、それぞれのリージョンへのデプロイが同時に行われます。

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

デプロイといっても URL を書き換えているだけなのであっという間に終わります。当然ながら設定後に Zip のプルが App Service 側で行われるのでコールドスタートは若干不利ですが、Azure のネットワーク内で完結するのでダウンロードは早いです。

ブラウザから適当に Function を実行してみると、バージョンとコミットハッシュが確認できます。

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

Storage Account にアップロードされたファイルを確認すると、コミットハッシュの先頭がビルドされたものと一致しています。デプロイを取り消したい場合は URL を元のパッケージに再設定すると戻ります。

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

実際に複数リージョンに向けてデプロイするフローを試してみましたが、URL だけを扱えばいいので失敗した時の対応が Zip を直接デプロイする方法よりもシンプルに出来そうです。*1

リージョンを追加したい場合にも URL を設定するだけでデプロイされるので、手動でイレギュラーな対応を行う必要が無いのもよさそうでした。これはどこかで試してみたいですね。

*1:途中でデプロイ失敗すると、どこまで反映されたのか分からなくなるので