しばやん雑記

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

Azure Static Web Apps の公式 GitHub Action でも Federated Credentials を使ってデプロイしたい

Azure Portal から GitHub リポジトリを指定して Static Web Apps を作成すると自動的にワークフローが作成されますが、作成されたワークフローは Deployment Token を使うようになっています。

この Deployment Token を使う方法は非常にシンプルで扱いやすく、立ち位置としては App Service の Publish Profile と同じもので単一 SWA へのデプロイのみ可能なものです。作成されたワークフローでは公式の GitHub Action を使うため、ビルド含めすべて自動でやってくれるのでお手軽です。

シンプルに 1 つの SWA へのデプロイであればそこまで困らないのですが、モノリポで複数の SWA へのデプロイを行う場合には Deployment Token の管理が煩雑になるため、App Service や Azure Functions へのデプロイと同じように Entra ID ベースの Federated Credentials でデプロイを行いたいところです。

以下で書いたように SWA CLI を使うと Azure CLI 経由で Federated Credentials でデプロイ出来ますが、公式の GitHub Action とは異なり Pull Request 環境には対応できません。*1

既に公式 GitHub Action を使って Pull Request 環境を作って運用している環境を、SWA CLI ベースに書き換えるのは正直手間なので、アプローチを変えて Azure CLI で該当 SWA の Deployment Token を Federated Credentials を使って取得し、その Deployment Token を公式 GitHub Action に渡す方法を考えました。

Azure CLI には az staticwebapp secret list コマンドが用意されているので、これを使うと Deployment Token が取得出来ます。権限としては Contributor 以上が必要になるはずです。

試しに手元の環境で以下のようなコマンドを実行すると、apiKey というプロパティ名で Deployment Token が返ってくることを確認しましたので、これがそのまま使えそうです。

az staticwebapp secrets list -n daruyanagi

このコマンドを GitHub Actions で動かせば良いので、後続の処理で扱いやすくするために apiKey の本体だけ返ってくるようにパラメータを追加すると良い感じです。

当然ながら Deployment Token はシークレットとして扱う必要があるので、以下のドキュメントに従って GitHub Actions で動かす際には ::add-mask:: を使って値のマスク設定を行って対応します。

Federated Credentials と Azure CLI を使って Deployment Token を取得し、後続のステップで利用できるように出力する例が以下のようになります。マスク設定済みの値を outputs として指定しているので、実行時に取得した値であっても安全に扱えるはずです。

- name: Azure Login
  uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Read deployment token
  id: read-token
  run: |
    deployment_token=$(az staticwebapp secrets list --name stapp-deploy-test --query "properties.apiKey" -o tsv)
    echo "::add-mask::$deployment_token"
    echo "deployment-token=$deployment_token" >> "$GITHUB_OUTPUT"

実は同じことを SWA CLI でも行っているので、特に問題にはならないと考えています。

Azure CLI で取得した Deployment Token を公式 GitHub Action に以下のように渡せば、理論上はシークレットを GitHub に登録しなくてもこれまで通り使えるはずです。

- name: Build And Deploy
  id: builddeploy
  uses: Azure/static-web-apps-deploy@v1
  with:
    azure_static_web_apps_api_token: ${{ steps.read-token.outputs.deployment-token }}

この修正を入れたワークフローを実行してみると、問題なく Deployment Token を Federated Credentials を使って実行時に取得し、公式 GitHub Action を使ってデプロイまで成功していることが確認出来ます。

公式 GitHub Action を使っているので、Pull Request にプレビュー環境の URL がコメントとして投稿されます。Federated Credentials で id-token を使うために permissions を明示的に指定している場合は pull-requests: write 権限の追加も必要になるのがはまりどころですね。

ここまでの変更で Azure CLI を使って実行時に Deployment Token を取る方法が有効だと分かったので、扱いやすいように Azure CLI で Federated Credentials を使う部分と、Deployment Token を取得する部分を以下のような Composite Action として切り出しておきます。

Composite Action は制約も多いですが、今回のように完全に閉じた処理の場合は便利です。

name: Read deployment token
inputs:
  app-name:
    description: 'Name of static web app'
    required: true
  client-id:
    description: 'Azure client ID'
    required: true
  tenant-id:
    description: 'Azure tenant ID'
    required: true
  subscription-id:
    description: 'Azure subscription ID'
    required: true

outputs:
  deployment-token:
    description: 'Deployment token'
    value: ${{ steps.read-token.outputs.deployment-token }}

runs:
  using: 'composite'
  steps:
  - name: Azure Login
    uses: azure/login@v2
    with:
      client-id: ${{ inputs.client-id }}
      tenant-id: ${{ inputs.tenant-id }}
      subscription-id: ${{ inputs.subscription-id }}

  - name: Read deployment token
    id: read-token
    run: |
      deployment_token=$(az staticwebapp secrets list --name ${{ inputs.app-name }} --query "properties.apiKey" -o tsv)
      echo "::add-mask::$deployment_token"
      echo "deployment-token=$deployment_token" >> "$GITHUB_OUTPUT"
    shell: bash

今回作成した Composite Action はリポジトリに含める形になるので、利用する際にはチェックアウトが必要になる点は注意が必要です。デプロイ用の Job ではチェックアウトを忘れがちになります。

実際に利用する際には、通常の Action と同様に use でパスを指定するだけなので簡単です。

- name: Read deployment token
  id: read-token
  uses: ./.github/actions/read-token
  with:
    app-name: ${{ env.SWA_NAME }}
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Build And Deploy
  id: builddeploy
  uses: Azure/static-web-apps-deploy@v1
  with:
    azure_static_web_apps_api_token: ${{ steps.read-token.outputs.deployment-token }}
    repo_token: ${{ secrets.GITHUB_TOKEN }}
    action: "upload"
    app_location: "/"
    output_location: "build"

Composite Action を使うように修正したワークフローを実行すると、こちらも問題なく動作することが確認出来ます。一般的に Pull Request の作成・更新時と閉じた時で 2 つの Job を用意する必要があるため、Composite Action で共通化しておくと便利に使えます。

本来であれば公式 GitHub Action がさっさと Federated Credentials に対応してくれれば苦労をしないのですが、正直なところ対応される気配はないので今回の方法で対応するのが無難でしょう。

*1:実体は Environment の 1 種類なので名前付き環境を使えば代替は可能