CLOVER🍀

That was when it all began.

Azure Functions(Java)のTimerTriggerをローカルで動かしてみる

これは、なにをしたくて書いたもの?

前に、Azure FunctionsのHttpTriggerをローカルで動かしてみました。

Azure Functions(Java)をローカルで動かしてみる - CLOVER🍀

今回は、TimerTriggerを動かしてみたいと思います。

TimerTrigger

TimerTriggerは、Azure Functionsで関数をスケジュールに沿って実行できる仕組みです。

Azure Functions のタイマー トリガー | Microsoft Docs

スケジュールの指定は、cron式とほぼ同じです(NCronTabというものを使うようです)。

Azure Functions のタイマー トリガー / NCRONTAB 式

今回は、TimerTriggerで実行する関数をJavaで作成してローカルで動かしてみます。

環境

今回の環境は、こちらです。

$ func --version
3.0.3388


$ java --version
openjdk 11.0.10 2021-01-19
OpenJDK Runtime Environment (build 11.0.10+9-Ubuntu-0ubuntu1.20.04)
OpenJDK 64-Bit Server VM (build 11.0.10+9-Ubuntu-0ubuntu1.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.10, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-70-generic", arch: "amd64", family: "unix"

プロジェクトを作成する

Azure Functionsで、TimerTriggerを使うプロジェクトを作成しましょう。

Mavenアーキタイプを使って作成できます。

クイックスタート: コマンド ラインから Azure に Java 関数を作成する / ローカル関数プロジェクトを作成する

$ mvn archetype:generate \
 -DarchetypeGroupId=com.microsoft.azure \
 -DarchetypeArtifactId=azure-functions-archetype \
 -DgroupId=org.littlewings \
 -DartifactId=hello-java-function-timer-trigger \
 -Dversion=0.0.1-SNAPSHOT \
 -Dpackage=org.littlewings.azure.function \
 -DappName=hello-java-function-timer-trigger \
 -DappRegion=japaneast \
 -Dtrigger=TimerTrigger \
 -DjavaVersion=11 \
 -DinteractiveMode=false

ポイントは、こちらの指定ですね。

-Dtrigger=TimerTrigger

プロジェクトが作成できたら、プロジェクト内へ移動。

$  cd hello-java-function-timer-trigger

作成されたファイル一式。

$ tree -a
.
├── .gitignore
├── host.json
├── local.settings.json
├── pom.xml
└── src
    └── main
        └── java
            └── org
                └── littlewings
                    └── Function.java

5 directories, 5 files

テストコードは作成されないみたいですね。

pom.xmlの主要な部分。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>11</java.version>
        <azure.functions.maven.plugin.version>1.9.2</azure.functions.maven.plugin.version>
        <azure.functions.java.library.version>1.4.0</azure.functions.java.library.version>
        <functionAppName>hello-java-function-timer-trigger</functionAppName>
        <stagingDirectory>${project.build.directory}/azure-functions/${functionAppName}</stagingDirectory>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.microsoft.azure.functions</groupId>
            <artifactId>azure-functions-java-library</artifactId>
            <version>${azure.functions.java.library.version}</version>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.4.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.23.4</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.microsoft.azure</groupId>
                <artifactId>azure-functions-maven-plugin</artifactId>
                <version>${azure.functions.maven.plugin.version}</version>
                <configuration>
                    <!-- function app name -->
                    <appName>${functionAppName}</appName>
                    <!-- function app resource group -->
                    <resourceGroup>java-functions-group</resourceGroup>
                    <!-- function app service plan name -->
                    <appServicePlanName>java-functions-app-service-plan</appServicePlanName>
                    <!-- function app region-->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-regions for all valid values -->
                    <region>japaneast</region>
                    <!-- function pricingTier, default to be consumption if not specified -->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-pricing-tiers for all valid values -->
                    <!-- <pricingTier></pricingTier> -->
                    
                    <!-- Whether to disable application insights, default is false -->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details for all valid configurations for application insights-->
                    <!-- <disableAppInsights></disableAppInsights> -->
                    <runtime>
                        <!-- runtime os, could be windows, linux or docker-->
                        <os>windows</os>
                        <javaVersion>11</javaVersion>
                        <!-- for docker function, please set the following parameters -->
                        <!-- <image>[hub-user/]repo-name[:tag]</image> -->
                        <!-- <serverId></serverId> -->
                        <!-- <registryUrl></registryUrl>  -->
                    </runtime>
                    <appSettings>
                        <property>
                            <name>FUNCTIONS_EXTENSION_VERSION</name>
                            <value>~3</value>
                        </property>
                    </appSettings>
                </configuration>
                <executions>
                    <execution>
                        <id>package-functions</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>3.1.0</version>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <overwrite>true</overwrite>
                            <outputDirectory>${stagingDirectory}</outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${project.basedir}</directory>
                                    <includes>
                                        <include>host.json</include>
                                        <include>local.settings.json</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.1.1</version>
                <executions>
                    <execution>
                        <id>copy-dependencies</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>copy-dependencies</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${stagingDirectory}/lib</outputDirectory>
                            <overWriteReleases>false</overWriteReleases>
                            <overWriteSnapshots>false</overWriteSnapshots>
                            <overWriteIfNewer>true</overWriteIfNewer>
                            <includeScope>runtime</includeScope>
                            <excludeArtifactIds>azure-functions-java-library</excludeArtifactIds>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <!--Remove obj folder generated by .NET SDK in maven clean-->
            <plugin>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>obj</directory>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>
        </plugins>

設定ファイル。

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java"
  }
}

host.json

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[1.*, 2.0.0)"
  } 
}

生成されたソースコード。TimerTrigger用になっていますね。

src/main/java/org/littlewings/Function.java

package org.littlewings;

import java.time.*;
import com.microsoft.azure.functions.annotation.*;
import com.microsoft.azure.functions.*;

/**
 * Azure Functions with Timer trigger.
 */
public class Function {
    /**
     * This function will be invoked periodically according to the specified schedule.
     */
    @FunctionName("Function")
    public void run(
        @TimerTrigger(name = "timerInfo", schedule = "0 * * * * *") String timerInfo,
        final ExecutionContext context
    ) {
        context.getLogger().info("Java Timer trigger function executed at: " + LocalDateTime.now());
    }
}

定義を見ると、どうやら毎分起動してログ出力するTimerTriggerのようです。

    @FunctionName("Function")
    public void run(
        @TimerTrigger(name = "timerInfo", schedule = "0 * * * * *") String timerInfo,
        final ExecutionContext context
    ) {
        context.getLogger().info("Java Timer trigger function executed at: " + LocalDateTime.now());
    }

動かしてみる

とりあえず、この状態でも動かしてみましょう。

パッケージングして

$ mvn package

実行。

$ mvn azure-functions:run
[INFO] Azure Function App's staging directory found at: /path/to/hello-java-function-timer-trigger/target/azure-functions/hello-java-function-timer-trigger
[INFO] Azure Functions Core Tools found.

Azure Functions Core Tools
Core Tools Version:       3.0.3388 Commit hash: fb42a4e0b7fdc85fbd0bcfc8d743ff7d509122ae 
Function Runtime Version: 3.0.15371.0

Missing value for AzureWebJobsStorage in local.settings.json. This is required for all triggers other than httptrigger, kafkatrigger. You can run 'func azure functionapp fetch-app-settings <functionAppName>' or specify a connection string in local.settings.json.
[ERROR] 
[ERROR] Failed to run Azure Functions. Please checkout console output.
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

エラーになってしまいました。どうやら、AzureWebJobsStorageというものを指定する必要があるようです。

Missing value for AzureWebJobsStorage in local.settings.json. This is required for all triggers other than httptrigger, kafkatrigger. You can run 'func azure functionapp fetch-app-settings <functionAppName>' or specify a connection string in local.settings.json.

local.settings.jsonを見ると、該当の部分が空欄になっていますね。

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java"
  }
}

TimerTriggerで使いそうなことが、ドキュメントに書かれています。

Azure Functions ランタイムでは、このストレージ アカウント接続文字列は通常の操作に使用されます。 このストレージ アカウントの使用方法としては、キー管理、タイマー トリガー管理、Event Hubs チェックポイントなどがあります。

Azure Functions のアプリケーション設定のリファレンス / AzureWebJobsStorage

こちらを見ても、必須のようですね。ローカルでエミュレーターを使う方法も書いてあるので、こちらに習うことにします。

Azure ストレージ アカウントの接続文字列を含みます。 HTTP 以外のトリガーを使用する場合には必須です。 詳しくは、AzureWebJobsStorage のリファレンスを参照してください。 Azure ストレージ エミュレーターがローカルにインストールされ、AzureWebJobsStorage を UseDevelopmentStorage=true に設定すると、Core Tools でエミュレーターが使用されます。

Azure Functions Core Tools の操作 / ローカル設定ファイル

Azure Storageエミュレーターとしては、Azuriteを使うことにします。

$ npx azurite --version
3.11.0

起動。

$ mkdir data
$ npx azurite -l data

この状態で、local.settings.jsonのAzureWebJobsStorageを以下のように指定します。

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "java"
  }
}

以下でもOKです。

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;",
    "FUNCTIONS_WORKER_RUNTIME": "java"
  }
}

再度パッケージングして、実行。

$ mvn package
$ mvn azure-functions:run

今度は起動しました。

[INFO] Azure Function App's staging directory found at: /path/to/hello-java-function-timer-trigger/target/azure-functions/hello-java-function-timer-trigger
[INFO] Azure Functions Core Tools found.

Azure Functions Core Tools
Core Tools Version:       3.0.3388 Commit hash: fb42a4e0b7fdc85fbd0bcfc8d743ff7d509122ae 
Function Runtime Version: 3.0.15371.0

[2021-03-26T05:30:50.795Z] Worker process started and initialized.

Functions:

    Function: timerTrigger

For detailed output, run func with --verbose flag.
[2021-03-26T05:30:56.321Z] Host lock lease acquired by instance ID '000000000000000000000000C3CC5E65'.

関数も、スケジュールに沿って実行されました。

[2021-03-26T05:30:56.321Z] Host lock lease acquired by instance ID '000000000000000000000000C3CC5E65'.
[2021-03-26T05:31:00.052Z] Executing 'Functions.Function' (Reason='Timer fired at 2021-03-26T14:31:00.0210745+09:00', Id=dab7f753-3f36-4fb8-a66f-2da51d77b0ff)
[2021-03-26T05:31:00.205Z] Java Timer trigger function executed at: 2021-03-26T14:31:00.193219
[2021-03-26T05:31:00.206Z] Function "Function" (Id: dab7f753-3f36-4fb8-a66f-2da51d77b0ff) invoked by Java Worker
[2021-03-26T05:31:00.227Z] Executed 'Functions.Function' (Succeeded, Id=dab7f753-3f36-4fb8-a66f-2da51d77b0ff, Duration=198ms)

定義を見返すと、毎分起動してログ出力するようになっています。

    @FunctionName("Function")
    public void run(
        @TimerTrigger(name = "timerInfo", schedule = "0 * * * * *") String timerInfo,
        final ExecutionContext context
    ) {
        context.getLogger().info("Java Timer trigger function executed at: " + LocalDateTime.now());
    }

以下が、ログ出力した結果ですね。

[2021-03-26T05:31:00.205Z] Java Timer trigger function executed at: 2021-03-26T14:31:00.193219

このまま起動させておくと、毎分実行されるのが確認できます。

[2021-03-26T05:31:00.052Z] Executing 'Functions.Function' (Reason='Timer fired at 2021-03-26T14:31:00.0210745+09:00', Id=dab7f753-3f36-4fb8-a66f-2da51d77b0ff)
[2021-03-26T05:31:00.205Z] Java Timer trigger function executed at: 2021-03-26T14:31:00.193219
[2021-03-26T05:31:00.206Z] Function "Function" (Id: dab7f753-3f36-4fb8-a66f-2da51d77b0ff) invoked by Java Worker
[2021-03-26T05:31:00.227Z] Executed 'Functions.Function' (Succeeded, Id=dab7f753-3f36-4fb8-a66f-2da51d77b0ff, Duration=198ms)
[2021-03-26T05:32:00.005Z] Executing 'Functions.Function' (Reason='Timer fired at 2021-03-26T14:32:00.0022770+09:00', Id=17c6812a-3adb-4140-b8b8-a07e657c5a7d)
[2021-03-26T05:32:00.014Z] Java Timer trigger function executed at: 2021-03-26T14:32:00.013418
[2021-03-26T05:32:00.018Z] Function "Function" (Id: 17c6812a-3adb-4140-b8b8-a07e657c5a7d) invoked by Java Worker
[2021-03-26T05:32:00.019Z] Executed 'Functions.Function' (Succeeded, Id=17c6812a-3adb-4140-b8b8-a07e657c5a7d, Duration=17ms)
[2021-03-26T05:33:00.002Z] Executing 'Functions.Function' (Reason='Timer fired at 2021-03-26T14:33:00.0008706+09:00', Id=8e78ed40-67b2-4076-b767-381f36b86b50)
[2021-03-26T05:33:00.008Z] Java Timer trigger function executed at: 2021-03-26T14:33:00.006725
[2021-03-26T05:33:00.009Z] Function "Function" (Id: 8e78ed40-67b2-4076-b767-381f36b86b50) invoked by Java Worker
[2021-03-26T05:33:00.010Z] Executed 'Functions.Function' (Succeeded, Id=8e78ed40-67b2-4076-b767-381f36b86b50, Duration=9ms)

Azurite側にも、アクセスが来ていることが確認できます。

127.0.0.1 - - [26/Mar/2021:05:31:00 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/timers/hostname-1510718391/Host.Functions.Function/status HTTP/1.1" 201 -
127.0.0.1 - - [26/Mar/2021:05:31:06 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:08 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:13 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:20 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:21 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:28 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:32 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:36 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:43 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:44 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:51 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:56 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:31:58 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:00 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/timers/hostname-1510718391/Host.Functions.Function/status HTTP/1.1" 201 -
127.0.0.1 - - [26/Mar/2021:05:32:06 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:08 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:13 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:20 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:21 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:28 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:32 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:36 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:43 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:44 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:51 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:56 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:32:58 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:00 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/timers/hostname-1510718391/Host.Functions.Function/status HTTP/1.1" 201 -
127.0.0.1 - - [26/Mar/2021:05:33:06 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:08 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:13 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:20 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:21 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:28 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:32 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:36 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:43 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:44 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:51 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:56 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/host?comp=lease HTTP/1.1" 200 -
127.0.0.1 - - [26/Mar/2021:05:33:58 +0000] "PUT /devstoreaccount1/azure-webjobs-hosts/locks/hostname-1510718391/Host.Functions.Function.Listener?comp=lease HTTP/1.1" 200 -

スケジュールを環境変数で指定する

とりあえずTimerTriggerを動かせたわけですが、実行するスケジュールがソースコードに固定されています。

    @FunctionName("Function")
    public void run(
        @TimerTrigger(name = "timerInfo", schedule = "0 * * * * *") String timerInfo,
        final ExecutionContext context
    ) {
        context.getLogger().info("Java Timer trigger function executed at: " + LocalDateTime.now());
    }

これを、実行時に変更できないかな?と。

変えられそうな雰囲気はあります。

スケジュール式をアプリ設定に含めて、たとえば "%ScheduleAppSetting%" のように、 % 記号で囲まれたアプリ設定名にこのプロパティを設定できます。

Azure Functions のタイマー トリガー / 構成

アプリ設定、とは…?

Azure Functions のアプリケーション設定のリファレンス | Microsoft Docs

function.jsonというファイルを見るのが良さそうな気もします。

次の表は、function.json ファイルと TimerTrigger 属性で設定したバインド構成のプロパティを説明しています。

Azure Functions のタイマー トリガー / 構成

target/azure-functionsディレクトリの中を見ると、function.jsonがありました。

$ tree target/azure-functions 
target/azure-functions
└── hello-java-function-timer-trigger
    ├── Function
    │   └── function.json
    ├── hello-java-function-timer-trigger-0.0.1-SNAPSHOT.jar
    ├── host.json
    └── local.settings.json

2 directories, 4 files

中身は、ソースコードの内容が反映されているようですね。

target/azure-functions/hello-java-function-timer-trigger/Function/function.json

{
  "scriptFile" : "../hello-java-function-timer-trigger-0.0.1-SNAPSHOT.jar",
  "entryPoint" : "org.littlewings.Function.run",
  "bindings" : [ {
    "type" : "timerTrigger",
    "direction" : "in",
    "name" : "timerInfo",
    "schedule" : "0 * * * * *"
  } ]
}

関数 は Azure Functions の主要な概念です。 関数には 2 つの重要な要素が含まれています。さまざまな言語で記述できるコードと、いくつかの構成、つまり function.json ファイルです。 コンパイル式の言語の場合、この構成ファイルはコード内の注釈から自動的に生成されます。

Azure Functions 開発者ガイド /関数のコード

でも、これはビルド時に作成されていることになります。実行時に変更するには…?

アプリ設定のドキュメントを見ていると、どうやら環境変数で変えられそうです。「ローカルで実行する場合」と但し書きが
ありますけど。

関数アプリのアプリケーション設定には、その関数アプリのすべての関数に影響するグローバル構成オプションが含まれています。 ローカルで実行する場合、これらの設定は、ローカルの環境変数としてアクセスされます。

Azure Functions のアプリケーション設定のリファレンス | Microsoft Docs

ローカルで実行しない場合は、どうなるんでしょう…。

まあ、今回はローカルでの実行のみなので、気にせず進んでみましょう。

scheduleを、%TIMER_SCHEDULE%としました。

src/main/java/org/littlewings/Function.java

package org.littlewings;

import java.time.*;
import com.microsoft.azure.functions.annotation.*;
import com.microsoft.azure.functions.*;

/**
 * Azure Functions with Timer trigger.
 */
public class Function {
    /**
     * This function will be invoked periodically according to the specified schedule.
     */
    @FunctionName("Function")
    public void run(
        @TimerTrigger(name = "timerInfo", schedule = "%TIMER_SCHEDULE%") String timerInfo,
        final ExecutionContext context
    ) {
        context.getLogger().info("Java Timer trigger function executed at: " + LocalDateTime.now());
    }
}

ビルド。

$ mvn package

生成されるfunction.jsonにも、反映されました。

target/azure-functions/hello-java-function-timer-trigger/Function/function.json

{
  "scriptFile" : "../hello-java-function-timer-trigger-0.0.1-SNAPSHOT.jar",
  "entryPoint" : "org.littlewings.Function.run",
  "bindings" : [ {
    "type" : "timerTrigger",
    "direction" : "in",
    "name" : "timerInfo",
    "schedule" : "%TIMER_SCHEDULE%"
  } ]
}

あとは、環境変数で実行スケジュールを指定しつつ、起動します。

$ TIMER_SCHEDULE='0 */3 * * * *' mvn azure-functions:run

3分に1回実行するようにしてみました。

[INFO] Azure Function App's staging directory found at: /path/to/hello-java-function-timer-trigger/target/azure-functions/hello-java-function-timer-trigger
[INFO] Azure Functions Core Tools found.

Azure Functions Core Tools
Core Tools Version:       3.0.3388 Commit hash: fb42a4e0b7fdc85fbd0bcfc8d743ff7d509122ae 
Function Runtime Version: 3.0.15371.0

[2021-03-26T10:10:33.029Z] Worker process started and initialized.

Functions:

    Function: timerTrigger

For detailed output, run func with --verbose flag.
[2021-03-26T10:10:38.317Z] Host lock lease acquired by instance ID '000000000000000000000000C3CC5E65'.

しばらく待っていると、ログが出力されます。

[2021-03-26T10:12:00.060Z] Executing 'Functions.Function' (Reason='Timer fired at 2021-03-26T19:12:00.0215774+09:00', Id=175912f7-8312-4d08-bc34-42df1672e59f)
[2021-03-26T10:12:00.179Z] Java Timer trigger function executed at: 2021-03-26T19:12:00.157054
[2021-03-26T10:12:00.180Z] Function "Function" (Id: 175912f7-8312-4d08-bc34-42df1672e59f) invoked by Java Worker
[2021-03-26T10:12:00.208Z] Executed 'Functions.Function' (Succeeded, Id=175912f7-8312-4d08-bc34-42df1672e59f, Duration=177ms)
[2021-03-26T10:15:00.004Z] Executing 'Functions.Function' (Reason='Timer fired at 2021-03-26T19:15:00.0018087+09:00', Id=413918ed-efe6-4b01-b42c-04c23f0d36fc)
[2021-03-26T10:15:00.021Z] Java Timer trigger function executed at: 2021-03-26T19:15:00.011291
[2021-03-26T10:15:00.026Z] Function "Function" (Id: 413918ed-efe6-4b01-b42c-04c23f0d36fc) invoked by Java Worker
[2021-03-26T10:15:00.028Z] Executed 'Functions.Function' (Succeeded, Id=413918ed-efe6-4b01-b42c-04c23f0d36fc, Duration=25ms)

上手く指定できたようです。

ちなみに、環境変数を指定しなかった場合は

$ mvn azure-functions:run

値が解決できなくなり、関数の起動に失敗します。

[INFO] Azure Function App's staging directory found at: /path/to/hello-java-function-timer-trigger/target/azure-functions/hello-java-function-timer-trigger
[INFO] Azure Functions Core Tools found.

Azure Functions Core Tools
Core Tools Version:       3.0.3388 Commit hash: fb42a4e0b7fdc85fbd0bcfc8d743ff7d509122ae 
Function Runtime Version: 3.0.15371.0

[2021-03-26T10:15:23.464Z] Worker process started and initialized.
[2021-03-26T10:15:23.520Z] Microsoft.Azure.WebJobs.Host: Error indexing method 'Functions.Function'. Microsoft.Azure.WebJobs.Host: '%TIMER_SCHEDULE%' does not resolve to a value.
[2021-03-26T10:15:23.541Z] Error indexing method 'Functions.Function'
[2021-03-26T10:15:23.542Z] Microsoft.Azure.WebJobs.Host: Error indexing method 'Functions.Function'. Microsoft.Azure.WebJobs.Host: '%TIMER_SCHEDULE%' does not resolve to a value.
[2021-03-26T10:15:23.542Z] Function 'Functions.Function' failed indexing and will be disabled.
[2021-03-26T10:15:23.543Z] No job functions found. Try making your job classes and methods public. If you're using binding extensions (e.g. Azure Storage, ServiceBus, Timers, etc.) make sure you've called the registration method for the extension(s) in your startup code (e.g. builder.AddAzureStorage(), builder.AddServiceBus(), builder.AddTimers(), etc.).

Functions:

    Function: timerTrigger

For detailed output, run func with --verbose flag.
[2021-03-26T10:15:23.583Z] The 'Function' function is in error: Microsoft.Azure.WebJobs.Host: Error indexing method 'Functions.Function'. Microsoft.Azure.WebJobs.Host: '%TIMER_SCHEDULE%' does not resolve to a value.
[2021-03-26T10:15:28.676Z] Host lock lease acquired by instance ID '000000000000000000000000C3CC5E65'.

簡単な確認でしたが、こんなところで。