Azure Functionsのお勉強メモ(10)Blob Storageにファイルが登録された時に処理を実行する(Queue利用編)

最近、Azure Functionsのお勉強をチマチマと始めました。色々と分からないことが多かったのでお勉強メモをまとめて記します。

どこまで続くかわからないお勉強メモ。今日は10回目です。前回・今回はBlob Storageにファイルが登録された時に処理を実行することをやってみました。前回うまくできなかったものを解決してみます。過去のものは以下を参照。

前回の振り返り

前回は、Blob Storageにファイルが登録された時にEvent Gridトリガーを使い、そこから画像変換処理を実行しようとしていました。しかしながら、出力ファイル名がうまく設定できなかったのが課題として残りました。

今回はBlob Storageにファイルが登録されると、以下の順番で処理をするように実行するようにします。

  1. Event Gridトリガーで処理を呼び出す
  2. 対象のファイル名をQueueに登録するだけのFunctionを実行する
  3. 別のFunctionにてQueueトリガーで処理を呼び出す
  4. 呼び出されたFunctionでBlobからファイルを読み込み、画像変換処理を実行する

それぞれ詳細を見ていきます。

Event Gridトリガーで処理を呼び出す

ここの部分は前回と同じです。マイクロソフトがドキュメントをしっかり書いているので、それを見ると良いかと思います。

docs.microsoft.com

対象のファイル名をQueueに登録するだけのFunctionを実行する

Queue Storage出力バインディングを使って対象のファイル名をQueueに登録する処理を実装します。出力バインディングについてはドキュメントがありますのでそれを参考にすると良いかと思います。

docs.microsoft.com

こんな感じの実装になります。

@QueueOutput(name = "queue", queueName = "myqueue", connection = "MyQueueConnection") OutputBinding<String> queue

値を設定するにはsetValueメソッドを使えばできます。アップロードしたファイルのURLはEvent GridトリガーのPOJOパラメータであるeventに対してevent.data.get("url")を実行することで取得できますので、それを使って以下のような実装をすれば良さそうです(サブディレクトリ切られた場合はもう少し考える必要あり)。

URL url = new URL(event.data.get("url").toString());
String filename = Paths.get(url.getPath()).getFileName().toString();
queue.setValue(filename);

これでこのFunctionでやることは終了です。

別のFunctionにてQueueトリガーで処理を呼び出す

新しくFunctionを作成し、Queueトリガーを使ったものを作ります。このFunctionで画像変換処理を実装します。

Queueトリガーは以下のドキュメントを見れば良いでしょう。

docs.microsoft.com

呼び出されたFunctionでBlobからファイルを読み込み、画像変換処理を実行する

画像変換処理の実装です。ほとんど同じようなサンプルがドキュメントに記載されていますのでそれに目を通しておきます。

docs.microsoft.com

入力ファイル名は{queueTrigger}を使えばよく、出力ファイル名の指定も{queueTrigger}を使ってpath = "thumbnails/s-{queueTrigger}"という感じで設定できます。

実装例としては、以下のようなものになりました。

public class ImageResizeHandler extends FunctionInvoker<byte[], byte[]> {
  @FunctionName("ImageResize")
  @StorageAccount("MyStorageAccount")
  @BlobOutput(name = "target", path = "thumbnails/s-{queueTrigger}", dataType = "binary")
  public byte[] imageResizeHandler(
      @QueueTrigger(name = "msg", queueName = "myqueue", connection = "MyQueueConnection")
          String msg,
      @BlobInput(name = "file", dataType ="binary", path = "images/{queueTrigger}") byte[] file,
      final ExecutionContext context) throws IOException {
    byte[] resizeImage = ResizeImage.resize(file);
    return resizeImage;
  }
}

アプリケーション設定の設定

Azure Blob StorageやQueue Storageに接続するために、Azure Functionsのアプリケーション設定を設定する必要があります。下図で赤枠で囲った部分のようにMyStorageAccountMyQueueConnectionに接続文字列を設定しておきます。

f:id:miyohide:20210516160943p:plain

実行

実際にファイルをアップロードしてみます。

f:id:miyohide:20210516161128p:plain

@BlobOutputにて指定したthumbnailsコンテナーを参照すると、アップロードしたファイル名の前にs-がついたファイルが作成されていることが確認できます。

f:id:miyohide:20210516161420p:plain

ソース

現時点でのソースです。そろそろ整理しよう。

github.com

Azure Functionsのお勉強メモ(9)Blob Storageにファイルが登録された時に処理を実行する(未完成)

最近、Azure Functionsのお勉強をチマチマと始めました。色々と分からないことが多かったのでお勉強メモをまとめて記します。

どこまで続くかわからないお勉強メモ。今日は9回目です。今回はBlob Storageにファイルが登録された時に処理を実行することをやってみました。今回のものは未完成で、いくつか課題が残っています。過去のものは以下を参照。

お題

お題として、Blob Storageに画像ファイルが登録された時にサムネイルを作る処理を実装してみます。参考になりそうなものとして、マイクロソフトC#の実装例を公開しています。

docs.microsoft.com

上記の例ではEvent Gridを使っています。これは、Blob Triggerの以下の記述に起因しているものと思われます。

ストレージ ログは "ベスト エフォート" ベースで作成されます。 すべてのイベントがキャプチャされる保証はありません。 ある条件下では、ログが欠落する可能性があります。

docs.microsoft.com

そこで私もEvent Triggerを使うことにします。

サムネイルを作成する

Javaでサムネイルを作成する方法はいくつか方法があるかと思いますが、Azure Functions上では任意のミドルウェアImageMagickとか)をインストールすることはできないので、Pure Javaで実装されたライブラリを利用することにしました。以下の記事にライブラリの紹介がありますが、私はThumbnailatorを使うことにしました。特に深い思いはありません。

www.baeldung.com

以下のようなクラス・メソッドを作りました。

public class ResizeImage {
    public static byte[] resize(byte[] original) throws IOException {
        BufferedImage image = ImageIO.read(new ByteArrayInputStream(original));
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        Thumbnails.of(image)
                .scale(0.50)
                .outputFormat("PNG")
                .toOutputStream(out);
        return out.toByteArray();
    }
}

Event Grid Triggerを使うためのJava実装

JavaでEvent Grid Triggerを使う方法はマニュアルに書かれていますが、@EventGridTriggerを使い、受け取るイベントのJSONパラメータを格納するためのクラスを作っておけばOKです。

docs.microsoft.com

登録されたファイルをBlob Storageの入力バインドとするためのJava実装

JavaでBlob Storageの入力バインドを設定する方法は以下のマニュアルに書かれています。

docs.microsoft.com

ここで問題となるのがpathの属性値です。アップロードされるファイル名は都度変わるので、固定値を記述することはできません。前述のC#の実装例を見ると{data.url}が指定されていたので、以下の実装にしました。

@BlobInput(name = "file", dataType = "binary", path = "{data.url}") byte[] content,

作成したサムネイル画像をBlob Storageの出力バインドとするためのJava実装(未解決)

JavaでBlob Storageの出力バインドを設定する方法は以下のマニュアルに書かれています。

docs.microsoft.com

ここで問題となるのがpathの属性値です。アップロードされるファイル名を流用したいのですが、{data.url}https://○○○/とURL全体が入るので不適切です。ここでは解決方法がわかりませんでしたので、

@BlobOutput(name = "target", path = "myblob/{DateTime}-sample.png")OutputBinding<byte[]> outputItem,

としました。

Event Gridを作成する

Blob Storageに紐づくEvent Gridを作成します。ここではAzureポータルで作成しました。参考となるドキュメントは以下です。

docs.microsoft.com

上記のドキュメントではWeb Hookなのですが、Azure Functionsに紐づけるには「エンドポイントの種類」を変えれば良いだけです。

f:id:miyohide:20210509141310p:plain

今回は、以下のことを実現するためにフィルター処理を設定する必要があります。

  • ファイルがアップロードされた時には処理を実行したい
  • 作成されたサムネイルを保存したときには処理を実行しない

フィルター処理については以下のドキュメントを参照しました。

docs.microsoft.com

結果、以下のような設定を実施しました。

f:id:miyohide:20210509141700p:plain

まとめ

以上のことを実装することで、Blob Storageに画像をアップロードするとサムネイルを作成することができました。実際に使うには、

  • アップロードされたファイルが画像であること
  • 元の画像フォーマット(jpegとかpngとかgifとか)と対応してサムネイルの画像フォーマットも変更する

などの対応が必要かと思います。

また前述の通り出力ファイル名がうまく設定できなかったので、その対応も必要です。

Azure Functionsのお勉強メモ(8)Blob Storage出力バインドを使ってBlobにファイルを出力する その3

最近、Azure Functionsのお勉強をチマチマと始めました。色々と分からないことが多かったのでお勉強メモをまとめて記します。

どこまで続くかわからないお勉強メモ。今日は8回目です。今回はBlob Storage出力バインドを使った実装の続きです。過去のものは以下を参照。

出力するファイル名に日時情報を付与する

前回まではBlob Storageに出力するファイル名が固定化したものでしたが、これに日時情報を付与したいと言うことはよくあります。これを実現するためには、バインド式というものを利用すると良さそうです。

docs.microsoft.com

日時のフォーマットは変えたいことがよくあるかと思います。フォーマットはここに載っているものが使えるようです。

docs.microsoft.com

Javaの場合は、以下のように実装すると良さそうです。

@BlobOutput(name = "target", path = "myblob/{DateTime:yyyy}/{DateTime:MM}/{DateTime:dd}/{DateTime:hhmmss}-sample.txt") OutputBinding<String> outputItem,

Azure StorageのBlobは以下のページ内の記述のように、フラットなパラダイムで組織化されます。

docs.microsoft.com

ただ、上記のように仮想的なフォルダを作成することはできます。上記の例では「年」/「月」/「日」という仮想的なフォルダを作成して以下にファイルを作成しています。実際にAzure Functionsのアプリを動かしたら以下のようにファイルが作成されました。

f:id:miyohide:20210502173113p:plain

日時情報は東日本リージョンでも日本時間(JST)ではなく、UTCのようです。

f:id:miyohide:20210502173312p:plain

負荷をかけてみる

この状態でabコマンドを使って負荷をかけてみます。結果としては、以下の通りでまだ失敗するリクエストが発生します(Failed requestsが0でない)。

Concurrency Level:      10
Time taken for tests:   51.195 seconds
Complete requests:      1000
Failed requests:        45
   (Connect: 0, Receive: 0, Length: 45, Exceptions: 0)
Non-2xx responses:      45
Total transferred:      237641 bytes
HTML transferred:       31515 bytes
Requests per second:    19.53 [#/sec] (mean)
Time per request:       511.945 [ms] (mean)
Time per request:       51.195 [ms] (mean, across all concurrent requests)
Transfer rate:          4.53 [Kbytes/sec] received

ランダムな文字列をファイル名に付与する

秒単位の日時情報だけでは複数のインスタンスで重複してしまうことがあるので、ランダムな文字列をファイル名に出力したいところです。これも先ほどのバインド式にある{rand-guid}を使えばできそうです。

docs.microsoft.com

Javaでの実装では以下のようになります。

@BlobOutput(name = "target", path = "myblob/{DateTime:yyyy}/{DateTime:MM}/{DateTime:dd}/{DateTime:hhmmss}-{rand-guid}-sample.txt") OutputBinding<String> outputItem,

実行する

再度abコマンドを使って負荷をかけてみます。結果は以下の通り。

Concurrency Level:      10
Time taken for tests:   43.493 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      239753 bytes
HTML transferred:       33000 bytes
Requests per second:    22.99 [#/sec] (mean)
Time per request:       434.925 [ms] (mean)
Time per request:       43.493 [ms] (mean, across all concurrent requests)
Transfer rate:          5.38 [Kbytes/sec] received

Failed requestsが0であることから失敗はなさそうです。

出力されたファイルを確認してみます。GUIDを付与しているのでやたらと長いですが、同一日時(ファイル名の先頭六文字が一致している)で複数のファイルが出力されていることがわかります。

f:id:miyohide:20210502175034p:plain

Azure Functionsのお勉強メモ(7)Blob Storage出力バインドを使ってBlobにファイルを出力する その2

最近、Azure Functionsのお勉強をチマチマと始めました。色々と分からないことが多かったのでお勉強メモをまとめて記します。

どこまで続くかわからないお勉強メモ。今日は7回目です。今回はBlob Storage出力バインドを使った実装の続きです。前回はローカル環境で試しましたが、今回は実際のAzure環境にデプロイしました。過去のものは以下を参照。

Azure環境へのデプロイ

前回作成したアプリケーションをAzure環境で動かすためには、アプリケーション以外に以下のものをデプロイ・設定する必要があります。事前に

gradlew azureFunctionsDeploy

でアプリケーションをデプロイした後に以下の設定を行います。

  • Azure Storageアカウント
  • Azure Functionsに環境変数OutputStorageを設定する

これらの設定はポータル画面から操作しても良いのですが、今回はAzure CLIBash)を使って操作してみます。

まずはAzure Storageアカウントの作成。

az storage account create --name 名前 --resource-group リソースグループ名 --location japaneast --sku Standard_LRS --encryption-services blob

docs.microsoft.com

Azure Storageアカウントに対する接続文字列を取得します。

az storage account show-connection-string -g リソースグループ名 -n 名前

docs.microsoft.com

接続文字列は長いので、変数に入れておきます。

connect_str=$(az storage account show-connection-string -g リソースグループ名 -n 名前 -o tsv)

docs.microsoft.com

環境変数を設定します。

az functionapp config appsettings set --name アプリ名 --resource-group リソースグループ名 --settings "OutputStorage=$connect_str"

docs.microsoft.com

ここまで設定しておくとアプリがAzure上で動くことを確認できました。

負荷をかけてみる

ブラウザで数回アクセスしても特に不具合は発生しなかったので、abコマンドでちょっと負荷をかけてみました。

ab -n 1000 -c 10 アプリのURL

すると、何件か例外を吐いている様子が確認できました。下の画像にあるException Rateが跳ね上がっている様子が確認できます。

f:id:miyohide:20210425165746p:plain

abコマンドのログでもFailed requestsが152と出力されていました。

(省略)
Concurrency Level:      10
Time taken for tests:   61.818 seconds
Complete requests:      1000
Failed requests:        152
   (Connect: 0, Receive: 0, Length: 152, Exceptions: 0)
Non-2xx responses:      152
(中略)
Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       34  101 115.4     87    2332
Processing:    99  432 1265.4    220   21840
Waiting:       98  430 1265.4    219   21840
Total:        153  532 1267.4    319   21879

Application InsightsのExceptionテーブルを見ると

The specified block list is invalid.

というメッセージを大量に目にしました。

確実なことは言えませんが、アプリは同時に同じBlobに対して書き込み処理を行なっていることが原因にありそうです。そこで以下の対応を行ってみます。

対策

手っ取り早い対策は、同時に実行する数を1つにすることです。以下のドキュメントにあるようにAzure Functionsの従量課金プランでは最大200インスタンスまで自動でスケールアウトされるようです。実際、上の画像でもサーバーの台数は5台で実行されていました。

docs.microsoft.com

スケールアウトの条件を変更することはできず、またその条件も示されていませんでした。ただ最大スケールアウトの台数を制限することはAzure Portalから設定することができました。

f:id:miyohide:20210425171622p:plain

この画面でスケールアウト制限の上限を1にしておくと良さそうです。

また、合わせてアプリケーションのhost.jsonにおいてHTTPトリガーのmaxConcurrentRequests1に設定する必要があります。

docs.microsoft.com

確認

再度abコマンドで実行してみます。以下のようにFailed requestsは0でした。

(省略)
Concurrency Level:      10
Time taken for tests:   98.539 seconds
Complete requests:      1000
Failed requests:        0
Total transferred:      240000 bytes
HTML transferred:       33000 bytes
Requests per second:    10.15 [#/sec] (mean)
Time per request:       985.393 [ms] (mean)
Time per request:       98.539 [ms] (mean, across all concurrent requests)
Transfer rate:          2.38 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       31   74  70.4     47    1098
Processing:   305  903  97.4    892    1574
Waiting:      305  903  97.4    891    1574
Total:        439  977 113.1    948    2038

Application InsightsのライブメトリックスでもException Rateは下に張り付いたままでした。

f:id:miyohide:20210425173305p:plain

Application Insightsでインスタンス数の経緯をcloud_RoleInstanceを使って見てましたが、再実行時は1のままでした。

f:id:miyohide:20210425173603p:plain

ソースコード

ここまでのソースコードは以下に載せています。

github.com

Azure Functionsのお勉強メモ(6)Blob Storage出力バインドを使ってBlobにファイルを出力する

最近、Azure Functionsのお勉強をチマチマと始めました。色々と分からないことが多かったのでお勉強メモをまとめて記します。

どこまで続くかわからないお勉強メモ。今日は6回目です。今回はBlob Storage出力バインドを使った実装をSpring Cloud Functionを使って実装しました。過去のものは以下を参照。

概要

これまでは主にFunctionsが動く契機となるTriggerを取り上げていましたが、Azure Functionsで抑えておきたい概念としてバインドという概念もあります。詳細は以下のドキュメントに書かれています。

docs.microsoft.com

ものすごく単純に言えば、予め定義しておいた入出力の設定といった感じでしょう。

バインドには色々なものがありますが、ここではBlob Storage出力バインドを使ってみます。

実装

環境

Blob Storageにファイルを出力するために、開発環境にAzure StorageエミュレーターであるAzuriteを使います。

docs.microsoft.com

Dockerイメージが用意されているので、今回はそれを使います。以下のようなdocker-compose.ymlファイルを作成しておくと良いかと思います。

version: '3'
services:
  storage:
    image: mcr.microsoft.com/azure-storage/azurite
    ports:
      - 10000:10000
      - 10001:10001
    environment:
      AZURITE_ACCOUNTS: "a1:k1;a2:k2"

上記の例ではアカウントキーを短いものにしていますが、今後の動作に不具合が生じることがあったので、実際には長い文字列(azuriteのデフォルトアクセスキーでOK)を指定した方が良いかと思います。

なお、本記事執筆時の最新バージョンであるAzure Storage Explorerの1.18.1には以下のIssueのようにカスタムアカウントへの接続に問題が発生するので、利用される場合は注意が必要です。

github.com

Functions

Azure Functionsの中身を実装してみます。以下のドキュメントを参考にしました。

docs.microsoft.com

接続先を@StorageAccountで指定し、出力パスを@BlobOutputpath属性で指定します。こんな感じで実装しました。

    @FunctionName("hello")
    @StorageAccount("OutputStorage")
    public HttpResponseMessage hello(
            @HttpTrigger(name = "req", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS)HttpRequestMessage<String> request,
            @BlobOutput(name = "target", path = "myblob/sample.txt") OutputBinding<String> outputItem,
            ExecutionContext context
            ) {
// 処理内容
}

注意点は以下2点。

  1. @StorageAccountで指定するのはDefaultEndpointsProtocolからはじまる接続文字列ではなく、環境変数名。ローカル実行の場合はlocal.settings.jsonにキーと値を設定する。
  2. Blobには予めコンテナ(上記の例ではmyblob)を作成しておく。

上記1.について@StorageAccountDefaultEndpointsProtocolからはじまる接続文字列を記述した場合、起動時に

Warning: Cannot find value named 'DefaultEndpointsProtocol=http;AccountName=a1(省略)QueueEndpoint=http://127.0.0.1:10001/a1;' in local.settings.json that matches 'connection' property set on 'blob' in '/xxxxx/function.json'. You can run 'func azure functionapp fetch-app-settings <functionAppName>' or specify a connection string in local.settings.json.

という文字列が出力され、Functions実行時に

[2021-04-18T06:19:39.526Z] System.Private.CoreLib: Exception while executing function: Functions.hello. Microsoft.Azure.WebJobs.Host: Storage account connection string 'AzureWebJobsDefaultEndpointsProtocol=http;AccountName=a1(省略)QueueEndpoint=http://127.0.0.1:10001/a1;' does not exist. Make sure that it is a defined App Setting.

と出力されます。

出力バインドに値を出力するには、setValueメソッドを使えば良さそうです。OutputBindingインターフェースには他にはgetValueメソッドしかないので、出力するにはsetValueメソッド一択でしょう。

docs.microsoft.com

こんな感じで実装しました。

public class HelloHandler extends FunctionInvoker<String, String> {
    @FunctionName("hello")
    @StorageAccount("OutputStorage")
    public HttpResponseMessage hello(
            @HttpTrigger(name = "req", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS)HttpRequestMessage<String> request,
            @BlobOutput(name = "target", path = "myblob/sample.txt") OutputBinding<String> outputItem,
            ExecutionContext context
            ) {
        context.getLogger().info("***** HTTP Trigger Start *****");
        outputItem.setValue("[" + LocalDateTime.now() + "] This is sample txt.");
        context.getLogger().info("***** HTTP Trigger End *****");
        return request.createResponseBuilder(HttpStatus.OK)
                .body("***** HTTP Trigger Response *****")
                .header("Content-Type", "application/json")
                .build();
    }
}

実行

今回はHTTP Triggerで実装しました。gradlew azureFunctionsRunでアプリを実行し、localhost:7071/api/helloにアクセスすると、以下の画面が出力されます。

f:id:miyohide:20210418162455p:plain

Blobを参照してみると、ファイルが作られていることがわかります。

f:id:miyohide:20210418162611p:plain

中身も意図したものが作成されています。

f:id:miyohide:20210418162653p:plain

再度localhost:7071/api/helloにアクセスすると、ファイルが上書きされていました。

f:id:miyohide:20210418162758p:plain

Azure Functionsのお勉強メモ(5)カスタムハンドラーを使ってRubyでTimer Triggerを実装する

最近、Azure Functionsのお勉強をチマチマと始めました。色々と分からないことが多かったのでお勉強メモをまとめて記します。

どこまで続くかわからないお勉強メモ。今日は5回目です。今回はRubyを使ったTimer Triggerの実装です。過去のものは以下を参照。

概要

今回の実装においてはカスタムハンドラーというものを使います。

docs.microsoft.com

前回の記事で実装したRubyを使ったHTTP Triggerの実装は、上記ドキュメント内にある「HTTPのみの関数」の機能を利用したものになります。

仕組みとしてはトリガーが実行されると、Azure Functionsの基盤がHTTP POSTリクエストを飛ばすようです。実際の処理はHTTP POSTリクエストに対応するメソッドで実装すれば良さそうです。

では実装してみます。

実装

実装は、前回のコンテナを使用したものを拡張します。

Timer Triggerの設定ファイルを作成する

以下のコマンドを実行してTimer Triggerの設定ファイルを作成します。

func new --name TimerExample --template "Timer trigger"

TimerExampleというディレクトリができ、その中にfunction.jsonが作成されます。scheduleの部分を適当なものに編集しておきます(ここでは10秒おきに実行する)。

{
  "bindings": [
    {
      "name": "myTimer",
      "type": "timerTrigger",
      "direction": "in",
      "schedule": "*/10 * * * * *"
    }
  ]
}

アプリケーションの実装

Timer Triggerを実行するHTTP POSTリクエストを実装します。こんな感じに実装しました。

  post '/TimerExample' do
    logger.info "----- Timer Schedule Start"
    header_data = request.env
    body_data = JSON.parse(request.body.read)
    content_type :json
    data = {
      "Outputs" => {
        "res" => { "body" => "abc" }
      },
      "Logs" => [header_data.to_s, body_data.to_s],
      "ReturnValue" => "hogehoge"
    }
    logger.info "----- Timer Schedule End"
    data.to_json
  end

単純にログを出力して、適当なデータを返すものです。戻り値の中にあるLogsに指定したものは実行時のログに出力されるようです。これは後ほどの実行ログをみて確認してみます。

開発環境の準備

Azure FunctionsにおいてはTimer Triggerを実行する場合、Azure Storageを用意する必要があります。開発環境においてはエミュレーターを利用すればOKです。

docs.microsoft.com

今回、アプリ自身もコンテナ化しているので、docker-compose.ymlを準備しておきます。以下のものを作成しました。

version: '3'
services:
  storage:
    image: mcr.microsoft.com/azure-storage/azurite
    ports:
      - "10000:10000"
      - "10001:10001"
  app:
    build: .
    ports:
      - "8080:80"
    depends_on:
      - storage
    environment:
      - AzureWebJobsStorage

環境変数の設定には、Docker Composeの機能を利用して.envファイルを用意します。この機能の詳細は以下のドキュメントを参照してください。

docs.docker.jp

設定値は以下のようにしました。

AzureWebJobsStorage="UseDevelopmentStorage=true;DevelopmentStorageProxyUri=http://storage"

DevelopmentStorageProxyUriに設定するURLはDocker Composeのサービス名を指定しておきます。

Dockerfileも修正し、環境変数AzureWebJobsStorageを見るようにします。一番最後の行にENV AzureWebJobsStorage=$AzureWebJobsStorageを追加しました。

FROM mcr.microsoft.com/azure-functions/dotnet:3.0-appservice
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

RUN apt update && \
    apt install -y \
    git \
    autoconf \
    bison \
    build-essential \
    libssl-dev \
    libyaml-dev \
    libreadline6-dev \
    zlib1g-dev \
    libncurses5-dev \
    libffi-dev \
    libgdbm6 \
    libgdbm-dev \
    libdb-dev \
    && rm -rf /var/lib/apt/lists/*

RUN git clone --depth=1 https://github.com/rbenv/ruby-build && PREFIX=/usr/local ./ruby-build/install.sh && rm -rf ruby-build
RUN ruby-build 2.7.2 /usr/local

WORKDIR /home/site/wwwroot

COPY Gemfile /home/site/wwwroot/
RUN bundler install
COPY . /home/site/wwwroot/

ENV AzureWebJobsStorage=$AzureWebJobsStorage

実行

docker-compose up --buildで実行します。以下のようなログが出力されます。

Starting OpenBSD Secure Shell server: sshd.
info: Host.Triggers.Warmup[0]
      Initializing Warmup Extension.
(省略)
info: Host.Triggers.Timer[5]
      The next 5 occurrences of the 'TimerExample' schedule (Cron: '0,10,20,30,40,50 * * * * *') will be:
      04/11/2021 08:20:50+00:00 (04/11/2021 08:20:50Z)
      04/11/2021 08:21:00+00:00 (04/11/2021 08:21:00Z)
      04/11/2021 08:21:10+00:00 (04/11/2021 08:21:10Z)
      04/11/2021 08:21:20+00:00 (04/11/2021 08:21:20Z)
      04/11/2021 08:21:30+00:00 (04/11/2021 08:21:30Z)
      
info: Host.Startup[413]
      Host started (237ms)
info: Host.Startup[0]
      Job host started
fail: Host.Function.Console[0]
      == Sinatra (v2.1.0) has taken the stage on 36205 for production with backup from Puma
info: Host.Function.Console[0]
      Puma starting in single mode...
info: Host.Function.Console[0]
      * Puma version: 5.2.2 (ruby 2.7.2-p137) ("Fettisdagsbulle")
(省略)

The next 5 occurrences of the ...という行でTimer Triggerの実行スケジュールが出力されています。時間がくると、以下のようにログが出力されます。

info: Function.TimerExample[1]
      Executing 'Functions.TimerExample' (Reason='Timer fired at 2021-04-11T08:20:50.0124322+00:00', Id=76abc40f-b7ff-4a2e-ae7d-fe64ff413f66)
fail: Host.Function.Console[0]
      I, [2021-04-11T08:20:50.066834 #45]  INFO -- : ----- Timer Schedule Start
fail: Host.Function.Console[0]
      I, [2021-04-11T08:20:50.069203 #45]  INFO -- : ----- Timer Schedule End
fail: Host.Function.Console[0]
      127.0.0.1 - - [11/Apr/2021:08:20:50 +0000] "POST /TimerExample HTTP/1.1" 200 4103 0.0029
info: Function.TimerExample.User[0]
      {"rack.version"=>[1, 6], "rack.errors"=>#<IO:<STDERR>>, "rack.multithread"=>true, "rack.multiprocess"=>false, "rack.run_once"=>false, "SCRIPT_NAME"=>"", "QUERY_STRING"=>"", "SERVER_PROTOCOL"=>"HTTP/1.1", "SERVER_SOFTWARE"=>"puma 5.2.2 Fettisdagsbulle", "GATEWAY_INTERFACE"=>"CGI/1.2", "REQUEST_METHOD"=>"POST", "REQUEST_PATH"=>"/TimerExample", "REQUEST_URI"=>"/TimerExample", "HTTP_VERSION"=>"HTTP/1.1", "HTTP_HOST"=>"127.0.0.1:36205", "HTTP_X_AZURE_FUNCTIONS_HOSTVERSION"=>"3.0.15417.0", "HTTP_X_AZURE_FUNCTIONS_INVOCATIONID"=>"76abc40f-b7ff-4a2e-ae7d-fe64ff413f66", "HTTP_USER_AGENT"=>"Azure-Functions-Host/3.0.15417.0", "HTTP_TRANSFER_ENCODING"=>"chunked", "CONTENT_TYPE"=>"application/json; charset=utf-8", "puma.request_body_wait"=>1, "CONTENT_LENGTH"=>"234", "SERVER_NAME"=>"127.0.0.1", "SERVER_PORT"=>"36205", info: 
(省略)
Function.TimerExample.User[0]
      {"Data"=>{"myTimer"=>{"Schedule"=>{"AdjustForDST"=>true}, "ScheduleStatus"=>nil, "IsPastDue"=>false}}, "Metadata"=>{"sys"=>{"MethodName"=>"TimerExample", "UtcNow"=>"2021-04-11T08:20:50.0482506Z", "RandGuid"=>"3dcd6dd9-263c-4b8d-803c-3e663a09f224"}}}
info: Function.TimerExample[2]
      Executed 'Functions.TimerExample' (Succeeded, Id=76abc40f-b7ff-4a2e-ae7d-fe64ff413f66, Duration=73ms)
(省略)

アプリ内の戻り値でLogsにリクエストヘッダーと本文を指定しているため、その内容が表示されています。今回はサンプル実装ですが、ここにある情報を使うことでいろいろな実装ができそうです。

Sinatraで出力しているログがfail扱いになっているのはrack.errorsがSTDERRに指定されているためかと思われます。

ソース

ここまでのソースです。

github.com

Azure Functionsのお勉強メモ(4)RubyでHTTP Triggerを実装する

最近、Azure Functionsのお勉強をチマチマと始めました。色々と分からないことが多かったのでお勉強メモをまとめて記します。

どこまで続くかわからないお勉強メモ。今日は4回目です。今回はRubyです。過去のものは以下を参照。

Azure Functions上のアプリをRubyで作る

Azure Functionsでサポートしている言語は2021年3月末現在、以下のものに限られています。

詳細は以下のドキュメントに書かれています。

docs.microsoft.com

自分が得意なRubyが入っていない...。ただ、Dockerを使えば動かせるというようなことが以下のドキュメントに書かれています(ドキュメントはR言語の例)。

docs.microsoft.com

ここでは、Rubyで動くように実装してみることにします。

プロジェクトの作成と設定

とは言っても、ほとんど上のドキュメントに書かれていることを実行するだけです。

func initコマンドでプロジェクトを作成します。

func init プロジェクト名 --worker-runtime custom --docker

これでプロジェクト名のディレクトリが作成されるので、そのディレクトリに移動後、func newで関数の設定ファイルであるfunction.jsonを作成します。

cd プロジェクト名
func new --name Hello --template "HTTP trigger"

これでHelloディレクトリにfunction.jsonが生成されます。

function.jsonを開き、"authLevel"の値を"anonymous"に変えておきます。これはAPIアクセスの時にAPIキーを求めるかどうかの設定です。詳細は以下のドッキュメントにあるauthLevelの項を参照してください。

docs.microsoft.com

最後にhost.jsonの設定をします。"customHandler"の部分を以下のように修正します。

  "customHandler": {
    "description": {
      "defaultExecutablePath": "ruby",
      "workingDirectory": "",
      "arguments": ["app.rb"]
    },
    "enableForwardingHttpRequest": true
  }

私は"enableForwardingHttpRequest"の記述をすっかり見落としており、うまく動かなくてかなり悩みました。この項目の意味は以下のドキュメントを参照すると良いと思います。

docs.microsoft.com

アプリの実装

アプリはSinatraを使って実装します。リクエストがあったらテキストを返すだけの簡単なものでしたらRailsよりもSinatraのほうが楽でしょう。実装もこれだけです。

require 'sinatra/base'

class MyApp < Sinatra::Base
  configure do
    set :environment, :production
    set :port, ENV['FUNCTIONS_CUSTOMHANDLER_PORT']
  end
  get '/api/Hello' do
    'Hello Ruby World'
  end

  run! if app_file === $0
end

:portでリクエストを受け付けるポートを環境変数FUNCTIONS_CUSTOMHANDLER_PORTで指定する必要があるのが注意点といえば注意点かと思います。

Gemfileの準備

Gemfileにて利用するライブラリを記述します。単純なSinatraの機能しか使わないので、以下の記述になります。

source "https://rubygems.org"

gem 'sinatra', '~> 2.1.0'

Dockerfileの準備

ドキュメントによればAzure Functionsの環境として作成するにはベースイメージとしてmcr.microsoft.com/azure-functions/dotnet:3.0-appserviceを使う必要があるとのこと。

docs.microsoft.com

イメージの実装はこのリポジトリのようです。

github.com

Rubyを動かすためにはいろいろな方法があるかと思いますが、手っ取り早くまずはapt-get install rubyでインストールしてみます。ここでインストールされるRubyは2.5系だったので、gem i bunderも実行しています。

FROM mcr.microsoft.com/azure-functions/dotnet:3.0-appservice
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
    AzureFunctionsJobHost__Logging__Console__IsEnabled=true

RUN apt update && \
    apt install -y \
    ruby \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /home/site/wwwroot

RUN gem install bundler --no-doc

COPY Gemfile /home/site/wwwroot/
RUN bundler install
COPY . /home/site/wwwroot/

実行

後は、docker buildでDockerイメージを作成し、docker runで動かした後にhttp://localhost:8080/api/Helloにアクセスすると、Hello Ruby Worldという文字列がブラウザ上に表示されます。

f:id:miyohide:20210404175651p:plain

コンソールログには以下のような文字列が出力されていました。Sinatraが出力しているログがfailになっているのがちょっと気になりますが、とりあえず動いているようです。

      Host lock lease acquired by instance ID '000000000000000000000000714A9D2E'.
info: Function.Hello[1]
      Executing 'Functions.Hello' (Reason='This function was programmatically called via the host APIs.', Id=66a1a41b-2d0c-4277-90d3-a5ef3990be81)
fail: Host.Function.Console[0]
      127.0.0.1 - - [01/Apr/2021:11:40:05 +0000] "GET /api/Hello HTTP/1.1" 200 16 0.0004
fail: Host.Function.Console[0]
      127.0.0.1 - - [01/Apr/2021:11:40:05 UTC] "GET /api/Hello HTTP/1.1" 200 16
fail: Host.Function.Console[0]
      - -> /api/Hello
info: Function.Hello[2]
      Executed 'Functions.Hello' (Succeeded, Id=66a1a41b-2d0c-4277-90d3-a5ef3990be81, Duration=66ms)

f:id:miyohide:20210404175720p:plain

Azure上で動かす

Azure上で動かしてみます。Dockerイメージをdocker pushでDocker Hubに格納したのち、Azure上で動かします。Azure Portal上で「関数アプリ」に必要な項目を入力し、作成。作成後、「デプロイセンター」から「完全なイメージの名前とタグ」を指定して保存すればAzure上で動かすことができました。

f:id:miyohide:20210404180822j:plain

ソース

現時点でのソースです。

github.com