🐧

Azure Functions を Spring Cloud Functionでやってみた

2022/11/25に公開

Azure での Spring Cloud Function の概要 が例のごとくMavenだったのでGradle版でやります。
基本、ここのやっていることをそのままやる感じです。

環境

Windows 11
OpenJDK 11 (Microsoft Build)
VSCode

補足

2022/11時点
WSL on Ubuntu でやってましたが、動きませんでした!!!😿
Azure Functions Core Toolsが見つからないって言われました。Pathとかうまく見れないのかなぁ。C#だといけたんだけど。
(これで結構時間を溶かしました)

準備

Azure Functions のインスタンスを建てる

Java 11 で建てる。割愛。

Spring Initializr でプロジェクトの作成

選んだのは

  • Spring Boot Dev Tools(いつものやつ)
  • Lombok(いつものやつ)
    Spring Initializr
生成されたbuild.gradle
plugins {
	id 'org.springframework.boot' version '2.7.5'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
	id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

:::

生成したプロジェクトをダウンロードして解凍して展開する。いつものフォルダ構成。

build.gradle を修正する

Azure Functions のライブラリは別途追加が必要なので追加する。
ついでにローカル起動&deploy用のpluginも追記する。

最終形態のbuild.gradle
plugins {
	id 'org.springframework.boot' version '2.7.5'
	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
	id 'java'
+	id "com.microsoft.azure.azurefunctions" version "1.11.0"
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

+ ext {
+   set('springCloudAzureVersion', "4.4.1")
+   set('springCloudVersion', "2021.0.5")
+ }

dependencies {
+    implementation 'com.microsoft.azure.functions:azure-functions-java-library:2.1.0'
+    implementation 'org.springframework.cloud:spring-cloud-function-adapter-azure:3.2.7'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

+ jar {
+   enabled = true
+   archiveFileName = "${rootProject.name}-${version}.jar"
+ 	manifest {
+     attributes 'Main-Class' : 'com.example.demo.DemoApplication'
+   }
+ }

tasks.named('test') {
	useJUnitPlatform()
}

+ azurefunctions {
+     subscription = 'サブスクリプションID'
+     resourceGroup = 'リソースグループ名'
+     appName = 'アプリ名'
+     pricingTier = 'Consumption'   // AppServicePlanだとB2などプラン名を記述
+     region = 'japaneast'
+     runtime {
+       os = 'linux'
+     }
+ 	appSettings {
+ 	  WEBSITE_RUN_FROM_PACKAGE = '1'
+         FUNCTIONS_EXTENSION_VERSION = '~4'
+     	  FUNCTIONS_WORKER_RUNTIME = 'java'
+     	  MAIN_CLASS = 'com.example.demo.DemoApplication'
+ 	}
+   auth {
+       type = 'azure_cli'
+     }
+ 	localDebug = "transport=dt_socket,server=y,suspend=n,address=5005"
+ }

Azure 構成ファイルを作成する

このへんは一緒のはず
Create Azure configuration files
※日本語サイトは更新がされていなかったので、英語サイトを参考にすること。

host.json のversionって別にFunctionsのバージョンってことではないのね。
Azure Functions 2.x 以降の host.json のリファレンス

host.json と local.settings.json を追加

host.json
{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[3.*, 4.0.0)"
  },
  "functionTimeout": "00:10:00"
}
local.settings.json
{
	"IsEncrypted": false,
	"Values": {
		"AzureWebJobsStorage": "",
		"FUNCTIONS_WORKER_RUNTIME": "java",
		"FUNCTIONS_EXTENSION_VERSION": "~4",
		"MAIN_CLASS": "com.example.demo.DemoApplication",
		"AzureWebJobsDashboard": ""
	}
}

ソースコードの編集

ドメインオブジェクトを作成する

ドメイン オブジェクトを作成する

com.example.demo.model.User.java
package com.example.demo.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {

    private String name;

}
com.example.demo.model.Greeting.java
package com.example.demo.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Greeting {

    private String message;
}

コンポーネントの作成

Spring Boot アプリケーションを作成する
DemoApplication.java は Initializr で生成されたのをそのまま流用。
Hello.javaはそのまま。

com.example.demo.Hello.java
package com.example.demo;

import java.util.function.Function;

import org.springframework.stereotype.Component;

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

import reactor.core.publisher.Mono;

@Component
public class Hello implements Function<Mono<User>, Mono<Greeting>> {

    @Override
    public Mono<Greeting> apply(Mono<User> mono) {
        return mono.map(user -> new Greeting("Hello, " + user.getName() + "!\n"));
    }

}

関数の作成

Azure 関数を作成する

ここはそのままで、エンドポイントが 『/hello』 の関数を作っている。
ControllerじゃなくてHandlerなんだな。

com.example.demo.HelloHandler.java
package com.example.demo;

import java.util.Optional;

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

import com.example.demo.model.Greeting;
import com.example.demo.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 FunctionInvoker<User, Greeting> {

    @FunctionName("hello")
    public HttpResponseMessage execute(@HttpTrigger(name = "request", methods = { HttpMethod.GET,
            HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<User>> request,
            ExecutionContext context) {
        User user = request.getBody()
                .filter((u -> u.getName() != null))
                .orElseGet(() -> new User(
                        request.getQueryParameters()
                                .getOrDefault("name", "world")));
        context.getLogger().info("Greeting user name: " + user.getName());
        return request
                .createResponseBuilder(HttpStatus.OK)
                .body(handleRequest(user, context))
                .header("Content-Type", "application/json")
                .build();
    }
}

単体テストの作成

単体テストを追加する
お勧めされたので、検証のために単体テストを書きます。
assertjは使わない派なので、JUnitのみの記述に変更してます。
testメソッドはFunction関係ないテストってことなんだろうか。

src/test/com.example.demo.HelloTest.java
package com.example.demo;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.logging.Logger;

import org.junit.jupiter.api.Test;
import org.springframework.cloud.function.adapter.azure.FunctionInvoker;

import com.example.demo.model.Greeting;
import com.example.demo.model.User;
import com.microsoft.azure.functions.ExecutionContext;

import reactor.core.publisher.Mono;

class HelloTest {

    @Test
    void test() {

        Mono<Greeting> result = new Hello().apply(Mono.just(new User("foo")));
        assertEquals("Hello, foo!\n", result.block().getMessage());
    }

    @Test
    void start() {
        FunctionInvoker<User, Greeting> handler = new FunctionInvoker<>(
                Hello.class);
        Greeting result = handler.handleRequest(new User("foo"), new ExecutionContext() {
            @Override
            public Logger getLogger() {
                return Logger.getLogger(HelloTest.class.getName());
            }

            @Override
            public String getInvocationId() {
                return "id1";
            }

            @Override
            public String getFunctionName() {
                return "hello";
            }
        });
        handler.close();
        assertEquals("Hello, foo!\n", result.getMessage());
    }
}

ローカルでデバッグ

> .\gradlew azureFunctionsRun


表示されたURLをクリックして、メッセージが表示されていればOK

Functions にデプロイ

  • (Option) サブスクリプションやテナントが複数ある場合は事前に設定しておく
    • build.gradle に設定していても、AZ CLI のデフォルトサブスクリプションが違うだけで失敗するので注意
> az login --tenant 'テナントID'
> az account set --subscription 'サブスクリプション名 or サブスクリプションID'
  • deploy task
> .\gradlew azurefunctionsDeploy

Deployment succeeded, but failed to list http trigger urls.

と言ってくるけど、https://functionsのドメイン/api/hello にアクセスすると、一応表示されます。
ちなみに、初回のデプロイはめちゃくちゃ遅くてタイムアウトとかするので、もう一回チャレンジすると成功するかも。

参考

とにかく情報が少ないのが辛いですね。。。がんばります。

Discussion