最近Azureの勉強を始めて、実際に何か動かしてみたくなったので、手始めにAzure Functionsで遊んでみました。
この記事でやっていること
- Azure Functionsのローカル開発環境構築(Java)
- コマンドによるAzureへのデプロイ
Microsoftのドキュメントに沿って実際に構築していきます。
この手の記事は他に沢山ありますが、自分なりに躓いたポイントなども残しておこうと思います。
※Javaの開発経験がある方向けの内容となっています
目次
- Azure Core Tools のインストール
- Azure CLI のインストール
- Maven のインストール
- Mavenプロジェクトの作成
- ローカルサーバー起動
- 関数呼び出し
- 関数本体のコード
- コードの変更
- Azure へのデプロイ
動作環境
作業時の環境は以下の通りです。
- OS
- Windows 11 Pro (21H2)
- Ubuntu 20.04 (WLS)
※最新版が動かなかったので古いバージョンを使用
- JDK
-
OpenJDK 11
※JDKのインストールについては手順に含まれていません
-
OpenJDK 11
Azure Core Tools のインストール
-
Windows
インストーラーでインストール。
-
Linux
-
パッケージの整合性検証。
curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg sudo mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg
-
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'
-
-
インストール
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プロジェクトの作成
-
プロジェクト作成
mvn archetype:generate -DarchetypeGroupId=com.microsoft.azure -DarchetypeArtifactId=azure-functions-archetype -DjavaVersion=11
-DjavaVersion=11
の部分で java のバージョンを指定できます。※2022/10/12 時点では
8
と11
が選択可能 (17
はプレビュー機能) -
以下の入力を求められるので入力する。
設定 値 説明 groupId com.azure_functions プロジェクトを一意に識別する値。 artifactId azure-functions バージョン番号のない、jar の名前。 version 1.0-SNAPSHOT jarのバージョン。規定値を選択。 package com.azure_functions パッケージ名。規定値は groupId と同じ。 -
「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
-
作成されたフォルダに移動し、以下のコマンドでビルド。
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
関数本体のコード
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();
}
}
}
やってること
- リクエストのクエリパラメータ、またはボディから
name
というパラメータを取得。 - name が取得できなかった場合は、レスポンスとして
Please pass a name on the query string or in the request body
を返す。 - name パラメータが取得できた場合は、レスポンスとして
Hello, <nameの値>
を返す。
@FunctionName
アノテーションで関数名を指定する。(Azure にデプロイされる名前)
次に説明する@HttpTrigger
アノテーションでroute
を指定しない場合は、/api/<関数名>
がエンドポイントとなる。
route
を指定することでエンドポイントをカスタマイズすることができます。
@HttpTrigger
アノテーションでトリガーを設定する。
サンプルでは、以下のようになっている。
設定 | 設定内容 | 説明 |
---|---|---|
name | req | リクエストまたはリクエスト本文の関数コードで使用される変数名。 (・・・??) |
methods | GET, POST | リクエストを受け付ける HTTP メソッド。 配列で複数指定可能。 |
authLevel | ANONYMOUS | 関数を呼び出す際の承認レベルを指定する。
|
@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
<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>
<!-- ↑追加↑ -->
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
で事前にシリアライズしたものを渡すようにしました。
/**
* 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;
}
}
@Data
@Builder
public class TodoItem {
/** 内容 */
private String content;
/** 完了フラグ */
private boolean done;
/** 作成日時 */
private LocalDateTime createdAt;
}
/**
* 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
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キーも指定できます。
実行すると、画面上のコンソールに標準・エラー出力がリアルタイムで表示されます。
うまく動かないときはここで確認してみると良いと思います。
私の環境だけかもしれませんが、関数自体が成功してもレスポンス: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を以下のように変更。
<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
コマンドが失敗しているらしいので、単体で実行してみましたが、何の問題もなく動きました。
結局、原因追いきれず、諦めて認証方式を「デバイスコード」に変更。
<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 続きの記事を投稿しました