対象者
- Azure FunctionsからAzure Database for MySQLに接続してみたい方
- Javaで開発したい方
- コマンドベースで操作したい方
概要
この記事では以下の内容について扱っています。
基本的にコマンドベースで進めていきます。
- Azure Database for MySQLフレキシブル サーバー のリソース作成
- Azure FunctionsからMySQLに接続
- 【おまけ】Azure FunctionsでAPIキーを使用する
前提
前回投稿した記事「Azure FunctionsでAPIを作る~Java編~」の続きとなります。
Azure Functionsの開発環境構築、デプロイ方法については記事をご参照ください。
<前回のあらすじ>
- Azure Functionsのローカル開発環境構築(Java)
- 適当なダミーJSONデータを返すAPIを作成
→ 【今回】このデータをMySQLから取得するように変更(ついでに登録・更新・削除も追加) - Azure へのデプロイ
動作環境
作業時の環境は以下の通りです。
- OS
- Windows 11 Pro (21H2)
- Ubuntu 20.04 (WLS)
- JDK
- Azure CLI 2.40.0
データベースの準備
Azure Database for MySQL フレキシブル サーバー の作成
Azure Database for MySQLには「シングルサーバー」と「フレキシブルサーバー」の2種類がありますが、シングルサーバーは提供終了予定のため、フレキシブルサーバーが推奨されているようです。
MySQLサーバーの作成には az mysql flexible-server create コマンドを使用します。
az mysql flexible-server create \
--name <MySQLサーバー名> \
--resource-group <リソースグループ名> \
--location eastus \
--admin-user <Adminユーザー名> \
--admin-password <Adminパスワード> \
--sku-name Standard_B1s \
--public-access 0.0.0.0 \
--version 8.0.21
PowerShell
az mysql flexible-server create `
--name <MySQLサーバー名> `
--resource-group <リソースグループ名> `
--location eastus `
--admin-user <Adminユーザー名> `
--admin-password <Adminパスワード> `
--sku-name Standard_B1s `
--public-access 0.0.0.0 `
--version 8.0.21
オプションの補足
上記オプション指定では、MySQLサーバーはパブリックアクセスモードで作成されます。
パブリックといっても、ファイアウォールによって許可されたIPアドレスのみが接続可能です。
ファイアウォールを設定しない場合、どのIPからの接続も受け付けません。
--public-access
を 0.0.0.0
に設定することで、Azure内リソースからMySQLサーバーにパブリックアクセス可能となるようファイアウォールが構成されます。
今回は触れませんが、VNet統合という仕組みを使うと、Azure FunctionsとMySQL間でプライベート通信を行うことが可能になるようです。
データベース作成
MySQLサーバーを作成すると、flexibleserverdb
というデータベースが自動で作成されますが、新たに追加してみたいと思います。
データベースの作成には az mysql flexible-server db create コマンドを使用します。
az mysql flexible-server db create \
--resource-group <リソースグループ名> \
--server-name <MySQLサーバー名> \
--charset utf8mb4 \
--collation utf8mb4_bin \
--database-name <データベース名>
PowerShell
az mysql flexible-server db create `
--resource-group <リソースグループ名> `
--server-name <MySQLサーバー名> `
--charset utf8mb4 `
--collation utf8mb4_bin `
--database-name <データベース名>
文字コード(charset)、照合順序(collation)等はお好みで。
ファイアウォールルールの追加
次は作成したデータベースにテーブルを作成・・・と行きたいところなのですが、このままではクライアント端末からMySQLサーバーにアクセスすることができません。
上記で書いたように、アクセスするにはファイアウォールで接続元のIP(グローバルIP)を許可する必要があります。
ファイアウォールルールの追加には az mysql flexible-server firewall-rule create コマンドを使用します。
az mysql flexible-server firewall-rule create \
--resource-group <リソースグループ名> \
--name <MySQLサーバー名> \
--rule-name <ファイアウォールルール名> \
--start-ip-address <クライアントのIPアドレス>
PowerShell
az mysql flexible-server firewall-rule create `
--resource-group <リソースグループ名> `
--name <MySQLサーバー名> `
--rule-name <ファイアウォールルール名> `
--start-ip-address <許可するIPアドレス>
オプションの補足
start-ip-address
と end-ip-address
で範囲指定も可能です。
start-ip-address
のみ指定した場合は指定した、IPのみが対象となります。
自分のIPアドレスを確認するには、「IPアドレス 確認」とかでググってください。
例えばここなどで確認できます。
(ここはコマンド使わんのかい!)
テーブルの作成
ファイアウォールルールの追加によりMySQLサーバーに接続できるようになっているはずなので、テーブルを作成します。
テーブル作成は az mysql flexible-server execute コマンドを使用して、クエリを実行します。
az mysql flexible-server execute \
--name <MySQLサーバー名> \
--admin-user <Adminユーザー名> \
--admin-password <Adminパスワード> \
--database-name <データベース名> \
--querytext \
"
create table todo (
id int(8) auto_increment,
content varchar(60),
done bit(1) not null default b'0',
created_at datetime not null default current_timestamp,
primary key (id)
);
"
PowerShell
az mysql flexible-server execute `
--name <MySQLサーバー名> `
--admin-user <Adminユーザー名> `
--admin-password <Adminパスワード> `
--database-name <データベース名> `
--querytext `
"
create table todo (
id int(8) auto_increment,
content varchar(60),
done bit(1) not null default b'0',
created_at datetime not null default current_timestamp,
primary key (id)
);
"
Azure Functions側の実装
細かい実装の話が続くので、面倒くさい方は環境変数の設定までスキップしてください。
ここで特筆したいのは接続情報を環境変数から取ってくるという部分です。
※ソースは抜粋しているので、全容はGithubをご覧ください
ORマッパーの導入
せっかくなのでORマッパーを導入したいと思います。
今回は MyBatis を使用します。(SQLは自分で書きたいし、コードと分離したい派なので...)
本筋から外れるので、MyBatisの説明は割愛します。
興味のある方は調べてみてください。
どのORマッパーを選んだらよいか迷ったときは、こちらのスライドがとても参考になります。(クリックで展開)
依存関係の追加
MyBatisとMySQLコネクタを使用するため、pom.xml
に依存関係を追加します。
・・・
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.11</version>
</dependency>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
・・・
設定ファイルの追加
MyBatisの設定ファイルを追加します。
DBの接続情報をソース管理されるソースに含めるのはセキュリティ上よろしくないので、変数化(${xxx}
)しています。
この変数には後述の環境変数の値が入ります。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<package name="com.azure_functions.models"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<!-- ↓ポイント↓ -->
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<!-- ↑ポイント↑ -->
</dataSource>
</environment>
</environments>
<mappers>
<package name="com.azure_functions.mappers"/>
</mappers>
</configuration>
SQL定義の追加
実際に発行されるSQL文を定義します。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.azure_functions.mappers.TodoItemMapper">
<!-- TODO一覧取得 -->
<select id="selectTodo" resultType="TodoItem">
select
id
, content
, done
, created_at
from
todo
</select>
・・・
</mapper>
SQLとJavaをつなぐインターフェースを追加
JavaからSQLを呼び出すためにインターフェースを追加します。
メソッド名とSQL定義の id
が紐づきます。
/**
* TodoItemのマッパー
*/
public interface TodoItemMapper {
/**
* TODO一覧を全件取得
*
* @return TODO一覧
*/
List<TodoItem> selectTodo();
・・・
}
DB接続周りのコードを追加
接続情報を環境変数から取得して、設定ファイルで変数化していた部分にセットしています。
@RequiredArgsConstructor
public class SqlSessionManager {
/** SQLセッションファクトリー */
private static SqlSessionFactory sqlSessionFactory = sqlSessionFactory();
/** 実行コンテキスト */
private final ExecutionContext context;
private static SqlSessionFactory sqlSessionFactory() {
// ↓ポイント↓
// DB接続情報を環境変数から取得
Properties mybatisProps = new Properties();
mybatisProps.put("url", System.getenv("MYSQL_URL"));
mybatisProps.put("username", System.getenv("MYSQL_USER"));
mybatisProps.put("password", System.getenv("MYSQL_PASSWORD"));
// ↑ポイント↑
try (Reader config = Resources.getResourceAsReader("mybatis-config.xml")) {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(config, mybatisProps);
return sqlSessionFactory;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
・・・
}
モデルの修正
作成したテーブルのカラムにあわせて、モデルを変更します。
@Data
@Builder
@JsonDeserialize(builder = TodoItem.TodoItemBuilder.class)
public class TodoItem {
// ↓追加↓
/** ID */
private int id;
// ↑追加↑
/** 内容 */
private String content;
/** 完了フラグ */
private boolean done;
/** 作成日時 */
private LocalDateTime createdAt;
}
エンドポイントの追加
前回はTODO一覧を取得するためのエンドポイントのみでしたが、登録・更新・削除用のエンドポイントを追加します。
HTTPトリガーの追加
@HttpTrigger
を付与したメソッドを作成します。
また、前回対応しなかったAPIキーの使用についても対応しています。
(authLevel
を AuthorizationLevel.FUNCTION
に変更するだけです!)
public class Function {
/**
* TODO一覧を取得
*
* @param request リクエスト
* @param context 実行コンテキスト
* @return レスポンス
*/
@FunctionName("fetchTodoItems")
public HttpResponseMessage fetchTodoList(
@HttpTrigger(
name = "req",
methods = { HttpMethod.GET },
// 承認レベルをFUNCTIONに変更(APIキーが必要となる)
authLevel = AuthorizationLevel.FUNCTION,
// エンドポイントを「/api/todos」に設定
route = "todos"
) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context
) {
・・・
}
/**
* TODOを登録
*
* @param request リクエスト
* @param context 実行コンテキスト
* @return レスポンス
*/
@FunctionName("createTodoItem")
public HttpResponseMessage createTodoItem(
@HttpTrigger(
name = "req",
methods = { HttpMethod.POST },
authLevel = AuthorizationLevel.FUNCTION,
// エンドポイントを「/api/todos」に設定
route = "todos"
) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context
) {
・・・
}
/**
* TODOを更新
*
* @param request リクエスト
* @param context 実行コンテキスト
* @return レスポンス
*/
@FunctionName("updateTodoItem")
public HttpResponseMessage updateTodoItem(
@HttpTrigger(
name = "req",
methods = { HttpMethod.PATCH },
authLevel = AuthorizationLevel.FUNCTION,
// エンドポイントを「/api/todos/<id>」に設定
route = "todos/{id:int}"
) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context,
// パスのidをバインド
@BindingName("id") int id
) {
・・・
}
/**
* TODOを削除
*
* @param request リクエスト
* @param context 実行コンテキスト
* @return レスポンス
*/
@FunctionName("deleteTodoItem")
public HttpResponseMessage deleteTodoItem(
@HttpTrigger(
name = "req",
methods = { HttpMethod.DELETE },
authLevel = AuthorizationLevel.FUNCTION,
// エンドポイントを「/api/todos/<id>」に設定
route = "todos/{id:int}"
) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context,
// パスのidをバインド
@BindingName("id") int id
) {
・・・
}
・・・
}
HTTPトリガーの設定内容(サマリ)
関数名 | HTTPメソッド | エンドポイント | 承認レベル | 処理 |
---|---|---|---|---|
fetchTodoItems | GET | /api/todos | FUNCTION | 一覧取得 |
createTodoItem | POST | /api/todos | FUNCTION | 登録 |
updateTodoItem | PATCH | /api/todos/{id} | FUNCTION | 更新 |
deleteTodoItem | DELETE | /api/todos/{id} | FUNCTION | 削除 |
リクエストボディ(JSON)をオブジェクトに変換するためのデシリアライズ処理を追加
前回はオブジェクト→JSON変換のためにシリアライズ処理を実装しましたが、今回はその逆で、JSON→オブジェクト変換のためのデシリアライズ処理を実装します。
/**
* JSON関連のユーティリティ
*/
@RequiredArgsConstructor
public class JsonUtil {
/** JSONシリアライズ用のマッパー */
private static final ObjectMapper MAPPER = objectMapper();
/** 実行コンテキスト */
private final ExecutionContext context;
private static ObjectMapper objectMapper() {
・・・
// ↓追加↓
// LocalDateTime用のデシリアライザを追加
javaTimeModule.addDeserializer(
LocalDateTime.class,
new LocalDateTimeDeserializer(
DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")
)
);
// ↑追加↑
・・・
return mapper;
}
・・・
// ↓追加↓
/**
* JSONからオブジェクトに変換
*
* @param <T> オブジェクトの型
* @param json JSON文字列
* @param type オブジェクトの型
* @return オブジェクト
*/
public <T> T deserialize(String json, Class<T> type) {
try {
return MAPPER.readValue(json, type);
} catch (JsonProcessingException e) {
context
.getLogger()
.severe(
"JSON→オブジェクトの変換に失敗しました。エラーメッセージ:" +
e.getMessage()
);
throw new IllegalArgumentException(e);
}
}
// ↑追加↑
}
データ操作ロジックの変更
前回ダミーデータを生成していた部分を、DBから取得するように変更します。
また、今回追加した登録・更新・削除についてもロジックを追加します。
/**
* TODOサービス
*/
@RequiredArgsConstructor
public class TodoService {
/** 実行コンテキスト */
private final ExecutionContext context;
/** SQLセッション管理 */
private final SqlSessionManager sqlSessionManager;
/**
* TODO一覧を取得
*
* @return TODO一覧
*/
public List<TodoItem> fetchTodoItems() {
context.getLogger().info("fetchTodoItemsが呼び出されました。");
// ↓DBからデータ取得するよう変更↓
// DBからTODO一覧取得
List<TodoItem> todoItems = sqlSessionManager.transaction(
sqlSession -> {
TodoItemMapper todoItemMapper = sqlSession.getMapper(
TodoItemMapper.class
);
return todoItemMapper.selectTodo();
}
);
// ↑DBからデータ取得するよう変更↑
return todoItems;
}
/**
* TODOを登録
* @param todo TODOインスタンス
* @return 登録件数
*/
public int insertTodo(TodoItem todo) {
・・・
}
/**
* TODOを更新
* @param todo TODOインスタンス
* @return 更新件数
*/
public int updateTodo(int id, TodoItem todo) {
・・・
}
/**
* TODOを削除
* @param id TODOのID
* @return 削除件数
*/
public int deleteTodo(int id) {
・・・
}
}
環境変数の設定
ローカル実行とAzure上で環境変数の設定方法が異なります。
ローカル実行時の環境変数
Mavenプロジェクト作成時に作成されている local.settings.json
内の Values
に設定を追加します。
こうすることで、実行時に設定が読み込まれ、環境変数にセットされます。
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "java",
// ↓追加↓
"MYSQL_URL": "jdbc:mysql://<MySQLサーバー名>.mysql.database.azure.com:3306/<データベース名>?useSSL=true",
"MYSQL_USER": "<Adminユーザー>",
"MYSQL_PASSWORD": "<Adminパスワード>"
// ↑追加↑
}
}
◎個人的躓きポイント
local.settings.json
は、テスト時は読み込まれません。
そのため、テスト実行する場合は事前にターミナル等で環境変数を設定しておく必要があります。
mvn clean package
でビルドした場合、デフォルトではテストが実行されてしまうので、スキップしたい場合は以下のようにオプションを指定する
mvn clean package -DskipTests=true
または、pom.xml
に設定を追加してデフォルトの挙動を変更します。
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
・・・
<properties>
・・・
<!-- ↓追加↓ -->
<skipTests>true</skipTests>
<!-- ↑追加↑ -->
</properties>
Azure上にデプロイした関数アプリの環境変数(アプリケーション設定)
Azure上にデプロイした関数アプリの環境変数(アプリケーション設定)を追加するには
az functionapp config appsettings set コマンドを使用します。
az functionapp config appsettings set \
--resource-group <リソースグループ> \
--name <関数アプリ名> \
--settings \
MYSQL_URL=jdbc:mysql://<MySQLサーバー名>.mysql.database.azure.com:3306/<データベース名>?useSSL=true \
MYSQL_USER=<Adminユーザー> \
MYSQL_PASSWORD=<Adminパスワード>
PowerShell
az functionapp config appsettings set `
--resource-group <リソースグループ> `
--name <関数アプリ名> `
--settings `
MYSQL_URL=jdbc:mysql://<MySQLサーバー名>.mysql.database.azure.com:3306/<データベース名>?useSSL=true `
MYSQL_USER=<Adminユーザー> `
MYSQL_PASSWORD=<Adminパスワード>
ここではアプリケーション設定に設定値をべた書きしましたが、Key Vault(Keyコンテナー) というサービスを使用することで、シークレットを一元管理して複数のアプリで共有するといったことも可能です。
↓の記事が参考になりました。
[Azure] Azure FunctionsからAzure Key Vaultの値を参照する - Qiita
最終的なフォルダ構成
azure-functions
├── README.md
├── host.json
├── local.settings.json -------------------------------- 変更
├── pom.xml -------------------------------------------- 変更
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── azure_functions
│ │ ├── Function.java ------------------ 変更
│ │ ├── db
│ │ │ └── SqlSessionManager.java ----- 追加
│ │ ├── mappers
│ │ │ └── TodoItemMapper.java -------- 追加
│ │ ├── models
│ │ │ └── TodoItem.java -------------- 変更
│ │ ├── services
│ │ │ └── TodoService.java ----------- 変更
│ │ └── utils
│ │ └── JsonUtil.java -------------- 変更
│ └── resources
│ ├── com
│ │ └── azure_functions
│ │ └── mappers
│ │ └── TodoItemMapper.xml --------- 追加
│ └── mybatis-config.xml --------------------- 追加
└── test
デプロイ
アプリをAzureにデプロイします。
デプロイ手順は前回と同様なので割愛します。
動作確認
APIキーの確認
今回は関数にAPIキーを設定しているため、パラメータとしてAPIキーを指定する必要があります。
事前にコマンド叩いて確認します。
# 関数アプリのホストを取得
azFunctionHost=$(az functionapp show \
--resource-group <リソースグループ名> \
--name <関数アプリ名> \
--query "defaultHostName" | sed 's/"//g') # 「"」を除去
# APIキーを取得
azFunctionKey=$(az functionapp keys list \
--resource-group <リソースグループ名> \
--name <関数アプリ名> \
--query "functionKeys.default" | sed 's/"//g') # 「"」を除去
PowerShell
# 関数アプリのホストを取得
$azFunctionHost = (az functionapp show `
--resource-group <リソースグループ名> `
--name <関数アプリ名> `
--query "defaultHostName").Trim('"') # 「"」を除去
# APIキーを取得
$azFunctionKey = (az functionapp keys list `
--resource-group <リソースグループ名> `
--name <関数アプリ名> `
--query "functionKeys.default").Trim('"') # 「"」を除去
上記コマンドの出力結果はJSONですが、
--query
オプションで絞り込み・ソート・整形ができます。
--query
オプションには JMESPath というJSON向けのクエリを指定することができます。
API呼び出し
code
パラメータにAPIキーを指定します。
一覧
curl -i -X GET https://${azFunctionHost}/api/todos?code=${azFunctionKey}
登録
curl -i -X POST -d '{\"content\":\"Azureの資格取得\"}' https://${azFunctionHost}/api/todos?code=${azFunctionKey}
PowerShell
# 日本語が文字化けするので一旦データをファイルに吐いて、ファイル指定にする
$tmpFile = New-TemporaryFile
Write-Output '{"content":"Azureの資格取得"}' | Set-Content $tmpFile
curl -i -X POST -d @$tmpFile https://${azFunctionHost}/api/todos?code=${azFunctionKey}
Remove-Item $tmpFile
更新
curl -i -X PATCH -d '{\"content\":\"Azureの資格取得(AZ-900)\", \"done\":true}' https://${azFunctionHost}/api/todos/1?code=${azFunctionKey}
PowerShell
# 日本語が文字化けするので一旦データをファイルに吐いて、ファイル指定にする
$tmpFile = New-TemporaryFile
Write-Output '{"content":"Azureの資格取得(AZ-900)", "done":true}' | Set-Content $tmpFile
curl -i -X PATCH -d @$tmpFile https://${azFunctionHost}/api/todos/1?code=${azFunctionKey}
Remove-Item $tmpFile
削除
curl -i -X DELETE https://${azFunctionHost}/api/todos/1?code=${azFunctionKey}
まとめ
- MySQLサーバーにローカルから接続するときはファイアウォールでIPを許可する
- DBの接続情報は環境変数から取ってくる
これで簡単なバックエンドの処理ができたので、次はフロント部分からAPIを呼び出せるようにして、簡易的なSPAアプリ的な感じにしてみたいと思います。
2022/10/24 続きの記事を投稿しました