Azure Cosmos DBについて③ ~アプリケーションに組み込む~

f:id:ecb_tkaihatsu:20210915115354p:plain

はじめに

ブンブンHello World.

どうも開発です。

前回前々回と続けてAzure Cosmos DBについての記事を投稿いたしました。 今回は第三回となります。 上記記事もあわせてご一読いただけると幸いです。

本記事では、実際にアプリケーションを作った場合にどうやってCosmos DBを組み込むかということを焦点に進めていきたいと思います。

前回のおさらい

どんなことをやっていたか

前回までの記事では、

といったことをやっていました。

Cosmos DBはAzureで利用できるNoSQLのデータベースで、様々なAPIを選択してデータを管理できるサービスです。 最大の特徴は速さで、RDBのように厳密な整合性を担保しない代わりに圧倒的な速さを実現しています。

今回は第三回ということで、これまでの内容をもとに実際にアプリケーションに組み込んで運用してみたいと思います。

想定されるシチュエーション

新しいアプリケーションを設計する際に、DBについて考えないといけないことは決して少なくありません。

今回は例として、「チーム内のTodoを管理するためのアプリケーション」を作成する場合を考えます。

あなたは新しくチーム内のTodoを管理するアプリケーションを作成して欲しいという依頼を受けました。 しかしあなたはメインのプロジェクトが忙しく、なかなか設計に時間を割くことができません。 幸いアプリケーションのフレームワークには慣れているため、DBの設計さえどうにかなれば実装できそうな雰囲気です。

また、このアプリケーションには下記のような要望があります。

  • 現在はTodoの管理のみだか、将来的にはもっといろいろな情報を管理したいという要望がある。
  • チームには外国から参加しているメンバーもおり、24時間常に稼働している必要がある。
  • できるだけリアルタイムでチーム内の課題を共有したい
  • Todoの取得や更新など高速にして欲しい

上記の問題を整理すると、下記のようになるかと思います。

  • カラム設計している時間がなく、ミニマムなコストで実装したい
  • カラム、テーブルに不確定要素がある
  • とにかく落ちないDBを構築したい
  • 同期性よりも強いリアルタイム性、高速さが求めらる場合

上記のことからCosmos DBを選択することにしましょう。

実際に組み込んでみる

アプリケーションに組み込む

今回は下記のような設計で実装したいと思います。 クライアントサイドはVue.js、サーバーサイドはExpressを利用して簡単なWebアプリケーションとします。 Cosmos DBとやり取りするのはサーバーサイド側で担保し、クライアントサイドからはサーバーサイドのAPIをコールすることで情報のやり取りを実現します。

f:id:ecb_tkaihatsu:20210915115528p:plain

アプリケーションを作る

アプリケーションを作成する。 今回利用する技術要素は下記になります

  • Node.js 14.17.3
    • @azure/cosmos 3.14.1
    • axios 0.21.4
    • dotenv 10.0.0
    • express 4.17.1
    • primevue 2.4.2
    • primeicons 4.1.0
    • vue 2.6.11
  • Azure AppService
  • Azure Cosmos DB

最終的なフォルダ構成は下記のようにします。

/
┣ cosmos  // CosmosDBの設定値とCRUDを管理
┃  ┣ Config.js
┃  ┗ CosmosRepository.js
┣ dist  // ビルドしたVueアプリケーション
┃  ┣ css
┃  ┣ img
┃  ┣ js
┃  ┣ favicon.ico
┃  ┗ index.html
┣ public
┣ src  // Vue.jsのコンポーネント
┣ .env  // ローカルの環境変数
┣ babel.config.js
┣ index.js  // webサーバーの機能を実装
┣ package-lock.json
┗ package.json

上記の状態を目指していきましょう。

なお、.envファイルはCosmos DBの接続情報などが記載されます。 リポジトリやアプリケーションの稼働するサーバーにアップロードしないようにご注意ください。 公開リポジトリに置いたり、クライアント側に配信されるコードに差し込んだりするとシークレット情報が外部に漏洩するので注意しましょう。 あくまでローカル環境で利用するのみにとどめてください。

完成版のアプリケーションは下記になります。

github.com

環境構築

Vue.jsの環境を作る

まずはvue-cliを使ってアプリケーションのひな形を作成します。

vue create todo-cosmos

todo-cosmosの部分については、任意のアプリケーション名に変更してください。 上記コマンドを流すことでVue.jsのアプリケーションのテンプレートが作成されます。

上記コマンドが完了したら、ディレクトリを移動して下記のコマンドを実行してビルドを行いましょう。

npm run build

これでテンプレートのアプリケーションがビルドされ、新しく distフォルダが作成されます。 distフォルダにはビルドしたVue.jsのアプリケーションが格納されます。

Expressを使ってWebサーバーを立てる

次にNode.jsのモジュールであるExpressをインストールしてNode.jsでWebサーバーを立てます。

npm install --save express

インストール後、アプリケーションのルートディレクトリにindex.jsを作成します。 index.jsの中身は下記のように設定します。

const express = require("express");
const app = express();

// リクエストのルーティング
app.use("/img", express.static(__dirname + "/dist/img/"));
app.use("/css", express.static(__dirname + "/dist/css/"));
app.use("/js", express.static(__dirname + "/dist/js/"));
app.use("/favicon.ico", express.static(__dirname + "/dist/favicon.ico"));
app.get("/", (req, res) => res.sendFile(__dirname + "/dist/index.html"));

// サーバープロセスの実行
const server = app.listen(3000, () => {
    console.log("Node.js is listening to PORT:" + server.address().port);
});

上記コードをindex.jsに保存したら、下記のコマンドでNode.jsをWebサーバーとして実行します。

node ./index.js

または

npm run start

上記コマンドを実行後、Webブラウザで http://localhost:3000 とアクセスすればNode.jsで実行したWebサーバーから配信されているVue.jsアプリケーションが表示されます。

Todoアプリを作る

環境構築が完了したので、実際にTodoを管理するアプリケーションを作成しましょう。 とはいっても上記は本記事の本質部分ではないので、細かい内容は省略したいと思います。

まずは簡単にtodo管理を実現するための、jsonファイルで管理したいと思います。

アプリケーションのルートディレクトリにtodo.jsonを用意し、Webサーバーとして実行される環境で上記JSONファイルを読み込みます。

const todoList = require("./todo.json");

また、読み込んだ内容をクライアント側に配信したり、追加削除などの操作を実現するためのAPIとルーティングを用意しておきましょう。

app.get("/api/todo/list", (req, res) => {
    res.json(getTodo());
});

app.post("/api/todo/add", (req, res) => {
    addTodo(req.body);
    res.json({ message: "success" });
});

app.post("/api/todo/update", (req, res) => {
    updateTodo(req.body);
    res.json({ message: "success" });
});

app.post("/api/todo/delete", (req, res) => {
    deleteTodo(req.body.id);
    res.json({ message: "success" });
});

これによりクライアント側でAPIがコールされれば、サーバー側で用意したAPIが実行されtodoが更新されます。

アプリケーションとしての下地作りはできたので、次に本題であるCosmosDBを用意しましょう。

この段階でのソースコードは下記になります。

getTodo()addTodo()updateTodo()deleteTodo()の細かい内部実装についてはこちらを参照ください。 基本的にはJSONファイルを読み込んだ変数の中身を操作しているだけになります。

github.com

DBを用意する

Cosmos DBのリソースを作成します。

詳しい用意の仕方については こちらの記事 で解説しているので、参考にしてみてください。

アプリケーションに組み込む

サーバーサイドで実行されるAPIの実装を書き換え、ローカルのJSONファイルとのやり取りからCosmosDBとのやり取りに変更しましょう。

まずは、Cosmos DBの設定値やCRUDを管理する機能を実装します。

// cosmos/Config.js

module.exports  = {
    endpoint: process.env.COSMOS_ENDPOINT,
    key: process.env.COSMOS_KEY,
    databaseId: process.env.COSMOS_DATABASE_ID,
    containerId: process.env.COSMOS_CONTAINER_ID,
    partitionKey: { kind: 'Hash', paths: ['/id'] },
};

上記のように環境変数からシークレット情報を読み取ることで、アプリケーションに安全に組み込むことができます。

環境変数から読み出すように設定したので、次は.envファイルを編集したいと思います。 下記のように定義することで、環境変数として登録することができます。

// .env
COSMOS_ENDPOINT=' { Cosmos DBのエンドポイントURL } '
COSMOS_KEY=' { Cosmos DBのプライマリキーまたはセカンダリキー} '
COSMOS_DATABASE_ID=' { Cosmos DBのデータベース名 } '
COSMOS_CONTAINER_ID=' { Cosmos DBのデータベースに含まれるコンテナ名 } '

上記を用意することで、Cosmos DBと通信するための情報を定義することができました。 次は、実際に通信をする部分を定義したいと思います。

CRUDについては、下記のように実装してみます。 同じフォルダのConfig.jsを読み出し、設定値を取得しています。

// cosmos/CosmosRepository.js
const { CosmosClient } = require('@azure/cosmos');
const config = require('./Config');

exports.createCosmosDbClient = async function createCosmosDbClient() {
  const { endpoint, key, databaseId, containerId, partitionKey } = config;
  const client = new CosmosClient({ endpoint, key });
  await client.databases.createIfNotExists({
    id: databaseId,
  });
  const { container } = await client
    .database(databaseId)
    .containers.createIfNotExists(
      { id: containerId, partitionKey },
      { offerThroughput: 400 }
    );
  return container;
}

exports.insertData = async function insertData(container, data) {
  return await container.items.create(data);
}

exports.selectAll = async function selectAll(container) {
  const querySpec = {
    query: 'SELECT * FROM c',
  };
  const { resources: items } = await container.items
    .query(querySpec)
    .fetchAll();
  return items;
}

exports.deleteItem = async function deleteItem(container, id, partitionKey) {
  const result = await container.item(id, partitionKey).delete();
  return result;
}

exports.updateItem = async function updateItem(container, id, partitionKey, updateItem) {
  const result = await container.item(id, partitionKey).replace(updateItem);
  return result;
}

これでCosmos DBとの通信部分はできました。 次に、別ファイルに定義したCosmos DBの操作を行う関数を呼び出すようにします。

beforeで実装したgetTodo()などの内部実装を変更します。 f:id:ecb_tkaihatsu:20211006093613p:plain

これでアプリケーションにCosmos DBとのやりとりをする機能を実装できました。

実際に実装したアプリケーションが下記になります。

github.com

ローカルで実行して正常に動作するかどうか確認しましょう。

Azureにデプロイする

それでは、作成したアプリケーションをAzureのAppServiceにデプロイしましょう。

AppServiceにデプロイする前に、ポートとディレクトリパスの設定を見直す必要があります。

AppServiceでは環境でポート番号を定義しているので、下手にアプリケーション側で設定すると通らないことがあります。 これまでのコードでは3000番固定でしたが、ローカルでは3000番、既定の番号があればそちらで設定するように調整する必要があります。

const app = express();
+ const port = process.env.PORT || 3000;
app.use(
    express.urlencoded({
        extended: true,
    })
);

...
- app.listen(3000, async () => {
+ app.listen(port, async () => {
    cosmosClient = await createCosmosDbClient();
    console.log("Server process is running.");
});

また、ディレクトリパスはローカルでは__dirnameで解決していましたが、AppService上では別のパスとなるのでこの辺をいい感じにしてやる必要があります。 手っ取り早く解決しようと思ったので、今回は環境変数で差し込むように設定いたしました。

const { createCosmosDbClient, insertData, selectAll, updateItem, deleteItem } = require('./cosmos/CosmosRepository');
+ const staticdir = process.env.STATIC_DIR || __dirname;

- app.use("/img", express.static(__dirname + "/dist/img/"));
+ app.use("/img", express.static(staticdir + "/dist/img/"));
...

上記の設定を行えばデプロイの準備は完了です。

ここまで設定したものは下記になります。

github.com

デプロイについては、Azure PipelinesやGit連携、VSCode拡張機能やFTPアップロードなど様々なものがあります。 各自の環境に適した方法でデプロイしてみてください。 ※.envファイルをアップロードしないように注意

Cosmos DBの接続文字列やキーについてはAppServiceの環境変数に設定することで、ソースコード内に直接記述することなく組み込むことができます。

f:id:ecb_tkaihatsu:20211007085238p:plain

使ってみる

出来上がったものを実際に使ってみましょう。

使い心地

使ってみてわかる圧倒的な速さ。

今回のサンプルアプリケーションでは、Cosmos DB連携前はJSONファイルで6オブジェクトほどしか要素として持たせていませんでしたが、実用となると数百~数千といったデータを連携することも現実的に考えられます。そのような場合で果たしてパフォーマンスが落ちることなく速さを維持できるのでしょうか?

そんなわけで、実際に計測して速さを視覚化してみましょう。

今回は、Cosmos DBに格納されたデータの取得時間を計測してみたいと思います。 計測は、下記のようにperformanceモジュールを用いて計測したいと思います。

const { performance } = require("perf_hooks");
…
exports.selectAll = async function selectAll(container) {
    const startTime = performance.now();  // 計測開始時刻
    const querySpec = {
        query: "SELECT * FROM c",
    };
    const { resources: items } = await container.items
        .query(querySpec)
        .fetchAll();
    const endTime = performance.now();  // 計測終了時刻
    const executeTime = endTime - startTime;  // 実行時間を算出
    console.log("execution time: " + executeTime);
    return items;
};

そのほか、環境設定としては下記になります。

  • Cosmos DB Freeプラン
  • コンテナのパーティションキーは id を指定

実際の計測結果がこちらになります。

データ件数を10件、100件、1000件、5000件、10000件の5段階に分けて計測しました。

10回実行したうちの平均値と中央値で結果を比較したいと思います。

10件 100件 1000件 5000件 10000件
1 9.795 26.726 136.782 768.563 1104.666
2 8.856 26.175 109.226 645.218 1138.359
3 9.316 25.836 130.878 670.123 1059.204
4 9.315 22.524 131.888 631.391 1626.672
5 12.581 21.253 131.532 818.818 1042.957
6 10.350 22.739 127.755 888.185 1017.619
7 9.979 26.131 149.249 711.048 1163.253
8 11.260 22.311 122.199 825.800 1372.680
9 9.299 21.869 127.754 667.148 1449.483
10 9.201 22.516 124.498 729.389 1185.132
Average 9.995 23.808 129.176 735.568 1216.003
Median 9.556 22.632 129.317 720.219 1150.806

みんな大好きエクセル先生で表に起こしてみましょう。

f:id:ecb_tkaihatsu:20211020093158p:plainf:id:ecb_tkaihatsu:20211020093653p:plain
(左)縦軸:実行時間、横軸:計測データ件数、(右)縦軸:実行時間、横軸:計測データ件数(等間隔)

上記の結果から、おおよそ件数と実行時間が比例の関係にあることがわかりますので、件数が増えたからといって急にパフォーマンスが悪くなるという心配がなさそうです。

各件数について軽く見ていきましょう。

10件の場合は実行時間が平均約10ミリ秒という結果になりました。 中央値も9.55ミリ秒ということで、そこまで大きなばらつきもありませんでした。

100、1000件の場合についても同様で、平均値と中央値のばらつきが少ないです。 1000件くらいまでのデータなら0.1秒以内くらいにデータが返ってくるのは高速と呼べるのではないでしょうか。

続いて5000件の平均になります。 平均735ミリ秒、中央値720ミリ秒という結果でした。 平均値と中央値が近い値になっているので、実行回数が少ないものの安定したパフォーマンスを発揮していると思います。

最後に10000件の平均になります。 平均1216ミリ秒、中央値が1150ミリ秒という結果に落ち着きました。 流石に10000件もデータがあると1秒くらいはかかってしまいますが、逆に言えば10000件のデータをわずか1秒で全件取得できるのがCosmos DBの高速さを表しているのではないでしょうか。

アプリケーションの進化に伴う拡張

しばらく運用を続けた後、todoの持つデータを拡張したくなることがあると思います。 そのような場合においてもCosmos DBなら簡単に拡張ができます。

CREATEやUPDATE処理を行う部分でプロパティを追加すれば勝手にCosmos DB側に反映されます。 過去のデータと整合性を保つ必要がないのも使いやすいポイントですね。 過去のデータも含めて一括で更新したい場合は、すべてのデータに対して同じプロパティを追加するコードを実行すればアップデートできます。 この辺は実行環境ごとによりますね。

まとめ

今回は実際にアプリケーションにCosmos DBを組み込んで利用してみるところまでやってみました。 実用例として若干無茶なお題設定ではありましたが、Node.js環境でアプリケーションにCosmos DBを組み込む方法を実際にお伝えすることはできたのではないかと思います。

弊部署ではNode.js+Vue.js+Cosmos DBで図書管理システムを構築し、AppServiceにデプロイして実際に運用しております。 この辺のお話はまた別の機会に…。

おわりに

全3回でお送りした「Azure Cosmos DBについて」ですが、本記事で完結となります。

Cosmos DBとは何か?というところから実際に活用してみる例まで一通りまとめたので、シリーズを通してご覧いただければ「Cosmos DB 完全に理解した」なエンジニアになれるかと思います!

また今後もこういった技術記事やキャッチアップの記事投稿も続けていきたいと思いますので、次回以降の記事投稿にも是非是非ご注目ください!

お知らせ

ebeingでは高速な開発をしたい仲間を募集してます! careers.ecbeing.tech