Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

DioDocsとAzure Functionsで帳票生成サービスを作ろう!

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

目次

Azure Functions上での実現検証

 では実現性の検証に入りましょう。ここでは次の順で検証していきます。

  1. Hello, Azure Functions!
  2. BLOBサービスからExcelテンプレートの読み込み
  3. BLOBサービスへの書き出し
  4. POSTデータの利用
  5. Azure Functionsのプラン選択

Hello, Azure Functions!

 さて、ここから先を実施するには事前にVisual Studio上でAzure開発のサポートが導入されている必要があります。インストーラーから[変更]を選択して「Azureの開発」がインストールされていることを確認してください。

「Azureの開発」がインストール済みであることを確認
「Azureの開発」がインストール済みであることを確認

 正しくインストールされていれば、Visual Studioから新しいプロジェクトを作成する際、下図のようにAzure Functionsのプロジェクトテンプレートが選択できるはずです。ここから新しいプロジェクトを作成しましょう。

Azure Functionsのプロジェクトテンプレートを選択
Azure Functionsのプロジェクトテンプレートを選択

 ここでは「InvoiceFunction」という名前のプロジェクトを新たに作成します。

「プロジェクト名」は「InvoiceFunction」と設定
「プロジェクト名」は「InvoiceFunction」と設定

 するとトリガーの選択が求められます。何を「トリガー」にFunctionを実行するか決定します。ここではHTTPのリクエストを想定しているので「Http trigger」を選択し[作成]を押下しましょう。

トリガーの設定
トリガーの設定

 プロジェクトが作成されたら早速ローカルで実行してみましょう。実行してしばらくするとコンソールで次のように表示されるはずです。

コンソールでの表示
コンソールでの表示

 Function1という関数が表示されているのが見て取れます。そのアドレスをブラウザに入力してみましょう。

URLをブラウザに入力
URLをブラウザに入力

Please pass a name on the query string or in the request body

と表示されています。どういうことでしょうか? Functions1.csのコードを見てみましょう。

Functions1.cs
[FunctionName("Function1")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    string name = req.Query["name"];

    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    name = name ?? data?.name;

    return name != null
        ? (ActionResult)new OkObjectResult($"Hello, {name}")
        : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}

 デフォルトの実装ではクエリ文字列かPOSTのBodynameというパラメーターがあればそれを表示し、なければエラーにしているようですね。

 という訳で先ほどのURLに?name=Worldを付与して実行してみましょう。

URLに「? data-cke-saved-name=World」を付与して実行 name=World」を付与して実行
URLに「?name=World」を付与して実行

 無事実行されたようです。ではこれを実際にAzure上で実行してみましょう。「InvoiceFunction」プロジェクトを右クリックし[発行]を押下しましょう。

[発行]を押下
[発行]を押下

 まずAzure Functionsを実行するプランを選択します。ここでは「Azure App Serviceプラン」を新規に作成しています。理由は後ほど説明します。

実行するプランを選択
実行するプランを選択

 名称に「InvoiceFunction」を指定し、リソースグループ・ホスティングプラン・Azure Storage共にすべて新規で作成します。後々リソースグループごとすべて削除することでうっかり削除漏れが……といったことを防ぐため、ここではすべて新しく作成しています。

新規作成
新規作成

 指定が済んだら[作成]を押し、Azure上にリソースを作成しましょう。やや時間がかかります。

リソースを作成中
リソースを作成中

 作成が完了すると発行画面が表示されるので、そのまま[発行]します。

発行する
発行する

 発行が済んだら、実際にAzureのポータルから正しく配置されているか確認しましょう。ポータルから「リソースグループ」を選び、先ほど作成したリソースグループを選択します。

 すると「InvoiceFunction」という雷マークをしたAzure Functionのリソースが表示されるでしょう。

Azure Functionのリソースが表示される
Azure Functionのリソースが表示される

 無事配備されているようですね。ここから下図の通り「Function1」を開いてください。「関数のURLを取得」という項目が開いて少し経つと表示されます。最初は表示されていないと思いますが慌てる必要はありません。

「Function1」を開いた画面
「Function1」を開いた画面

 ここからURLをコピーし、末尾に&name=Workd!を付与してWebブラウザで開いてみてください。これで無事Azure Functionsデビューできたはずです。

BLOBサービスからExcelテンプレートの読み込み

 では続いてBLOBサービスにExcelをアップロードして関数実行時に読み込んでみましょう。

 まずはExcelのテンプレートを保管する「コンテナ」を作成します。

 Azure BLOB Storageには「ストレージアカウント」と「コンテナ」という概念があります。Azure上には複数のストレージアカウントを作成することが可能で、そのストレージ内に複数の用途のコンテナを作成することができます。コンテナの中にはフォルダやファイルを作成できるので、コンテナが論理的な1つのストレージと考えていいかもしれません。

 実はAzure Functionsを配備する際に、Azure Functionsの実行ファイルなどを保管するため、1つのストレージアカウントと2つのコンテナが作成されています。

 今回はそのストレージアカウントに新しく次の2つのコンテナを追加して利用します。

  • 帳票テンプレートとなるExcelを保管する「templates」コンテナ
  • 作成された帳票PDFを保管する「reports」コンテナ

 ではポータルを開き、InvoiceFunctionリソースグループ内のストレージアカウントを開いてください。その中から「BLOB」を選択します。

「BLOB」を選択
「BLOB」を選択

 すでにコンテナが2つあるのが見て取れますね。

すでにコンテナが2つ存在している
すでにコンテナが2つ存在している

 ここで[+コンテナー]を押下して次の2つのコンテナを追加してください。

  • 「templates」
  • 「reports」
コンテナを追加
コンテナを追加

 作成したらtemplatesコンテナを開いて適当なファイルをアップロードしてください。ここでは「Invoice.xlsx」というファイルをアップロードしました。こちらをダウンロードして利用します。

ファイルをアップロード
ファイルをアップロード

 それではVisual Studioを開き、新しい関数を追加しましょう。プロジェクトを右クリックし[追加]から[新しいAzure 関数]を選択します。

[新しいAzure 関数]を選択
[新しいAzure 関数]を選択

 「CreateReport」という名前を指定し、Http triggerを指定して作成してください。

名前を指定
名前を指定
Http triggerを指定
Http triggerを指定

 この関数に実装していきます。

 Azure FunctionsではBLOB Storageにアクセスするにあたり、関数内から明示的に取得しに行く方法と、「バインド」という機能を利用して関数の引数に受け渡してもらう方法の2つがあります。詳細は以下をご覧ください。

 利用するBLOBが実装時に自明なのであればバインドを利用したほうがいいでしょう。

 新しく作成したCreateReport.csファイルを開き、次の通り実装を修正します。

CreateReport.cs
[FunctionName("CreateReport")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    [Blob("templates/Report.xlsx", FileAccess.Read)]Stream input,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    return new OkObjectResult($"templates/Report.xlsx Size:{input.Length}");
}

 次の引数が追加されています。

[Blob("templates/Report.xlsx", FileAccess.Read)]Stream input

 引数にBlobAttributeを指定することで、宣言されたBLOBを操作するためのStreamを受け渡すことができます。コンテナの名称とファイル名とアクセスのモード(ここでは読み取り専用)を指定しているのが見て取れるでしょう。

 ここではStreamからファイルのサイズを取得しています。実行してみましょう。実行時は先ほどのFunction1と同じようにポータルからURLを取得します。下図のようにファイルサイズが表示されれば成功です。

成功するとファイルサイズが表示される
成功するとファイルサイズが表示される

BLOBサービスへの書き出し

 それでは今度は書き出しを実装してみましょう。

 書き出しも、書き出すBLOBが実行前に決定できるのであれば、書き出し可能なStreamをバインドすることができます。

 ただ今回のケースではCreateReport関数が実行される都度ファイルを作成します。そのため実行時にファイルを決定することとします。その方法を紹介しましょう。以下をご覧ください。

[FunctionName("CreateReport")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    [Blob("templates/Invoice.xlsx", FileAccess.Read)]Stream input,
    [Blob("reports", FileAccess.Write)] CloudBlobContainer outputContainer,
    ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    await outputContainer.CreateIfNotExistsAsync();

    var fileName = $"{Guid.NewGuid()}.txt";
    var cloudBlockBlob = outputContainer.GetBlockBlobReference(fileName);
    using (var writer = new StreamWriter(await cloudBlockBlob.OpenWriteAsync()))
    {
        writer.Write("Hello, Azure Functions!");
    }


    return new OkObjectResult($"fileName:{fileName}");
}

 次の新たな引数で、関数を追加しました。

[Blob("reports", FileAccess.Write)] CloudBlobContainer outputContainer,

 ポータルからreportsというコンテナを追加しました。ここではコンテナ内部のBLOBではなくBLOBコンテナそのものを引数に受け渡してもらいます。そして新しいBLOBを作成して追加しています。BLOBを追加する際、名称としてGUIDを生成して利用しています。

 GUIDを作成後、次の通りBLOBを作成しそこに文字列を書き出しています。

var fileName = $"{Guid.NewGuid()}.txt";
var cloudBlockBlob = outputContainer.GetBlockBlobReference(fileName);
using (var writer = new StreamWriter(await cloudBlockBlob.OpenWriteAsync()))
{
    writer.Write("Hello, Azure Functions!");
}

 そしてレスポンスとして新しく作成されたファイルの名称を返却しています。

return new OkObjectResult($"fileName:{fileName}");

 実行するとGUIDが表示されます。そのGUIDのファイルをreportsコンテナで探して開いてみましょう。「Hello, Azure Functions!」と表示されているのが確認できます。

「Hello, Azure Functions!」と表示されている
「Hello, Azure Functions!」と表示されている

 これで書き出しができるようになりました。

POSTデータの利用

 Functions側の実装課題はあとひとつ、帳票を作成するためのデータをどのように渡すかです。

 Functionsは「トリガー」と呼ばれる仕組みで実行されます。HTTPリクエストやBLOBファイルの作成、Queueへのコマンドの追加など多種多様なトリガーが用意されています。具体的には以下をご覧ください。

 今回の想定シナリオの場合、アプリケーションからの要求を受けて開始するためHTTPトリガーを利用します。HTTPトリガーで実行するのですから、帳票データはHTTPのBodyにJSONをのせて渡すのが自然でしょう。

 POSTデータは関数の引数で渡されているHttpRequestオブジェクトのBodyプロパティからStreamを取得できます。Bodyの値をそのまま先ほどの出力先BLOBにコピーしてみましょう。

var fileName = $"{Guid.NewGuid()}.txt";
var cloudBlockBlob = outputContainer.GetBlockBlobReference(fileName);
using (var output = await cloudBlockBlob.OpenWriteAsync())
{
    req.Body.CopyTo(output);
}

 では実行してみましょう。今度はJSONをPOSTする必要があるので、新しくコンソールアプリケーションを作成し、次のコードを記述して実行しましょう。endpointには作成した関数の実行URLを記載してください。

var endpoint = "YOUR_URL";
var jsonContent =
    new StringContent(
        "{\"Message\":\"Hello, Azure Functions with JSON!\"}",
        Encoding.UTF8,
        "application/json");

var httpClient = new HttpClient();
var response = await httpClient.PostAsync(endpoint,jsonContent);

Console.WriteLine(await response.Content.ReadAsStringAsync());

 実行後、ポータルからBLOBを確認すると、ちゃんとJSONが書き出されているのが見て取れるでしょう。

JSONが書き出されている
JSONが書き出されている

 これで関数実装のためのすべての課題がクリアされました。

Azure Functionsのプラン選択

 さて、関数を実装するための課題は解決されましたが、Azure Functions側の大切な課題がもうひとつ残っています。「どのプランで実行するか」です。現在のAzure Functionsは大きく分けて次の3種類の課金プランがあります。

  • 従量課金プラン
  • App Serviceプラン
  • Premiumプラン

 基本的に上から下にいくほど実行性能が高く、実行コストも高くなります。

 そこで実際にDioDocsでExcelからPDFを生成し、レスポンスタイムを計測してみました。計測に利用したプログラムは次のような関数です。

[FunctionName("CreatePdfForStreamNull")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    [Blob("templates/Report.xlsx", FileAccess.Read)]Stream input,
    ILogger log)
{
    var workbook = new Workbook();
    workbook.Open(input);
    workbook.Save(Stream.Null, SaveFileFormat.Pdf);
    return new OkObjectResult("OK");
}

 Excelを開いてPDFをNullデバイスへ書き出しています。この関数をBenchmarksDotNetを利用して各プランで計測してみた結果が次の通りです。

計測方法 Mean Error StdDev Median Rank
ローカルPC 212.6 ms 21.49 ms 63.35 ms 260.3 ms 2
従量課金プラン 1069.6 ms 315.26 ms 899.46 ms 644.1 ms 5
S1(App Service) 683.0 ms 147.74 ms 430.97 ms 551.1 ms 4
S2(App Service) 353.3 ms 47.22 ms 138.50 ms 382.7 ms 3
S3(App Service) 327.2 ms 44.43 ms 130.99 ms 325.6 ms 3
EP1(Premium) 325.3 ms 61.07 ms 180.07 ms 341.3 ms 3
EP2(Premium) 180.2 ms 30.77 ms 89.75 ms 144.5 ms 1
EP3(Premium) 173.2 ms 19.98 ms 58.90 ms 176.0 ms 1

 一番上は参考値のローカルPCでの計測です。ローカルPCのスペックは以下の通りです。

  • CPU:Core i7-7700T
  • メモリ:16G

 この実行結果を見る上でもっとも重要なのは、従量課金プランの実行性能です。

 他のプランに比べて大幅にレスポンスタイムで劣ります。個別に実行時間を見るとかなり大きな波があります。従量課金プランの場合、継続して負荷をかけると、インスタンスがスケールアウトします。しかし個別のインスタンスについてはCPUは1コアでメモリーは1.5Gで固定です。また上記のベンチマークはシングルスレッドで逐次実行しており、スケールアウトでは性能は改善しません。

 またS1プランを見てみると、これもあまりS2以上のプランと比較すると速度が出ていません。そしてS1プランのメモリーは1.75Gです。

 個別のレスポンスタイムを見てみると、従量課金プランの場合、300ms~600msが連続する中に1000ms~3000msの大きな値が20%~30%程度の割合で含まれています。

 おそらくDioDocsを実行するのに従量課金プランやS1プランでは性能が不足しているのでしょう。

 そのため、ある程度の性能を見込む場合はS2以上のプランを選んだほうが良さそうです。具体的にどのプランにするかは、個々の帳票生成次第なので別途評価が必要となるでしょう。従量課金プラン以外の場合、利用していない時間帯も継続的に課金されることになるため、その場合はプランを負荷状況に応じて調整する必要もあるかもしれません。


  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

バックナンバー

連載:クラウド時代にマッチする、ドキュメント生成・更新APIライブラリ「DioDocs(ディオドック)」
All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5