Azure CosmosDBとFIWARE Orionを連携させる

2020年6月に、[Azure Managed ServiceとFIWAREを連携させる](https://qiita.com/nmatsui/items/cdd9a37a613dc6ac19c6)という記事を書きました。この時点では[FIWARE Orion 2.4.0](https://github.com/telefonicaid/fiware-orion/releases/tag/2.4.0)で検証したのですが、Azure CosmosDBのMongoDB APIではOrionを動作させることができませんでした。

> FIWARE OrionがMongoDBの細かい挙動に依存する実装になっていたため、多少のパッチ程度ではAzure CosmosDB上に実装されたMongoDB互換APIで動作させることができませんでした。

その後FIWARE Orionが3系にメジャーバージョンアップした際に、MongoDB 4系へ対応するためにDB連携周りのコードにもいくつか手が入りました。そこで今回は、最新の[FIWARE Orion 3.4.0](https://github.com/telefonicaid/fiware-orion/releases/tag/3.4.0)がAzure CosmosDBで動作するのか、再度検証したいと思います。

# 結果
先に結果を書きますと、**全く問題無しというわけではないが、概ね動作する**ことがわかりました。
## Indexの問題
* [Orionのデータベースのマニュアル](https://fiware-orion.readthedocs.io/en/master/admin/database_admin/index.html#multiservicemultitenant-database-separation)にあるように、本来Orionはdatabaseやcollection、indexを自動生成します。しかしCosmosDBのMongoDB APIでは、その機能が動作しません。そのため、Indexを自力で作成しておく必要があります。
    * Orionが推奨するIndexについては、[Orionのパフォーマンス・チューニングマニュアル](https://fiware-orion.readthedocs.io/en/master/admin/perf_tuning/index.html#database-indexes)を参照。
* [中の人曰く](https://github.com/telefonicaid/fiware-orion/issues/3797#issuecomment-802060531)、Orionが自動生成する2つのIndexのうち、`creDate`というIndexがあればとりあえず動作は可能なようです。しかしCosmosDBの仕様上、 `_id.servicePath, _id.id, _id.type` というEntityIdとTypeで検索するためのネストされた複合Indexが設定できません。そのため自身のEntityの利用方法をもとに自力でイイカンジのIndexを設定しないと、パフォーマンス上のペナルティがあるかもしれません。

## dateExpiresの問題
* Orionには有効期限を持ったEntityである[Transient Entity](https://fiware-orion.readthedocs.io/en/master/user/transient_entities/index.html)を作成する機能があります。これはMongoDBのTTL Index機能を用いて実現されていますが、CosmosDBのMongoDB APIには「TTL Indexは`_ts`フィールドのみ」という[制約](https://docs.microsoft.com/ja-jp/azure/cosmos-db/mongodb/mongodb-time-to-live)があります。そのためエンティティに有効期限を設定しても無視され、指定した期限が来てもエンティティは自動削除されません。

```text
time=2022-01-03T13:16:39.180Z | lvl=ERROR | corr=62900700-6c97-11ec-a8c3-0242ac130002 | trans=1641171277-902-00000000126 | from=172.19.0.1 | srv=tenant_a | subsrv=<none> | comp=Orion | op=AlarmManager.cpp[185]:dbError | msg=Raising alarm DatabaseError: collection: oriondb-tenant_a.entities - runCommand(): { "createIndexes" : "entities", "indexes" : [ { "key" : { "expDate" : 1 }, "name" : "expDate_1", "expireAfterSeconds" : 0 } ] } - exception: The 'expireAfterSeconds' option is supported on '_ts' field only.
```

# 検証環境のインフラ構成
インフラ構成は次図のようにしました。CosmosDBをインターネットに公開し、そこへインターネット経由で手元のOrionから接続することも可能ではありますが、データベースがむき出しになるのは好みではありません。そこで今回は、Azure VM上にOrionを立て、インターネットに公開されていないCosmosDBへサービスエンドポイント経由で接続するようにしました。
また検証用にOrion以外のプロセスも必要となるため、dockerコンテナとして動作させることにしました。

![orion_cosmosdb.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37648/d5ab6213-70e3-c5a2-82b1-88eb6dfbaf1e.png)

## 検証環境のバージョン
||バージョン|
|:--|:--|
|Azure CLI|2.31.0|
|CosmosDBのMongoDB API|4.0|
|FIWARE Orion|3.4.0|

## FIWAREのパラメータ
|パラメータ|値|
|:--|:--|
|database|oriondb|
|fiware-service|tenant_a|
|fiware-servicepath|指定無し|

OrionはEntityをMongoDBに格納する際、以下のようなルールでdatabaseを決めます（[MULTISERVICE/MULTITENANT DATABASE SEPARATION](https://fiware-orion.readthedocs.io/en/master/admin/database_admin/index.html#multiservicemultitenant-database-separation)）。

* デフォルトテナントのEntityは`<db>`というdatabaseに格納
* `FIWARE-SERVICE`によってテナントが指定されたEntityは`<db>-<tenant>`というdatabaseに格納

今回はDB名を`oriondb`、テナント名を`tenant_a`にすることを前提に、命名規則に従ってdatabaseや指定したIndexを持つcollectionをazコマンドから生成しておきます。



# 検証手順
## 環境構築
### リソースグループの準備
<details><summary>リソースグループの作成</summary><div>

```bash
$ az group create \
  --name orionCosmosDB \
  --location japaneast
```
</div></details>

### VNETの準備
<details><summary>VNETの作成</summary><div>

```bash
$ az network vnet create \
  --resource-group orionCosmosDB \
  --name orionCosmosDBVNet \
  --address-prefix 10.0.0.0/16
```
</div></details>
<details><summary>CosmosDBへのサービスエンドポイントを持ったサブネットの作成</summary><div>

```bash
$ az network vnet subnet create \
  --resource-group orionCosmosDB \
  --vnet-name orionCosmosDBVNet \
  --name orionCosmosDBSubnet \
  --address-prefix 10.0.1.0/24 \
  --service-endpoints Microsoft.AzureCosmosDB
```
</div></details>
<details><summary>SSHのみ許可したセキュリティグループの作成</summary><div>

```bash
$ az network nsg create \
  --resource-group orionCosmosDB \
  --name orionCosmosDBNSG
```
```bash
$ az network nsg rule create \
  --resource-group orionCosmosDB \
  --nsg-name orionCosmosDBNSG \
  --name ssh \
  --access allow \
  --protocol Tcp \
  --direction Inbound \
  --priority 200 \
  --source-address-prefix "*" \
  --source-port-range "*" \
  --destination-address-prefix "*" \
  --destination-port-range 22
```
</div></details>
<details><summary>サービスエンドポイントのIDを確認</summary><div>

```bash
$ AzureCosmosDBEndpoint=$(az network vnet subnet show \
  --resource-group orionCosmosDB \
  --vnet-name orionCosmosDBVNet \
  --name orionCosmosDBSubnet \
  --query 'id' \
  --output tsv); \
  echo ${AzureCosmosDBEndpoint}
```
</div></details>

### MongoDB APIを持つCosmosDBを準備
<details><summary>CosmosDBの作成</summary><div>

```bash
$ az cosmosdb create \
  --resource-group orionCosmosDB \
  --name orioncosmosdbaccount \
  --kind MongoDB \
  --server-version 4.0 \
  --default-consistency-level Eventual \
  --locations regionName=japaneast failoverPriority=0 isZoneRedundant=False \
  --capabilities EnableServerless \
  --enable-virtual-network true \
  --virtual-network-rules ${AzureCosmosDBEndpoint}
```
</div></details>
<details><summary>databaseの作成</summary><div>

```bash
$ az cosmosdb mongodb database create \
  --resource-group orionCosmosDB \
  --account-name orioncosmosdbaccount \
  --name oriondb-tenant_a
```
</div></details>
<details><summary>collectionとindexの作成</summary><div>

```bash
$ cat << __EOF__ > oriondb-tenant_a-idx.json
[
    {
        "key": {"keys": ["creDate"]}
    }
]
__EOF__
```
```bash
$ az cosmosdb mongodb collection create \
--resource-group orionCosmosDB \
--account-name orioncosmosdbaccount \
--database-name oriondb-tenant_a \
--name entities \
--idx @oriondb-tenant_a-idx.json
```
```bash
$ rm oriondb-tenant_a-idx.json
```
</div></details>
<details><summary>接続文字列の取得</summary><div>

```bash
$ AzureConnectionString=$(az cosmosdb keys list \
  --resource-group orionCosmosDB \
  --name orioncosmosdbaccount \
  --type connection-strings \
  --query "connectionStrings[?description == 'Primary MongoDB Connection String']" \
  | jq -r '.[].connectionString'); \
  echo ${AzureConnectionString}
```
</div></details>

### Orion用のVMの準備
<details><summary>VMの作成</summary><div>

ssh-keygenで鍵ペアは生成済みだとします。

```bash
$ az vm create \
  --resource-group orionCosmosDB \
  --name clientVM \
  --vnet-name orionCosmosDBVNet \
  --subnet orionCosmosDBSubnet \
  --nsg orionCosmosDBNSG \
  --public-ip-sku Standard \
  --image UbuntuLTS \
  --size Standard_A2_v2 \
  --admin-username ubuntu \
  --ssh-key-values ~/.ssh/azure.pub
```
</div></details>
<details><summary>パブリックIPの確認</summary><div>

```bash
$ AzureVMIP=$(az vm list-ip-addresses \
  --resource-group orionCosmosDB \
  --query "[].virtualMachine.network.publicIpAddresses[?name == 'clientVMPublicIP']" \
  | jq -r '.[][].ipAddress'); \
  echo ${AzureVMIP}
```
</div></details>
<details><summary>CosmosDBへの接続文字列を渡してVMへSSH</summary><div>

`az cosmosdb keys list`で取得したCosmosDBへの接続文字列を環境変数として引き渡し、bashをVM上に起動します。

```bash
$ ssh -i ~/.ssh/azure -t ubuntu@${AzureVMIP} \
  "AzureConnectionString=$(printf %q "$AzureConnectionString") bash"
```
</div></details>

以下はVM上での作業です。

<details><summary>dockerとdocker-composeをインストール</summary><div>

```bash
ubuntu@clientVM:~$ sudo apt update && sudo apt upgrade -y
```
```bash
ubuntu@clientVM:~$ sudo apt install ca-certificates curl gnupg lsb-release -y
```
```bash
ubuntu@clientVM:~$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
                   | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
```
```bash
ubuntu@clientVM:~$ echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```
```bash
ubuntu@clientVM:~$ sudo apt update && sudo apt-get install docker-ce docker-ce-cli containerd.io -y
```
```bash
ubuntu@clientVM:~$ sudo docker run hello-world
```
```bash
ubuntu@clientVM:~$ sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" \
  -o /usr/local/bin/docker-compose
```
```bash
ubuntu@clientVM:~$ sudo chmod +x /usr/local/bin/docker-compose
```
```bash
ubuntu@clientVM:~$ sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
```
```bash
ubuntu@clientVM:~$ docker-compose --version
```
</div></details>
<details><summary>mongo shellの起動スクリプトを生成</summary><div>

```bash
ubuntu@clientVM:~$ cat << __EOF__ > mongo_shell.bash
#!/bin/bash

sudo docker run -it mongo:4.4 mongo "${AzureConnectionString}"
__EOF__
```
```bash
ubuntu@clientVM:~$ chmod +x mongo_shell.bash
```
</div></details>
<details><summary>通知先サーバのDockerfileを生成</summary><div>

```bash
ubuntu@clientVM:~$ cat << '__EOF__' > Dockerfile
FROM python:2.7-slim
LABEL MAINTAINER Nobuyuki Matsui <nobuyuki.matsui@gmail.com>

RUN apt update && apt upgrade -y && \
    apt install wget -y && \
    wget https://raw.githubusercontent.com/telefonicaid/fiware-orion/master/scripts/accumulator-server.py -O /opt/accumulator-server.py && \
    chmod +x /opt/accumulator-server.py && \
    pip install Flask==1.0.2 pyOpenSSL==19.0.0 paho-mqtt==1.5.1

ENV PYTHONUNBUFFERED 1

WORKDIR /opt
ENTRYPOINT ["/opt/accumulator-server.py"]
CMD ["--port", "1028", "--url", "/accumulate", "--host", "0.0.0.0", "--pretty-print", "-v"]
__EOF__
```
</div></details>
<details><summary>orionと通知先サーバを起動するdocker-compose.ymlを生成</summary><div>

```bash
ubuntu@clientVM:~$ USER=$(echo $AzureConnectionString \
                   | sed -r 's/^mongodb:\/\/([^:]+):([^@]+)@([^/]+).*$/\1/')
```
```bash
ubuntu@clientVM:~$ PASSWORD=$(echo $AzureConnectionString \
                   | sed -r 's/^mongodb:\/\/([^:]+):([^@]+)@([^/]+).*$/\2/')
```
```bash
ubuntu@clientVM:~$ HOST=$(echo $AzureConnectionString \
                   | sed -r 's/^mongodb:\/\/([^:]+):([^@]+)@([^/]+).*$/\3/')
```
```bash
ubuntu@clientVM:~$ cat << __EOF__ > docker-compose.yml
version: "3"

services:
  orion:
    image: fiware/orion:3.4.0
    ports:
      - "1026:1026"
    command: -logLevel "debug" -db "oriondb" -dbSSL -dbDisableRetryWrites -dbhost "${HOST}" -rplSet "globaldb" -dbuser "${USER}" -dbpwd "${PASSWORD}"

  accumulator:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "1028:1028"
__EOF__
```
</div></details>

### VM上のmongo shellからの確認
<details><summary>mongo container経由でCosmosDBのMongoDB APIに接続</summary><div>

```bash
ubuntu@clientVM:~$ ./mongo_shell.bash
```

正しく環境構築できていれば、`globaldb:PRIMARY>`とプロンプトが表示されます。

```text
MongoDB shell version v4.4.11
connecting to: mongodb://orioncosmosdbaccount.mongo.cosmos.azure.com:10255/?appName=%40orioncosmosdbaccount%40&compressors=disabled&gssapiServiceName=mongodb&maxIdleTimeMS=120000&replicaSet=globaldb&retrywrites=false&ssl=true
Implicit session: session { "id" : UUID("628ae6c3-d4f6-42d6-9617-d70e058c5f96") }
MongoDB server version: 4.0.0
WARNING: shell and server versions do not match
Welcome to the MongoDB shell.
For interactive help, type "help".
For more comprehensive documentation, see
	https://docs.mongodb.com/
Questions? Try the MongoDB Developer Community Forums
	https://community.mongodb.com
globaldb:PRIMARY>
```
</div></details>
<details><summary>database、collection、indexの確認</summary><div>

```text
globaldb:PRIMARY> show dbs
```
```text
oriondb-tenant_a  0.000GB
```
```text
globaldb:PRIMARY> use oriondb-tenant_a
```
```text
switched to db oriondb-tenant_a
```
```text
globaldb:PRIMARY> show collections
```
```text
entities
```
`oriondb-tenant_a`databaseに`entities`collectionが存在することが確認できます。

```json
globaldb:PRIMARY> db.entities.getIndexes()
[
	{
		"v" : 1,
		"key" : {
			"_id" : 1
		},
		"name" : "_id_",
		"ns" : "oriondb-tenant_a.entities"
	},
	{
		"v" : 1,
		"key" : {
			"creDate" : 1
		},
		"name" : "creDate_1",
		"ns" : "oriondb-tenant_a.entities"
	}
]
```
`entities`collection`に`creDate`インデックスが存在していることが確認できます。

```text
globaldb:PRIMARY> db.entities.find({})
```
`entities`collection`にはまだドキュメントが存在しません。

```text
globaldb:PRIMARY> exit
```
mongo shellから抜けます。
</div></details>

### VM上にorionを起動
<details><summary>docker-composeでorionを起動</summary><div>

```bash
ubuntu@clientVM:~$ sudo docker-compose up -d
```
</div></details>

orionが無事に起動すると、`msg=Startup completed`というメッセージが表示されるはずです。

```bash
ubuntu@clientVM:~$ sudo docker logs ubuntu_orion_1
```

```text
...
time=2022-01-02T23:58:32.892Z | lvl=INFO | corr=N/A | trans=N/A | from=N/A | srv=N/A | subsrv=N/A | comp=Orion | op=contextBroker.cpp[1088]:main | msg=start command line </usr/bin/contextBroker -fg -multiservice -ngsiv1Autocast -disableFileLog -logLevel debug -db oriondb -dbSSL -dbDisableRetryWrites -dbhost orioncosmosdbaccount.mongo.cosmos.azure.com:10255 -rplSet globaldb -dbuser orioncosmosdbaccount -dbpwd ******>
time=2022-01-02T23:58:32.892Z | lvl=INFO | corr=N/A | trans=N/A | from=N/A | srv=N/A | subsrv=N/A | comp=Orion | op=contextBroker.cpp[1162]:main | msg=Orion Context Broker is running
time=2022-01-02T23:58:36.153Z | lvl=INFO | corr=N/A | trans=N/A | from=N/A | srv=N/A | subsrv=N/A | comp=Orion | op=mongoConnectionPool.cpp[506]:mongoConnectionPoolInit | msg=Connected to mongodb://orioncosmosdbaccount:******@orioncosmosdbaccount.mongo.cosmos.azure.com:10255/?replicaSet=globaldb&tls=true&tlsAllowInvalidCertificates=true&retryWrites=false&connectTimeoutMS=10000 (dbName: oriondb, poolsize: 10)
time=2022-01-02T23:58:36.349Z | lvl=ERROR | corr=N/A | trans=N/A | from=N/A | srv=N/A | subsrv=N/A | comp=Orion | op=AlarmManager.cpp[185]:dbError | msg=Raising alarm DatabaseError: collection: oriondb-tenant_a.entities - runCommand(): { "createIndexes" : "entities", "indexes" : [ { "key" : { "expDate" : 1 }, "name" : "expDate_1", "expireAfterSeconds" : 0 } ] } - exception: The 'expireAfterSeconds' option is supported on '_ts' field only.
time=2022-01-02T23:58:36.354Z | lvl=ERROR | corr=N/A | trans=N/A | from=N/A | srv=N/A | subsrv=N/A | comp=Orion | op=AlarmManager.cpp[209]:dbErrorReset | msg=Releasing alarm DatabaseError
time=2022-01-02T23:58:36.438Z | lvl=INFO | corr=N/A | trans=N/A | from=N/A | srv=N/A | subsrv=N/A | comp=Orion | op=contextBroker.cpp[1294]:main | msg=Startup completed
```
ただし上記のように、Indexの生成でエラーが出ているかもしれません。これはCosmosDBのIndex仕様によるものです。あきらめてください。

## OrionのNGSIv2 APIを検証
それでは[FIWARE NGSI APIV2 WALKTHROUGH](https://fiware-orion.readthedocs.io/en/master/user/walkthrough_apiv2/index.html)に沿って、OrionのAPIが一通り動作していることを確認しましょう。

### コンテキスト管理
<details><summary>エンティティの作成</summary><div>

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @- <<EOF
{
  "id": "Room1",
  "type": "Room",
  "temperature": {
    "value": 23,
    "type": "Float"
  },
  "pressure": {
    "value": 720,
    "type": "Integer"
  }
}
EOF
```
```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @- <<EOF
{
  "id": "Room2",
  "type": "Room",
  "temperature": {
    "value": 21,
    "type": "Float"
  },
  "pressure": {
    "value": 711,
    "type": "Integer"
  }
}
EOF
```
リクエストに成功し、`HTTP/1.1 201 Created`が返ってくるはずです。
またmongo shellからCosmosDBにクエリを投げると、上記２つのドキュメントが格納されていることが確認できます。

```bash
ubuntu@clientVM:~$ ./mongo_shell.bash
```
```text
globaldb:PRIMARY> use oriondb-tenant_a
```
```text
globaldb:PRIMARY> db.entities.find({})
```
```json
{ "_id" : { "id" : "Room1", "type" : "Room", "servicePath" : "/" }, "attrNames" : [ "temperature", "pressure" ], "attrs" : { "temperature" : { "type" : "Float", "creDate" : 1641171853.6182125, "modDate" : 1641171853.6182125, "value" : 23, "mdNames" : [ ] }, "pressure" : { "type" : "Integer", "creDate" : 1641171853.6182125, "modDate" : 1641171853.6182125, "value" : 720, "mdNames" : [ ] } }, "creDate" : 1641171853.6182125, "modDate" : 1641171853.6182125, "lastCorrelator" : "10faf7d0-6c31-11ec-ad66-0242ac130002" }
{ "_id" : { "id" : "Room2", "type" : "Room", "servicePath" : "/" }, "attrNames" : [ "temperature", "pressure" ], "attrs" : { "temperature" : { "type" : "Float", "creDate" : 1641171975.6809523, "modDate" : 1641171975.6809523, "value" : 21, "mdNames" : [ ] }, "pressure" : { "type" : "Integer", "creDate" : 1641171975.6809523, "modDate" : 1641171975.6809523, "value" : 711, "mdNames" : [ ] } }, "creDate" : 1641171975.6809523, "modDate" : 1641171975.6809523, "lastCorrelator" : "59c165bc-6c31-11ec-a31a-0242ac130002" }
```
</div></details>
<details><summary>エンティティの取得</summary><div>
Room1エンティティ全体を取得

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1?type=Room -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
{
    "id": "Room1",
    "pressure": {
        "metadata": {},
        "type": "Integer",
        "value": 720
    },
    "temperature": {
        "metadata": {},
        "type": "Float",
        "value": 23
    },
    "type": "Room"
}
```
Room1エンティティの属性値のみ取得

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1?options=keyValues -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
{
    "id": "Room1",
    "pressure": 720,
    "temperature": 23,
    "type": "Room"
}
```
```bash
ubuntu@clientVM:~$ curl 'localhost:1026/v2/entities/Room1?options=values&attrs=temperature,pressure' -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    23,
    720
]
```
特定の属性のみ取得

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1/attrs/temperature -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
{
    "metadata": {},
    "type": "Float",
    "value": 23
}
```
特定の属性の値のみ取得

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1/attrs/temperature/value -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: text/plain' \
                   | python -mjson.tool
```
```text
23
```
存在しないエンティティ

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room5 -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```text
...
< HTTP/1.1 404 Not Found
...
{
    "description": "The requested entity has not been found. Check type and id",
    "error": "NotFound"
}
```
存在しない属性

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1/attrs/humidity -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```text
...
< HTTP/1.1 404 Not Found
...
{
    "description": "The entity does not have such an attribute",
    "error": "NotFound"
}
```
</div></details>
<details><summary>エンティティのフィルタリング</summary><div>
すべてのエンティティの取得

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "id": "Room1",
        "pressure": {
            "metadata": {},
            "type": "Integer",
            "value": 720
        },
        "temperature": {
            "metadata": {},
            "type": "Float",
            "value": 23
        },
        "type": "Room"
    },
    {
        "id": "Room2",
        "pressure": {
            "metadata": {},
            "type": "Integer",
            "value": 711
        },
        "temperature": {
            "metadata": {},
            "type": "Float",
            "value": 21
        },
        "type": "Room"
    }
]
```
```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities?options=keyValues -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "id": "Room1",
        "pressure": 720,
        "temperature": 23,
        "type": "Room"
    },
    {
        "id": "Room2",
        "pressure": 711,
        "temperature": 21,
        "type": "Room"
    }
]
```
typeでフィルタリング

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities?type=Room -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "id": "Room1",
        "pressure": {
            "metadata": {},
            "type": "Integer",
            "value": 720
        },
        "temperature": {
            "metadata": {},
            "type": "Float",
            "value": 23
        },
        "type": "Room"
    },
    {
        "id": "Room2",
        "pressure": {
            "metadata": {},
            "type": "Integer",
            "value": 711
        },
        "temperature": {
            "metadata": {},
            "type": "Float",
            "value": 21
        },
        "type": "Room"
    }
]
```
IDパターンでフィルタリング

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities?idPattern=^Room[2-5] -g -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "id": "Room2",
        "pressure": {
            "metadata": {},
            "type": "Integer",
            "value": 711
        },
        "temperature": {
            "metadata": {},
            "type": "Float",
            "value": 21
        },
        "type": "Room"
    }
]
```
属性の条件でフィルタリング

```bash
ubuntu@clientVM:~$ curl 'localhost:1026/v2/entities?q=temperature>22' -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "id": "Room1",
        "pressure": {
            "metadata": {},
            "type": "Integer",
            "value": 720
        },
        "temperature": {
            "metadata": {},
            "type": "Float",
            "value": 23
        },
        "type": "Room"
    }
]
```
</div></details>
<details><summary>エンティティの更新</summary><div>
属性の更新

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1/attrs -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -X PATCH -d @- <<EOF
{
  "temperature": {
    "value": 26.5,
    "type": "Float"
  },
  "pressure": {
    "value": 763,
    "type": "Float"
  }
}
EOF
```
```text
< HTTP/1.1 204 No Content
```
属性値の更新

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1/attrs/temperature/value -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: text/plain' \
                   -X PUT -d 28.5
```
```text
< HTTP/1.1 204 No Content
```
</div></details>
### サブスクリプション
<details><summary>サブスクリプションの登録と一覧取得</summary><div>
Room1のpressureが変更されると、temperatureを通知するsubscriptionを登録

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/subscriptions -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @- <<EOF
{
  "description": "A subscription to get info about Room1",
  "subject": {
    "entities": [
      {
        "id": "Room1",
        "type": "Room"
      }
    ],
    "condition": {
      "attrs": [
        "pressure"
      ]
    }
  },
  "notification": {
    "http": {
      "url": "http://accumulator:1028/accumulate"
    },
    "attrs": [
      "temperature"
    ]
  },
  "expires": "2040-01-01T14:00:00.00Z"
}
EOF
```
```text
< HTTP/1.1 201 Created
< Connection: Keep-Alive
< Content-Length: 0
< Location: /v2/subscriptions/61d2636b5a72ee5ace05f95c
< Fiware-Correlator: 4ab4ed24-6c3f-11ec-ab7e-0242ac130002
< Date: Mon, 03 Jan 2022 02:46:03 GMT
```
subscriptionの一覧を取得

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/subscriptions -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "description": "A subscription to get info about Room1",
        "expires": "2040-01-01T14:00:00.000Z",
        "id": "61d2636b5a72ee5ace05f95c",
        "notification": {
            "attrs": [
                "temperature"
            ],
            "attrsFormat": "normalized",
            "http": {
                "url": "http://accumulator:1028/accumulate"
            },
            "onlyChangedAttrs": false
        },
        "status": "active",
        "subject": {
            "condition": {
                "attrs": [
                    "pressure"
                ]
            },
            "entities": [
                {
                    "id": "Room1",
                    "type": "Room"
                }
            ]
        }
    }
]
```
</div></details>
<details><summary>通知されるようにエンティティを更新</summary><div>
Room1のpressureを更新

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1/attrs/pressure/value -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: text/plain' \
                   -X PUT -d 802
```
```text
< HTTP/1.1 204 No Content
```
</div></details>
通知サーバのログを確認すると、登録したsubscriptionから通知が送信されたことが確認できます。

<details><summary>通知内容の確認</summary><div>

```bash
ubuntu@clientVM:~$ sudo docker logs ubuntu_accumulator_1
```
```text
...
POST http://accumulator:1028/accumulate
Fiware-Servicepath: /
Content-Length: 141
User-Agent: orion/3.4.0 libcurl/7.61.1
Ngsiv2-Attrsformat: normalized
Host: accumulator:1028
Accept: application/json
Fiware-Service: tenant_a
Content-Type: application/json; charset=utf-8
Fiware-Correlator: a043c616-6c3f-11ec-a6b0-0242ac130002; cbnotif=1

{
    "data": [
        {
            "id": "Room1",
            "temperature": {
                "metadata": {},
                "type": "Float",
                "value": 28.5
            },
            "type": "Room"
        }
    ],
    "subscriptionId": "61d2636b5a72ee5ace05f95c"
}
=======================================

172.19.0.2 - - [03/Jan/2022 02:48:26] "POST /accumulate HTTP/1.1" 200 -
```
</div></details>
<details><summary>フィルタリング式付きサブスクリプションの検証</summary><div>
Room1のpressureが700~800の範囲内で変更されると、temperatureを通知するsubscriptionを登録

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/subscriptions -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @- <<EOF
{
  "description": "A subscription to get info about Room1 with filtering expresion",
  "subject": {
    "entities": [
      {
        "id": "Room1",
        "type": "Room"
      }
    ],
    "condition": {
      "attrs": [
        "pressure"
      ],
      "expression": {
        "q": "pressure:700..800"
      }
    }
  },
  "notification": {
    "http": {
      "url": "http://accumulator:1028/accumulate"
    },
    "attrs": [
      "temperature"
    ]
  },
  "expires": "2040-01-01T14:00:00.00Z"
}
EOF
```
```text
< HTTP/1.1 201 Created
< Connection: Keep-Alive
< Content-Length: 0
< Location: /v2/subscriptions/61d2ac6b5a72ee5ace05f95f
< Fiware-Correlator: cde16c4c-6c6a-11ec-b4fa-0242ac130002
< Date: Mon, 03 Jan 2022 07:57:31 GMT
```
次の更新はフィルタ式にヒットするため、`61d2636b5a72ee5ace05f95c`と`61d2ac6b5a72ee5ace05f95f`は共に通知を送信

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1/attrs/pressure/value -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: text/plain' \
                   -X PUT -d 715
```
```text
POST http://accumulator:1028/accumulate
Fiware-Servicepath: /
Content-Length: 141
User-Agent: orion/3.4.0 libcurl/7.61.1
Ngsiv2-Attrsformat: normalized
Host: accumulator:1028
Accept: application/json
Fiware-Service: tenant_a
Content-Type: application/json; charset=utf-8
Fiware-Correlator: f5396e5c-6c6a-11ec-8f10-0242ac130002; cbnotif=1

{
    "data": [
        {
            "id": "Room1",
            "temperature": {
                "metadata": {},
                "type": "Float",
                "value": 28.5
            },
            "type": "Room"
        }
    ],
    "subscriptionId": "61d2636b5a72ee5ace05f95c"
}
=======================================

172.19.0.2 - - [03/Jan/2022 07:58:37] "POST /accumulate HTTP/1.1" 200 -
POST http://accumulator:1028/accumulate
Fiware-Servicepath: /
Content-Length: 141
User-Agent: orion/3.4.0 libcurl/7.61.1
Ngsiv2-Attrsformat: normalized
Host: accumulator:1028
Accept: application/json
Fiware-Service: tenant_a
Content-Type: application/json; charset=utf-8
Fiware-Correlator: f5396e5c-6c6a-11ec-8f10-0242ac130002; cbnotif=2

{
    "data": [
        {
            "id": "Room1",
            "temperature": {
                "metadata": {},
                "type": "Float",
                "value": 28.5
            },
            "type": "Room"
        }
    ],
    "subscriptionId": "61d2ac6b5a72ee5ace05f95f"
}
=======================================

172.19.0.2 - - [03/Jan/2022 07:58:37] "POST /accumulate HTTP/1.1" 200 -
```
次の更新はフィルタ式にヒットしないため、`61d2636b5a72ee5ace05f95c`のみ通知を送信

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room1/attrs/pressure/value -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: text/plain' \
                   -X PUT -d 801
```
```text
POST http://accumulator:1028/accumulate
Fiware-Servicepath: /
Content-Length: 141
User-Agent: orion/3.4.0 libcurl/7.61.1
Ngsiv2-Attrsformat: normalized
Host: accumulator:1028
Accept: application/json
Fiware-Service: tenant_a
Content-Type: application/json; charset=utf-8
Fiware-Correlator: 25b1fed2-6c6b-11ec-ba6b-0242ac130002; cbnotif=1

{
    "data": [
        {
            "id": "Room1",
            "temperature": {
                "metadata": {},
                "type": "Float",
                "value": 28.5
            },
            "type": "Room"
        }
    ],
    "subscriptionId": "61d2636b5a72ee5ace05f95c"
}
=======================================

172.19.0.2 - - [03/Jan/2022 07:59:59] "POST /accumulate HTTP/1.1" 200 -
```
</div></details>

### 型情報の取得
<details><summary>すべての型の情報を取得</summary><div>
`type=Room`のエンティティしか登録していないので、１種類のみ

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/types -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "attrs": {
            "pressure": {
                "types": [
                    "Float",
                    "Integer"
                ]
            },
            "temperature": {
                "types": [
                    "Float"
                ]
            }
        },
        "count": 2,
        "type": "Room"
    }
]
```
</div></details>
<details><summary>Room型の情報を取得</summary><div>

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/types/Room -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
{
    "attrs": {
        "pressure": {
            "types": [
                "Float",
                "Integer"
            ]
        },
        "temperature": {
            "types": [
                "Float"
            ]
        }
    },
    "count": 2
}
```
</div></details>

### バッチ処理
<details><summary>バッチで追加</summary><div>

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/op/update -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @- <<EOF
{
  "actionType": "append",
  "entities": [
    {
      "type": "Room",
      "id": "Room3",
      "temperature": {
        "value": 21.2,
        "type": "Float"
      },
      "pressure": {
        "value": 722,
        "type": "Integer"
      }
    },
    {
      "type": "Room",
      "id": "Room4",
      "temperature": {
        "value": 31.8,
        "type": "Float"
      },
      "pressure": {
        "value": 712,
        "type": "Integer"
      }
    }
  ]
}
EOF
```
```text
< HTTP/1.1 204 No Content
```
Room3とRoom4を一挙に追加

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities?options=keyValues -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "id": "Room1",
        "pressure": 801,
        "temperature": 28.5,
        "type": "Room"
    },
    {
        "id": "Room2",
        "pressure": 711,
        "temperature": 21,
        "type": "Room"
    },
    {
        "id": "Room3",
        "pressure": 722,
        "temperature": 21.2,
        "type": "Room"
    },
    {
        "id": "Room4",
        "pressure": 712,
        "temperature": 31.8,
        "type": "Room"
    }
]
```
</div></details>
<details><summary>バッチで更新</summary><div>

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/op/update -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @- <<EOF
{
  "actionType": "update",
  "entities": [
    {
      "type": "Room",
      "id": "Room3",
      "temperature": {
        "value": 29.9,
        "type": "Float"
      }
    },
    {
      "type": "Room",
      "id": "Room4",
      "pressure": {
        "value": 709,
        "type": "Integer"
      }
    }
  ]
}
EOF
```
```text
< HTTP/1.1 204 No Content
```
Room3とRoom4を一挙に更新

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities?options=keyValues -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "id": "Room1",
        "pressure": 801,
        "temperature": 28.5,
        "type": "Room"
    },
    {
        "id": "Room2",
        "pressure": 711,
        "temperature": 21,
        "type": "Room"
    },
    {
        "id": "Room3",
        "pressure": 722,
        "temperature": 29.9,
        "type": "Room"
    },
    {
        "id": "Room4",
        "pressure": 709,
        "temperature": 31.8,
        "type": "Room"
    }
]
```
</div></details>
<details><summary>バッチで検索</summary><div>

```bash
ubuntu@clientVM:~$ cat << EOF | curl localhost:1026/v2/op/query -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @- \
                   | python -mjson.tool
{
  "entities": [
    {
      "idPattern": "Room[1-2]",
      "type": "Room"
    }
  ],
  "attrs": [
    "temperature",
    "pressure"
  ],
  "expression": {
    "q": "pressure==700..720"
  }
}
EOF
```
```json
[
    {
        "id": "Room2",
        "pressure": {
            "metadata": {},
            "type": "Integer",
            "value": 711
        },
        "temperature": {
            "metadata": {},
            "type": "Float",
            "value": 21
        },
        "type": "Room"
    }
]
```
</div></details>

### レジストレーション
docker-composeで別のOrion（`provider-orion`）を起動し、Room5エンティティを登録します。

<details><summary>provider-orion用のdocker-compose.ymlを生成</summary><div>

```bash
ubuntu@clientVM:~$ cat << __EOF__ > provider-docker-compose.yml
version: "3"

services:
  provider-orion:
    image: fiware/orion:3.4.0
    ports:
      - "1027:1026"
    depends_on:
      - mongo
    command: -dbhost mongo
    networks:
      - ubuntu_default

  mongo:
    image: mongo:4.4
    command: --nojournal
    networks:
      - ubuntu_default

networks:
  ubuntu_default:
    external: true
__EOF__
```
</div></details>
<details><summary>provider-orionを起動</summary><div>

```bash
ubuntu@clientVM:~$ sudo docker-compose -f provider-docker-compose.yml up -d
```
</div></details>
<details><summary>provider-orionにRoom5を作成</summary><div>
provider-orionにRoom5を作成し、登録されていることを確認

```bash
ubuntu@clientVM:~$ curl localhost:1027/v2/entities -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @- <<EOF
{
  "id": "Room5",
  "type": "Room",
  "temperature": {
    "value": 12.3,
    "type": "Float"
  },
  "pressure": {
    "value": 718,
    "type": "Integer"
  }
}
EOF
```
```bash
ubuntu@clientVM:~$ curl localhost:1027/v2/entities/Room5?options=keyValues -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
{
    "id": "Room5",
    "pressure": 718,
    "temperature": 12.3,
    "type": "Room"
}
```
CosmosDBをバックエンドとする元のOrionにRoom5は存在しない

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room5?options=keyValues -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```text
< HTTP/1.1 404 Not Found
...
{
    "description": "The requested entity has not been found. Check type and id",
    "error": "NotFound"
}
```
</div></details>
Room5のレジストレーションを登録し、元のOrion経由でRoom5の情報が取得できることを確認します。

<details><summary>レジストレーションを作成</summary><div>

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/registrations -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -d @-  <<EOF
{
  "description": "Registration for Room5",
  "dataProvided": {
    "entities": [
      {
        "id": "Room5",
        "type": "Room"
      }
    ],
    "attrs": [
      "temperature",
      "pressure"
    ]
  },
  "provider": {
    "http": {
      "url": "http://provider-orion:1026/v2"
    }
  }
}
EOF
```
```text
< HTTP/1.1 201 Created
< Connection: Keep-Alive
< Content-Length: 0
< Location: /v2/registrations/61d2eeaa5a72ee5ace05f961
< Fiware-Correlator: 49e9cbb4-6c92-11ec-bde4-0242ac130002
< Date: Mon, 03 Jan 2022 12:40:10 GMT
```
</div></details>
<details><summary>レジストレーションを検索</summary><div>

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/registrations -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
[
    {
        "dataProvided": {
            "attrs": [
                "temperature",
                "pressure"
            ],
            "entities": [
                {
                    "id": "Room5",
                    "type": "Room"
                }
            ]
        },
        "description": "Registration for Room5",
        "id": "61d2eeaa5a72ee5ace05f961",
        "provider": {
            "http": {
                "url": "http://provider-orion:1026/v2"
            },
            "legacyForwarding": false,
            "supportedForwardingMode": "all"
        },
        "status": "active"
    }
]
```
</div></details>
<details><summary>レジストレーションを通じてRoom5の情報を取得</summary><div>
先程は404になっていたRoom5の情報を、レジストレーション経由で取得

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room5?options=keyValues -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
{
    "id": "Room5",
    "pressure": 718,
    "temperature": 12.3,
    "type": "Room"
}
```
</div></details>
<details><summary>レジストレーションを通じてRoom5の情報を更新</summary><div>

```bash
ubuntu@clientVM:~$ curl localhost:1026/v2/entities/Room5/attrs?type=Room -v \
                   -H 'fiware-service: tenant_a' \
                   -H 'Content-Type: application/json' \
                   -X PATCH -d @- <<EOF
{
  "temperature": {
    "value": 10.8,
    "type": "Float"
  },
  "pressure": {
    "value": 721,
    "type": "Float"
  }
}
EOF
```
```text
< HTTP/1.1 204 No Content
```
provider-orionのRoom5が更新されている

```bash
ubuntu@clientVM:~$ curl localhost:1027/v2/entities/Room5?options=keyValues -s -S \
                   -H 'fiware-service: tenant_a' \
                   -H 'Accept: application/json' \
                   | python -mjson.tool
```
```json
{
    "id": "Room5",
    "pressure": 721,
    "temperature": 10.8,
    "type": "Room"
}
```
</div></details>

# まとめ
ということで、CosmosDBに上手くIndexを設定すれば、**全く問題無しというわけではないが、NGSIv2のAPIは概ね動作する**ことがわかりました。
ただし今回は、geolocationalなフィルタリングやメタデータの検索など、NGSIv2が備える全ての機能を検証できたわけではありません。またパフォーマンスも評価できていないですし、プロダクションで利用する場合にはさらなる検証を重ねる必要がありそうです。