📝

Ocelotを使ってAzure BlobにGETリクエストをプロキシする

6 min read

たとえば画像をAzure Blobに置いて画面に表示するときに、画像を表示するために認証とか認可とかの処理をしたいことはよくある。なので、ASP.NET Coreの中でAzure Blob SDKとかを使ってバイナリでとってきて、MVCのFileContentで返すとかはよくやる方法の一つ。

でもそれをすると、etagとかlast-modifiedのレスポンスヘッダーをいちいちつけたりするのが煩わしくって、ブラウザキャッシュを有効に使えなくって、変わってもいないのに毎回リクエストが飛んで非効率。

そんな時にOcelot

https://github.com/ThreeMammals/Ocelot

Ocelot is a .NET API Gateway. This project is aimed at people using .NET running a micro services / service oriented architecture that need a unified point of entry into their system. However it will work with anything that speaks HTTP and run on any platform that ASP.NET Core supports.

って書いてある通り、ASP.NET Coreの中に組み込めるAPI Gatewayの実装。こいつを使って、リクエストをAzure Blobに流して、結果を返すことで、Azure Blobから返ってくるetagとかlast-modifiedをそのまま呼びもとに返せる。また前後に処理が挟めるので独自に認可処理とかHTTPキャッシュの微妙な調整とかもできる。

サンプル作った

サンプルを作ったので以下に置いておく。

https://github.com/k-maru/azure-blob-proxy-sample

ここではいくつかポイントだけ取り上げる。

UseOcelot().Wait()は最後に追加する

なんかいろんなOcelotを解説したブログを見てると、Middlewareパイプラインの途中にUseOcelot().Wait()を書いてるのがあるけど、これをすると、Ocelotのルーティングに引っかからないとそのままパイプラインが終了して404が返ってしまう。なので、UseOcelot().Wait()は最後に書くこと。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    // app.UseOcelot().Wait(); <-- ここだとルートにアクセスしても404になってしまう。。

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    });

    app.UseOcelot().Wait(); // <-- ここに書く
}

Blob側の認証用のヘッダーをくっつける

Blob側の認証を行うためにAuthorizationヘッダーをくっつける必要がある。OcelotにはDelegatingHandlerを挟みこんでリクエスト/レスポンスを改変する機構があるのでそれを利用する。

Authorizationヘッダーにくっつける内容は次に詳しい。

https://docs.microsoft.com/ja-jp/rest/api/storageservices/authorize-with-shared-key

サンプルでの実装のAzureStorageAuthenticationHelperは次を参考にして作った。

https://github.com/Azure-Samples/storage-dotnet-rest-api-with-auth/blob/master/StorageRestApiAuth/AzureStorageAuthenticationHelper.cs

これでは403が出てうまく動かなかったときがあるので、本チャンのSDKのコードも参考にした。

https://github.com/Azure/azure-sdk-for-net/blob/master/sdk/storage/Azure.Storage.Common/src/Shared/StorageSharedKeyPipelinePolicy.cs

こいつを呼び出すDelegatingHandlerを作って、Ocelotから呼ぶようにしている。ちなみにx-ms-dateヘッダーもつけるようにしている。azuriteではこれらのヘッダーがなくても動いたが、本物では動かなかった。まぁ、ドキュメントでも必須って書いてあるからazuriteが間違ってるようだ。x-ms-versionヘッダーはとりあえずサンプルに乗っていたのでつけているだけ。たぶん必須ではない。と思われる。

Azuriteと本物のURLにつけられるアカウント名の位置の違いを吸収する

AzuriteではBlobにアクセスするアカウント名はURLのFragmentのルートに配置される。それに対して、本物はドメインに含まれる。この違いによって何に影響するかというと、DownstreamPathTemplateの値をAzuriteで動かすときと、本物で動かすときとで書き換える必要がある。

// Azurite で動かすとき
"Routes": [
    {
      "DownstreamPathTemplate": "/devstoreaccount1/profileimage/{id}",
      "DownstreamScheme": "http",
      // ...
    }
]

// 本物で動かすとき
"Routes": [
    {
      "DownstreamPathTemplate": "/profileimage/{id}",
      "DownstreamScheme": "http",
      // ...
    }
]

まぁ、本番では環境変数で上書きするとか、ローカル開発の時は手で書き換えるとか方法はあるんやけど、設定漏れ/修正漏れが大いに予想できる。本番時で値が変わるから設定ファイルに外だししてるんやけど、変更点は無いに限る。0に近ければ近いほうがよい。

ということで、これもDelegatingHandlerで吸収してみる。AppendAccountNameToTopSegmentHandlerがそれ。なんてことはない、実行時にセグメントのトップにアカウント名をつけてるだけ。で、このDelegatingHandlerDevelopmentモードのときだけ動くようにocelot.Development.jsonにだけ指定している。

これでDownstreamPathTemplateの違いは吸収できた。まだDownstreamHostAndPortsとかDownstreamSchemeとかの違いは残るけど、これは仕方ない・・と思う。

不要なレスポンスヘッダーを削除する

Azure Blobからのレスポンスをそのまま返しているので、いろんなレスポンスヘッダーがついてくる。結構詳細なのまでついてくる。これはちょっとキケン。

accept-ranges: bytes
content-length: 24832
content-md5: MHRO0LmRaEiMrzYB6NWEKg==
content-type: image/png
date: Sun, 04 Apr 2021 04:38:36 GMT
etag: "0x21276E18BA7F960"
last-modified: Sun, 04 Apr 2021 04:38:10 GMT
server: Azurite-Blob/3.11.0
x-ms-blob-content-md5: MHRO0LmRaEiMrzYB6NWEKg==
x-ms-blob-type: BlockBlob
x-ms-copy-completion-time: Sun, 04 Apr 2021 04:38:10 GMT
x-ms-copy-id: 5cafaa91-c326-4a9b-af80-109ff1c30747
x-ms-copy-progress: 24832/24832
x-ms-copy-source: http://127.0.0.1:10000/devstoreaccount1/profileimage/boy_01.png?sv=2018-03-28&st=2021-04-04T04%3A23%3A10Z&se=2021-04-11T04%3A23%3A10Z&sr=c&sp=rl&sig=Tc34wSxEg7%2F7JSThhkPOPhmobV6%2F2cp0TQNk4%2BhICIk%3D
x-ms-copy-status: success
x-ms-lease-state: available
x-ms-lease-status: unlocked
x-ms-request-id: be699624-1c29-48a0-90a7-e44ec14cfd5b
x-ms-server-encrypted: true
x-ms-version: 2020-06-12
x-powered-by: ASP.NET

ということでこれも消すとする。いつものごとくDelegatingHandlerで。それがRemoveUnexpectedBlobResponseHeaderHandlerx-ms-で始まるヘッダーとserverヘッダーを消している。

そうすると次のような感じ。

accept-ranges: bytes
content-length: 24832
content-md5: MHRO0LmRaEiMrzYB6NWEKg==
content-type: image/png
date: Sun, 04 Apr 2021 04:42:24 GMT
etag: "0x21276E18BA7F960"
last-modified: Sun, 04 Apr 2021 04:38:10 GMT
server: Microsoft-IIS/10.0
x-powered-by: ASP.NET

serverはAzure Blobから返ってきたものは消えたけど、ついてないからIIS Expressが新たにつけたものが乗ってきてる。まぁ、ここらへんはASP.NET Core側の世界の話なんで別途消すとしよう。