EventGridを使用してAzureリソースのデプロイ完了をフックする

# はじめに

　業務では、Github Actionsを利用してIaC化したAzureリソースをデプロイしています。AzureリソースのデプロイはPowershellコマンドの[New-AzDeployment](https://learn.microsoft.com/ja-jp/powershell/module/az.resources/new-azdeployment?view=azps-12.0.0)を使用しており、デプロイ処理をAzure側におまかせしています。我々の使い方として、Github ActionsのランナーはAzureリソースのデプロイ開始を命令してランナーを完了させています。そのため、Azureリソースのデプロイが正常に完了したかどうかはAzure Portalを直接確認する必要があり非常に面倒です。
　ユーザーに通知する安易な方法として、Github ActionsのランナーがAzureリソースのデプロイ完了をポーリングすることでGithub側の機能でユーザーに通知することができます。しかし、デプロイするAzureリソースによってはデプロイ時間が長時間になり、その分Github Actionsの課金が発生してしまいます。
　本記事では、Azureリソースのデプロイ完了をEventGridでフックしてユーザーに成功/失敗を通知する機構を紹介します。

# Azure Event Grid

https://learn.microsoft.com/ja-jp/azure/event-grid/overview

公式ドキュメントでは、以下のように謳っています。
```
MQTT および HTTP プロトコルを使用した柔軟なメッセージ消費パターンを提供する、スケーラブルでフル マネージドの 発行/サブスクライブ メッセージ配信サービスです。
```
このサービスを使用することで、Azureリソースのデプロイに関するイベントをフックすることが可能になります。

## Event Gridからフックできるデータ形式の確認

### IaCコード

以下のIaCコードは本記事の説明用に簡易的に書いたものです。

- ディレクトリ構成
  ```
  |- test
  |  |- main.bicep
  |  |- test.bicep // 適当なAzureリソースをデプロイ
  |
  |- deploy.ps1
  ```

  ```bicep: main.bicep
  targetScope = 'subscription'

  resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
    name: 'test_rg'
    location: deployment().location
  }

  module test 'test.bicep' = {
    scope: rg
    name: 'test_${deployment().name}' // deployment().nameにはdeploy.ps1のNameに入った値が入ります。
    params: {
      location: deployment().location
    }
  }
  ```

  ```powershell: deploy.ps1
  $params = @{
      Name          = 'az_deployment'
      Location      = 'japaneast'
      TemplateFile  = './test/main.bicep'
  }
  New-AzDeployment @params
  ```

### Event Gridの設定

今回Event Gridでフックしたい項目は[Azure サブスクリプション](https://learn.microsoft.com/ja-jp/azure/event-grid/event-schema-subscriptions?tabs=cloud-event-schema)になります。

ほぼ既定値で問題ないので、以下の設定でEventGridを作成します。
1. システムトピックを「Azure Subscriptions」を選択し、作成する
2. サブスクリプショントピックを既定値で作成する
   1. エンドポイントは [smee](https://smee.io/) を指定ことでイベントのデータを確認することができます

EventGrid作成後にIaCコードをデプロイすると、以下のようなjson形式を取得することができます。

```json: EventGridから取得できたデータ
{
    "payload": [
        {
            "subject": "/subscriptions/????????-????-????-????-????????????/resourcegroups/test_rg/providers/Microsoft.Resources/deployments/test_az_deployment",
            "eventType": "Microsoft.Resources.ResourceWriteSuccess",
            "id": "????????-????-????-????-????????????",
            "data": {
                "authorization": {
                    ...
                },
                "claims": {
                    ...
                },
                "correlationId": "????????-????-????-????-????????????",
                "httpRequest": {},
                "resourceProvider": "Microsoft.Resources",
                "resourceUri": "/subscriptions/????????-????-????-????-????????????/resourcegroups/test_rg/providers/Microsoft.Resources/deployments/test_az_deployment",
                "operationName": "Microsoft.Resources/deployments/write",
                "status": "Succeeded",
                "subscriptionId": "????????-????-????-????-????????????",
                "tenantId": "????????-????-????-????-????????????"
            },
            "dataVersion": "2",
            "metadataVersion": "1",
            "eventTime": "2024-??-??T??:??:??.???????Z",
            "topic": "/subscriptions/????????-????-????-????-????????????"
        }
    ]
}
```

ここで注目したいのは以下のプロパティです。
- `subject`
  - IaCコードのmodule testのnameに入れた値である
`test_az_deployment`が含まれているので、test.bicepをデプロイした際のイベントであることがわかります。
- `status`
  - このステータスはデプロイの完了ではなく、デプロイ処理開始が完了したことを表しています。

上記より、EventGridの`status`情報からはAzureリソースのデプロイ完了を判断することが難しそうです。

## EventGridからAzureリソースのデプロイ完了を判定する

　前述より、EventGridから取れる`status`からはデプロイ完了を判定ができないことがわかりました。一方で、[サブスクリプションの履歴](https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/templates/deployment-history?tabs=azure-portal#subscription-deployments)を確認すると、`az_deployment`という名称で履歴が残っています。`az_deployment`の履歴の詳細を追うと、[デプロイ操作とエラーメッセージ](https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/templates/deployment-history?tabs=azure-portal#deployment-operations-and-error-message)で記載されているような`操作の詳細`を確認することができる。デプロイ完了を判断するためには、`操作の詳細`の[プロビジョニングの状態](https://learn.microsoft.com/ja-jp/azure/azure-resource-manager/management/async-operations#provisioningstate-values)を確認する必要があります。

以下のようなコードを用いることで、EventGridの情報からtest.bicepのデプロイ完了結果を取得することができる。このコードは私が自前で用意したものです。

```csharp: EventGridの情報からtest.bicepのデプロイ完了結果を取得
using Azure.ResourceManager.Resources;

public void processEventGrid(string subject)
{
  // ... subjectをsplit

  // 1. subjectのプロパティからsubscriptionIdとdeploymentNameを取得する
  var subscriptionId = "????????-????-????-????-????????????"
  var deploymentName = "az_deployment"

  var armDeploymentResourceId = ArmDeploymentResource.CreateResourceIdentifier($"/subscriptions/{subscriptionId}", deploymentName);
  var armDeployment = armClient.GetArmDeploymentResource(armDeploymentResourceId);
  var operations = armDeployment.GetDeploymentOperations(); // 2. サブスクリプションに対するデプロイ操作を全て取得
  var operation = operations.FirstOrDefault(operation => operation.Properties.TargetResource?.ResourceName.Contains(deploymentName)); // 3. test.bicepのデプロイ操作を取得

  // 4. test.bicepのデプロイ完了結果を表示
  System.WriteLine(operation.Properties.ProvisioningState)

  // ... 後続処理
}

```

処理の説明
1. `subject`のプロパティから`subscriptionId`と`deploymentName`を取得する
2. `az_deployment`のサブスクリプションに対するデプロイ操作を全て取得する
    - `test.bicep`のデプロイ操作はその中に入っている
3. `test.bicep`のデプロイ操作を取得する
4. `test.bicep`のデプロイ操作からプロビジョニングの状態を取得する

上記はコードを用いた判定となるため、EventGridのエンドポイント先はAzure Functionsになります。

以上より、EventGridからAzureリソースのデプロイ完了を判定することが可能になりました。

# ユーザーに通知する

通知に使用するのはAzure Communication Serviceです。

## Azure Communication Serviceとは

公式ドキュメントでは、以下のように謳っています。
```
Azure Communication Services は、音声、ビデオ、チャット、テキスト メッセージ/SMS、メールなどをすべてのアプリケーションに追加するためのマルチチャネル コミュニケーション API を提供します。
```

メールの機能以外にも様々なコミュニケーションAPIを提供してくれているようです。
今回はメールの機能を使っていきます。

[SDKとREST API](https://learn.microsoft.com/ja-jp/azure/communication-services/concepts/sdk-options)が提供されているので、C#のコード例を以下に載せておきます。

```csharp: ユーザーに通知
using Azure.Communication.Email;

public void sendEmail(string provisioningState)
{
  // メールによる通知
  var htmlContent = $"{provisioningState}";
  var responseEmail = await emailClient.SendAsync(
      WaitUntil.Completed,
      senderAddress: "DoNotReply@example.com",
      recipientAddress: "myemaile@example.com",
      subject: "Deploy Complete",
      htmlContent: htmlContent
  );
}
```

# システム構成図

完成したシステム構成は以下になります。

![azure-eventgrid-trigger.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592580/6b6d3335-52cd-d70e-c19a-32a608eb046c.png)

1. ユーザーがGithub Actionsを実行する
2. Github ActionsのランナーがIaCコードを`deploy.ps1`を実行し、AzureリソースのデプロイはAzure Deploymentにおまかせしてランナーは終了します
3. AzureリソースのデプロイのイベントをEventGridでフックし、Azure Functionsに流します
4. EventGridの情報を元に、Azureリソースのデプロイが完了したかどうかを判定する
5. 完了していた場合、ユーザーに通知する

このシステム構成により、Github ActionsのランナーはAzureリソースのデプロイを命令処理すると終了するため、課金が抑えられます。尚且つ、Azureリソースのデプロイ完了をユーザーに知らせることができます。

# 応用編

今回のシステム構成だと、Azureリソースのデプロイ完了後にGithub Actions側が何かしらの処理を実施したい場合には適用できません。

## Github Environment

Github EnvironmentにはDeployment Protection Rulesという機能があります。
この機能を使用することで、ジョブの実行を承認があるまで待機させることができます。注意点として、本機能はGithub Enterpriseのライセンスが必要になります。
アプリケーションから承認を実施するにはGithub Appを使用して[Custom Deployment Protection Rules](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/creating-custom-deployment-protection-rules#approving-or-rejecting-deployments)という機能を使います。

この機能を使用した場合のGithub Actionsのyamlは以下になります。
  ```yml: githubactions-test.yml
  ...

  jobs:
    new-az-deployment:
      runs-on: ubuntu-latest

      steps:
        ...

        - name: New Az Deployment
          shell: pwsh
          run: New-AzDeployment deploy.ps1 -RunId ${{ github.run_id }} # Event Gridのsubject値内に埋め込むためにRunIdを引数として取る(次節で説明)

    post-process:
      needs: new-az-deployment
      runs-on: ubuntu-latest
      environment: test_environment_protection # Deployment Protection Rulesを適用したGithub Environmentのため、承認がないと処理をしません。

      steps:
        ...

        - name: Update Az Resource
          shell: pwsh
          run: |
            // 後続処理
  ```

  `new-az-deployment`はAzureリソースをデプロイのみを実施し、後続に実行するジョブである`post-process`には`Deployment Protection Rule`を適用したGithub Environmentを指定しています。`post-process`は承認がない限り処理されることはありません。
  この承認を、Azureリソースのデプロイ完了後にEventGridでフックしAzure Functionsで承認処理を実施することで、後続処理を走らせます。

## EventGridからAzureリソースのデプロイ完了を判定し、Deployment Protection Rulesを承認する

[公式ドキュメント](https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/creating-custom-deployment-protection-rules#approving-or-rejecting-deployments)を確認する限り、Deployment Protection Ruleの承認をする方法として、REST APIが提供されているようです。承認が必要なGithub ActionsのワークフローのIDが必要になります。EventGridでフックした際のワークフローIDを渡すために、`deploy.ps1`を以下のように修正します。

  ```powershell: deploy.ps1
  [CmdletBinding()]
  param (
      [Parameter()]
      [Alias('RunId')]
      [string]$RunId,
  )
  $params = @{
      Name          = "az_deployment_{$RunId}"
      Location      = 'japaneast'
      TemplateFile  = './test/main.bicep'
  }
  New-AzDeployment @params
  ```
  Name箇所にワークフローIDを組み込むことで、EventGridのプロパティである`subject`から取得することができます。

プロビジョニング状態を元に、Azure FunctionsからREST APIを実行した一部コードを以下になります。コードは上部に記載したコードの続きに記載します。

```csharp: Deplyment Protection Rulesに承認/否認をPOST
using Azure.ResourceManager.Resources;

public void processEventGrid(string subject)
{
  // ... subjectをsplit

  var subscriptionId = "????????-????-????-????-????????????"
  var deploymentName = "az_deployment"

  var armDeploymentResourceId = ArmDeploymentResource.CreateResourceIdentifier($"/subscriptions/{subscriptionId}", deploymentName);
  var armDeployment = armClient.GetArmDeploymentResource(armDeploymentResourceId);
  var operations = armDeployment.GetDeploymentOperations();
  var operation = operations.FirstOrDefault(operation => operation.Properties.TargetResource?.ResourceName.Contains(deploymentName));

  // プロビジョニング状態を元にDeployment Protection Ruleに承認/否認をPOST
  var state = operation.Properties.ProvisioningState == "Successed" ? "approved" : "reject";
  var runId = "99999999999" // subjectから値を抽出
  var url = $"https://api.github.com/repos/test-owner/test-repo/actions/runs/{runId}/deployment_protection_rule";
  var response = await httpClient.PostAsJsonAsync(url, new
  {
      environment_name = "test_environment_protection",
      state = state,
      comment = "",
  });
}

```

## システム構成図

応用編で完成したシステム構成は以下のようになります

![azure-eventgrid-trigger-deployment-protection.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3592580/f64ad5d2-2824-1bae-828b-45f6e65a834c.png)

1. ユーザーがGithub Actionsを実行する
2. Github ActionsのランナーがIaCコードを`deploy.ps1`を実行し、AzureリソースのデプロイはAzure Deploymentにおまかせします
3. `post-process`はDeployment Protection Rulesにより、承認があるまで待ちの状態です
4. AzureリソースのデプロイのイベントをEventGridでフックし、Azure Functionsに流します
5. EventGridの情報を元に、Azureリソースのデプロイが完了したかどうかを判定する
6. Azureリソースのデプロイの成功/失敗によって、Deployment Protection Rulesに承認/否認をします
   - 承認の場合、`post-porcess`はジョブが開始され、ジョブが完了するとGithub Actionsは成功になります
   - 否認の場合、`post-porcess`はジョブが開始されず、Github Actionsは失敗となります
7. Github Actionsが終了すると、Githubの機能でユーザーに通知します

このシステム構成により、Github ActionsのランナーはAzureリソースのデプロイを命令処理すると終了するため、課金が抑えられます。尚且つ、Azureリソースのデプロイ完了をユーザーに知らせることができます。

# まとめ

　本記事では、Azureリソースのデプロイ完了をEventGridでフックしてユーザーに成功/失敗を通知する機構を紹介しました。Github ActionsのランナーはAzureリソースのデプロイ開始を命令だけで終了し、Azureリソースのデプロイが完了したらユーザーに通知します。また、EventGridからフック可能であることがわかったため、それを応用した機構も紹介しました。
どちらもGithub ActionsのランナーがAzureリソースのデプロイをポーリングしなくて済み、ユーザーに完了を知らせることができます。

　余談ですが、EventGridはドキュメントが少なくAzureリソースのどのイベントが取れる/取れないについて調査/検証が非常に辛かったです。