LoginSignup
8
3

More than 1 year has passed since last update.

Azure FunctionsでAPIを作る~Java編~

Last updated at Posted at 2022-10-12

最近Azureの勉強を始めて、実際に何か動かしてみたくなったので、手始めにAzure Functionsで遊んでみました。

この記事でやっていること

  • Azure Functionsのローカル開発環境構築(Java)
  • コマンドによるAzureへのデプロイ

Microsoftのドキュメントに沿って実際に構築していきます。
この手の記事は他に沢山ありますが、自分なりに躓いたポイントなども残しておこうと思います。

※Javaの開発経験がある方向けの内容となっています

目次

動作環境

作業時の環境は以下の通りです。

  • OS
    • Windows 11 Pro (21H2)
    • Ubuntu 20.04 (WLS)
      ※最新版が動かなかったので古いバージョンを使用
  • JDK
    • OpenJDK 11
      ※JDKのインストールについては手順に含まれていません

Azure Core Tools のインストール

  • Windows

    インストーラーでインストール。

  • Linux

    1. パッケージの整合性検証。

      curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg
      sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
      
    2. apt ソースリストを設定。

      • Ubuntu の場合

        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'
        
      • Debian の場合

        sudo sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/debian/$(lsb_release -rs | cut -d'.' -f 1)/prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list'
        
    3. インストール

      sudo apt-get update
      sudo apt-get install azure-functions-core-tools-4
      

Azure CLI のインストール

  • Windows

    インストーラーでインストール。

  • Linux

    sudo apt install azure-cli
    

    私の作業環境(Ubuntu20.04)ではインストールされるバージョンが古いようなので、ここを参考に、インストールしました。

    curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
    

以下のコマンドを実行してバージョンが表示されればインストール完了。

az --version

Maven のインストール

  • Windows

    この記事を参考にインストール。

  • Linux

    sudo apt install maven
    

Mavenプロジェクトの作成

  1. プロジェクト作成

    mvn archetype:generate -DarchetypeGroupId=com.microsoft.azure -DarchetypeArtifactId=azure-functions-archetype -DjavaVersion=11
    

    -DjavaVersion=11 の部分で java のバージョンを指定できます。

    ※2022/10/12 時点では 811 が選択可能 (17はプレビュー機能)

  2. 以下の入力を求められるので入力する。

    設定 説明
    groupId com.azure_functions プロジェクトを一意に識別する値。
    artifactId azure-functions バージョン番号のない、jar の名前。
    version 1.0-SNAPSHOT jarのバージョン。規定値を選択。
    package com.azure_functions パッケージ名。規定値は groupId と同じ。
  3. 「Y」を入力し、「BUILD SUCCESS」と表示されれば OK。

    作成されるもの
    azure-functions/
    ├── host.json
    ├── local.settings.json
    ├── pom.xml
    └── src
        ├── main
        │   └── java
        │       └── com
        │           └── azure_functions
        │               └── Function.java
        └── test
            └── java
                └── com
                    └── azure_functions
                        ├── FunctionTest.java
                        └── HttpResponseMessageMock.java
    
  4. 作成されたフォルダに移動し、以下のコマンドでビルド。

    mvn clean package
    

ローカルサーバー起動

mvn azure-functions:run

正常に起動すると以下のようにエンドポイントが表示される。

・・・
Functions:

        HttpExample: [GET,POST] http://localhost:7071/api/HttpExample
・・・

関数呼び出し

表示されたエンドポイントにアクセスすると関数を呼び出すことができます。

コマンドで確認

curl http://localhost:7071/api/HttpExample
結果
Please pass a name on the query string or in the request body

パラメータを指定して呼び出し

curl http://localhost:7071/api/HttpExample?name=AzureLearn
結果
Hello, AzureLearn

関数本体のコード

Function.java
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,
            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 {
            return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
        }
    }
}

やってること

  1. リクエストのクエリパラメータ、またはボディからnameというパラメータを取得。
  2. name が取得できなかった場合は、レスポンスとしてPlease pass a name on the query string or in the request bodyを返す。
  3. name パラメータが取得できた場合は、レスポンスとしてHello, <nameの値>を返す。

@FunctionNameアノテーションで関数名を指定する。(Azure にデプロイされる名前)
次に説明する@HttpTriggerアノテーションでrouteを指定しない場合は、/api/<関数名>がエンドポイントとなる。
routeを指定することでエンドポイントをカスタマイズすることができます。

@HttpTriggerアノテーションでトリガーを設定する。

サンプルでは、以下のようになっている。

設定 設定内容 説明
name req リクエストまたはリクエスト本文の関数コードで使用される変数名。 (・・・??)
methods GET, POST リクエストを受け付ける HTTP メソッド。
配列で複数指定可能。
authLevel ANONYMOUS 関数を呼び出す際の承認レベルを指定する。
  • ANONYMOUS - API キーなしでアクセス可能。
  • FUNCTION - 関数固有の API キーが必要。
  • ADMIN - マスターキーが必要。

@HttpTriggerの詳細については
Azure Functions の HTTP トリガー を参照。

コードの変更

そのままではつまらないので、適当なJSONデータを返すようにコードを変更してみます。

azure-functions/
├── host.json
├── local.settings.json
├── pom.xml ------------------------------------ 変更
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── azure_functions
    │               ├── Function.java ---------- 変更
    │               ├── models
    │               │   └── TodoItem.java ------ 追加
    │               ├── services
    │               │   └── TodoService.java --- 追加
    │               └── utils
    │                   └── JsonUtil.java ------ 追加
    └── test
        └── java
            └── com
                └── azure_functions
                    ├── FunctionTest.java
                    └── HttpResponseMessageMock.java
pom.xml
    <dependencies>
        <dependency>
            <groupId>com.microsoft.azure.functions</groupId>
            <artifactId>azure-functions-java-library</artifactId>
            <version>${azure.functions.java.library.version}</version>
        </dependency>

        <!-- ↓追加↓(Lombok使いたかった) -->
        <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>

        <!-- JSONシリアライズのカスタマイズ用 -->
        <dependency>
            <groupId>com.azure</groupId>
            <artifactId>azure-core-serializer-json-jackson</artifactId>
            <version>1.2.22</version>
        </dependency>
        <!-- ↑追加↑ -->
Function.java (関数本体)
public class Function {

   /**
    * TODO一覧を取得
    * 
    * @param request リクエスト
    * @param context 実行コンテキスト
    * @return レスポンス
    */
   @FunctionName("fetchTodoItems")
   public HttpResponseMessage fetchTodoList(
               @HttpTrigger(name = "req", methods = { HttpMethod.GET,
                               HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS,
                               // エンドポイントを「/api/todo/list」に設定
                               route = "todo/list") HttpRequestMessage<Optional<String>> request,
               final ExecutionContext context) {
      context.getLogger().info("Java HTTP trigger processed a request.");

      // TODO一覧取得
      var todoService = new TodoService(context);
      List<TodoItem> todoItems = todoService.fetchTodoItems();

      // TODO一覧をJSONに変換
      var jsonUtil = new JsonUtil(context);
      String json = jsonUtil.serialize(todoItems);

      return request.createResponseBuilder(HttpStatus.OK)
                      .header("Content-Type", "application/json;")
                      .body(json).build();

   }

}
  • TODO一覧を返すように変更
  • エンドポイントをカスタマイズ

躓いたポイント①

最後のレスポンスを生成する部分で、body()にはtodoItemsをそのまま渡しても勝手にシリアライズしてくれますが、LocalDateTime型が綺麗にフォーマットされないので、後述のJsonUtil.javaで事前にシリアライズしたものを渡すようにしました。

TodoService.java (TODO一覧を取ってくるクラス)
/**
 * TODOサービス
 */
@RequiredArgsConstructor
public class TodoService {

   /** 実行コンテキスト */
   private final ExecutionContext context;

   /**
    * TODO一覧を取得
    * 
    * @return TODO一覧
    */
   public List<TodoItem> fetchTodoItems() {
      context.getLogger().info("fetchTodoItems");
      // ダミーデータ生成
      List<TodoItem> todoItems = List.of(
                      TodoItem.builder()
                              .content("Azureの勉強")
                              .done(false)
                              .createdAt(LocalDateTime.now())
                              .build(),
                      TodoItem.builder()
                              .content("Javaの勉強")
                              .done(true)
                              .createdAt(LocalDateTime.now())
                              .build(),
                      TodoItem.builder()
                              .content("TypeScriptの勉強")
                              .done(false)
                              .createdAt(LocalDateTime.now())
                              .build());

      return todoItems;
   }

}
TodoItem.java (TODOの入れ物)
@Data
@Builder
public class TodoItem {

    /** 内容 */
    private String content;

    /** 完了フラグ */
    private boolean done;

    /** 作成日時 */
    private LocalDateTime createdAt;

}
JsonUtil.java (JSON変換用ユーティリティ)
/**
 * JSON関連のユーティリティ
 */
@RequiredArgsConstructor
public class JsonUtil {

    /** JSONシリアライズ用のマッパー */
    private static final ObjectMapper MAPPER = objectMapper();

    /** 実行コンテキスト */
    private final ExecutionContext context;

    private static ObjectMapper objectMapper() {
        var mapper = new ObjectMapper();

        // LocalDateTime用のシリアライザを追加
        var javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class,
                new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")));
        mapper.registerModule(javaTimeModule);

        return mapper;
    }

    /**
     * オブジェクトをJSONに変換
     * 
     * @param <T> オブジェクトの型
     * @param obj オブジェクト
     * @return JSON文字列
     */
    public <T> String serialize(T obj) {
        try {
            return MAPPER.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            context.getLogger().severe("オブジェクト→JSONの変換に失敗しました。エラーメッセージ:" + e.getMessage());
            throw new IllegalArgumentException(e);
        }
    }

}

Azure へのデプロイ

Azure にログイン

az login

コマンド実行すると、ブラウザが起動してログインしろと言われるので従います。
※ログインは初回のみでOKです

デプロイ

mvn azure-functions:deploy

デプロイが成功すると、エンドポイントが表示されます。
/api/todo/listになっていますね。

結果
[INFO] HTTP Trigger Urls:
[INFO]   fetchTodoItems : https://azure-learn-functions796.azurewebsites.net/api/todo/list
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

デプロイ結果確認

作成されるリソース

デプロイ時、以下のリソースが自動的に作成されます。

  • リソースグループ
    • 関数アプリ
    • ストレージアカウント
    • App Service プラン
    • Application Insights

image.png

pom.xml の build.plugins.plugin.configuration で、作成されるリソースの設定をカスタマイズできます。

設定名 既定値 説明
appName プロジェクト作成時に指定したartifactId + プロジェクト作成日時 関数アプリ名
resourceGroup java-functions-group 関数アプリが属するリソースグループ名。
appServicePlanName java-functions-app-service-plan App Service プラン名
region westus 作成されるリソースが属するリージョン

コマンドで確認

curl https://azure-learn-functions796.azurewebsites.net/api/todo/list
結果 (見やすいように整形済)
[
  {
    "content": "Azureの勉強",
    "done": false,
    "createdAt": "2022-10-10 18:38:26"
  },
  {
    "content": "Javaの勉強",
    "done": true,
    "createdAt": "2022-10-10 18:38:26"
  },
  {
    "content": "TypeScriptの勉強",
    "done": false,
    "createdAt": "2022-10-10 18:38:26"
  }
]

レスポンスとして、body()に設定したJSONが返されました。

Azure Portal上で確認

Azure上にデプロイした関数は、Azure Portal 上からも実行できます。
関数アプリ<関数アプリ名>関数<関数名>コードとテストテストと実行
パラメータやヘッダ、APIキーも指定できます。
実行すると、画面上のコンソールに標準・エラー出力がリアルタイムで表示されます。
うまく動かないときはここで確認してみると良いと思います。

image.png

私の環境だけかもしれませんが、関数自体が成功してもレスポンス:500が返ってきてしまいます。
https://functions.azure.com/api/passthroughというURLにリクエストを投げて、その先で実際の関数を呼んでいるようで、そこが500を返していました。

[2022/11/07] 再度確認したら正常に返ってきました。以前試したときは何かが足りなかったのかもしれません。

躓いたポイント②

Windows(PowerShell)では問題ありませんでしたが、私の環境ではWSL上でデプロイコマンドを実行すると、エラーとなりプロイできませんでした。

エラー
Failed to execute goal com.microsoft.azure:azure-functions-maven-plugin:1.21.0:deploy (default-cli) on project azure-functions: login with (AUTO): device code consumer is not configured.

login with (AUTO)となっており、認証方式が自動で選択されるみたいですが、実際にはデバイスコード認証が使われているようです。(エラー内容から推測)
「デバイス認証になってるけど、デバイスコードが設定されてないよ」って感じでしょうか。
そこで、認証方式を「Azure CLI」に変更するため、pom.xmlを以下のように変更。

pom.xml
   <build>
      <plugins>
         ・・・
         <plugin>
            <groupId>com.microsoft.azure</groupId>
            <artifactId>azure-functions-maven-plugin</artifactId>
            <version>${azure.functions.maven.plugin.version}</version>
            <configuration>
               ・・・
               <!-- ↓追加↓ -->
               <auth>
                  <type>azure_cli</type>
               </auth>
               <!-- ↑追加↑ -->
            </configuration>
            ・・・
         </plugin>
         ・・・
      </plugins>
   </build>

デプロイすると、今度は以下のエラーが発生...

エラー
Failed to execute goal com.microsoft.azure:azure-functions-maven-plugin:1.21.0:deploy (default-cli) on project azure-functions: login with (AZURE_CLI): execute Azure Cli command 'az account list --output json' failed due to error: Process exited with an error: 2 (Exit value: 2).

az account list --output jsonコマンドが失敗しているらしいので、単体で実行してみましたが、何の問題もなく動きました。

結局、原因追いきれず、諦めて認証方式を「デバイスコード」に変更。

pom.xml
<auth>
   <type>device_code</type>
</auth>

デプロイコマンドを実行すると、URLとデバイスコードが表示されるので、ブラウザで指定されたURLを開き、表示されたダイアログにデバイスコードを入力します。
今度はちゃんとデバイスコード入力しているので、デバイスコード認証でも問題なく通りました。

[INFO] Auth type: DEVICE_CODE
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code H6HNEDQCQ to authenticate.

あとがき

WSL上でのデプロイで躓きましたが、意外と簡単に関数アプリの作成&デプロイができました。
今回はAPIキーは使用していないため、誰でもアクセスできるようになっていますが、今後はその辺りも考慮していきたいと思います。

次は今回作った関数からAzureのマネージドデータベース(MySQL)に接続してみたいと思います。

最後まで読んでいただき、ありがとうございました!

2022/10/17 続きの記事を投稿しました

参考文献

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3