Cosmos DB入出力バインドを使ったAzure FunctionsをJavaScriptで実装する

Azure Functionsを書くときはもっぱらVisual StudioとC#で作っていますが、
とある機会があって久々にJavaScriptのFunctionをAzureポータル上で作成してみたところ、
(ポータル上での実装は避けていましたが)これはこれでありかなと思えてきましたので、自分用のメモとしてざっと手順を残しておきます。

今回作成するFunctionは以下のような感じで構成しています。

  • Azureポータル上で実装
  • 言語はJavaScript
  • FunctionsのランタイムはV1
  • HTTPトリガー
  • Cosmos DB バインディング(入出力)
  • HTTPバインディング(出力)

ユースケースとしては「Function Appが公開するAPIにPOSTされたJSONデータをCosmos DBにUpsertする」を想定しています。

Azure Functions + Azure Cosmos DB

Function Appの準備

Function(関数)を実装するには、その受け皿としてAzure上にFunction Appを作っておく必要があります。ちなみに”Function”と”Function App”は言葉は似ていてもそれが指している意味は異なるので、用語の使い分けにちょっと気を遣います(しかもサービス名としての”Azure Functions”が入ってくるとさらに混乱しやすい・・)。

ポータルを使ったFunction Appの作成手順は、公式Docsの Azure Portal で初めての関数を作成する あたりを参照しつつ実装言語を適宜JavaScriptに置き換えれば良いと思います。

Function App作成直後

また、今回の例ではFunctionの入出力バインド先としてCosmos DBを利用するので、Azure Cosmos DB の構築(NoOpsコミュニティのハンズオンマテリアル)あたりを参考にCosmos DBを構築しておきます。ちなみにCosmos DBはサーバレスなFunctionとの相性が非常に良く、トリガー・入力バインディング・出力バインディング全てが対応しています。

Functionの設定

ポータルから作成する方式のFunctionでは、その設定は基本的に function.json に記述します。今回のケースだと以下のようになると思います。

{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req"
},
{
"type": "http",
"direction": "out",
"name": "res"
},
{
"name": "inputDocument",
"type": "documentDB",
"databaseName": "ToDoList",
"collectionName": "Items",
"id": "{id}",
"connection": "COSMOSDB_CONNECTION",
"direction": "in"
},
{
"name": "outputDocument",
"type": "documentDB",
"databaseName": "ToDoList",
"collectionName": "Items",
"connection": "COSMOSDB_CONNECTION",
"direction": "out"
}
],
"disabled": false
}

bindingsの1ブロック目はhttpトリガーの設定、2つ目はhttpレスポンスの設定なので、特に変わったことはありません。なお、このFunctionでは、以下のようなHTTPリクエストボディを受け取ることを期待しています。

{
"id": "xxx",
"title": "xxxxxx",
"description": "xxxxxxxxxxxx"
}

この仕様を前提に、3ブロック目のCosmos DBの入力バインディングでは、ドキュメントのIDをhttpのリクエストボディのidと照合するように設定しています。公式Docsではキュートリガーの例しかないので、HTTPトリガーでCosmos DBの入力バインディングを使う場合は上記の設定が参考になると思います。

Functionの実装

あとは、実際のコードを完成させるだけです。入出力は全てバインディングを使っているので、基本的には実際の処理部分だけを書くだけになります。

module.exports = function (context, req) {

// 入力バインディングからのドキュメントを変数docにセットする
var doc = context.bindings.inputDocument;

// 出力バインディングを使ってCosmos DBにデータを保存
if (doc != null) {

// 既存データがある場合は更新
context.bindings.outputDocument = JSON.stringify({
id: doc.id,
title: req.body.title,
description: req.body.description
});
}
else {

// 新規追加
context.bindings.outputDocument = JSON.stringify({
title: req.body.title, // idは自動採番
description: req.body.description
});
}

// HTTPレスポンスを作成
context.res = {
status: 201,
body: {
"result": "success",
"docId": (doc === null) ? "new" : doc.id
}
};
context.done();
};

説明するまでもなく非常にシンプルなコードになりました。この実装だけで以下の処理ステップ全てを実行してるのには改めて驚かされます。

  1. HTTPリクエストのBODYから入力データを取得
  2. Cosmos DBにクエリを実行しデータを取得
  3. ロジックの実行
  4. Cosmos DBへのUpsert(新規、上書き)
  5. HTTPレスポンスの作成

特にJavaScriptで書くFunctionは、C#でいうところのPOCOの定義が不要な分、さらにすっきりしますね。今後、ポータルで軽くFunctionを書くときはJavaScript、VSでガッツリ書くときはC#と使い分けようかなと思っています。