Azure Functions を Java の App Service でサービスエンドポイント経由の Azure Storage Queue とバインドしてみた

Azure Functions を Java の App Service でサービスエンドポイント経由の Azure Storage Queue とバインドしてみました。

## 検証環境を準備

```bash:bash
# 環境変数をセットします
prefix=mnrfncjq
region=japaneast

# リソースグループを作成します
az group create \
  --name ${prefix}-rg \
  --location $region

# VNET を作成します
az network vnet create \
  --name ${prefix}-vnet \
  --resource-group ${prefix}-rg \
  --address-prefixes 10.0.0.0/24

# NSG を作成します
az network nsg create \
  --resource-group ${prefix}-rg \
  --name ${prefix}-nsg

# サブネットを作成します
az network vnet subnet create \
  --resource-group ${prefix}-rg \
  --vnet-name ${prefix}-vnet \
  --name fun-subnet \
  --address-prefix 10.0.0.0/26 \
  --network-security-group ${prefix}-nsg \
  --service-endpoints Microsoft.Storage

# Azure Storage を作成します
az storage account create \
  --name ${prefix}stor \
  --resource-group ${prefix}-rg \
  --sku Standard_LRS

# キューを作成します
az storage queue create \
  --name messages \
  --account-name ${prefix}stor

# Azure Storage をネットワーク制限します
az storage account update \
  --name ${prefix}stor \
  --resource-group ${prefix}-rg \
  --default-action deny

# 自分の IP アドレスからのアクセスを許可します
az storage account network-rule add \
  --account-name ${prefix}stor \
  --resource-group ${prefix}-rg \
  --ip-address $(curl -s inet-ip.info)

# サブネットからのアクセスを許可します
az storage account network-rule add \
  --account-name ${prefix}stor \
  --resource-group ${prefix}-rg \
  --vnet-name ${prefix}-vnet \
  --subnet fun-subnet

# Application Insights を作成します
az monitor app-insights component create \
  --app ${prefix}-ai \
  --location $region \
  --resource-group ${prefix}-rg

# App Service Plan を作成します
az appservice plan create \
  --name ${prefix}-funplan \
  --resource-group ${prefix}-rg \
  --sku B1

# Azure Functions を作成します
az functionapp create \
  --name ${prefix}-fun \
  --resource-group ${prefix}-rg \
  --plan ${prefix}-funplan \
  --runtime java \
  --functions-version 4 \
  --storage-account ${prefix}stor \
  --app-insights ${prefix}-ai \
  --https-only \
  --os-type Windows \
  --assign-identity

# FTP を無効にします
az webapp config set \
  --name ${prefix}-fun \
  --resource-group ${prefix}-rg \
  --ftps-state Disabled

# 自分の IP アドレスからのアクセスを許可します
az webapp config access-restriction add \
  --name ${prefix}-fun \
  --resource-group ${prefix}-rg \
  --priority 100 \
  --rule-name MyIP \
  --action Allow \
  --ip-address $(curl -s inet-ip.info)

# VNET 統合して Functions から Azure Storage にアクセスできるようにします
az webapp vnet-integration add \
  --name ${prefix}-fun \
  --resource-group ${prefix}-rg \
  --vnet ${prefix}-vnet \
  --subnet fun-subnet
```

## ローカルで Azure Functions を作成

```bash:bash
# Maven で Java 11 の Functions　雛形を作成します 
mvn archetype:generate -DarchetypeGroupId=com.microsoft.azure -DarchetypeArtifactId=azure-functions-archetype -DjavaVersion=11

# 入力サンプル
# groupId: com.example
# artifactId: java-func-example
# version	1.0-SNAPSHOT
# package	com.example

# 作成されたディレクトリに移動します
cd java-func-example

# Maven で Functions アプリをビルドします
mvn clean package

# Functions アプリをローカルで動かします
mvn azure-functions:run
```

## Functions アプリに QueueOutput バインドを追加

```java:src/main/java/com/example/Functions.java
package com.example;

// 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 com.microsoft.azure.functions.annotation.*;
import com.microsoft.azure.functions.*;

import java.util.Optional;

/**
 * Azure Functions with HTTP Trigger.
 */
public class Function {
    /**
     * This function listens at endpoint "/api/HttpExample". Two ways to invoke it using "curl" command in bash:
     * 1. curl -d "HTTP Body" {your host}/api/HttpExample
     * 2. curl "{your host}/api/HttpExample?name=HTTP%20Query"
     */
    @FunctionName("HttpExample")
    public HttpResponseMessage run(
            @HttpTrigger(
                name = "req",
                methods = {HttpMethod.GET, HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS)
                HttpRequestMessage<Optional<String>> request,
            @QueueOutput(
                name = "msg",
                queueName = "messages",
                connection = "AzureWebJobsStorage")
                OutputBinding<String> msg,
            final ExecutionContext context) {
        context.getLogger().info("Java HTTP trigger processed a request.");

        // Parse query parameter
        final String query = request.getQueryParameters().get("name");
        final String name = request.getBody().orElse(query);

        if (name == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a name on the query string or in the request body").build();
        } else {
            msg.setValue(name);
            return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
        }
    }
}
```

## Functions アプリのデプロイ設定

```xml:pom.xmlの該当箇所
                <configuration>
                    <!-- function app name -->
                    <appName>mnrfncjq-fun</appName>
                    <!-- function app resource group -->
                    <resourceGroup>mnrfncjq-rg</resourceGroup>
                    <!-- function app service plan name -->
                    <appServicePlanName>mnrfncjq-funplan</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>B1</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>
                    </runtime>
                    <appSettings>
                        <property>
                            <name>FUNCTIONS_EXTENSION_VERSION</name>
                            <value>~4</value>
                        </property>
                    </appSettings>
                </configuration>
```

## Azure Functions にデプロイして動作確認

```bash:bash
# テスト用のコードを無効化します
mv src/test/java/com/example/FunctionTest.java src/test/java/com/example/FunctionTest.java.bak
mv src/test/java/com/example/HttpResponseMessageMock.java src/test/java/com/example/HttpResponseMessageMock.java.bak

# Functions アプリをビルドします
mvn package

# Functions アプリをデプロイします
mvn azure-functions:deploy

# Functions 経由でキューにメッセージを登録します
curl https://mnrfncjq-fun.azurewebsites.net/api/httpexample?name=HelloMessage

# キューにメッセージ登録されていれば成功
# Application Insights の trace ログにもメッセージが表示されていれば成功
```

## 参考

https://learn.microsoft.com/ja-jp/azure/azure-functions/create-first-function-cli-java?tabs=bash%2Cazure-cli%2Cbrowser

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-add-output-binding-storage-queue-cli?pivots=programming-language-java&tabs=in-process%2Cv1%2Cbash%2Cbrowser

https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-storage-queue-trigger?tabs=python-v2%2Cin-process%2Cextensionv5&pivots=programming-language-java