LoginSignup
1
2

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-12-20

この記事は bitFlyer Advent Calendar 2022 の 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

構成は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

  • "selec the tokens you would like to be issued by the authorization endpoint:"の箇所では ID tokens を選択する
    ts3.jpeg

  • API permissionsで Azure Service Management のpermissionを追加し、管理者に"Grant admin consent"を押してもらう("Admin consent required"がnoになっていてもこの作業が必要になるらしい)
    ts4.jpeg

Blazor

こちらのMicrosoftの記事を参考にBlazorでAADログインするアプリを作成します。

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

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

AzureのRestAPIを実行するためには、API実行時にAccessTokenを一緒に送信する必要があります。HttpClientに仕組みが用意されていて、AuthorizationMessageHandlerを利用します。(scopeはBlobStorageやKeyValutでは変わります)

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します

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はこちらにあります。ADFのpipelineのparameterはBodyでjsonで送信します

 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();
        }
    }
   
<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エンジニアを募集しています。
またSREだけではなく広くサーバーサイドのエンジニアを募集しています。
C#でのサービス開発にご興味ある方はぜひご応募ください!

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2