この記事は 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が実行されます。実際のアプリは複数テーブルに対応しています。
構成は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
を入力
-
"selec the tokens you would like to be issued by the authorization endpoint:"の箇所では
ID tokens
を選択する
-
API permissionsで
Azure Service Management
のpermissionを追加し、管理者に"Grant admin consent"を押してもらう("Admin consent required"がnoになっていてもこの作業が必要になるらしい)
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#でのサービス開発にご興味ある方はぜひご応募ください!