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