Azure基本

Azure Functions上でSpring Bootを利用したJavaアプリケーションを作成してみた

前回は、Azure Potal上でAzure Functionsを作成してみたが、今回は、前回作成したAzure FunctionsにSpring Bootを利用したJavaアプリケーションを配置してみたので、その手順を共有する。

前提条件

下記記事に従ってAzure上でFunctionsを作成済であること。

Azure Potal上でAzure Functionsを作成してみたAzureが提供するサービスに、さまざまなイベントによって駆動し、サーバーの構築や保守をすることなくプログラムを実行できる「Azure ...

また、下記記事に従って、STSをダウンロード済であること。

STSインストール_1
STS(Spring Tool Suite)をWindows端末上にダウンロードしてみたSpring BootアプリケーションをEclipse上で開発するには、Spring用のプラグインを同梱した「STS(Spring To...

さらに、下記サイトの内容に従って、Apache Mavenをダウンロード済であること。
https://qiita.com/Junichi_M_/items/20daee936cd0c03c3115

また、下記サイトの内容に従って、Azure CLIをインストール済であること。
https://docs.microsoft.com/ja-jp/cli/azure/install-azure-cli-windows?tabs=azure-cli

さらに、下記サイトの「Azure Functions Core Tools のインストール」が完了していること。
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local?tabs=windows%2Ccsharp%2Cbash

やってみたこと

  1. Mavenプロジェクトの作成
  2. Spring Bootを利用したJavaアプリケーションの作成
  3. Spring Bootを利用したJavaアプリケーションのローカル環境での実行
  4. Azure FunctionsへのSpring Bootを利用したJavaアプリケーションのデプロイ

Mavenプロジェクトの作成

Azure Functions上で動作する、Spring Bootを利用したJavaアプリケーションを作成するには、まずは空のMavenプロジェクトを作成する。その手順は以下の通り。

1) STSを起動し、ファイルメニューの「新規」から「プロジェクト」を選択する。
Mavenプロジェクトの作成_1

2)「Mavenプロジェクト」を選択し、「次へ」ボタンを押下する。
Mavenプロジェクトの作成_2

3)「シンプルなプロジェクトの作成」にチェックを入れ、「次へ」ボタンを押下する。
Mavenプロジェクトの作成_3

4) グループID、アーティファクトIDを指定し、「完了」ボタンを押下する。
Mavenプロジェクトの作成_4

5) しばらく待つと空のMavenプロジェクトの作成が完了し、以下のようなプログラム構成になる。
Mavenプロジェクトの作成_5

また、作成後の「pom.xml」の内容は以下の通り。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>demoAzureFunc</artifactId>
  <version>0.0.1-SNAPSHOT</version>
</project>



Spring Bootを利用したJavaアプリケーションの作成

作成した空のMavenプロジェクトに、Azure Functionsにデプロイ(配置)するための、Spring Bootを利用したJavaアプリケーションを作成する。

作成後のプログラム構成は以下の通り。なお、赤枠は追加・変更したプログラムである。
Springアプリケーションの作成

なお、サンプルプログラムの内容は、以下のサイトを参照している。
https://github.com/Azure-Samples/hello-spring-function-azure

pom.xmlの内容は以下の通りで、Spring Boot関連とAzure Functionsを動かすために必要なライブラリを追加している。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>demoAzureFunc</artifactId>
  <version>0.0.1-SNAPSHOT</version>

    <name>Hello Spring Function on Azure</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <azure.functions.maven.plugin.version>1.9.0</azure.functions.maven.plugin.version>

        <!-- customize those properties. The functionAppName should be unique across Azure -->
        <functionResourceGroup>azureAppDemo</functionResourceGroup>
        <functionAppName>azureFuncDemoApp</functionAppName>
        <functionAppServicePlan>ASP-azureAppDemo-8679</functionAppServicePlan>
        <functionPricingTier>B1</functionPricingTier>

        <functionAppRegion>japaneast</functionAppRegion>
        <stagingDirectory>${project.build.directory}/azure-functions/${functionAppName}</stagingDirectory>
        <start-class>com.example.HelloFunction</start-class>
        <spring.boot.wrapper.version>1.0.25.RELEASE</spring.boot.wrapper.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-azure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-function-web</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- Spring Boot Webの設定  -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-function-dependencies</artifactId>
                <version>2.0.1.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>com.microsoft.azure</groupId>
                    <artifactId>azure-functions-maven-plugin</artifactId>
                    <version>${azure.functions.maven.plugin.version}</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-dependency-plugin</artifactId>
                    <version>3.1.2</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
            </plugins>
        </pluginManagement>

        <plugins>
            <plugin>
                <groupId>com.microsoft.azure</groupId>
                <artifactId>azure-functions-maven-plugin</artifactId>
                <configuration>
                    <resourceGroup>${functionResourceGroup}</resourceGroup>
                    <appName>${functionAppName}</appName>
                    <appServicePlanName>${functionAppServicePlan}</appServicePlanName>
                    <region>${functionAppRegion}</region>
                    <pricingTier>${functionPricingTier}</pricingTier>
                    <runtime>
                    	<os>Linux</os>
                    	<javaVersion>8</javaVersion>
                    </runtime>
                    <appSettings>
                        <!-- Run Azure Function from package file by default -->
                        <property>
                            <name>WEBSITE_RUN_FROM_PACKAGE</name>
                            <value>1</value>
                        </property>
                        <property>
                            <name>FUNCTIONS_EXTENSION_VERSION</name>
                            <value>~3</value>
                        </property>
                        <property>
                            <name>FUNCTIONS_WORKER_RUNTIME</name>
                            <value>java</value>
                        </property>
                    </appSettings>
                </configuration>
                <executions>
                    <execution>
                        <id>package-functions</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-resources-plugin</artifactId>
                <executions>
                    <execution>
                        <id>copy-resources</id>
                        <phase>package</phase>
                        <goals>
                            <goal>copy-resources</goal>
                        </goals>
                        <configuration>
                            <overwrite>true</overwrite>
                            <outputDirectory>
                                ${project.build.directory}/azure-functions/${functionAppName}
                            </outputDirectory>
                            <resources>
                                <resource>
                                    <directory>${project.basedir}/src/main/azure
                                    </directory>
                                    <includes>
                                        <include>**</include>
                                    </includes>
                                </resource>
                            </resources>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <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>
                        </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>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <dependencies>
                    <dependency>
                        <groupId>org.springframework.boot.experimental</groupId>
                        <artifactId>spring-boot-thin-layout</artifactId>
                        <version>${spring.boot.wrapper.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/libs-snapshot-local</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/libs-milestone-local</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/release</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/libs-snapshot-local</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
            <releases>
                <enabled>false</enabled>
            </releases>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/libs-milestone-local</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release-local</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>

なお、下記サイトの情報によると、spring-cloud-function-dependenciesのバージョンが3.0以上だとClassCastExceptionが発生する場合があるので、spring-cloud-function-dependenciesのバージョンを2.0.1.RELEASEに変更している。
https://spring.io/blog/2018/09/25/spring-cloud-function-2-0-and-azure-functions

また、そうするとObjectMapperクラスが参照できなくなるため、spring-boot-starter-webをpom.xmlに追加している。

<2021年4月13日 追記>
spring-cloud-function-dependenciesのバージョンは、2021年3月16日にリリースしたバージョン3.1.2を利用すると、1つのAzure Functions内に複数のファンクションを含む場合の不具合が解消できている。


その場合、Handlerクラスの継承するクラスを「AzureSpringBootRequestHandler」クラスから「FunctionInvoker」クラスに変更する。


spring-cloud-function-dependenciesの3.1.2を利用した実装サンプルは、以下の記事を参照のこと。

spring-cloud-function-dependenciesのバージョンを最新(3.1.2)にしてみたこれまでこのブログで取り上げてきたAzure Functionsのサンプルプログラムでは、spring-cloud-function-d...



host.json、local.settings.jsonの内容は以下の通り。

{
  "version": "2.0",
  "functionTimeout": "00:10:00"
}
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java",
    "MAIN_CLASS":"com.example.HelloFunction",
    "AzureWebJobsDashboard": ""
  }
}

なお、host.json、local.settings.jsonの設定内容は以下のサイトを参照のこと。
●host.json
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-host-json

●local.settings.json
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-run-local?tabs=linux%2Ccsharp%2Cbash#local-settings-file

また、Javaの各クラスの内容は以下の通りで、Azure FunctionsのAPIを呼び出したタイミングで、HelloHandlerクラスのexecuteメソッドが呼び出されるようになっている。

package com.example.model;

public class Greeting {

    public Greeting() {
    }

    public Greeting(String message) {
        this.message = message;
    }

    private String message;

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}
package com.example.model;

public class User {

    public User() {
    }

    public User(String name) {
        this.name = name;
    }

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}



package com.example;

import com.example.model.Greeting;
import com.example.model.User;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import java.util.function.Function;

@SpringBootApplication
public class HelloFunction {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(HelloFunction.class, args);
    }

    /**
     * Userオブジェクトを引数に、Greetingオブジェクトを返却する関数
     * @return 生成した関数
     */
    @Bean
    public Function<User, Greeting> hello() {
        return user -> new Greeting("Hello, " + user.getName());
    }
}
package com.example;

import java.util.Optional;
import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler;
import com.example.model.Greeting;
import com.example.model.User;
import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;

public class HelloHandler extends AzureSpringBootRequestHandler<User, Greeting> {

    /**
     * HTTP要求に応じて、HelloFunctionクラスのhelloメソッドを呼び出し、
     * その戻り値をボディに設定したレスポンスを返す
     * @param request リクエストオブジェクト
     * @param context コンテキストオブジェクト
     * @return レスポンスオブジェクト
     */
    @FunctionName("hello")
    public HttpResponseMessage execute(@HttpTrigger(name = "request"
       , methods = { HttpMethod.GET, HttpMethod.POST }
       , authLevel = AuthorizationLevel.ANONYMOUS) 
            HttpRequestMessage<Optional<User>> request,
         ExecutionContext context) {
             // リクエストオブジェクトからUserオブジェクトを取得する
             User user = request.getBody().filter((u -> u.getName() != null))
                          .orElseGet(() -> new User(
                            request.getQueryParameters().getOrDefault(
                               "name", "<no name supplied> please provide a name as "
                             + "either a query string parameter or in a POST body")));
             context.getLogger().info("Greeting user name: " + user.getName());
             // handleRequestメソッド内でHelloFunctionクラスのhelloメソッドを呼び出し、
             // その戻り値をボディに設定したレスポンスを、JSON形式で返す
             return request.createResponseBuilder(HttpStatus.OK)
                 .body(handleRequest(user, context))
                 .header("Content-Type", "text/json").build();
    }
}



Spring Bootを利用したJavaアプリケーションのローカル環境での実行

先ほど作成したアプリケーションを、ローカル環境で実行する方法は以下の通り。

1) コマンドプロンプトを起動し、作成したアプリケーションのpom.xmlの存在するディレクトリに移動する。
Springアプリケーションの実行_1

2) 「mvn clean」コマンドを実行し、デプロイされたjarファイルが含まれるtarget ディレクトリ内をクリアする。
Springアプリケーションの実行_2

3) 「mvn package」コマンドを実行し、デプロイ用のjarファイルを作成する。
Springアプリケーションの実行_3_1

Springアプリケーションの実行_3_2
なお、上記画像では、コピーしている箇所を一部省略している。

また、「mvn package」コマンドの実行が完了すると、以下のように「demoAzureFunc-0.0.1-SNAPSHOT.jar」が作成されていることが確認できる。
Springアプリケーションの実行_3_3

4) 「mvn azure-functions:run」コマンドを実行し、Azure Functiosを実行する。
Springアプリケーションの実行_4

5) WEBブラウザ上で「http://localhost:7071/api/hello?name=Azure」とアクセスすると、以下のように、JSON形式で「”message”: “Hello, (nameで指定したパラメータ値)”」が表示されることが確認できる。
Springアプリケーションの実行_5

6) Azure Functionsを終了するには、「Ctrl+C」コマンドで「バッチジョブを終了しますか(Y/N)?」というメッセージが表示後、「Y」を選択しエンターキーを押下する。
Springアプリケーションの実行_6



Azure FunctionsへのSpring Bootを利用したJavaアプリケーションのデプロイ

Azure FunctionsへのSpring Bootを利用したJavaアプリケーションのデプロイ手順は、以下の通り。

1) 「az login」コマンドを実行し、Azure Portalにログインするアカウントでログインする。
AzureFunctionsのデプロイ_1_1

下記ダイアログが表示されるため、ログインアカウントを選択する。
AzureFunctionsのデプロイ_1_2

ログインに成功すると以下の画面に遷移する。
AzureFunctionsのデプロイ_1_3

また、コンソールには以下のログイン情報が表示される。
AzureFunctionsのデプロイ_1_4

2) 「mvn azure-functions:deploy」コマンドを実行し、jarファイルをAzure Functionsにデプロイする。
AzureFunctionsのデプロイ_2

3) デプロイが完了したら、画面から確認する。デプロイ対象のAzure Functions概要の、URLの右「クリップボードにコピー」を押下し、URLをコピーした後で、「(コピーしたURL)/api/hello?name=Azure」にアクセスすると、以下のように、デプロイしたアプリケーションのAPI呼出結果が画面が表示される。
AzureFunctionsのデプロイ_3_1

AzureFunctionsのデプロイ_3_2

要点まとめ

  • Azure Functionsにデプロイするための、Spring Bootを利用したJavaアプリケーションを作成するには、サンプルプログラムを流用する。
  • Azure Functionsにデプロイするためのjarファイルを作成するには、「mvn clean」コマンドを実行後、「mvn package」コマンドを実行する。
  • 「mvn azure-functions:run」コマンドを実行すると、ローカル環境でAzure Functionsの動作検証ができる。
  • Azure Functionsに作成したjarファイルをデプロイするには、「mvn azure-functions:deploy」コマンドを実行する。