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

Azure Functionsのお勉強メモ(3)Spring Cloud FunctionでTimer Triggerを実装する

はじめに

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

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

Timer Triggerを試す

HTTP Triggerが実装できたので、次は定期的にジョブを実行するTimer Triggerを実装してみます。

APIを確認する

HTTP Triggerの場合は、@HttpTriggerアノテーションを使って色々と書きましたが、 Timer Triggerの場合はどのように書くかを調べます。

Azure Functions Java Libraryのマニュアルに書かれていました。

docs.microsoft.com

今回はTimer Triggerですが、他のTriggerもAzure Functions Java Libraryの以下のページを見ると良いかと思います。

docs.microsoft.com

NCRON

Timer Triggerの発生頻度を指定するにはNCronの書式で指定する必要があるのですが、毎回どうやって指定すればいいか忘れます。何か便利なツールないかなと探していたら、以下のオンラインツールを見つけました。

https://ncrontab.swimburger.net/

Web上でNCronの書式を入力したらいつジョブが実行されるかがすぐ計算してくれるツールです。

ローカル設定ファイルの編集

Timer Triggerにおいては、実行するジョブの管理などのためにAzure Storageを利用する必要があります(参考)。ローカル環境においてはシミュレーターを使うことができます。 具体的な設定は、local.settings.jsonファイルにおいて、"AzureWebJobsStorage"の値を"UseDevelopmentStorage=true"を指定すればOKです。 詳細は、以下のドキュメントを参照してください。

docs.microsoft.com

Azure Storageのシミュレーター

Azure Storageのシミュレーターとして、2021年3月時点ではAzuriteというものを使うようです。

docs.microsoft.com

Node.jsやDockerを使ってインストールできます。私はDockerを使って環境を準備しました。

実装

良い実装例が思いつかなかったので、単にログを出力しています。HTTP Triggerと同じようにHandlerをつくり、handleRequestにて処理の実態を呼び出します。まずはHandler。

public class TimerTriggerHandler extends FunctionInvoker<String, String> {
    @FunctionName("minutesBatch")
    public void minutesBatch(
            @TimerTrigger(name = "minutesBatch", schedule = "0 * * * * *") String timerInfo,
            ExecutionContext context
    ) {
        context.getLogger().info("Java Timer trigger process start.");
        handleRequest("minutesBatch start", context);
        context.getLogger().info("Java Timer trigger process end.");
    }
}

次に処理の実態。

@Component("minutesBatch")
public class MinutesBatchFunction implements Function<Message<String>, String> {
    @Override
    public String apply(Message<String> m) {
        ExecutionContext context = m.getHeaders().get("executionContext", ExecutionContext.class);
        context.getLogger().info("start minutes batch");
        context.getLogger().info("end minutes batch");
        return "SUCCESS";
    }
}

実行

Azuriteを起動したあとにgradlew azureFunctionsRunでアプリを実行すると、毎分以下のようなログが出力されます。

[2021-03-28T12:03:00.818Z] Java Timer trigger process start.
[2021-03-28T12:03:00.825Z] start minutes batch
[2021-03-28T12:03:00.825Z] end minutes batch
[2021-03-28T12:03:00.825Z] Java Timer trigger process end.
[2021-03-28T12:03:00.826Z] Function "minutesBatch" (Id: e000b1f6-7c95-4f59-a973-987cba50ad74) invoked by Java Worker
[2021-03-28T12:03:00.845Z] Executed 'Functions.minutesBatch' (Succeeded, Id=e000b1f6-7c95-4f59-a973-987cba50ad74, Duration=818ms)

ソース

この時点のソースです。

github.com