Azure Functions で Cosmos DB と MySQL のデータ連動

先日、いつものように Azure Cosmos DB を使ってサービスを構築していたら、どうしてもGroup Byを使わなければならない場面に遭遇しました。しかし現時点ではCosmos DBではGroup Byがサポートされていません(そのうち実現するかもしれないのでこちらからVoteしましょう)。
こういう時、普通ならGroup Byに相当するストアドプロシージャを頑張って作ったりするのでしょうが、そういう頑張りが嫌いな私は、 CosmosDB TriggerのFunctionを使って集計専用のセカンダリRDBを作って解決してしまえ、というなんともクラウドネイティブ?な設計に走りがちです。

Cosmos DB と MySQL のデータ連動例

セカンダリデータベースとして使うRDBは、別システム用に使っていた Azure DB for MySQL がわりとパワーを持て余していた様子だったのでそこを間借りして集計専用のDBを作成することにしました。ちなみに、 Azure DB for MySQL はサーバインスタンス単位の契約なので、インスタンスに余力があれば個別データベースを追加する分には追加費用は発生しません。
それにしても、Azure Database for MySQL/PostgreSQL がGAしたことで、もはやAzureだからといってSQL Databaseだけが選択肢ではなくなったのは嬉しいですね。

Azure Functionsの実装

Azure Functionsでは最近お気に入りのNode.jsで実装してみます。Cosmos DB TriggerのFunctionはNode.jsでももちろんサポートされており、実装もC#よりシンプルになるかもしれません。Cosmos DBのSQL APIはデータモデルがJSONベースなので、Node.jsとの相性が良いのは当然かもしれませんね。

Cosmos DB Trigger部分は以下のようなコードになりました(ここまではほぼテンプレートのまま)。

index.js

module.exports = function (context, documents) {
if (!!documents && documents.length > 0) {
context.log('Document Id: ', documents[0].id);
}
}

function.json

{
"bindings": [
{
"type": "cosmosDBTrigger",
"name": "documents",
"direction": "in",
"leaseCollectionName": "leases",
"connectionStringSetting": "miyake-lab_DOCUMENTDB",
"databaseName": "ToDoList",
"collectionName": "Items",
"createLeaseCollectionIfNotExists": true
}
],
"disabled": false
}

Azure Functionsは、ステートフルなコネクションを必要とするRDBのようなデータストアとはあまり相性はよくありません。特に細かいデータI/Oが並列に処理されるタイプのFunctionにはできるだけ使わない方が良いと思っています。ただし今回のように単一パーティションの比較的小規模なCosmos DBをトリガーとしたFunctionであれば、基本的にシーケンシャルに起動され、かつ連続したデータ変更もある程度まとめてインプットされるので、RDBに保存する処理を実装してもそれほど無理はなさそうです。

MySQLとの接続

Node.jsからMySQLへの接続には、 mysql2 というnpmパッケージを使うのが定番のようです。npmパッケージは、kuduでコンソールを使って以下のように実装対象のFunctionディレクトリでパッケージをインストールしておきます。

npm install -save mysql2

MySQL用の出力バインドは用意されていないので、DBへの接続部分を以下のように個別実装することになります。

const mysql = require('mysql2');

// connect mysql
const conn = mysql.createConnection({
host: process.env['MYSQL_HOST'],
user: process.env['MYSQL_USER'],
password: process.env['MYSQL_SECRET'],
ssl: true
});

MySQLの接続文字列はAppServiceの環境変数を読むようにしています。また、PaaSである Azure DB for MySQL は基本的にネットワーク越しに接続するので、SSLでの接続は必須と考えた方が良いでしょう。

MySQLへのデータ保存

MySQLへの接続が確立してしまえば、あとは好きなようにSQLを発行が可能です。Cosmos DB Triggerの場合、一定時間内に発生した変更はまとめて処理できるようにコレクションとして読み込まれてくるため、MySQLへのINSERTは一括バッチで処理させて、MySQLへの接続をできるだけ一回で終わらせるようにしています。

// INSERT文でバッチ更新できるように、インプットのオブジェクトをRDBのカラム順にValueを並べ変える
var todos = [];
for(var i in documents){
var todo = [
documents[i].id,
documents[i].name,
documents[i].category,
documents[i].completed,
new Date(documents[i].date)
];
todos.push(todo);
}

// INSERT実行
conn.query(
'INSERT INTO labdb.todos (id, name, category, is_completed, updated_on) VALUES ?',
[todos], (err) => {
if(err) throw err;
conn.end();
}
);

このアーキテクチャの狙いは、ユーザに近いフロントのデータストアは高速でアプリからアクセスしやすいNoSQLに任せ、溜まったデータで集計したいといった二次加工ニーズはRDBに任せようという、いわゆる「ラムダアーキテクチャ」を軽めに応用したようなイメージです。得意なことは得意なデータストアに任せようという戦略ですね。こういうことが気軽にやりやすい時代になりました。