Azure Functions に Spring Cloud Function で関数アプリをデプロイする

# Azure Functions に Spring Cloud Function で関数アプリをデプロイする

こんにちは、@studio_meowtoon です。今回は、Azure Functions 環境で Spring Cloud Function 関数アプリを起動する方法を紹介します。
![spring-boot_on_azure-functions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/015c6d3b-c265-b642-2ecf-c5e035065640.png)

## 目的

Windows 11 の Linux でクラウド開発します。

こちらから記事の一覧がご覧いただけます。

https://qiita.com/studio_meowtoon/items/6119ab05d8b13527efd8   

## 実現すること

Microsoft Azure Functions に、JAR ファイル形式の Spring Cloud Function 関数アプリをデプロイします。  

## 技術トピック

### Microsoft Azure Functions とは？  

<details>
<summary>こちらを展開してご覧いただけます。</summary>

### [Microsoft Azure Functions](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-overview)

:::note info
Azure Functions は、クラウド上で実行されるサーバーレスのコンピューティングサービスで、アプリケーションを開発するために使用されます。

|特徴とメリット|
|:--|
|Azure Functions を使用することで、開発者はサーバーの設定や管理などの面倒な作業をせずに、コードを実行することができます。|
|Azure Functions は、イベントに応答して実行されることが一般的で、トリガーと呼ばれる外部イベントが発生すると、自動的にコードを実行します。トリガーには、HTTP リクエスト、メッセージキュー、ファイルの変更などがあります。|
|Azure Functions は、多くのプログラミング言語をサポートしており、C#、Java、JavaScript、Python などが利用可能です。|
|また、Azure Functions は、Azure の他のサービスとの統合が容易で、Azure Event Grid、Azure Cosmos DB、Azure Blob Storage などのサービスと簡単に連携することができます。|

:::

</details>

## 開発環境

+ Windows 11 Home 22H2 を使用しています。  

:::note info
[WSL](https://learn.microsoft.com/ja-jp/windows/wsl/about) の Ubuntu を操作していきますので macOS の方も参考にして頂けます。  
:::

<details>

WSL (Microsoft Store アプリ版) [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
> wsl --version
WSL バージョン: 1.0.3.0
カーネル バージョン: 5.15.79.1
WSLg バージョン: 1.0.47
```

Ubuntu [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/2aaa6579cb8e4fa1d874)

```sh
$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.1 LTS
Release:        22.04
```

Java JDK [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/4d11e94a2389758759cd)

```sh
$ java -version
openjdk version "11.0.18" 2023-01-17
OpenJDK Runtime Environment (build 11.0.18+10-post-Ubuntu-0ubuntu122.04)
OpenJDK 64-Bit Server VM (build 11.0.18+10-post-Ubuntu-0ubuntu122.04, mixed mode, sharing)
```

Maven [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/f4cdc4039158153365fb)

```sh
$ mvn -version
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 11.0.18, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
```

Azure CLI [※ こちらの関連記事からインストール方法をご確認いただけます](https://qiita.com/studio_meowtoon/items/9721041f9e6e3e987cf0)

```sh
$ az --version
azure-cli                         2.45.0
core                              2.45.0
telemetry                          1.0.8
```

</details>

:::note warn
この記事では基本的に **Ubuntu のターミナルで操作**を行います。**Vim を使用してコピペする方法**を初めて学ぶ人のために、以下の記事で手順を紹介しています。ぜひ挑戦してみてください。
:::

https://qiita.com/studio_meowtoon/items/7771ad48676926e41463

## 作成する Web アプリケーションの仕様

|No|エンドポイント|HTTPメソッド|MIME タイプ|
|:--|:--|:--|:--|
|1|/api/data|GET|application/json|

## Hello World を表示する手順

### Azure Functions Core Tools のインストール
※ 初回のみ必要です。  

```sh
$ curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
```

Functions Core Tool を含むリポジトリを登録します。

```sh
$ sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list'
```

Functions Core Tool をインストールします。

```sh
$ sudo apt update
$ sudo apt install azure-functions-core-tools-4
```

Functions Core Tool のバーションを確認します。

```sh
$ func --version
4.0.4895
```

:::note info
ここまでの作業で、**Ubuntu** に **Azure Functions Core Tool** をインストールすることができました。
:::

### プロジェクトの作成

プロジェクトフォルダを作成します。  
※ ~/tmp/hello-spring-func をプロジェクトフォルダとします。

```sh
$ mkdir -p ~/tmp/hello-spring-func
$ cd ~/tmp/hello-spring-func
```

### アプリケーションクラスの作成

アプリケーションクラスを作成します。

```sh
$ mkdir -p src/main/java/com/example/springboot
$ vim src/main/java/com/example/springboot/Application.java
```

ファイルの内容

```java:Application.java
package com.example.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
```

### 関数クラスの作成

関数クラスを作成します。

```sh
$ mkdir -p src/main/java/com/example/springboot/function
$ vim src/main/java/com/example/springboot/function/Hello.java
```

ファイルの内容

```java:Data.java
package com.example.springboot.function;

import java.util.Map;
import java.util.function.Function;

import reactor.core.publisher.Mono;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Component("data")
public class Hello implements Function<Mono<Map<String, String>>, Mono<Map<String, String>>> {

    private static final Logger log = LoggerFactory.getLogger(Hello.class);

    public Mono<Map<String, String>> apply(Mono<Map<String, String>> mono) {
        return mono.map(map -> Map.of("message", "Hello World!"))
            .doOnNext(map -> log.info("Sending message: {}", map));
    }
}
```

### Azure 関数の作成

Azure 関数を作成します。

```sh
$ vim src/main/java/com/example/springboot/function/HelloHandler.java
```

ファイルの内容

```java:HelloHandler.java
package com.example.springboot.function;

import java.util.Map;
import java.util.Optional;

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;

import org.springframework.cloud.function.adapter.azure.FunctionInvoker;

public class HelloHandler extends FunctionInvoker<Map<String, String>, Map<String, String>> {

    @FunctionName("data")
    public HttpResponseMessage execute(
        @HttpTrigger(
            name = "request", 
            methods = { HttpMethod.GET, HttpMethod.POST }, 
            authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<Map<String, String>>> request, ExecutionContext context) {
        context.getLogger().info(HelloHandler.class + " is called.");
        return request
                .createResponseBuilder(HttpStatus.OK)
                .body(handleRequest(Map.of("", ""), context))
                .header("Content-Type", "application/json")
                .build();
    }
}
```

### Azure 構成ファイルの作成

**host.json** ファイルを作成します。

```sh
$ mkdir -p src/main/azure
$ vim src/main/azure/host.json
```

ファイルの内容

```json:host.json
{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  },
  "functionTimeout": "00:10:00"
}
```

**local.settings.json** ファイルを作成します。

```sh
$ vim src/main/azure/local.settings.json
```

```json:local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java",
    "FUNCTIONS_EXTENSION_VERSION": "~4",
    "AzureWebJobsDashboard": ""
  }
}
```

### ログの出力設定

+ application.properties ファイルを作成します。
+ logback-spring.xml ファイルを作成します。

こちらの関連記事で手順がご確認いただけます。

https://qiita.com/studio_meowtoon/items/ffbcb6ac9179852b2816

:::note alert
検証時点では、**logback-spring.xml** 設定で **springProfile** 要素の削除が必要でした。こちらについては、運用時の設定切り替えが不可能となるため調査を継続します。
:::

### pom.xml の作成

pom.xml ファイルを作成します。

```sh
$ vim pom.xml
```

:::note alert
検証時点では、**logback-core**、**logback-classic**、**slf4j-api** 依存の明示的な設定が必要でした。
:::

ファイルの内容  

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>func-hello-spring-boot</artifactId>
    <version>1.0</version>
    <name>func-hello-spring-boot</name>

    <properties>
        <azure.functions.maven.plugin.version>1.22.0</azure.functions.maven.plugin.version>
        <azure.functions.java.library.version>3.0.0</azure.functions.java.library.version>
        <spring.cloud.function.dependencies>4.0.0</spring.cloud.function.dependencies>
        <start-class>com.example.springboot.Application</start-class>

        <!-- customize those two properties. The functionAppName should be unique across Azure -->
        <functionAppRegion>japaneast</functionAppRegion>
        <functionResourceGroup>rg-example</functionResourceGroup>
        <functionAppServicePlanName>asp-example</functionAppServicePlanName>
        <functionAppName>func-example</functionAppName>

        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </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-webflux</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- Log -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.6</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.6</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.30</version>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-function-dependencies</artifactId>
                <version>${spring.cloud.function.dependencies}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.microsoft.azure.functions</groupId>
                <artifactId>azure-functions-java-library</artifactId>
                <version>${azure.functions.java.library.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <finalName>app</finalName>
        <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.3.0</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-dependency-plugin</artifactId>
                    <version>3.4.0</version>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-clean-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>io.gatling</groupId>
                    <artifactId>gatling-maven-plugin</artifactId>
                    <version>4.1.5</version>
                    <configuration>
                        <includes>
                            <include>com.example.loadtest.*</include>
                        </includes>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
        <plugins>
            <plugin>
                <groupId>com.microsoft.azure</groupId>
                <artifactId>azure-functions-maven-plugin</artifactId>
                <configuration>
                    <region>${functionAppRegion}</region>
                    <resourceGroup>${functionResourceGroup}</resourceGroup>
                    <appName>${functionAppName}</appName>
                    <appServicePlanName>${functionAppServicePlanName}</appServicePlanName>
                    <pricingTier>${functionPricingTier}</pricingTier>
                    <hostJson>${project.basedir}/src/main/azure/host.json</hostJson>
                    <localSettingsJson>${project.basedir}/src/main/azure/local.settings.json</localSettingsJson>
                    <runtime>
                        <os>linux</os>
                        <javaVersion>${java.version}</javaVersion>
                    </runtime>
                    <appSettings>
                        <!-- Run Azure Function from package file by default -->
                        <property>
                            <name>FUNCTIONS_EXTENSION_VERSION</name>
                            <value>~4</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-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <encoding>${project.build.sourceEncoding}</encoding>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
```

### アプリをビルド

Java アプリをビルドします。
※ target/app.jar が作成されます。

```sh
$ mvn clean package
```

:::note info
ここまでの手順で、ローカル環境の **Ubuntu** に アプリの**JAR ファイル**をビルドすることができました。
:::

### アプリを起動

ローカル環境の **Ubuntu** でアプリを起動して確認します。

```sh
$ mvn azure-functions:run
```

### アプリの動作確認 (※ローカル)

別ターミナルから curl コマンドで確認します。  

```sh
$ curl -v http://localhost:7071/api/data -w "\n" 
```

```sh
*   Trying 127.0.0.1:7071...
* Connected to localhost (127.0.0.1) port 7071 (#0)
> GET /api/data HTTP/1.1
> Host: localhost:7071
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: application/json; charset=utf-8
< Date: Tue, 21 Mar 2023 09:07:35 GMT
< Server: Kestrel
< Transfer-Encoding: chunked
<
{
  "message": "Hello World!"
* Connection #0 to host localhost left intact
}
```

:::note info
ターミナルに **{"message":"Hello World!"}** と表示され、**JSON データ**を取得することが出来ました。
:::

### pom.xml の修正

pom.xml ファイルを修正します。

:::note warn
仮の設定値を、実際に関数アプリを Azure にデプロイする際の設定値に修正します。状況により、関数アプリの名前を変更する必要があります。
:::

```sh
$ vim pom.xml
```

ファイルの内容  

```xml:pom.xml ※一部抜粋
<properties>
    ※省略
    <!-- customize those two properties. The functionAppName should be unique across Azure -->
    <functionAppRegion>japaneast</functionAppRegion>
    <functionResourceGroup>rg-hello</functionResourceGroup>
    <functionAppServicePlanName>asp-hello</functionAppServicePlanName>
    <functionAppName>func-hello-spring-boot</functionAppName>
    ※省略
</properties>
```

### Azure にサインイン

こちらの関連記事で手順がご確認いただけます。

https://qiita.com/studio_meowtoon/items/9721041f9e6e3e987cf0

Azure CLI でログインします。  

```sh
$ az login
```

### アプリのデプロイ

Azure に関数アプリをデプロイします。

:::note info
ここでは、**Maven** の**プラグイン**から Azure へ関数アプリのデプロイを実行しています。
:::

```sh
$ mvn clean package
$ mvn azure-functions:deploy
```

#### Azure Portal の確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/5a8d1854-1c02-3ab3-f2c1-317155d1af1b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3206449/a76734b8-566a-ba3a-0fcc-602028b92d7c.png)

:::note info
ここまでの手順で、**Azure** に**必要なリソース**と、**関数アプリ**が作成されたことが確認できました。
:::

### アプリの動作確認

**Ubuntu** にシェル変数を作成します。

```sh
$ resource_group_name=rg-hello
$ functionapp_name=func-hello-spring-boot
```

関数アプリの FQDN をシェル変数 **functionapp_fqdn** として取得します。

```sh
$ functionapp_fqdn=$(az functionapp show \
    --resource-group $resource_group_name \
    --name $functionapp_name \
    --query 'hostNames' \
    --output tsv)
```

関数アプリの FQDN を確認します。

```sh
$ set | grep functionapp_fqdn
functionapp_fqdn=func-hello-spring-boot.awesomewebsites.net
```

ターミナルから curl コマンドで確認します。

```s
$ curl https://$functionapp_fqdn/api/data -w '\n'
{
  "message": "Hello World!"
}
```

:::note info
ターミナルに **{"message":"Hello World!"}** と表示され、**JSON データ**を取得することが出来ました。
:::

## まとめ

Azure Functions 環境で、JAR ファイル形式の Spring Boot Web サービスを起動することができました。

:::note warn
**Maven**、**Azure CLI** を使って、Spring Boot アプリの開発から、Azure 環境へのデプロイまで、**すべてをターミナルから行うことができます**。このように、クラウドでのシステム開発に必要なスキルや理解を深めることができます。初めての人でも簡単に手順を追うことができるので、ぜひ挑戦してみてください。
:::

どうでしたか？ 検証目的として、WSL Ubuntu で、Spring Boot Web アプリケーションを Azure Functions 環境で手軽に起動することができます。ぜひお試しください。今後も Azure の開発環境などを紹介していきますので、ぜひお楽しみにしてください。