BlazorとDataFactoryで作るお手軽ELアプリ

この記事は [bitFlyer Advent Calendar 2022](https://qiita.com/advent-calendar/2022/bitflyer) の 21 日目の記事となります。

Blazor WebAssemblyからRestAPIでAzure Data FactoryのPipelineを実行するアプリを作成しています。ここではコンセプト版をご紹介します。

## 背景
筆者はSRE部に所属しています。毎年この時期は監査の対応があり、SRE部には監査法人に提出するためのデータ抽出の依頼が来ます。
データソースはRDBのデータであることが多く、1年以上の期間のデータが必要で、複数のテーブルが対象になります。

bitFlyerではAzureを利用しています。こんなときはAzure Data Factory(以下ADF)の出番です。RDBからデータを抽出しBlobStorageに出すpipelineを作成します。あとは監査の担当者にBlobStorageから取ってとお願いして、めでたしめでたし...とはいきません。

監査が進むにつれて
- 新しいテーブルのデータ抽出が必要
- このテーブルはやっぱりあと2年分欲しい、このテーブルは半年分...

と繰り返し依頼がきます。そのたびにADFの変更をしてデプロイして...というのは結構手間です。
依頼者的にも、何度も依頼して気まずい、依頼してからのリードタイムも長い時があって...とお互いによくありません。

抽出の条件はほぼ日付のfrom/toのみなので、データが欲しい人が対象テーブルと条件を入力してADFを実行できればいいでしょうということで、このアプリを作成することにしました。

Table名を入力して日付を入れてsubmitを押すとADFのpipelineが実行されます。実際のアプリは複数テーブルに対応しています。
![ts1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952758/744e354c-23b8-2277-1db3-be3b5dc61d55.jpeg)


構成はStandAloneのBlazor WebAssembly + Static Web Apps + Azure Data Factoryです。認証はAzureAD(AAD)です。
利用者にSubscriptionのDataFactoryへのAccessRoleを割り当てます。Pipelineの実行、結果の確認などができるCustom Roleがいいですね。

### Azure ADでの作業
サービスプリンシパルを作成します。主な設定するべき項目は以下になります
- Authentication>Platform configurationsでSingle-page Applicationを選択
- urlには `https://YOURDOMAIN/authencation/logincallback`を入力

![ts2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952758/43f619ef-006d-be79-107e-ffeee3cab1ac.jpeg)

- "selec the tokens you would like to be issued by the authorization endpoint:"の箇所では `ID tokens` を選択する
![ts3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952758/c05d3a3d-1090-c99b-ca90-54ae8237f07b.jpeg)



- API permissionsで `Azure Service Management` のpermissionを追加し、管理者に"Grant admin consent"を押してもらう("Admin consent required"がnoになっていてもこの作業が必要になるらしい)
![ts4.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952758/bbcb1720-8226-f3d6-b3af-80f9e7608ab2.jpeg)


### Blazor
[こちらのMicrosoftの記事](https://learn.microsoft.com/ja-jp/aspnet/core/blazor/security/webassembly/standalone-with-azure-active-directory)を参考にBlazorでAADログインするアプリを作成します。

先ほど作成したサービスプリンシパルのClientIdとAADのtenantIDが必要になります。ログイン付きのBlazorアプリはtemplateを利用して下記のコマンドで作成できます。

```bash
dotnet new blazorwasm -au SingleOrg --client-id "{CLIENT ID}" -o {APP NAME} --tenant-id "{TENANT ID}"
```

AzureのRestAPIを実行するためには、API実行時にAccessTokenを一緒に送信する必要があります。HttpClientに仕組みが用意されていて、[AuthorizationMessageHandler](https://learn.microsoft.com/ja-jp/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-7.0#attach-tokens-to-outgoing-requests)を利用します。(scopeはBlobStorageやKeyValutでは変わります)

```csharp
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
public class AzureResourceAuthorizationMessageHandler : AuthorizationMessageHandler
{
    public AzureResourceAuthorizationMessageHandler(IAccessTokenProvider provider,
        NavigationManager navigationManager)
        : base(provider, navigationManager)
    {
        ConfigureHandler(
            authorizedUrls: new[] { "https://management.azure.com" },
            scopes: new[] { "https://management.azure.com/.default" });
    }
}
```

Program.csでAzureResourceAuthorizationMessageHandlerをServiceProviderに登録し、作成したMessageHandlerをHttpClientにAddします

```csharp
var builder = WebAssemblyHostBuilder.CreateDefault(args);
....
builder.Services.AddScoped<AzureResourceAuthorizationMessageHandler>();
builder.Services.AddHttpClient("AzureDataFactoryClient", client => client.BaseAddress = new Uri("https://management.azure.com"))
    .AddHttpMessageHandler<AzureResourceAuthorizationMessageHandler>();
```

あとはADFのpipelineを実行するAPIを作成してRazorページで実行します。APIのReferenceは[こちら](https://learn.microsoft.com/en-us/rest/api/datafactory/pipelines/create-run?tabs=HTTP)にあります。ADFのpipelineのparameterはBodyでjsonで送信します

```csharp
 public class DataFactoryClient
    {
        private readonly HttpClient _httpClient;
        public DataFactoryClient(IHttpClientFactory HttpClientFactory)
        {
            _httpClient = HttpClientFactory.CreateClient("AzureDataFactoryClient");
        }

        public async Task<string> Run(string factoryName, string pipelineName, string tables, DateTime dateFrom, DateTime dateTo)
        {
            var response = await _httpClient.PostAsync(
                $"subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.DataFactory/factories/{factoryName}/pipelines/{pipelineName}/createRun?api-version=2018-06-01",              
                new StringContent(JsonSerializer.Serialize(
                    new
                    {
                        Tables = tables,
                        DateTimeFrom = dateFrom,
                        DateTimeTo = dateTo
                    },
                    new JsonSerializerOptions { WriteIndented = true })
                ));
            return await response.Content.ReadAsStringAsync();
        }
    }
   
```



 ```xml
<PageTitle>Index</PageTitle>

<AuthorizeView>
    <Authorized>
        <EditForm Context="sss" Model="@model" OnValidSubmit="@RunPipeline" >
        <DataAnnotationsValidator />
        <ValidationSummary />
        <InputText id="Table" @bind-Value="model.Table" />
        <InputDate id="DateFrom" @bind-Value="model.DateFrom"/>
        <InputDate id="DateTo" @bind-Value="model.DateTo"/>
        <button type="submit">Submit</button>
            <p> @response </p>
        </EditForm>
    </Authorized>
    <NotAuthorized>
        Please Login
    </NotAuthorized>

</AuthorizeView>

@code {
    private PipelineRunModel model = new();
    private string? response;
    private async Task RunPipeline()
    {
        var res = await DataFactoryClient.Run(model.DataFactoryName,
                model.PipelineName,
                model.Table, 
                model.DateFrom,
                model.DateTo
        );
        response = res;
    }
}
```

### Azure Data Factory
詳細は割愛します。画面での入力が直接ADFのparameterとして渡るのでADF側はそれを考慮して作ります。
- Parameterのテーブル名を直接queryで利用しない入れない(テーブル一覧とかはLookUp Activityとかで取得し、ParameterのテーブルはFilter Activityで利用する)
- 日付ParameterのValidation
- ADFで利用するDatabase Userはselectのみの権限かつ必要なテーブルのみ。


Publicに公開すると危険な構成ですが、社内利用であればこんな構成もありなのかなーと思いました。


最後までお読み頂きありがとうございました。
bitFlyerでは[SREエンジニア](https://bitflyer.com/ja-jp/recruit/offers/224)を募集しています。
またSREだけではなく広く[サーバーサイドのエンジニア](https://bitflyer.com/ja-jp/recruit)を募集しています。
C#でのサービス開発にご興味ある方はぜひご応募ください！